前言

近两年网课上到处是《一天精通springboot》《2小时精通springboot》等课程,相信很多新手入门Java后端也都是从springboot开始的。2小时用搭建spring + mvc + mybatis后端常常让人有一种架构师的感觉,那么学完springboot到底还有必要学spring? 答案很负责任告诉你肯定是:有必要!简单、开箱即用等特性让springboot大受欢迎,但是请一定记住:一个框架越简单易用,说明框架在背后为开发人员做了越多复杂的工作。在笔者眼里springboot只不过是spring的一个整合,整合了tomcat、spring mvc一些常用组件,而本质依旧是spring

如果你还意识不到spring的重要性,以下有几个很基础的真实案例,如果发现不了案例问题的, 精通Spring 请再斟酌下要不要写入你的简历中

springBoot 需要学javase吗 学了spring boot还有必要学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项目中,将有且仅有一个实例。

  1. 当A、B营业处同时导入票证时,使用了同一个ImportExcelListener的同一个list,当A导入向list写入数据时B也同时在同一个list写入,因此导致A发现其导入的数据中杂糅着B的数据。
  2. 并且当导入数据发生异常抛出时,例如读取到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 循环依赖

springBoot 需要学javase吗 学了spring boot还有必要学spring么?_java_02


难度★★★:那就让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点

  1. 必须提前曝光对象
  2. 曝光时机必须在实例化之后

解决方案

就上述问题,归纳起来就是单例如何引用多例,使其在每次调用时都能保证使多例的问题。spring作为Java后端的元老,早就提供了解决方案,这里提供3种解决方案供参考:

  1. spring官方使用@Lookup或<lookup-method>标签,解决单例引用多例的问题
  2. 阿里官方推荐,这种方法最暴力,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);
	}
}
  1. 实现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);
	}
}

总结

  1. spring单例绝对不能使用非静态成员变量(静态成员变量一个类只有一份,也就不存在线程安全问题)
  2. 单例依赖多例,想确保每次调用都使用全新的多例,需要使用@Lookup或lookup-method标签
  3. spring只能解决单例下set注入但是的循环依赖问题

笔者也使用springboot,但是精通springboot并不是指2小时学几个类似@Cacheable这样的标签就算精通了,如果是这样相信我在面试中你会被锤的很惨。我的建议是好好学习spring,如果有想进大厂的同学最好能够系统的学习一下spring 源码,对代码风格,设计思想都有很大帮助。毕竟大厂的面试官是不可能问出你会不会用@Cacheable、@Tranactional等标签的,而是想让你说出@Cacheable是如何通过AOP切面编程实现的、更甚是如何使用Jdk动态代理或者Cglib代理实现@Cacheable