最近项目在整合shiro权限认证模块时,给自己挖了一个深坑,也是分析了好久才定位到问题的所在,根本原因还是对spring相关的技术点掌握的不够娴熟。本文基于springboot 2.1.5进行分析。下面会用简单的Demo去还原问题的场景。
简单将遇到的问题还原一下,这段代码中ShiroProperties 始终注入不到TestController中去。也就是 shiroProperties
始终是null。
1@ConfigurationProperties(prefix = "test") 2@Data 3public class ShiroProperties { 4 5 private String name; 6 private List<String> chain; 7} 复制代码
1@Configuration 2@EnableConfigurationProperties(ShiroProperties.class) 3public class ShiroConfiguration { 4} 复制代码
1@Controller 2public class TestController { 3 4 @Autowired 5 private ShiroProperties shiroProperties; 6 7 @Bean 8 public Test test(){ 9 System.out.println(shiroProperties); 10 return new Test(); 11 } 12 13 @Bean 14 private TestProcessor testProcessor(){ 15 return new TestProcessor(); 16 } 17} 复制代码
1public class Test { 2} 复制代码
1public class TestProcessor implements 2 BeanFactoryPostProcessor, PriorityOrdered { 3 4 @Override 5 public void postProcessBeanFactory 6 (ConfigurableListableBeanFactory beanFactory) throws BeansException { 7 8 } 9 10 @Override 11 public int getOrder() { 12 return 0; 13 } 14} 复制代码
配置文件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 这个属性根本没有在依赖注入的范围内。所以压根就没初始化。
propertyValues
属性中。
例如@Autowired注解的属性会通过类AutowiredAnnotationBeanPostProcessor去处理。咱们简单看一下AutowiredAnnotationBeanPostProcessor的构造函数。
实例化的时候就会将Autowired和Value给缓存在一个 autowiredAnnotationTypes
集合中。在后面进行BeanDefinition合并的时候,会遍历 autowiredAnnotationTypes
集合,取出注解对应的字段(例如Autowired和Value对应的字段),最后存放到BeanDefinition的 propertyValues
属性中,供后面的 populateBean
调用时进行属性的注入。具体调用方法如下图:
上面的方法主要干了两个活:
shiroProperties propertyValues
所以这里咱们需要把思路往前推一下,需要判断出了什么问题才会导致TestController的 propertyValues
属性为空。beanDefinition的合并发生在 doCreateBean
方法中。如下图:
通过这个方法进去,咱们将所有的BeanPostProcessor打印出来,结果如下图所示, 并没有AutowiredAnnotationBeanPostProcessor这个后置处理器。这也就能解释咱们的ShiroProperties为啥注入不进去了 ,虽然找到了原因,但是这不是一个正常的结果,正常情况下依赖注入都是没问题的,毕竟依赖注入是Spring的核心三大板块之一。
这里咱们深入分析一下AutowiredAnnotationBeanPostProcessor这个类是啥时候注册到Spring容器的。
众所周知,在容器初始化的过程中,有一个关键性的方法 refresh
,在 refresh
方法调用过程中,会调用 invokeBeanFactoryPostProcessors
方法,所有的后置处理器都在这里进行初始化。咱们看看一共有多少个BeanPostProcessor:
从上图可以观察到正常情况下所有的postProcessorNames都是会被注册到Spring容器中的。但是这里有个例外,从上面的图片可以看出还有个testProcessor也在其中,这个是咱们自定义的BeanPostProcessor。Spring在注册这些BeanPostProcessor时会按一种规则去注册:
所以TestProcessor会在AutowiredAnnotationBeanPostProcessor之前进行注册,而TestProcessor是在TestController中的,所以说TestController作为TestProcessor的 factoryBean
,当然要先进行初始化,这样最后导致TestController在初始化时无法正常使用AutowiredAnnotationBeanPostProcessor的功能,然后使得ShiroProperties无法正常注入。
到这里一切疑惑迎刃而解~,针对上述问题,咱们可以另起一个无需初始化的PostProcessorConfig类去专门处理类似的BeanPostProcessor即可。
看一下Spring对于PriorityOrdered这个接口的注释
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注解的类交由Spring容器进行托管,在容器中注册BeanDefinition,以供后期bean的装配和获取。
下面的内容将以第一种组合展开进行讲解。我们可以先看看@EnableConfigurationProperties这个注解。
1@Target(ElementType.TYPE) 2@Retention(RetentionPolicy.RUNTIME) 3@Documented 4@Import(EnableConfigurationPropertiesImportSelector.class) 5public @interface EnableConfigurationProperties { 6 7 /** 8 * Convenient way to quickly register {@link ConfigurationProperties} annotated beans 9 * with Spring. Standard Spring Beans will also be scanned regardless of this value. 10 * @return {@link ConfigurationProperties} annotated beans to register 11 */ 12 Class<?>[] value() default {}; 13 14} 复制代码
这个注解上有@Import(EnableConfigurationPropertiesImportSelector.class)这样一个注解,对于@Import这个注解,感兴趣的朋友可以去看看ConfigurationClassParser中的 processImports
方法。大概流程就是调用EnableConfigurationPropertiesImportSelector中的 selectImports
方法,并将解析出来的所有类注册到容器中。
这里一共是将两个类注入到了容器中。他们都实现了ImportBeanDefinitionRegistrar接口,这个接口只有一个方法 registerBeanDefinitions
,看方法名也就略知一二了吧。
ConfigurationPropertiesBeanRegistrar这个类的主要目的是用来收集EnableConfigurationProperties 中value值指定的类,并将其注册到容器中。代码量并不多,我截取下关键性代码。
一共分为两步:
Class[]
注册到Spring容器中。 ConfigurationPropertiesBindingPostProcessorRegistrar这个类注册了两个后置处理器。
从代码不难看出,上面的逻辑在一个容器中有且只会执行一次。执行过程中会注册一个实现BeanPostProcessor的bean后置处理器,还会注册一个实现BeanFactoryPostProcessor的beanFactory后置处理器。
beansFactoryMetadata
集合中。 bind
方法中进行。解析过程略显复杂,这里不做过多说明。
前面介绍了两个ImportBeanDefinitionRegistrar接口的实现类,但是 registerBeanDefinitions
方法的触发点还未揭秘。了解过spring源码的同学想必对 refresh
这个方法应该不陌生。咱们从这里开始挖掘一波~
refresh中有一个方法 invokeBeanFactoryPostProcessors
。
这个是触发点的入口,一步步点进去,直到 PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors 方法,这里面会执行一些定义好的BeanFactoryPostProcessor中的 postProcessBeanDefinitionRegistry
方法,
咱们要关注的就是 ConfigurationClassPostProcessor 这个类。这个类其实干了很多活,包括前面提到的对于@Import这个注解的处理等等。
归纳成为三步,每一步都内嵌在前一步中:
1 private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) { 2 registrars.forEach((registrar, metadata) -> 3 registrar.registerBeanDefinitions(metadata, this.registry)); 4 } 复制代码
这一块就是咱们的 registerBeanDefinitions
方法触发的地方了。也与前面一块的讲解也就串起来了。
解决问题的同时也是对自己成长的一种促进,这次源码分析补充了前面很多 强行看源码 时的一些疑惑点。毕竟spring盘子太大了,不一定所有的使用注意事项都会在官方文档加以注释,碰见了搜索引擎解决不了的问题还是得自己手撸源码------>O(∩_∩)O哈哈~。