美文网首页
disconf问题引发对spring boot配置加载的探究

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

作者: zeody | 来源:发表于2019-05-07 18:11 被阅读0次

    问题

    今天小伙伴跑过来说,搭建框架的时候出现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. 观察调用栈,找到关键方法

    如下图

    @Configuration加载调用栈

    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

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

    beanDefinitionNames顺序

    我们端点是打在了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找一下原因。这里给个小提示,是跟代理有关。

    相关文章

      网友评论

          本文标题:disconf问题引发对spring boot配置加载的探究

          本文链接:https://www.haomeiwen.com/subject/yxvyoqtx.html