-
spring源码系列(一)——spring循环引用
2019-09-30 21:22:06spring循环依赖如何解的?传销一波-正文之前说一下我在CSDN有一门收费课程,需要的可以点击购买
https://edu.csdn.net/course/detail/32499觉得之前那篇阅读性比价差,主要第一次用csdn博客,很多语法不懂,导致文章可读性不好,我彻底更新一下;打算把spring集合写完;
长文警告
正文开始众所周知spring在默认单例的情况下是支持循环引用的
为了节省图片大小我把那些可以动得gif图片做成了只循环一次,如果看到图片不动了请右键选择在新标签打开,那么图片就会动,手机用户则更简单,直接手指点击图片便能看到动图,每张gif我都标识了,如果没有标识则为静态图片;
Appconfig.java
类的代码@Configurable @ComponentScan("com.shadow") public class Appconfig { }
X.java
类的代码package com.shadow.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class X { @Autowired Y y; public X(){ System.out.println("X create"); } }
Y.java了的代码
package com.shadow.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class Y { @Autowired X x; public Y(){ System.out.println("Y create"); } }
这两个类非常简单,就是相互引用了对方,也就是我们常常的说的循环依赖,spring是允许这样的循环依赖(前提是单例的情况下的,非构造方法注入的情况下)
运行这段代码的结果下图
注意这是张gif,如果你看着不动请参考我上面说的方法
上面代码从容器中能正常获取到Xbean
,说明循环依赖成功。但是spring
的循环依赖其实是可以关闭的,spring提供了api来关闭循环依赖的功能。当然你也可以修改spring源码来关闭这个功能,这里笔者为了提高逼格,就修改一下spring的源码来关闭这个功能,老话说:要想高明就得装逼。
下图是我修改spring源码运行的结果
我在AnnotationConfigApplicationContext的构造方法中加了一行setAllowCircularReferences(false);
结果代码异常,循环依赖失败那么为什么
setAllowCircularReferences(false);
会关闭循环依赖呢?首要明白spring的循环依赖是怎么做到的呢?spring源码当中是如何处理循环依赖的? 分析一下所谓的循环依赖其实无非就是属性注入,或者就是大家常常说的自动注入, 故而搞明白循环依赖就需要去研究spring自动注入的源码;spring的属性注入属于spring bean的生命周期一部分;怎么理解spring bean的生命周期呢?注意笔者这里并不打算对bean的生命周期大书特书,只是需要读者理解生命周期的概念,细节以后在计较;
要理解bean的生命周期首先记住两个概念
请读者一定记住两个概念——spring bean(一下简称bean)和对象;
1、spring bean——受spring容器管理的对象,可能经过了完整的spring bean生命周期(为什么是可能?难道还有bean是没有经过bean生命周期的?答案是有的,具体我们后面文章分析),最终存在spring容器当中;一个bean一定是个对象
2、对象——任何符合java语法规则实例化出来的对象,但是一个对象并不一定是spring bean;所谓的bean的生命周期就是磁盘上的类通过spring扫描,然后实例化,跟着初始化,继而放到容器当中的过程;
我画了一张简单图来阐述一下spring bean的生命周期大概有哪些步骤
上图就是spring容器初始化bean的大概过程(至于详细的过程,后面文章再来介绍);
文字总结一下:
1:实例化一个ApplicationContext的对象;
2:调用bean工厂后置处理器完成扫描;
3:循环解析扫描出来的类信息;
4:实例化一个BeanDefinition对象来存储解析出来的信息;
5:把实例化好的beanDefinition对象put到beanDefinitionMap
当中缓存起来,以便后面实例化bean;
6:再次调用bean工厂后置处理器;
7:当然spring还会干很多事情,比如国际化,比如注册BeanPostProcessor等等,如果我们只关心如何实例化一个bean的话那么这一步就是spring调用finishBeanFactoryInitialization
方法来实例化单例的bean,实例化之前spring要做验证,需要遍历所有扫描出来的类,依次判断这个bean是否Lazy,是否prototype,是否abstract等等;
8:如果验证完成spring在实例化一个bean之前需要推断构造方法,因为spring实例化对象是通过构造方法反射,故而需要知道用哪个构造方法;
9:推断完构造方法之后spring调用构造方法反射实例化一个对象;注意我这里说的是对象、对象、对象;这个时候对象已经实例化出来了,但是并不是一个完整的bean,最简单的体现是这个时候实例化出来的对象属性是没有注入,所以不是一个完整的bean;
10:spring处理合并后的beanDefinition(合并?是spring当中非常重要的一块内容,后面的文章我会分析);
11:判断是否支持循环依赖,如果支持则提前把一个工厂存入singletonFactories——map;
12:判断是否需要完成属性注入
13:如果需要完成属性注入,则开始注入属性
14:判断bean的类型回调Aware接口
15:调用生命周期回调方法
16:如果需要代理则完成代理
17:put到单例池——bean完成——存在spring容器当中用一个例子来证明上面的步骤,结合一些运行时期的动态图片
为了节省图片大小我把那些可以动得gif图片做成了只循环一次,如果看到图片不动了请右键选择在新标签打开,那么图片就会动,手机用户则更简单,直接手指点击图片便能看到动图,每张gif我都标识了,如果没有标识则为静态图片;
Z.java的源码
package com.shadow.service; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; @Component public class Z implements ApplicationContextAware { @Autowired X x;//注入X //构造方法 public Z(){ System.out.println("Z create"); } //生命周期初始化回调方法 @PostConstruct public void zinit(){ System.out.println("call z lifecycle init callback"); } //ApplicationContextAware 回调方法 @Override public void setApplicationContext(ApplicationContext ac) { System.out.println("call aware callback"); } }
来看看Z的生命周期,注意下图当中的字幕,会和上面的17个步骤一一对应
下图是第一步到第六步,请自行对应
接下来我们通过各种图片分析一下springbean的生命周期,读者只需要看图搞明白流程,至于图中涉及的源码,分析完流程之后再来解释;图① 注意这是张gif,如果你看着不动请参考我上面说的方法
在研究其他步骤之前,首先了解spring大概在什么时候实例化bean的图② 注意这是张gif,如果你看着不动请参考我上面说的方法
上图可以知道spring在AbstractApplicationContext#finishBeanFactoryInitialization
方法中完成了bean的实例化。这点需要记住然后通过图片来说明一下第7步
图③ 注意这是张gif,如果你看着不动请参考我上面说的方法
接下来spring需要推断构造方法,然后通过推断出来的构造方法反射实例化对象,也就是上面说的第8步和第9步当然有可能推断不出来构造方法;关于这块知识博主后面更新文章
图④ 注意这是张gif,如果你看着不动请参考我上面说的方法
上图说明spring是通过createBeanInstance(beanName, mbd, args)
;完成了推断构造方法和实例化的事情那么接下来便要执行第10步处理合并后的beanDefinition对象,这一块内容特别多,读者可以先不必要理解,后面文章会解释;图⑤ 注意这是张gif,如果你看着不动请参考我上面说的方法
仔细看上图,其实这个时候虽然Z被实例化出来了,但是并没有完成属性的注入;其中的X属性为null,而且里面的Aware接口的方法也没有调用,再就是@PostConstruct
方法也没有调用,再一次说明他不是一个完整的bean,这里我们只能说z是个对象;
继而applyMergedBeanDefinitionPostProcessors
方法就是用来处理合并后的beanDefinition对象;跟着第11步,判断是否支持循环依赖,如果支持则提前暴露一个工厂对象,注意是工厂对象
图⑥ 注意这是张gif,如果你看着不动请参考我上面说的方法
第12步,spring会判断是否需要完成属性注入(spring默认是需要的,但是程序员可以扩展spring,根据情况是否需要完成属性注入);如果需要则spring完成13步——属性注入,也就是所谓的自动注入;图⑦ 注意这是张gif,如果你看着不动请参考我上面说的方法
第14、15、16步图⑧ 注意这是张gif,如果你看着不动请参考我上面说的方法
默认情况 至此一个bean完成初始化,被put到单例池,也是对上文说的17个步骤的一个证明;这说明一个bean在spring容器当中被创建出来是有一个过程的,这个过程就是所谓的bean的生命周期,我们的循环依赖也是在这个生命周内完成的。下面我们具体来分析这些步骤由于bean的生命周期特别复杂本文只对涉及到循环依赖的步骤做分析,其他生命周期的步骤我会在后续博客中分析,可以继续关注博主
回顾上面的图② 和 图③ 我们知道spring的bean是在
AbstractApplicationContext#finishBeanFactoryInitialization()
方法完成的初始化,即循环依赖也在这个方法里面完成的。该方法里面调用了一个非常重要的方法doGetBean
的方法照例用图片来说明一下吧
图⑨ 注意这是张gif,如果你看着不动请参考我上面说的方法
doGetBean
方法内容有点多,这个方法非常重要,不仅仅针对循环依赖,甚至整个spring bean生命周期中这个方法也有着举足轻重的地位,读者可以认真看看笔者的分析。需要说明的是我为了更好的说清楚这个方法,我把代码放到文章里面进行分析;但是删除了一些无用的代码;比如日志的记录这些无关紧要的代码。下面重点说这个doGetBean
方法首先笔者把精简后的代码贴出来方便大家阅读
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType, @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { //读者可以简单的认为就是对beanName做一个校验特殊字符串的功能 //我会在下次更新博客的时候重点讨论这个方法 //transformedBeanName(name)这里的name就是bean的名字 final String beanName = transformedBeanName(name); //定义了一个对象,用来存将来返回出来的bean Object bean; //deGetBean-1 Object sharedInstance = getSingleton(beanName); //deGetBean-2 if (sharedInstance != null && args == null) { bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); }else{ deGetBean-3 if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); }else{ //doGetBean-4 if (mbd.isSingleton()) { sharedInstance = getSingleton(beanName, () -> { try { return createBean(beanName, mbd, args); } catch (BeansException ex) { destroySingleton(beanName); throw ex; } }); } } }
注意:上面的代码是我对doGetBean方法进行了删减的代码,只保留了和本文讨论的循环依赖有关的代码,完整版可以参考spring的源码
org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
接着笔者对上述代码逐行来解释
1、deGetBean-1
Object sharedInstance = getSingleton(beanName);
首先这行代码上有一句spring作者写的注释Eagerly check singleton cache for manually registered singletons.
大概的意思就是检查一下单例池当中有没有手动注册的单例对象,说白了spring在创建一个bean之前先检查一下
beanName
是否被手动注册过到单例池当中;别小看这句spring作者写的javadoc背后的意义,其实这里有两重意思;要搞清楚这两重意思首先知道当代码执行到这里的时候其实是spring在初始化的时候执行过来的;既然spring在初始化的时候他肯定知道这个类X.java肯定没有在容器当中,为什么还需要去验证一下呢?好比说你第一次去天上人间,你几乎都能确定这是你一次去你不可能跑到那里问一下前台你有没有办会员吧?但是spring确这样做了,他问了,他问问自己有没有办会员;为什么呢?回到你自己,如果你去问自己有没有办会员无非就是怕别人拿着你的身份证去办了一个会员,或者各种原因阴差阳错别人吧身份证名字写错了,导致你成了天上人间的会员;其实spring也是这个意思,因为一个bean被put到单例池的渠道有很多;除了spring容器初始化—扫描类----实例化-----put到容器这条线之外还有很多方法可以把一个对象put到单例池;我这里只列举一种,其他的有机会再讨论,看下图 注意注释;
这就相当于在你第一次抱着紧张心态去天上人间的时候,发现你朋友以前拿着你的身份证去那里办了一个会员卡一样;所以上面提到的这句注释的两重意思①第一重意思判断spring当前正准备初始化的bean有没有提前被put到容器;
那么第二重意思是什么呢?既然这里用来做spring初始化的工作,为什么这个方法名叫做doGetBean呢?讲道理应该叫做createBean啊才合理啊;有读者可能会说这个方法命名可能作者乱写的,请注意spring之所以经久不衰命名规范绝对是一个重要原因,作者是不会这么乱给方法命名的。诚然有的读者会说讨论这个的意义不大,其实博主觉得讨论这个非常重要;之所这里叫做doGetBean的原因就是因为这个方法就是用来获取bean的,他主要的工作不仅仅服务于spring bean的初始化;这个方法的作用不仅仅是为了spring 在初始化bean的过程中去判断一下这个bean是否被注册了这么简单;笔者认为这个方法最主要的作用是为了从容器中得到一个bean,也就是说当我们在spring代码中调用getBean(“a”)其背后的意义就是调用这个doGetBean,同样用一段代码来证明图⑩ 注意这是张gif,如果你看着不动请参考我上面说的方法
可以看到当我调用ac.getBean(“x”)的时候,底层其实就调用doGetBean获取这X对象的;spring之所以这么设计就是因为判断bean是否初始化好和get一个bean都需要从单例池当中获取,所以创建bean和getBean都需要调用这个doGetBean方法;也就是第②重意思,这个方法其实就是程序员getBean的底层实现;换成天上人间,你第一次跑去前台,人家前台直接说:先生请出示会员卡;你可能会奇怪——我是来全套的,你应该问我要什么服务,不是问会员卡;但是人家前台的职责有两,办会员和问你要什么服务;所以才会说出这句话;doGetBean也是这个意思,于是解释了这个方法名的意义了;
总结一下
Object sharedInstance = getSingleton(beanName);
目前看来主要是用于在spring初始化bean的时候判断bean是否在容器当中;以及供程序员直接get某个bean。注意笔者这里用了
目前
这个词;因为getSingleton(beanName);这个方法代码比较多;他里面的逻辑是实现循环依赖最主要的代码,文章下面我会回过头再来讲这个方法的全部意义;请注意我们当前代码的场景,当前代码是spring容器在初始化的时候,初始化X这个bean的场景;运行到了
Object sharedInstance = getSingleton(beanName);
根据上面的分析,这个时候我的X Bean肯定没有被创建,所以这里返回sharedInstance = =null;跟着解析
//deGetBean-2
//deGetBean-2 if (sharedInstance != null && args == null) { bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
由于
sharedInstance = =null
故而不会进入这个if分支,那么什么时候不等于null呢?两种情况1、在spring初始化完成后程序员调用getBean(“x”)的时候得到的sharedInstance 就不等于null;2、循环依赖的时候第二次获取对象的时候这里也不等于空;比如X 依赖 Y;Y依赖X;spring做初始化第一次执行到这里的时候X 肯定等于null,然后接着往下执行,当执行到属性注入Y的时候,Y也会执行到这里,那么Y也是null,因为Y也没初始化,Y也会接着往下执行,当Y执行到属性注入的时候获取容器中获取X,也就是第二次执行获取X;这个时候X则不为空;至于具体原因,读者接着往下看;至于这个if分支里面的代码干了什么事情,本文不讨论,放到后面写factoryBean的时候讨论,现在你可以理解if分支里面就把sharedInstance 原原本本的返回出来就行;即这个if分支没有意义;
上文说了本次不进入if分支,所以这行代码解析完毕;
接下解析 doGetBean -3
else{ deGetBean-3 if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } 如果把throw删了可能更加清晰吧,下面是删除后的代码 if (isPrototypeCurrentlyInCreation(beanName)) {}
不进if分支,则进入这个else分支,把throw删了 就一句代码;判断当前初始化的bean----X 是不是正在创建原型bean集合当中当中?
spring源码当中关于这行代码有两行javadoc
比较简单我就不翻译了,一般情况下这里返回false,也就是不会进入if分支抛异常;为什么呢说一般情况下呢?首先这里是判断当前的类是不是正在创建的原型集合
当中,即里面只会存原型;一般情况下我们的类不是原型,而是单例的,大家都知道spring默认是单例;所以返回false,再就是即使这个bean是原型也很少会在这里就存在**正在创建的原型集合
**当中。因为不管单例还是原型,bean在创建的过程中会add到这个集合当中,但是创建完成之后就会从这个集合remove掉(关于这个文章后面有证明),原型情况第一次创建的时候会add到这个集合,但是不是在这里,而是在后面的创建过程中add,所以这里肯定不会存在,即使后面过程中add到这个集合了,但是创建完成之后也会remove掉,故而下一次实例化同一个原型bean(原型可以实例化无数次)的时候当代码执行到这里也不可能存在集合当中了;除非循环依赖会在bean还没有在这个集合remove之前再次判断一次,才有可能会存在,故而我前面说了一般情况下这里都返回false;那么单例情况我们已经说了一定返回false,原型情况只有循环依赖才会成立,但是只要是正常人就不会对原型对象做循环依赖的;即使你用原型做了循环依赖这里也出抛异常(因为if成立,进入分支 throw exception)。再一次说明原型不支持循环依赖(当然你非得用原型做循环依赖,其实有办法,以后文章说明,本文忽略);画了一幅图说明上面的文字,因为这个集合非常重要,但是读者如果这里不理解也没关系,文章下面我还会结合代码分析一次;重点来了:说明叫做正在创建的原型集合呢? 还有一个与之对应的叫做正在创建的单例集合
唯一的区别就是集合里面存的是单例和原型
故而我们统称正在创建的集合,关于正在创建的集合是什么我下面会解释
但是需要记住的,这个集合是我的一家之言,说白了这是笔者自己翻译的,叫做正在创建的集合,没有官方支持,至少我也没在书上看到过这个名词下面解析doGetBean-4
else{ //doGetBean-4 if (mbd.isSingleton()) { sharedInstance = getSingleton(beanName, () -> { try { return createBean(beanName, mbd, args); } catch (BeansException ex) { destroySingleton(beanName); throw ex; } }); 同样把抛异常的代码删了,如下 //doGetBean-4 if (mbd.isSingleton()) { sharedInstance = getSingleton(beanName, () -> { return createBean(beanName, mbd, args); });
代码有点多;
if (mbd.isSingleton())
比较简单,判断当前bean是否单例;本文环境下是成立的;继而sharedInstance = getSingleton(beanName, () -> { return createBean(beanName, mbd, args); });
这里又调用了一次getSingleton,如果有印象上面也调用了一次getSingleton,这是方法重载,两个getSingleton方法并不是同一个方法,读者自己看参数就行,为了区别我这这里叫做第二次调用getSingleton;上文的叫做第一次调用getSingleton;
由于这里使用lamda表达式,有些读者看起来不是很理解;笔者改一下吧
ObjectFactory<?> singletonFactory = new ObjectFactory(){ public Object getObject(){ //其实这是个抽象类,不能实例化 //createBean是子类实现的,这里就不关心了 //你就理解这不是一个抽象类吧 AbstractBeanFactory abf = new AbstractBeanFactory(); Object bean = abf.createBean(beanName, mbd, args); return bean; }; }; //传入 beanName 和singletonFactory 对象 sharedInstance = getSingleton(beanName,singletonFactory); 这样看是不是明白多了呢?
当然第二次getSingleton就会把我们bean创建出来,换言之整个bean如何被初始化的都是在这个方法里面;至此本文当中笔者例举出来的doGetBean方法的核心代码看起来解析完成了;
注意我说的是本文当中例举的doGetBean代码,前面我已经说了我删了很多和循环依赖无关的代码,实际spring源码当中这个方法的代码很多,以后文章介绍吧;
接下来就要研究第二次getSingleton方法的内容了,因为我说了整个bean初始化过程都在里面体现了;
我先把spring源码贴出来,读者可以忽略这里,因为下面会精简代码;之所以贴出源码就是想告诉读者,为了研究循环依赖,本文中的很代码我是做了删减的;
spring源码:-----读者可以忽略 public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { Assert.notNull(beanName, "Bean name must not be null"); synchronized (this.singletonObjects) { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { if (this.singletonsCurrentlyInDestruction) { throw new BeanCreationNotAllowedException(beanName, "Singleton bean creation not allowed while singletons of this factory are in destruction " + "(Do not request a bean from a BeanFactory in a destroy method implementation!)"); } if (logger.isDebugEnabled()) { logger.debug("Creating shared instance of singleton bean '" + beanName + "'"); } beforeSingletonCreation(beanName); boolean newSingleton = false; boolean recordSuppressedExceptions = (this.suppressedExceptions == null); if (recordSuppressedExceptions) { this.suppressedExceptions = new LinkedHashSet<>(); } try { singletonObject = singletonFactory.getObject(); newSingleton = true; } catch (IllegalStateException ex) { // Has the singleton object implicitly appeared in the meantime -> // if yes, proceed with it since the exception indicates that state. singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { throw ex; } } catch (BeanCreationException ex) { if (recordSuppressedExceptions) { for (Exception suppressedException : this.suppressedExceptions) { ex.addRelatedCause(suppressedException); } } throw ex; } finally { if (recordSuppressedExceptions) { this.suppressedExceptions = null; } afterSingletonCreation(beanName); } if (newSingleton) { addSingleton(beanName, singletonObject); } } return singletonObject; } }
下面是我删减后只和循环依赖有关的代码
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { //getSingleton2 -1 Object singletonObject = this.singletonObjects.get(beanName); //getSingleton2 -2 if (singletonObject == null) { //getSingleton2 -3 if (this.singletonsCurrentlyInDestruction) { throw new Exception(beanName, "excepition"); } //getSingleton2 -4 beforeSingletonCreation(beanName); //getSingleton2 -5 singletonObject = singletonFactory.getObject(); } return singletonObject; }
//getSingleton2 -1 开始解析
Object singletonObject = this.singletonObjects.get(beanName);
第二次getSingleton上来便调用了this.singletonObjects.get(beanName),直接从单例池当中获取这个对象,由于这里是创建故而一定返回null;singletonObjects是一个map集合,即所谓的单例池;用大白话说spring所有的单例bean实例化好都存放在这个map当中,这也是很多读者以前认为的spring容器,但是笔者想说这种理解是错误的,因为spring容器的概念比较抽象,而单例池只是spring容器的一个组件而已;但是你如果一定要找一个平衡的说法,只能说这个map——singletonObjects仅仅是狭义上的容器;比如你的原型bean便不在这个map当中,所以是狭义的spring容器;下图为这个map在spring源码当中的定义
//getSingleton2 -2 开始解析
if (singletonObject == null) { 上面解释了,在spring 初始化bean的时候这里肯定为空,故而成立
//getSingleton2 -3 开始解析
if (this.singletonsCurrentlyInDestruction) { throw new Exception(beanName, "excepition"); }
这行代码其实比较简单,判断当前实例化的bean是否正在销毁的集合里面;spring不管销毁还是创建一个bean的过程都比较繁琐,都会先把他们放到一个集合当中标识正在创建或者销毁;所以如果你理解了前面那个正在创建集合那么这个正在销毁集合也就理解了;但是不理解也没关系,下面会分析这些集合;
如果一个bean正在创建,但是有正在销毁那么则会出异常;为什么会有这种情况?其实也很简单,多线程可能会吧;
//getSingleton2 -4 假设解析
beforeSingletonCreation(beanName);
这段代码就比较重要了,关于上面说那个正在创建和正在销毁的集合;这段代码就能解释,所以如果上面你没看明白那个集合的意义,笔者这里用spring源码来说明一下;先看看当代码执行到这里的时候语境
当spring觉得可以着手来创建bean的时候首先便是调用
beforeSingletonCreation(beanName);
判断当前正在实例化的bean是否存在正在创建的集合当中,说白了就是判断当前是否正在被创建;因为spring不管创建原型bean还是单例bean,当他需要正式创建bean的时候他会记录一下这个bean正在创建(add到一个set集合当中);故而当他正式创建之前他要去看看这个bean有没有正在被创建(是否存在集合当中); 为什么spring要去判断是否存在这个集合呢?原因很多除了你们能想到了(你们能想到的基本不会出现,比如并发啊,重复创建什么的,因为他已经做了严格并发处理),其实这个集合主要是为了循环依赖服务的,怎么服务的呢?慢慢看吧,首先我们来看下这行 代码的具体内容
源码:protected void beforeSingletonCreation(String beanName) { if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } }
1、
this.inCreationCheckExclusions.contains(beanName)
这里是判断当前需要创建的bean是否在Exclusions集合,被排除的bean,程序员可以提供一些bean不被spring初始化(哪怕被扫描到了,也不初始化),那么这些提供的bean便会存在这个集合当中;一般情况下我们不会提供,而且与循环依赖无关;故而所以这里不做深入分析,后面文章如果写到做分析;this.singletonsCurrentlyInCreation.add(beanName)
,如果当前bean不在排除的集合当中那么则这个bean添加到singletonsCurrentlyInCreation(当然这里只是把bean名字添加到集合,为了方便我们直接认为把bean添加到集合吧,因为他能根据名字能找打对应的bean);关于
singletonsCurrentlyInCreation
的定义参考下图
其实就是一个set集合,当运行完this.singletonsCurrentlyInCreation.add(beanName)
之后结果大概如下图这样
我们可以通过debug来调试证明一下上面这幅图** 注意这是张gif,如果你看着不动请参考我上面说的方法**
结果分析:当代码运行完this.singletonsCurrentlyInCreation.add(beanName)
之后可以看到singletonsCurrentlyInCreation集合当中只存在一个x,并且后天并没有执行x的构造方法,说明spring仅仅是把x添加到正在创建的集合当中,但是并没有完成bean的创建(因为连构造方法都没调用);请一定注意这个集合的数据情况(目前只有一个x);因为这和循环依赖有天大的关系;add完x之后代码接着往下执行;
//getSingleton2 -5 开始分析
singletonObject = singletonFactory.getObject();
可能有读者已经忘记了singletonFactory这个对象怎么来的了;笔者再把代码贴一遍吧ObjectFactory<?> singletonFactory = new ObjectFactory(){ public Object getObject(){ //其实这是个抽象类,不能实例化 //createBean是子类实现的,这里就不关心了 //你就理解这不是一个抽象类吧 AbstractBeanFactory abf = new AbstractBeanFactory(); Object bean = abf.createBean(beanName, mbd, args); return bean; }; }; //传入 beanName 和singletonFactory 对象 sharedInstance = getSingleton(beanName,singletonFactory);
singletonFactory.getObject();
调用的就是上面代码中getObject方法,换言之调用的是abf.createBean(beanName, mbd, args);把创建好的bean返回出来;至此第二次getSingleton方法结束,bean通过singletonFactory.getObject();
调用createBean建完成;接下来分析createBean的源码,继续探讨循环依赖的原理;AbstractAutowireCapableBeanFactory#createBean()
方法中调用了doCreateBean方法创建bean;下图是dubug流程** 注意这是张gif,如果你看着不动请参考我上面说的方法**
结果分析:因为执行完doCreateBean之后X和Y的构造方法都已经完成了调用,说明这个方法里面对X做了实例化,也就是把bean创建好了,而且完成了循环依赖(因为Y的构造方法也打印说明X在完成属性注入的时候注入了Y,所以Y也实例化了,Y bean也创建好了);接下来重点分析这个doCreateBean方法内容。我先给出这个方法的源码全貌;重点我用红色标记了,并且会在进行代码解析;黄色线下面的读者可以不用管,和本文内容没多大关系;
读者可以好好看看下图:方便你阅读下面的代码解析
//doCreateBean -1
instanceWrapper = createBeanInstance(beanName, mbd, args);
createBeanInstance 顾名思义就是创建一个实例,注意这里仅仅是创建一个实例对象,还不能称为bean;因为我文章一开头就解释了什么是bean,什么是对象;好吧再啰嗦一下吧,文章比较长,不方便翻阅;
1、spring bean——受spring容器管理的对象,可能经过了完整的spring bean生命周期(为什么是可能?难道还有bean是没有经过bean生命周期的?答案是有的,具体我们后面文章分析),最终存在spring容器当中;一个bean一定是个对象
2、对象——任何符合java语法规则实例化出来的对象,但是一个对象并不一定是spring bean;同样用dubug来说明一下:
** 注意这是张gif,如果你看着不动请参考我上面说的方法**
运行完createBeanInstance之后控制打印了X构造方法的内容,说明X对象已经被创建了,但是这个时候的x不是bean,因为bean的生命周期才刚刚开始;这就好比你跑到天上人间,问了各种你想问的问题之后交了1000块钱,但是这个时候你仅仅是个消费者,还不是渣男,因为一条龙的服务是从交钱开始,接下来的各种服务完成你才是一个名副其实的渣男,不知道这么解释有没有偏差;为了把前面知识串起来,照例画一下当前代码的语境吧
这个createBeanInstance方法是如何把对象创建出来的呢?对应文章开头说的bean的生命周期一共17步,其中的第8步(推断构造方法)和第9步(利用构造方法反射来实例化对象);具体如何推断构造方法我会在后面的博客分析;这里截个图看看代码就行,不做分析;推断构造方法的代码运行结果分析——注意这张图比较长,读者可以多看几遍;因为推断构造方法笔者以为是属于spring源码中特别重要和特别难的一块知识;后面会有单独博客来分析,所以读者可以先多看看这张图;
** 注意这是张gif,如果你看着不动请参考我上面说的方法**
至此x对象已经实例化出来,代码往下执行到合并beanDefinition,看图吧
但是其实合并beanDefinition和本文讨论的循环依赖无关,故而先跳过;//doCreateBean-2 开始解析
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
这段代码其实比较简单,就是给
earlySingletonExposure
这个布尔类型的变量赋值;这个变量的意义是——是否支持(开启了)循环依赖;如果返回true则spring会做一些特殊的操作来完成循环依赖;如果返回false,则不会有特殊操作;回到天上人间那个问题,好比你去一条龙的时候;人家会分析你是否是雏,如果你是雏则随便给你安排一个技师;当然如果你是笔者这样的资深玩家,可能会安排新亘结衣也说不定;
那么这个布尔变量的赋值逻辑是怎样的呢?上面代码可知三个条件做&&运算,同时成立才会返回true;
1、mbd.isSingleton();判断当前实例化的bean是否为单例;再一次说明原型是不支持循环依赖的;因为如果是原型这里就会返回false,由于是&&运算,整个结果都为false;相当于人家判断你是雏;那么新亘结衣什么的就别想了;在本文环境里X是默认单例的,故而整个条件是true。
2、this.allowCircularReferences
;整个全局变量spring 默认为true;当然spring提供了api供程序员修改,这个在本文开头笔者解释过(笔者是通过修改spring源码来改变这个值为false),在没有修改的情况下这里也返回true
3、isSingletonCurrentlyInCreation(beanName);判断当前正在创建的bean是否在正在创建bean的集合当中;还记得前文笔者已经解释过singletonsCurrentlyInCreation这个集合现在里面存在且只有一个x;故而也会返回true;其实这三种情况需要关心的只有第二种;因为第一种是否单例一般都是成立的,因为如果是原型的循环依赖前面代码已经报错了;压根不会执行到这里;第三种情况也一般是成立,因为这个集合是spring操作的,没有提供api给程序员去操作;而正常流程下代码执行到这里,当前正在创建的bean是一定在那个集合里面的;换句话说这三个条件1和3基本恒成立;唯有第二种情况可能会不成立,因为程序员可以通过api来修改第二个条件的结果;
总结:spring的循环依赖,不支持原型,不支持构造方法注入的bean;默认情况下单例bean是支持循环依赖的,但是也支持关闭,关闭的原理就是设置allowCircularReferences=false;spring提供了api来设置这个值;
至此我们知道
boolean earlySingletonExposure=true
,那么代码接着往下执行;判断这个变量;
if
成立,进入分支;//doCreateBean-3 开始分析
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); 这段代码又用了lamda表达式;笔者为了初学者看懂,还是改成传统代码 ObjectFactory<?> singletonFactory = new ObjectFactory<?>(){ public T getObject(){ //至于这个getEarlyBeanReference方法的代码,后面再来说 // 现在可以理解为就是返回 bean getEarlyBeanReference(beanName, mbd, bean); //getEarlyBeanReference 的代码稍微复杂一点,可以简单理解为下面这样 getEarlyBeanReference(beanName, mbd, bean){ return bean; } } } 也就是singletonFactory.getObject();其实就是返回当前正在实例化的bean 改完之后的代码可以理解成这样: addSingletonFactory(beanName,singletonFactory);
addSingletonFactory(beanName,singletonFactory);
顾名思义添加一个单例工厂;其实这里要非常注意,因为大部分资料里面在说到spring循环依赖的时候都说是提前暴露一个半成品bean;笔者觉得这个不严格;甚至算错误了,所谓的提前暴露就是这里的add,但是我们看到源码并不是add一个bean的,而是add一个工厂对象——singletonFactory;两种说法有什么区别呢?区别可大了,简直天壤之别;我们慢慢分析;这里bean和工厂有什么区别呢?在当前的语境下面bean就是x对象经历完spring生命周期之后;所谓的半成品bean,可能还没有经历完整的生命周期;而工厂对象呢?如果你去ObjectFactory的源码或者直接顾名思义他是一个能够产生对象的工厂,或者叫能够产生bean的工厂;换句话说bean是一个产品,而工厂是产生这些产品的公司;如果还不能理解换成天上人间可能好理解——冰火和全套的区别,冰火是全套里面的一个项目,除了冰火还有其他项目;那么spring在这里add的是singletonFactory这个工厂对象(这个工厂可以产生半成品对象),而不是一个半成品对象;相当于这里add的是全套,而不是冰火;将来拿出来的时候是得到工厂,继而通过工厂得到半成品bean;将来拿出来的是全套,你可以在全套里面肆意选择一个项目;不知道我又没有解释清楚这个问题;
当然说了这么多可能你还是没明白为什么需要在这里add这个工厂对象呢?还有add到哪里去呢?
我们首先分析bean工厂对象到底add到哪里去了,查看源码
读者可以好好看看上图,笔者在spring源码当中把注释写上了(注释的信息很重要,认真看看),整个方法其实就是对三个map操作,至于这三个map的意义,参考下图
通过代码可以得知singletonFactory主要被add到二级缓存中;至于为什么要add到这个map?主要了循环依赖,提前暴露这个工厂;当然如果你不理解为什么要提前暴露,没关系往下看,看完文章一定会知道的;保持好习惯照例画个图,让读者知道现在的情况吧
当然这里还是用一幅图来秒杀一下这个三个map的各种情况吧
一级缓存:可能存在很多bean,比如spring各种内置bean,比如你项目里面其他的已经创建好的bean,但是在X的创建过程中,一级缓存中绝对是没有xbean的,也没用y;因为spring创建bean默认的顺序是根据字母顺序的;二级缓存:里面现在仅仅存在一个工厂对象,对应的key为x的beanName,并且这个bean工厂对象的getObect方法能返回现在的这个时候的x(半成品的xbean)
put完成之后,代码接着往下执行;三级缓存:姑且认为里面什么都没有吧
//doCreateBean-4 开始解析
populateBean(beanName, mbd, instanceWrapper);
populateBean这个方法可谓大名鼎鼎,主要就是完成属性注入,也就是大家常常说的自动注入;假设本文环境中的代码运行完这行代码那么则会注入y,而y又引用了x,所以注入进来的y对象,也完成了x的注入;什么意思呢?首先看一下没有执行populateBean之前的情况
没有执行populateBean之前只实例化了X,Y并没实例化,那么Y也不能注入了;接下来看看执行完这行代码之后的情况
populateBean里面的代码以后我更新文章来说明,本文先来猜测一下这个方法里面究竟干了什么事;
x 填充 y (简称 xpy)首先肯定需要获取y,调用getBean(y),getBean的本质上文已经分析过货进入到第一次调用getSingleton,读者可以回顾一下上文我对doGetBean方法名字的解释里说了这个方法是创建bean和获取共用的;第一次getSingleton会从单例池获取一下y,如果y没有存在单例池则开始创建y;
创建y的流程和创建x一模一样,都会走bean的生命周期;比如把y添加到正在创建的bean的集合当中,推断构造方法,实例化y,提前暴露工厂对象(二级缓存里面现在有两个工厂了,分别是x和y)等等。。。。重复x的步骤;
直到y的生命周期走到填充x的时候ypx,第一次调用getSingletion获取x?这里问个问题,能否获取到x呢?
在回答这个问题之前我们先把该画的图贴出来,首先那个正在被创建bean的集合已经不在是只有一个x了;(读者可以对比一下上文的图)
然后我们再把xpy到ypx的流程图贴出来,请读者仔细看看
是否能够获取到x呢?首先我们想如果获取失败则又要创建x—>实例化x—填充属性----获取y--------。。。。。。。就无限循环了;所以结果是完成了循环依赖,那么这里肯定能够获取到x;那么获取到x后流程是怎样呢?
那么为什么能够获取到x呢?讲道理联系上文第一次调用getSingleton是无法获取到x的?因为我们上面说过第一次调用getSingleton是从单例池当中获取一个bean,但是x显然没有完成生命周期(x只走到了填充y,还有很多生命周期没走完),所以应该是获取不到的?为了搞清楚这个原因得去查看第一次getSingleton的源码;如果读者有留意的话笔者前面只是凭只管告诉你第一次getSingleton是从单例池当中获取一个bean,并没有去证明,也就是没有去分析第一次getSingleton的源码;而且我在总结第一次getSingleton的时候用了目前这个词;证据如下(图是本文前面的内容,为了翻阅方便我直接贴这里了)
显然这是笔者前面故意挖的坑,所以各位读者在阅读别人的文章或者书籍的时候一定要小心验证;包括笔者的文章如果有错误一定记得告诉我;下面来开始对第一次getSIngleton源码做深入分析;首先把源码以及我写的注释贴出来,分为图片和源代码,建议大家看图片,可读性好
源码:如果你仔细看了上面的图片可以跳过这里的源码展示protected Object getSingleton(String beanName, boolean allowEarlyReference) { //从单例池当(一级缓存)中直接拿,也就是文章里面'目前'的解释 //这也是为什么getBean("xx")能获取一个初始化好bean的根本代码 Object singletonObject = this.singletonObjects.get(beanName); //如果这个时候是x注入y,创建y,y注入x,获取x的时候那么x不在容器 //第一个singletonObject == null成立 //第二个条件判断是否存在正在创建bean的集合当中,前面我们分析过,成立 //进入if分支 if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { //先从三级缓存那x?为什么先从三级缓存拿?下文解释 singletonObject = this.earlySingletonObjects.get(beanName); //讲道理是拿不到的,因为这三个map现在只有二级缓存中存了一个工厂对象 //回顾一下文章上面的流程讲工厂对象那里,把他存到了二级缓存 //所以三级缓存拿到的singletonObject==null 第一个条件成立 //第二个条件allowEarlyReference=true,这个前文有解释 //就是spring循环依赖的开关,默认为true 进入if分支 if (singletonObject == null && allowEarlyReference) { //从二级缓存中获取一个 singletonFactory,回顾前文,能获取到 //由于这里的beanName=x,故而获取出来的工厂对象,能产生一个x半成品bean ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); //由于获取到了,进入if分支 if (singletonFactory != null) { //调用工厂对象的getObject()方法,产生一个x的半成品bean //怎么产生的?下文解释,比较复杂 singletonObject = singletonFactory.getObject(); //拿到了半成品的xbean之后,把他放到三级缓存;为什么?下文解释 this.earlySingletonObjects.put(beanName, singletonObject); //然后从二级缓存清除掉x的工厂对象;?为什么,下文解释 this.singletonFactories.remove(beanName); } } } }
针对上面的源码我做一个简单的总结:首先spring从单例池当中获取x,前面说过获取不到,然后判断是否在正在创建bean的集合当中,前面分析过这个集合现在存在x,和y;所以if成立进入分支;进入分支spring直接从三级缓存中获取x,根据前面的分析三级缓存当中现在什么都没有,故而返回nll;进入下一个if分支,从二级缓存中获取一个ObjectFactory工厂对象;根据前面分析,二级缓存中存在x,故而可以获取到;跟着调用singletonFactory.getObject();拿到一个半成品的x bean对象;然后把这个x对象放到三级缓存,同时把二级缓存中x清除(此时二级缓存中只存在一个y了,而三级缓存中多了一个x);
问题1、为什么首先是从三级缓存中取呢?主要是为了性能,因为三级缓存中存的是一个x对象,如果能取到则不去二级找了;哪有人会问二级有什么用呢?为什么一开始要存工厂呢?为什么一开始不直接存三级缓存?这里稍微有点复杂,如果直接存到三级缓存,只能存一个对象,假设以前存这个对象的时候这对象的状态为xa,但是我们这里y要注入的x为xc状态,那么则无法满足;但是如果存一个工厂,工厂根据情况产生任意xa或者xb或者xc等等情况;比如说aop的情况下x注入y,y也注入x;而y中注入的x需要加代理(aop),但是加代理的逻辑在注入属性之后,也就是x的生命周期周到注入属性的时候x还不是一个代理对象,那么这个时候把x存起来,然后注入y,获取、创建y,y注入x,获取x;拿出来的x是一个没有代理的对象;但是如果存的是个工厂就不一样;首先把一个能产生x的工厂存起来,然后注入y,注入y的时候获取、创建y,y注入x,获取x,先从三级缓存获取,为null,然后从二级缓存拿到一个工厂,调用工厂的getObject();spring在getObject方法中判断这个时候x被aop配置了故而需要返回一个代理的x出来注入给y。当然有的读者会问你不是前面说过getObject会返回一个当前状态的xbean嘛?我说这个的前提是不去计较getObject的具体源码,因为这块东西比较复杂,需要去了解spring的后置处理器功能,这里先不讨论,总之getObject会根据情况返回一个x,但是这个x是什么状态,spring会自己根据情况返回;
问题2、为什么要从二级缓存remove?因为如果存在比较复杂的循环依赖可以提高性能;比如x,y,z相互循环依赖,那么第一次y注入x的时候从二级缓存通过工厂返回了一个x,放到了三级缓存,而第二次z注入x的时候便不需要再通过工厂去获得x对象了。因为if分支里面首先是访问三级缓存;至于remove则是为了gc吧;
至此循环依赖的内容讲完,有错误欢迎指正,欢迎留言提问;如果觉得笔者写的对你有帮助可以多多点赞转发吧;
-
Spring源码分析(一):从哪里开始看spring源码(系列文章基于Spring5.0)
2019-01-19 01:01:11一、概述 对于大多数第一次看spring源码的人来说,都会感觉不知从哪开始看起,因为spring项目源码由多个子项目组成,如spring-beans,spring-context,spring-core,spring-aop,spring-web,spring-webmvc等,整个...一、概述
- 对于大多数第一次看spring源码的人来说,都会感觉不知从哪开始看起,因为spring项目源码由多个子项目组成,如spring-beans,spring-context,spring-core,spring-aop,spring-web,spring-webmvc等,整个项目结构如图:
- 可能有人会觉得,既然spring是一个IOC容器或者说是一个bean的容器,那么应该从spring-beans看起,先了解spring是如何从xml文件配置获取需要创建的bean的信息,但是这里有个问题就是虽然知道怎么遍历初始化,但是不知道哪里用到或者说哪里让这些初始化开始,而且像BeanFactory,FactoryBean,Environment,PropertySource等接口还是比较抽象的,比较难看懂,所以很容易让人感觉枯燥,然后就放弃了。
- 我们可以换个思路,从能接触到的角度开始,即我们通常会使用spring-mvc来进行web开发,如@Controller,@RequestMapping都是再熟悉不过的了。如果搭过spring-mvc项目都知道,通常需要在web.xml文件中,配置一个ContextLoaderListener,contextConfigLocation,DispatcherServlet,可能很多人都是从网上copy了一份配置过来或者知道contextConfigLocation是指定spring配置文件的位置,DispatcherServlet是接收所有请求的前端控制器,需要指定拦截路由:“/”,从而拦截所有URL中带“/”的请求,但是在spring源码中是怎么使用这些组件的呢?以及怎么配置了一个@Controller,@RequestMapping中指定了一个url,就可以访问了呢?还有就是通常我们的web项目都会部署在web容器,如tomcat当中,那么tomcat和spring有啥关系呢?所以我们可以带着这些问题去查看spring源码找到答案。
- 所以我推荐是从spring-mvc开始看spring源码,因为这个是我们使用得比较多,比较容易理解的一个模块,然后一层一层往上剥,找到与spring-context,spring-beans,spring-aop等的关系。如果真的对JavaWeb开发,Java EE很感兴趣,或者更容易读懂spring的源码,可以先看servlet规范和Tomcat的设计与Tomcat的请求处理工作流。我目前也在结合这两个方面看,也可以看下我的Tomcat源码分析系列。
二、Servlet规范
- 在servlet的规范当中,servlet容器或者叫web容器,如tomcat,中运行的每个应用都由一个ServletContext表示,在web容器中可以包含多个ServletContext,即可以有多个web应用在web容器中运行。如在tomcat的webapp目录下,每个war包都对应一个web应用,tomcat启动时会解压war包,并启动相关的应用。
- 在web容器启动的时候,会初始化web应用,即创建ServletContext对象,加载解析web.xml文件,获取该应用的Filters,Listener,Servlet等组件的配置并创建对象实例,作为ServletContext的属性,保存在ServletContext当中。之后web容器接收到客户端请求时,则会根据请求信息,匹配到处理这个请求的Servlet,同时在交给servlet处理之前,会先使用应用配置的Filters对这个请求先进行过滤,最后才交给servlet处理。
- 了解web容器启动,之后接受客户端请求这些知识有啥用处呢?这里我们需要回过头来看我们的spring项目。我们在日常开发中,直接接触的是spring相关的组件,然后打成war包,放到web容器中,如拷贝到tomcat的webapp目录,并不会直接和web容器打交道。经过以上的分析,其实一个spring项目就是对应web容器里的一个ServletContext,所以在ServletContext对象的创建和初始化的时候,就需要一种机制来触发spring相关组件的创建和初始化,如包含@Controller和@RequestMapping注解的类和方法,这样才能处理请求。
三、Listener监听器机制:ContextLoaderListener
- servlet规范当中,使用了Listener监听器机制来进行web容器相关组件的生命周期管理以及Event事件监听器来实现组件之间的交互。
- 其中一个重要的生命周期监听器是ServletContextListener。web容器在创建和初始化ServletContext的时候,会产生一个ServletContextEvent事件,其中ServletContextEvent包含该ServletContext的引用。然后交给在web.xml中配置的,注册到这个ServletContext的监听器ServletContextListener。ServletContextListener在其contextInitialized方法中定义处理逻辑,接口定义如下:
从contextInitialized的注释可知:通知所有的ServletContextListeners,当前的web应用正在启动,而且这些ServletContextListeners是在Filters和Servlets创建之前接收到通知的。所以在这个时候,web应用还不能接收请求,故可以在这里完成底层处理请求的组件的加载,这样等之后接收请求的Filters和Servlets创建时,则可以使用这些创建好的组件了。spring相关的bean就是这里所说的底层处理请求的组件,如数据库连接池,数据库事务管理器等。/** * Implementations of this interface receive notifications about changes to the * servlet context of the web application they are part of. To receive * notification events, the implementation class must be configured in the * deployment descriptor for the web application. * * @see ServletContextEvent * @since v 2.3 */ public interface ServletContextListener extends EventListener { /** ** Notification that the web application initialization process is starting. * All ServletContextListeners are notified of context initialization before * any filter or servlet in the web application is initialized. * @param sce Information about the ServletContext that was initialized */ public void contextInitialized(ServletContextEvent sce); /** ** Notification that the servlet context is about to be shut down. All * servlets and filters have been destroy()ed before any * ServletContextListeners are notified of context destruction. * @param sce Information about the ServletContext that was destroyed */ public void contextDestroyed(ServletContextEvent sce); }
- ContextLoaderListener:spring-web包的ContextLoaderListener就是一个ServletContextListener的实现类。ContextLoaderListener主要用来获取spring项目的整体配置信息,并创建对应的WebApplicationContext来保存bean的信息,以及创建这些bean的对象实例。默认去WEB-INF下加载applicationContext.xml配置,如果applicationContext.xml放在其他位置,或者使用其他不同的名称,或者使用多个xml文件,则与指定contextConfigLocation。具体spring源码的实现过程后续文章详细分析。
<listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- 修改配置文件路径 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/applicationContext.xml</param-value> </context-param>
四、DispatcherServlet:前端控制器
- 在web容器中,web.xml中的加载顺序:context-param -> listener -> filter -> servlet。其中ContextLoaderListener是属于listener阶段。我们通常需要在项目的web.xml中配置一个DispatcherServlet,并配置拦截包含“/”路径的请求,即拦截所有请求。这样在web容器启动应用时,在servlet阶段会创建这个servlet,由Servlet规范中servlet的生命周期方法可知:
web容器在创建这个servlet的时候,会调用其init方法,故可以在DispatcherServlet的init方法中定义初始化逻辑,核心实现了创建DispatcherServlet自身的一个WebApplicationContext,注意在spring中每个servlet可以包含一个独立的WebApplicationContext来维护自身的组件,而上面通过ContextLoaderListener创建的WebApplicationContext为共有的,通常也是最顶层,即root WebApplicationContext,servlet的WebApplicationContext可以通过setParent方法设值到自身的一个属性。DispatcherServlet默认是加载WEB-INF下面的“servletName”-servlet.xml,来获取配置信息的,也可以与ContextLoaderListener一样通过contextLoaderConfig来指定位置。DispatcherServlet具体的源码设计在之后文章详细分析。public interface Servlet { void init(ServletConfig var1) throws ServletException; ServletConfig getServletConfig(); void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; String getServletInfo(); void destroy(); }
五、总结
- 从上面的分析,可知spring相关配置解析和组件创建其实是在web容器中,启动一个web应用的时候,即在其ServletContext组件创建的时候,首先解析web.xml获取该应用配置的listeners列表和servlet列表,然后保存在自身的一个属性中,然后通过分发生命周期事件ServletContextEvent给这些listeners,从而在listeners感知到应用在启动了,然后自定义自身的处理逻辑,如spring的ContextLoaderListener就是解析spring的配置文件并创建相关的bean,这样其实也是实现了一种代码的解耦;其次是创建配置的servlet列表,调用servlet的init方法,这样servlet可以自定义初始化逻辑,DispatcherServlet就是其中一个servlet。
- 所以在ContextLoaderListener和DispatcherServlet的创建时,都会进行WebApplicationContext的创建,这里其实就是IOC容器的创建了,即会交给spring-context,spring-beans包相关的类进行处理了,故可以从这里作为一个入口,一层一层地剥spring的源码了。
- 对于大多数第一次看spring源码的人来说,都会感觉不知从哪开始看起,因为spring项目源码由多个子项目组成,如spring-beans,spring-context,spring-core,spring-aop,spring-web,spring-webmvc等,整个项目结构如图:
-
Spring源码——IDEA读Spring源码环境搭建
2018-07-23 17:45:50IDEA读Spring源码环境搭建 I. Spring介绍 还有什么好介绍的,做 Java 的都知道。至于为什么读源码,还有什么好说的,还不是被逼的! II. Gradle安装 下载:https://gradle.org/install/ Windows平台下,...前言
最新在B站发现一个视频教学如何搭建Spring5源码阅读环境,特意添加在此:https://www.bilibili.com/video/av61188907
视频中的笔记链接:http://note.youdao.com/ynoteshare1/index.html?id=c3f11aab5e0a0083709cc64984a3c41a&type=noteI. Spring介绍
还有什么好介绍的,做 Java 的都知道。至于为什么读源码,还有什么好说的,还不是被逼的!
II. Gradle安装
下载:https://gradle.org/install/
Windows平台下,需要配置gradle的环境变量。
-
新增
GRADLE_HOME
环境变量,指向Gradle解压目录 -
配置Path环境变量:新增
%GRADLE_HOME%\bin
III. 下载Spring源码
可以利用git下载源码或者下载源码包。
- github: https://github.com/spring-projects/spring-framework
- 找了一个4.2的注释版: https://github.com/wanwanpp/spring-framework-4.2.0
- 源码包:https://github.com/spring-projects/spring-framework/releases
IV. Spring源码编译
进入
spring-framework
文件夹下,打开cmd,输入gradlew :spring-oxm:compileTestJava
进行编译。中间可能会各种报错,可能都是网络原因,多重试几次就好了。至于需不需要科学上网,反正我是校园网没有翻墙报错多试几次直接最后就成功了。
V. IDEA导入源码
打开IDEA,File->New->Project From Existing Sources…,选中spring-framework源码文件夹,点击OK,选择Import project from external model,选中Gradle,点击Next。
点击Finish之前,可以修改一些默认的配置。
等待IDEA构建项目完成即可。ps:等的有点久….
VI. 新建测试Module
对于IDEA的project和module概念,在此就不赘述了。在此贴个链接:戳戳戳
在Project Structure中需要将spring-aspectj这个module除去,因为build报错。我在build的时候还有context下的money啥的也报错,同样exclude掉了。
spring-aspects does not compile due to references to aspect types unknown to IntelliJ IDEA. See http://youtrack.jetbrains.com/issue/IDEA-64446 for details. In the meantime, the ‘spring-aspects’ can be excluded from the project to avoid compilation errors.
为了方便测试spring源码,这里我们也在spring-framework下单独再创建一个module,专门用于编写我们自己的代码。已有的工程结构是名为spring的project下包含各种module,如图所示。所以我们新建的module也选在spring的project下,如图中的
:mytest
的module。File–>New–>Module…–>Gradle–>Java–>Next会出现如下界面,填写module名后点击next后点击Finish即可。
创建完成后,在
mytest
的gradle配置文件中添加compile(project(":spring-beans"))
依赖,便于下面进行容器BeanFactory
的使用。点击build project可能会遇到的错误:
Error:Kotlin: [Internal Error] java.lang.IllegalStateException: The provided plugin org.jetbrains.kotlin.scripting.compiler.plugin.ScriptingCompilerConfigurationComponentRegistrar is not compatible with this version of compiler at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment.<init>(KotlinCoreEnvironment.kt:181) at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment.<init>(KotlinCoreEnvironment.kt:117) at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment$Companion.createForProduction(KotlinCoreEnvironment.kt:413) at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.createCoreEnvironment(K2JVMCompiler.kt:276) at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:154) at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:63) at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.java:108) at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.java:52) at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:92) at org.jetbrains.kotlin.daemon.CompileServiceImpl$compile$$inlined$ifAlive$lambda$1.invoke(CompileServiceImpl.kt:389) at org.jetbrains.kotlin.daemon.CompileServiceImpl$compile$$inlined$ifAlive$lambda$1.invoke(CompileServiceImpl.kt:97) at org.jetbrains.kotlin.daemon.CompileServiceImpl$doCompile$$inlined$ifAlive$lambda$2.invoke(CompileServiceImpl.kt:909) at org.jetbrains.kotlin.daemon.CompileServiceImpl$doCompile$$inlined$ifAlive$lambda$2.invoke(CompileServiceImpl.kt:97) at org.jetbrains.kotlin.daemon.common.DummyProfiler.withMeasure(PerfUtils.kt:137) at org.jetbrains.kotlin.daemon.CompileServiceImpl.checkedCompile(CompileServiceImpl.kt:939) at org.jetbrains.kotlin.daemon.CompileServiceImpl.doCompile(CompileServiceImpl.kt:908) at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:387) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:324) at sun.rmi.transport.Transport$1.run(Transport.java:200) at sun.rmi.transport.Transport$1.run(Transport.java:197) at java.security.AccessController.doPrivileged(Native Method) at sun.rmi.transport.Transport.serviceCall(Transport.java:196) at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:568) at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:826) at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:683) at java.security.AccessController.doPrivileged(Native Method) at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:682) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) Caused by: java.lang.AbstractMethodError: org.jetbrains.kotlin.scripting.compiler.plugin.ScriptingCompilerConfigurationComponentRegistrar.registerProjectComponents(Lcom/intellij/mock/MockProject;Lorg/jetbrains/kotlin/config/CompilerConfiguration;)V at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment.<init>(KotlinCoreEnvironment.kt:179) ... 33 more
这个主要是Kotlin插件的版本问题,解决方案是更新新版本的Kotlin插件。贴个链接:IntelliJ IDEA手动更新Kotlin.
VII. 添加日志输出
Spring5使用了log4j2,想要输出日志信息,我们需要在自己定义的模块下配置配置文件。在 src/main/resources 文件夹下新建 log4j2.xml 文件,在 src/test/resources 目录下创建 log4j2-test.xml 文件。文件配置内容为:
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="DEBUG"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" /> </Console> </Appenders> <Loggers> <Logger name="org.springframework" level="DEBUG" /> <Root level="DEBUG"> <AppenderRef ref="Console" /> </Root> </Loggers> </Configuration>
-
-
spring源码系列(六)——番外篇如何编译spring的源码
2020-07-12 20:24:36学习spring源码的第一步便是需要去编译源码;很多网友在编译spring源码的时候遇到了困难继而放弃了spring源码的学习; 这篇文章主要来阐述如何正确的编译spring源码;首先你得检查你的网线能正常上网;最好网速还行...正文之前说一下我在CSDN有一门收费课程,需要的可以点击购买
https://edu.csdn.net/course/detail/32499学习spring源码的第一步便是需要去编译源码;很多网友在编译spring源码的时候遇到了困难继而放弃了spring源码的学习;
这篇文章主要来阐述如何正确的编译spring源码;首先你得检查你的网线能正常上网;最好网速还行;不然你会疯掉下载spring源码
下载spring源码可以选择github或者码云(国内的);下载方式可以选择git clone 或者直接下载一个zip包;笔者推荐使用git clone这种方式;我在本地的
d:\workspace
下面clone了spring5.1.x
的源码(写文章的时候spring已经有了5.2了,版本你自己选择)阅读spring的编译文档
如何编译spring的源码其实spring提供了详细的文档;离线版和在线版都有(离线版在我们下过来的源码当中的README.md);这里我们选择在线版作为参考吧;在github上
spring-framework
这个项目的底下有一个build from source
的章节点击这个链接之后就是构建编译spring源码的文档
编译文档地址 https://github.com/spring-projects/spring-framework/wiki/Build-from-Source本文中我会参考这个文档来重新编译一个
spring5.1.x
的源码;按照文档说的首先你得具备一个jdk8以上的环境;笔者使用的是jdk11;然后通过git clond一份源码下来;这个事情我们已经做完了;继而到项目的根目录运行./gradlew build
但是文档上是linux命令,如果是windows系统其实我可以看到项目的根目录下面有个gradlew.bat
的文件;可以直接运行这个文件(不建议双击直接运行,因为如果有错误你看不到,建议通过cmd去运行这样就能看到他提示的错误);其实我们不着急去运行这个文件;运行这个命令spring会首先去下载一个gradle到你本地,然后通过gradle去编译spring源码;为什么运行这个命令会自动下载gradle呢?从哪下载呢?版本是什么呢?
首先看看这个命令的是否存在根目录
上述gradlew.bat就是等下我们需要运行的命令;但是先不着急运行;因为这个命令回去读取一个文件;在这个文件当中配置了gradle的版本和下载地址;一旦运行变化自动下载;有时候会下载不过来;而且有时候可能需要翻墙等等,就算下载过来了目录在哪里也是他自动指定的,下载过来后解压目录等等都是在那个文件里面配置了;索性我们自己下载过来然后修改这个配置文件;
0、配置文件的位置——D:\workspace\spring-framework\gradle\wrapper\gradle-wrapper.properties
1、可以修改这个下载地址——默认是一个http的url;如果你自己下载过来后可以修改为自己的本地目录
2、下载后的存放目录、以及解压目录都可以修改——但是不建议修改可以使用默认的distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=file\:///d\:/tools/java/gradle-6.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists
distributionBase 下载之后的目录(从distributionUrl下载过来是一个zip文件,存放的目录)
GRADLE_USER_HOME 如果是windows系统默认问c:\Users\你的用户.gradle
distributionPath 会自动拼接 distributionBase
zipStoreBase 是解压目录上述配置的大概意思就是如果我们运行gradlew.bat 会自动去 file:///d:/tools/java/gradle-6.5.1-bin.zip这个目录下载一个gradle到
C:\Users\java_\.gradle\wrapper\dists
然后解压到C:\Users\java_\.gradle\wrapper\dists
;但是实际目录不是这个,这个命令还会自己生成一些目录;下图是笔者电脑实际gradle的解压目录但是关于gradle-6.5.1-bin.zip这个文件哪里能下载,可以自己百度;当然也可以从笔者的网盘上下载
链接:https://pan.baidu.com/s/1CyfaZvBGBczJ8QSj66JFKA 提取码:5qvd改完这个配置也不要急着运行命令编译;因为当自动下载完gradle之后会通过gradle去编译spring源码;这里就涉及到一个gradle的中央仓库的问题了,编译过程中会下载大量的依赖;如果你采用默认的依赖那么这个编译过程一定特别漫长;而且会失败;于是我们需要修改中央仓库为国内的阿里云;这样就会快很多;如何修改呢?
找到项目根目录的
build.gradle
文件,打开并且编辑添加阿里云的仓库repositories { mavenCentral() maven { url "https://repo.spring.io/libs-spring-framework-build" } maven { url "https://repo.spring.io/snapshot" } // Reactor maven {url 'https://maven.aliyun.com/nexus/content/groups/public/'} //阿里云 maven {url 'https://maven.aliyun.com/nexus/content/repositories/jcenter'} }
至此万事具备只欠东风了,cmd到项目根目录运行
gradlew.bat
(当然这些优化是笔者自己总结出来的,如果你的网速够快你完全可以不用优化,直接运行那个命令);这个过程会相当漫长取决于你的网速;我再截几张笔者编译过程中的图
这个结果可能会最后显示失败;如果显示失败就再运行一遍gradlew.bat直到他编译成功;下图是显示编译成功的结果
编译成功后就可以导入idea了(一共用了36分钟;差不多一个子路的时间);至于文档上说的publishToMavenLocal这些操作可以不用;如何导入idea他也提供了文档
导入idea的文档地址:https://github.com/spring-projects/spring-framework/blob/master/import-into-idea.md根据文档的提示在导入idea之前需要去
Precompile spring-oxm
也就是预编译 oxm这个项目;运行gradlew :spring-oxm:compileTestJava
这个预编译很快笔者的破电脑38s便编译完了;导入idea
导入之前你随便打开一个项目,然后在该项目当中对idea进行一些设置(当然如果你知道如何去对idea进行全局设置可以不必要打开一个项目)
这个设置尤为重要特别是第二个设置一定要指定前面我们自动下载过来解压的gradle;不然又会下载一遍jar;设置完成之后开始把源码导入到idea
选择项目的目录;继而选择根目录当中的build.gradle文件导入
导入idea之后会开始建立索引这个过程也是很漫长,你只能等,大概10-20分钟吧;看电脑性能
正确构建之后如下图
接下来要对idea进行设置,不然每次idea运行都会通过gradle去编译运行——gradle运行编译特别慢;需要改成idea自己编译运行再次说明一下这里不是一定要改,但是如果你不改用默认的则会特别慢,改成idea快的不止一点点
改完之后便可以建一个子model来测试了,但是一定得建gradle的项目,因为spring源码这个父项目就是用gradle来开发的
建好项目之后再gradle的配置文件中添加spring的依赖——相当于你建了一个maven项目,在pom文件中添加spring的依赖;
compile(project(":spring-context"))
然后开始开始完善项目的其他类配置类的代码
@Configuration @ComponentScan("com.shadow") public class AppConfig { //扫描com.shadow包下面的所有bean }
service的代码
@Component public class IndexService { }
测试类的代码
public class ShadowTest { public static void main(String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); System.out.println(ac.getBean(IndexService.class)); } }
右键运行——可能你会出现一些错误;比如博主这里就出现了某些类找不到的错误
遇到这种问题是因为这个类所在的项目没有编译这个类——说白了就是没有产生class文件;比如上图那个错误便是没有找到
InstrumentationSavingAgent
这个类,那么你首先找到这个类所在的项目(InstrumentationSavingAgent类所在的项目为spring-instrument);再看看out文件下面是不是有这个class;比如博主这里连out文件夹没有——build文件下下面是gradle产生的,我们上面已经把编译方式改成了idea,idea默认编译文件为out——说白了就是这个build可以直接删除;out文件夹都没有表示这个项目当中的java类idea压根没有编译;我们可以运行这个项目下面的测试类test让idea去帮我们编译这些java类上图可以看到我已经吧build文件删除了;找到test文件夹下面的java文件夹右键 run all tests
遇到其他问题也类似这种办法解决,最后再次运行我们自己的main方法;
一些正常;spring5.1.x的源码编译导入idea完成最近比较忙二宝刚出生;看完如果觉得对你有帮助点赞评论都是支持博主更新的动力,我会尽快出下一篇spring的源码的文章
最后大家B搜索一下**“边境线java”** 这是我个人的b站账号,我会在上面一直更新一些java技术
-
spring源码系列(三)——beanDefinition(1)
2019-10-18 22:32:07如果想系统的学习spring源码那么第一个需要搞明白的知识便是spring当中的BeanDefinition——spring bean的建模对象; 那么什么是spring bean的建模对象呢?一言概之就是把一个bean实例化出来的模型对象?有人会问把... -
IDEA阅读spring源码并调试
2018-06-15 16:39:42目标:搭建起Spring源码阅读和代码调试跟踪的环境,顺便建立一个简单的Demo,能够调试Spring的源代码 本节,主要介绍一下Spring源码阅读和调试的相关环境搭建,并使用MVN创建一个非常简单的Demo,以便可以跟踪和... -
IDEA导入Spring源码环境搭建
2018-05-17 21:39:521,Spring源码包 下载地址:https://github.com/spring-projects/spring-framework 2,gradle工具 下载地址:http://downloads.gradle.org/distributions/gradle-4.6-bin.zip 3,IDEA工具(这是废话)... -
深入Spring源码系列(一)——导入Spring源码包
2019-01-05 11:17:36要学习Spring源码,导入Spring源码到IDE是必不可少的一步,因为Spring源码各个包、各个类之间的各种关联关系非常复杂。如果仅仅是通过Spring源码文档来看,相信没多少人能坚持学下去。因此将Spring源码包导入IDE是... -
spring源码阅读环境(几分钟下载包)
2019-11-09 17:42:26下载spring源码:2.安装gradle:3.构建源码:导入idea: 简述: 不能忍受慢(看到这里你就准备笑吧!!) 其他大神的博客实在让我难受 自己动手,下载spring,编译spring源码,导入idea,开启学习模式!! 笔者环境:... -
深入Spring源码系列(补充篇)——程序调用Spring源码
2019-06-26 10:13:52之前下载好了Spring源码之后,并成功导入到IDEA中了,可是光导入源码但是没有调用Spring源码就显得特别的菜了,且不利于深入学习Spring源码。本人花了点时间来尝试使用IDEA程序调用Spring源码,遂写下这篇文章来记录... -
spring源码01: spring源码编译 环境搭建
2020-03-14 10:24:26下文介绍spring源码在idea环境下的搭建,目的是为了更好的阅读和调试代码,已搭建完的小火伴可跳过本节,直接开启被锤之路… #备选方案 为什么把备选方案写在前面呢,因为单纯的看源码调试,直接用IDEA新建Spring... -
spring源码构建
2020-12-15 20:46:17spring源码构建 spring源码地址:https://github.com/spring-projects/spring-framework.git。 由于github下载速度较慢,可以先将spring的源码导入到gitee,然后再从gitee进行下载。 gitee导入github仓库的地址:...