前言
最近项目在整合shiro权限认证模块时,给自己挖了一个深坑,也是分析了好久才定位到问题的所在,根本原因还是对spring相关的技术点掌握的不够娴熟。本文基于springboot 2.1.5进行分析。下面会用简单的Demo去还原问题的场景。
示例
简单将遇到的问题还原一下,这段代码中ShiroProperties 始终注入不到TestController中去。也就是shiroProperties
始终是null。
@ConfigurationProperties(prefix = "test")
@Data
public class ShiroProperties {
private String name;
private List<String> chain;
}
@Configuration
@EnableConfigurationProperties(ShiroProperties.class)
public class ShiroConfiguration {
}
@Controller
public class TestController {
@Autowired
private ShiroProperties shiroProperties;
@Bean
public Test test(){
System.out.println(shiroProperties);
return new Test();
}
@Bean
private TestProcessor testProcessor(){
return new TestProcessor();
}
}
public class Test {
}
public class TestProcessor implements
BeanFactoryPostProcessor, PriorityOrdered {
@Override
public void postProcessBeanFactory
(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
@Override
public int getOrder() {
return 0;
}
}
配置文件application.properties:
test.name=mars
test.chain[0]=aa
test.chain[1]=bb
初始定位
最开始问题定位为ShiroProperties 没有注册到容器中,所以注入时通过getBean无法取到相应的bean。但是Debug也会发现ShiroProperties 的BeanDefinition已经在容器中持有,所以这个方向基本被排除了。
重定位
重新定位问题,暂时定位在populateBean
属性注入时初始化ShiroProperties 失败。后面debug查看了一下populateBean
时的TestController的BeanDefinition,发现ShiroProperties 这个属性根本没有在依赖注入的范围内。所以压根就没初始化。
所以这里咱们就需要分析一下Autowired的流程了,一般情况下,在getBean时会通过元数据后置处理器去取出类中需要依赖的其他类,也就是取出@Autowired注解的属性,放到BeanDefinition的
propertyValues
属性中。
@Autowired注入流程
例如@Autowired注解的属性会通过类AutowiredAnnotationBeanPostProcessor去处理。咱们简单看一下AutowiredAnnotationBeanPostProcessor的构造函数。
实例化的时候就会将Autowired和Value给缓存在一个autowiredAnnotationTypes
集合中。在后面进行BeanDefinition合并的时候,会遍历autowiredAnnotationTypes
集合,取出注解对应的字段(例如Autowired和Value对应的字段),最后存放到BeanDefinition的propertyValues
属性中,供后面的populateBean
调用时进行属性的注入。具体调用方法如下图:
上面的方法主要干了两个活:
- 取出要注入的元数据,即类似@Autowired注解的属性,配合上面的例子就是
shiroProperties
属性。 - 将这些属性注入到beanDefinition的
propertyValues
属性中。
所以这里咱们需要把思路往前推一下,需要判断出了什么问题才会导致TestController的propertyValues
属性为空。beanDefinition的合并发生在doCreateBean
方法中。如下图:
通过这个方法进去,咱们将所有的BeanPostProcessor打印出来,结果如下图所示,并没有AutowiredAnnotationBeanPostProcessor这个后置处理器。这也就能解释咱们的ShiroProperties为啥注入不进去了,虽然找到了原因,但是这不是一个正常的结果,正常情况下依赖注入都是没问题的,毕竟依赖注入是Spring的核心三大板块之一。
这里咱们深入分析一下AutowiredAnnotationBeanPostProcessor这个类是啥时候注册到Spring容器的。
众所周知,在容器初始化的过程中,有一个关键性的方法refresh
,在refresh
方法调用过程中,会调用invokeBeanFactoryPostProcessors
方法,所有的后置处理器都在这里进行初始化。咱们看看一共有多少个BeanPostProcessor:
从上图可以观察到正常情况下所有的postProcessorNames都是会被注册到Spring容器中的。但是这里有个例外,从上面的图片可以看出还有个testProcessor也在其中,这个是咱们自定义的BeanPostProcessor。Spring在注册这些BeanPostProcessor时会按一种规则去注册:
- 先注册实现了PriorityOrdered接口的BeanPostProcessor。
- 再注册实现了Ordered接口的BeanPostProcessor。
- 最后注册无顺序的BeanPostProcessor。
所以TestProcessor会在AutowiredAnnotationBeanPostProcessor之前进行注册,而TestProcessor是在TestController中的,所以说TestController作为TestProcessor的factoryBean
,当然要先进行初始化,这样最后导致TestController在初始化时无法正常使用AutowiredAnnotationBeanPostProcessor的功能,然后使得ShiroProperties无法正常注入。
到这里一切疑惑迎刃而解~,针对上述问题,咱们可以另起一个无需初始化的PostProcessorConfig类去专门处理类似的BeanPostProcessor即可。
科普PriorityOrdered接口
看一下Spring对于PriorityOrdered这个接口的注释
<p>Note: {@code PriorityOrdered} post-processor beans are initialized in a special phase, ahead of other post-processor beans. This subtly affects their autowiring behavior: they will only be autowired against beans which do not require eager initialization for type matching.
其实官方也有给咱们提示这一点的,用了这个接口的后置处理器将会影响依赖注入的过程,推荐放在不需要初始化的bean中进行装配。
PS:当然,这种情况一般也遇不到,只是最近在整合shiro时,shiro有提供一个LifecycleBeanPostProcessor 处理器去管理相关bean的生命周期,需要注册到Spring容器。然后就导致上面的情况,和LifecycleBeanPostProcessor 在的同一个类的@Autowired,@Value声明的字段均无法正常注入。
下面是关于这个问题可能涉及到的一些源码的分析。
源码分析
一般情况下,有两种组合可以使用。
- @ConfigurationProperties与@EnableConfigurationProperties进行配合使用。
- @ConfigurationProperties与@Configuration进行配合使用。
两则实现的目的一致,都是将@ConfigurationProperties注解的类交由Spring容器进行托管,在容器中注册BeanDefinition,以供后期bean的装配和获取。
下面的内容将以第一种组合展开进行讲解。我们可以先看看@EnableConfigurationProperties这个注解。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesImportSelector.class)
public @interface EnableConfigurationProperties {
/**
* Convenient way to quickly register {@link ConfigurationProperties} annotated beans
* with Spring. Standard Spring Beans will also be scanned regardless of this value.
* @return {@link ConfigurationProperties} annotated beans to register
*/
Class<?>[] value() default {};
}
这个注解上有@Import(EnableConfigurationPropertiesImportSelector.class)这样一个注解,对于@Import这个注解,感兴趣的朋友可以去看看ConfigurationClassParser中的processImports
方法。大概流程就是调用EnableConfigurationPropertiesImportSelector中的selectImports
方法,并将解析出来的所有类注册到容器中。
这里一共是将两个类注入到了容器中。他们都实现了ImportBeanDefinitionRegistrar接口,这个接口只有一个方法registerBeanDefinitions
,看方法名也就略知一二了吧。
ConfigurationPropertiesBeanRegistrar
ConfigurationPropertiesBeanRegistrar这个类的主要目的是用来收集EnableConfigurationProperties 中value值指定的类,并将其注册到容器中。代码量并不多,我截取下关键性代码。
一共分为两步:
- getTypes:通过注解@EnableConfigurationProperties获取其value中的值。
- register:将上面获取的
Class[]
注册到Spring容器中。
ConfigurationPropertiesBindingPostProcessorRegistrar
ConfigurationPropertiesBindingPostProcessorRegistrar这个类注册了两个后置处理器。
从代码不难看出,上面的逻辑在一个容器中有且只会执行一次。执行过程中会注册一个实现BeanPostProcessor的bean后置处理器,还会注册一个实现BeanFactoryPostProcessor的beanFactory后置处理器。
- ConfigurationBeanFactoryMetadata:将beanFactory中所有的beandefinition都保存一份到
beansFactoryMetadata
集合中。 - ConfigurationPropertiesBindingPostProcessor:这个类是处理ConfigurationProperties的重点类,它将会帮我们解析配置文件里面的配置并赋值到bean中。
绑定具体实体类和配置就在
bind
方法中进行。解析过程略显复杂,这里不做过多说明。
触发点
前面介绍了两个ImportBeanDefinitionRegistrar接口的实现类,但是registerBeanDefinitions
方法的触发点还未揭秘。了解过spring源码的同学想必对refresh
这个方法应该不陌生。咱们从这里开始挖掘一波~
refresh中有一个方法
invokeBeanFactoryPostProcessors
。
这个是触发点的入口,一步步点进去,直到PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors方法,这里面会执行一些定义好的BeanFactoryPostProcessor中的postProcessBeanDefinitionRegistry
方法,
咱们要关注的就是ConfigurationClassPostProcessor这个类。这个类其实干了很多活,包括前面提到的对于@Import这个注解的处理等等。
归纳成为三步,每一步都内嵌在前一步中:
- this.reader.loadBeanDefinitions(configClasses);
- loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);控制是否跳过一些bean的处理,例如咱们有时候会配置@ConditionalOnBean @ConditionalOnClass等等条件,若不满足,则直接跳过这些bean的处理。
- loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
registrars.forEach((registrar, metadata) ->
registrar.registerBeanDefinitions(metadata, this.registry));
}
这一块就是咱们的registerBeanDefinitions
方法触发的地方了。也与前面一块的讲解也就串起来了。
总结
解决问题的同时也是对自己成长的一种促进,这次源码分析补充了前面很多强行看源码时的一些疑惑点。毕竟spring盘子太大了,不一定所有的使用注意事项都会在官方文档加以注释,碰见了搜索引擎解决不了的问题还是得自己手撸源码------>O(∩_∩)O哈哈~。