所有文章已迁移至csdn,csdn个人主页https://blog.csdn.net/chaitoudaren
前言
近两年网课上到处是《一天精通springboot》《2小时精通springboot》等课程,相信很多新手入门Java后端也都是从springboot开始的。2小时用搭建spring + mvc + mybatis后端常常让人有一种架构师的感觉,那么学完springboot到底还有必要学spring? 答案很负责任告诉你肯定是:有必要!简单、开箱即用等特性让springboot大受欢迎,但是请一定记住:一个框架越简单易用,说明框架在背后为开发人员做了越多复杂的工作。在笔者眼里springboot只不过是spring的一个整合,整合了tomcat、spring mvc一些常用组件,而本质依旧是spring
如果你还意识不到spring的重要性,以下有几个很基础的真实案例,如果发现不了案例问题的, 精通Spring 请再斟酌下要不要写入你的简历中
案例
难度★:单例(如: service, dao层)禁止使用非静态成员变量
使用误区
业务需求是需要导入10w张机票左右的Excel,PagService(单例)依赖ImportExcelListener(单例)进行Excel导入。使用到阿里的easyexcel,这里不讨论该框架,只需要知道每读取一行数据ImportExcelListener将调用invoke返回一行数据,读到末行将触发doAfterAllAnalysed。考虑到数据量较大,每读一行插入一次显然不可行,10w行插入一次也太大,于是小A同学经过评估后决定1000行插入一次
@Service
public class PagService extends BaseService<Pag> {
@Autowired
private ImportExcelListener importExcelListener;
public void improt(File file) {
// 调用importExcelListener实现excel导入
importExcelListener.read(file);
}
}
@Component
public class ImportExcelListener extends AnalysisEventListener<Pag> {
@Autowired
private PagService pagService;
// 用于保存1000张票
List<Pag> list = new ArrayList<Pag>();
@Override
public void invoke(DemoData data, AnalysisContext context) {
// 每获取一张票将票加入list
list.add(data);
// 票数到达1000则批量保存
if (list.size() >= 1000) {
pagService.save(list);
list.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 读到最后一行则批量保存
pagService.save(list);
list.clear();
}
}
造成BUG
此时的Web项目是正常使用的,但是将引发一个严重的BUG,线程不安全!1. 营业点A与营业点B同时导入数据,A和B的数据将完全混在一起 2. 一旦程序出现异常,之后的Excel提交将永远抛出唯一索引冲突异常
造成原因
ImportExcelListener
被@Component
设置成了单例,同时使用到了非静态成员变量List<Pag> list = new ArrayList<Pag>();
这违反了单例的使用原则,造成线程不安全的问题。ImportExcelListener
作为单例,也就是说在该Web项目中,将有且仅有一个实例。
- 当A、B营业处同时导入票证时,使用了同一个
ImportExcelListener
的同一个list
,当A导入向list
写入数据时B也同时在同一个list
写入,因此导致A发现其导入的数据中杂糅着B的数据。 - 并且当导入数据发生异常抛出时,例如读取到501行数据出现异常,但是list中将包含前500行数据,且没有被清空。而第二次导入因为
ImportExcelListener
为单例,缓存在Ioc容器中,因此ImportExcelListener
还是原来那个ImportExcelListener
,list
也还是之前那个list
,后续导入的数据又将1-500行加入list
,导致永远导入都是唯一索引冲突
难度★★:加上@Scope("Prototype")就是多例(原型模式)了吗?
使用误区
小A在发现问题后,也知道了问题的原因出在单例上,于是想当然的将上@Scope("Prototype")
,把ImportExcelListener
变成多例(也就是原型模式),不就解决问题了吗?这样的做法真的能解决问题吗?
@Component
@Scope("Prototype")
public class ImportExcelListener extends AnalysisEventListener<Pag> {
...
}
造成BUG
小A改完代码提交后,业务人员反应BUG依然存在,并没有解决
造成原因
BUG依旧存在,那么说明线程不安全的问题依旧存在,这说明多例同样也是线程不安全的吗?显然不是,多例情况下每次请求使用的ImportExcelListener
都是新的实例,不存在互相干扰的情况,也就没有所谓的线程安全问题可言。那么问题出在哪里?主要问题出在,即使小A加了@Scope("Prototype")
但是在本项目中ImportExcelListener
依旧是单例。
原因是PagService
依赖ImportExcelListener
,PagService
是单例ImportExcelListener
是多例。当spring创建PagService
时,发现其依赖ImportExcelListener
,而ImportExcelListener
是多例,因此新创建出importExcelListener@001
对象并且把地址给PagService
,创建完成后,PagService
将被缓存到spring Ioc容器中,下次需要PagService
时则直接从缓存中取。因此,在创建完成后,A售票处与B售票处实际上调用的PagService
是同一个实例,而导致其引用的importExcelListener@001
也是同一个,也就造成了即使加上@Scope("Prototype")
却还是同一个实例,因此线程不安全的BUG依旧存在。
这里贴一张单例setter的循环依赖流程图,a为PagService
,b为ImportExcelListener
。当PagService
第一次创建时,将走完1-17步骤
完成PagService
的创建。但是第二次再需要ImportExcelListener
时,将在步骤1. 尝试从各级缓存中获取bean
就会直接返回缓存中的PagService
,而不会再去管ImportExcelListener
是不是多例是不是需要重新创建。详情请参考spring 循环依赖
难度★★★:那就让PagService也变成多例!
使用误区
小A同学发现错误后,最后发狠,那我就让他们全部都变成多例!这总可以了吧?且先不论小A同学并不知道所有的spring mvc的controller都是单例(注:Struts框架的Action则是多例,这也是跟spring mvc最大的区别),并解决不了问题。但是出发点是好的,让他们都是多例,这似乎解决了问题。代码如下
@Component
@Scope("Prototype")
public class PagService extends BaseService<Pag> {
@Autowired
private ImportExcelListener importExcelListener;
...
}
@Component
@Scope("Prototype")
public class ImportExcelListener extends AnalysisEventListener<Pag> {
@Autowired
private PagService pagService;
...
}
造成BUG
如果你也觉得上面的代码没有问题,那么我们来看下结果,项目直接连跑都跑不起来了
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'pagService': Unsatisfied dependency expressed through field 'importExcelListener'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'importExcelListener': Unsatisfied dependency expressed through field 'pagService'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'pagService': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:667)
...
造成原因
其实从异常上面也看出来了,PagService
和ImportExcelListener
循环依赖了。小A表示很疑惑,我第一次第二次的代码也是循环依赖啊,为什么第一次第二次就没问题?这次就有问题了?这里先给出结论,spring仅可以解决单例setter方式注入的循环依赖问题,对于原型模式和单例构造器注入模式都解决不了。
通俗一点来说,PagService
为多例,也就是说每次获取都需要是一个新的实例。而当PagService
创建时,发现需要依赖ImportExcelListener
也是多例,因此又新生产了ImportExcelListener
实例,此时ImportExcelListener
又发现需要依赖PagService
,同时PagService
是多例,便不会去缓存中取,而是又新建一个多例。造成的结果就是循环依赖死循环,spring能做的就是帮你抛出异常...
spring循环依赖是面试最喜欢问的题目,如何检测循环依赖?spring能解决哪些循环依赖?总的可以归结成2点
- 必须提前曝光对象
- 曝光时机必须在实例化之后
缺一不可,关于循环依赖请参考spring 循环依赖
解决方案
就上述问题,归纳起来就是单例如何引用多例,使其在每次调用时都能保证使多例的问题。spring作为Java后端的元老,早就提供了解决方案,这里提供3种解决方案供参考:
- spring官方使用@Lookup或<lookup-method>标签,解决单例引用多例的问题
- 阿里官方推荐,这种方法最暴力,
ImportExcelListener
直接不使用spring管理,也就是不加@Component
标签,让用户调用方法时,自己new一个。这种做法即完全脱离spring管理,用户自己负责实例的生命周期
@Service
public class PagService extends BaseService<Pag> {
public void improt(File file) {
ImportExcelListener importExcelListener = new ImportExcelListener(this);
// 调用importExcelListener实现excel导入
importExcelListener.read(file);
}
}
- 实现BeanFactoryAware,从BeanFactory中获取
ImportExcelListener
多例,以确保每次调用都新建一个。这种方案并不可取,他加大了代码的耦合程度,只是提供给大家另外一种思路。对spring Aware感知器不熟悉的可以参考spring 生命周期
@Service
public class PagService extends BaseService<Pag> implements BeanFactoryAware{
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
public void improt(File file) {
ImportExcelServic importExcelServic = (ImportExcelServic)beanFactory.getBean("importExcelServic", this);
// 调用importExcelServic实现excel导入
importExcelServic.read(file);
}
}
总结
- spring单例绝对不能使用非静态成员变量(静态成员变量一个类只有一份,也就不存在线程安全问题)
- 单例依赖多例,想确保每次调用都使用全新的多例,需要使用@Lookup或lookup-method标签
- spring只能解决单例下set注入但是的循环依赖问题
笔者也使用springboot,但是精通springboot并不是指2小时学几个类似@Cacheable这样的标签就算精通了,如果是这样相信我在面试中你会被锤的很惨。我的建议是好好学习spring,如果有想进大厂的同学最好能够系统的学习一下spring 源码,对代码风格,设计思想都有很大帮助。毕竟大厂的面试官是不可能问出你会不会用@Cacheable、@Tranactional等标签的,而是想让你说出@Cacheable是如何通过AOP切面编程实现的、更甚是如何使用Jdk动态代理或者Cglib代理实现@Cacheable