问题
今天小伙伴跑过来说,搭建框架的时候出现disconf配置好的信息不能够及时注入到实体类中的情况。他通过实践发现,spring 加载Configuration 的时候,通过@Autowired注入的RedisProperties 实体类里面没有值。等到容器加载完成后,在Controller 层注入的RedisProperties是有数据的,搞了接近一天。我在他控制台看到了如下信息(简化):
**** DISCONF START FIRST SCAN **** //此处省略 **** DISCONF END FIRST SCAN **** //@configuration 注册bean的信息(可以自己添加日志) **** DISCONF START SECOND SCAN **** //此处省略 **** DISCONF END SECOND SCAN ****
通过信息可以看出,关键问题出现在了第二次扫描在Bean注册之后。第二次扫描负责将配置注入实体类中,详细可以参考disconf-client设计
那么第二次扫描在什么时候进行的呢,打开DisconfMgrBeanSecond 类
public class DisconfMgrBeanSecond{
public void init(){
DisconfMgr.getInstance().secondScan(); //此处进行第二次扫描
}
public void destroy(){
DisconfMgr.getInstance().close();
}
}
复制代码
现在的问题一下明了了,我们需要做的也就是将 DisconfMgrBeanSecond 的Bean注册提前,提前至@Configuration之前。我这里用的是@DependsOn注解,将其放在Properties实体类上。表明当前Bean依赖于另外一个Bean,可以用来控制顺序。
思考
上面的方法只是使用技巧解决了实际问题,我们不禁要思考了,spring加载的顺序到底是怎么样的?为什么有的项目没有加载顺序问题,有的就会出bug。接下来我们就来深入撸一下spring的源码。(本文基于的源码为 spring boot 2.0.0.RELEASE)
调试方法
很多人不太会调试源码,一上手就从入口函数开始,点几下就自己犯晕了。还有些人习惯看类图,从全局去看,也会很累。这里不是说类图方式不好,而是分情况而定。比如你读 Java 集合框架,类图就是一个不错的选择,一来集合类功能相对独立,二来集合本身很符合面向对象的思想。面对spring这种名字很相似,代码庞大的大型框架时,建议还是以点入面,有目的的去看。这里介绍一下我自己使用的方法:
- 编写测试工程,比如我要理解spring @Configuration的加载过程,先用spring boot 快速搭建一个可以运行的工程
- 在自己需要了解的地方打断点
- 观察调用栈,找到关键方法
如下图
Debugger 菜单栏中我们很容易找到调用栈的信息,观察这些方法,我们可以看到这三个方法的方法名很像我们想知道的加载过程
在仔细点开源码会发现 refresh()方法下的如下代码
this.postProcessBeanFactory(beanFactory); //上下文子类对beanFactory进行后置处理
this.invokeBeanFactoryPostProcessors(beanFactory);//调用工厂处理器,对bean进行注册
this.registerBeanPostProcessors(beanFactory); // 注册bean的拦截处理器
this.initMessageSource(); //初始化消息源
this.initApplicationEventMulticaster(); //初始化上下文事件多播器
this.onRefresh(); //初始化其他子类上下文的特殊beans
this.registerListeners(); //检查监听类的bean,并注册他们
this.finishBeanFactoryInitialization(beanFactory); //实例化剩余非懒加载的bean单利
this.finishRefresh(); //完成后刷新,发布相应的事件
复制代码
如果你通过idea把源码下载下来的话,可以看到光标停在 this.finishBeanFactoryInitialization(beanFactory)处,表明此时具体进入的方法。好了,调试方法暂时就说到这里,还是来看源码吧。
源码分析
上面提了一下@Configuration注解的bean 入口在finishBeanFactoryInitialization(beanFactory)方法中,接着往下走到preInstantiateSingletons()方法中
我们发现这个方法里有一个特别显眼的属性,beanDefinitionNames,这个就是容器的注册顺序。
我们端点是打在了Test类初始化的地方,但通过debugger 可以发现入口方法加载的反而是TestController类,并且中间方法的调用并没有出现HelloServiceimpl类和TestServiceImpl类的加载。可见真实bean初始化的顺序并不是这样的。
回头去找 beanDefinitionNames在哪里初始化的,可以发现在registerBeanDefinition(String beanName, BeanDefinition beanDefinition)方法中,循环添加的,接下来再去找registerBeanDefinition 在什么地方调用。
再次打断点定位到 ClassPathBeanDefinitionScanner.doscan() 方法上
protected Set doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
//扫描package,寻找候选组件
Set candidates = findCandidateComponents(basePackage);
//候选组件进行处理,处理其他注解
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}
复制代码
首先通过扫描找出候选组件,扫描的范围包含basePackages目录下的所有class文件,如果符合条件,将其放在LinkedHashSet中,使其保证唯一有序。判断条件在ClassPathScanningCandidateComponentProvider.isCandidateComponent()方法中。这个类有两个属性,excludeFilters和includeFilters,分别控制着候选类的排除链和包含链。我debugger不进行设置的话,默认选取下面三种接口子类作为候选加载类,org.springframework.stereotype.Component,javax.annotation.ManagedBean,javax.inject.Named,而@Configuration,@Controller,@Service,@Repository,都是基于Component的注解。
真实bean的加载
上面只是说明白了类文件的注册顺序,他是通过扫描包名,类名这样排下来的,只是一个初步顺序。
先来看一下之前调试的初步顺序 testConfig-->helloController-->testController-->helloServiceImpl-->testServiceImpl-->test
整体看下来,他是按照包名和类型排序的,只不过有一点需要注意 test 所在的包实际上是在Impl 前面的,且Test类上没有任何注解,这表明他们的注册顺序其实是:先扫描Component,在扫描@Bean注解。
当bean真正加载的时候是这样加载的,每加载一个类,看他有没有依赖,有的话同时加载依赖bean。这也就解释了为什么testController为什么跳过impl 直接加载test。
如何控制加载顺序
其实有很多方法控制顺序,依赖注入提前,@DepensOn 和 @Order注解,实现Ordered接口等等。像面对disconf这种第三方框架类的bean,最好是使用@DepensOn 来控制加载顺序
总结
bean的加载还有很多其他的细节,这里就不一一展开了。本文主要专注加载顺序,顺便聊一下初学如何去看源码。总结起来就是一句话,小目标,不拓展。
写到最后才发现上面的问题,加载顺序并不是主要原因!!(°ロ°٥) 好吧,下次一定搞清楚了再动笔,这里也买一个关子,感兴趣的童鞋可以自己Debugger找一下原因。这里给个小提示,是跟代理有关。