资讯专栏INFORMATION COLUMN

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

W4n9Hu1 / 2940人阅读

摘要:问题今天小伙伴跑过来说,搭建框架的时候出现配置好的信息不能够及时注入到实体类中的情况。他通过实践发现,加载的时候,通过注入的实体类里面没有值。等到容器加载完成后,在层注入的是有数据的,搞了接近一天。表明当前依赖于另外一个,可以用来控制顺序。

问题

今天小伙伴跑过来说,搭建框架的时候出现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找一下原因。这里给个小提示,是跟代理有关。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/74477.html

相关文章

  • Spring-Boot自定义Starter实践

    摘要:此文已由作者王慎为授权网易云社区发布。欢迎访问网易云社区,了解更多网易技术产品运营经验。网易云免费体验馆,成本体验款云产品更多网易技术产品运营经验分享请点击。文章来源网易云社区 此文已由作者王慎为授权网易云社区发布。 欢迎访问网易云社区,了解更多网易技术产品运营经验。 disconf-spring-boot-starter使用方法:引入maven依赖: com.netease.hai...

    goji 评论0 收藏0
  • 源码解读 Spring Boot Profiles

    摘要:有了配置文件之后,启动程序,我们首先可以看到日志输入,由此可以看出程序读取了的配置。首先,根据的全局查找功能,直接搜索这些词出现的位置,进行定位,可以找到这个日志出现于方法之中。由于我们的配置文件在下,所以只要留意当为的程序执行情况即可。 前言 上文《一文掌握 Spring Boot Profiles》 是对 Spring Boot Profiles 的介绍和使用,因此本文将从源码角度...

    Dionysus_go 评论0 收藏0
  • 从零到百亿互联网金融架构发展史

    摘要:总体介绍在互联网金融行业一百多亿其实也算不上大平台,也就是二级阵营吧,其实每次的架构升级都是随着业务重大推进而伴随的,在前一代系统架构上遇到的问题,业务开发过程中积累一些优秀的开发案例,在下一代系统开发中就会大力推进架构升级。 回想起从公司成立敲出的第一行代码算起到现在也快三年了,平台的技术架构,技术体系也算是经历了四次比较重大的升级转化(目前第四代架构体系正在进行中),临近年底也想抽...

    mrcode 评论0 收藏0
  • 从零到百亿互联网金融架构发展史

    摘要:总体介绍在互联网金融行业一百多亿其实也算不上大平台,也就是二级阵营吧,其实每次的架构升级都是随着业务重大推进而伴随的,在前一代系统架构上遇到的问题,业务开发过程中积累一些优秀的开发案例,在下一代系统开发中就会大力推进架构升级。 回想起从公司成立敲出的第一行代码算起到现在也快三年了,平台的技术架构,技术体系也算是经历了四次比较重大的升级转化(目前第四代架构体系正在进行中),临近年底也想抽...

    U2FsdGVkX1x 评论0 收藏0
  • 如何优雅关闭 Spring Boot 应用

    摘要:除了,还有十余种,有的是特定操作,比如转储内存日志有的是信息展示,比如显示应用健康状态。 showImg(http://ww1.sinaimg.cn/large/006tNc79gy1g5qb2coyfoj30u00k0tan.jpg); 前言 随着线上应用逐步采用 SpringBoot 构建,SpringBoot应用实例越来多,当线上某个应用需要升级部署时,常常简单粗暴地使用 kil...

    xiyang 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<