前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >disconf问题引发对spring boot配置加载的探究

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

原创
作者头像
zeody
修改2019-05-07 18:11:30
9910
修改2019-05-07 18:11:30
举报
文章被收录于专栏:Java杂货店Java杂货店

问题

今天小伙伴跑过来说,搭建框架的时候出现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 类

代码语言:java
复制
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加载调用栈
@Configuration加载调用栈

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

寻找相对靠后的入口方法
寻找相对靠后的入口方法

在仔细点开源码会发现 refresh()方法下的如下代码

代码语言:java
复制
                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,这个就是容器的注册顺序。

beanDefinitionNames顺序
beanDefinitionNames顺序

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

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

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

代码语言:java
复制
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找一下原因。这里给个小提示,是跟代理有关。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题
  • 思考
    • 调试方法
      • 源码分析
        • 真实bean的加载
          • 如何控制加载顺序
            • 总结
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档