disconf问题引发对spring boot 配置加载的探究

问题

今天小伙伴跑过来说,搭建框架的时候出现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这种名字很相似,代码庞大的大型框架时,建议还是以点入面,有目的的去看。这里介绍一下我自己使用的方法:

  1. 编写测试工程,比如我要理解spring @Configuration的加载过程,先用spring boot 快速搭建一个可以运行的工程
  2. 在自己需要了解的地方打断点
  3. 观察调用栈,找到关键方法

如下图

disconf问题引发对spring boot 配置加载的探究

Debugger 菜单栏中我们很容易找到调用栈的信息,观察这些方法,我们可以看到这三个方法的方法名很像我们想知道的加载过程

disconf问题引发对spring boot 配置加载的探究

在仔细点开源码会发现 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()方法中

disconf问题引发对spring boot 配置加载的探究

我们发现这个方法里有一个特别显眼的属性,beanDefinitionNames,这个就是容器的注册顺序。

disconf问题引发对spring boot 配置加载的探究

我们端点是打在了Test类初始化的地方,但通过debugger 可以发现入口方法加载的反而是TestController类,并且中间方法的调用并没有出现HelloServiceimpl类和TestServiceImpl类的加载。可见真实bean初始化的顺序并不是这样的。

回头去找 beanDefinitionNames在哪里初始化的,可以发现在registerBeanDefinition(String beanName, BeanDefinition beanDefinition)方法中,循环添加的,接下来再去找registerBeanDefinition 在什么地方调用。

再次打断点定位到 ClassPathBeanDefinitionScanner.doscan() 方法上

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Assert.notEmpty(basePackages, "At least one base package must be specified");
        Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
        for (String basePackage : basePackages) {
            //扫描package,寻找候选组件
            Set<BeanDefinition> 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找一下原因。这里给个小提示,是跟代理有关。