阅读 1280

Autowired无法正常注入的疑难杂症

前言

最近项目在整合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        BeanFactoryPostProcessorPriorityOrdered 
{
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 这个属性根本没有在依赖注入的范围内。所以压根就没初始化。


所以这里咱们就需要分析一下Autowired的流程了,一般情况下,在getBean时会通过元数据后置处理器去取出类中需要依赖的其他类,也就是取出@Autowired注解的属性,放到BeanDefinition的propertyValues属性中。

@Autowired注入流程

例如@Autowired注解的属性会通过类AutowiredAnnotationBeanPostProcessor去处理。咱们简单看一下AutowiredAnnotationBeanPostProcessor的构造函数。

实例化的时候就会将Autowired和Value给缓存在一个autowiredAnnotationTypes集合中。在后面进行BeanDefinition合并的时候,会遍历autowiredAnnotationTypes集合,取出注解对应的字段(例如Autowired和Value对应的字段),最后存放到BeanDefinition的propertyValues属性中,供后面的populateBean调用时进行属性的注入。具体调用方法如下图:

上面的方法主要干了两个活:

  1. 取出要注入的元数据,即类似@Autowired注解的属性,配合上面的例子就是shiroProperties属性。
  2. 将这些属性注入到beanDefinition的propertyValues属性中。

所以这里咱们需要把思路往前推一下,需要判断出了什么问题才会导致TestController的propertyValues属性为空。beanDefinition的合并发生在doCreateBean方法中。如下图:

通过这个方法进去,咱们将所有的BeanPostProcessor打印出来,结果如下图所示,并没有AutowiredAnnotationBeanPostProcessor这个后置处理器。这也就能解释咱们的ShiroProperties为啥注入不进去了,虽然找到了原因,但是这不是一个正常的结果,正常情况下依赖注入都是没问题的,毕竟依赖注入是Spring的核心三大板块之一。

这里咱们深入分析一下AutowiredAnnotationBeanPostProcessor这个类是啥时候注册到Spring容器的。

众所周知,在容器初始化的过程中,有一个关键性的方法refresh,在refresh方法调用过程中,会调用invokeBeanFactoryPostProcessors方法,所有的后置处理器都在这里进行初始化。咱们看看一共有多少个BeanPostProcessor:

从上图可以观察到正常情况下所有的postProcessorNames都是会被注册到Spring容器中的。但是这里有个例外,从上面的图片可以看出还有个testProcessor也在其中,这个是咱们自定义的BeanPostProcessor。Spring在注册这些BeanPostProcessor时会按一种规则去注册:

  1. 先注册实现了PriorityOrdered接口的BeanPostProcessor。
  2. 再注册实现了Ordered接口的BeanPostProcessor。
  3. 最后注册无顺序的BeanPostProcessor。

所以TestProcessor会在AutowiredAnnotationBeanPostProcessor之前进行注册,而TestProcessor是在TestController中的,所以说TestController作为TestProcessor的factoryBean,当然要先进行初始化,这样最后导致TestController在初始化时无法正常使用AutowiredAnnotationBeanPostProcessor的功能,然后使得ShiroProperties无法正常注入。

到这里一切疑惑迎刃而解~,针对上述问题,咱们可以另起一个无需初始化的PostProcessorConfig类去专门处理类似的BeanPostProcessor即可。

科普PriorityOrdered接口

看一下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声明的字段均无法正常注入。


下面是关于这个问题可能涉及到的一些源码的分析。

源码分析

一般情况下,有两种组合可以使用。

  1. @ConfigurationProperties与@EnableConfigurationProperties进行配合使用。
  2. @ConfigurationProperties与@Configuration进行配合使用。

两则实现的目的一致,都是将@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

ConfigurationPropertiesBeanRegistrar这个类的主要目的是用来收集EnableConfigurationProperties 中value值指定的类,并将其注册到容器中。代码量并不多,我截取下关键性代码。


一共分为两步:

  • getTypes:通过注解@EnableConfigurationProperties获取其value中的值。
  • register:将上面获取的Class[]注册到Spring容器中。
ConfigurationPropertiesBindingPostProcessorRegistrar

ConfigurationPropertiesBindingPostProcessorRegistrar这个类注册了两个后置处理器。

从代码不难看出,上面的逻辑在一个容器中有且只会执行一次。执行过程中会注册一个实现BeanPostProcessor的bean后置处理器,还会注册一个实现BeanFactoryPostProcessor的beanFactory后置处理器。

  • ConfigurationBeanFactoryMetadata:将beanFactory中所有的beandefinition都保存一份到beansFactoryMetadata集合中。
  • ConfigurationPropertiesBindingPostProcessor:这个类是处理ConfigurationProperties的重点类,它将会帮我们解析配置文件里面的配置并赋值到bean中。
ConfigurationPropertiesBindingPostProcessor
ConfigurationPropertiesBindingPostProcessor

绑定具体实体类和配置就在bind方法中进行。解析过程略显复杂,这里不做过多说明。

触发点

前面介绍了两个ImportBeanDefinitionRegistrar接口的实现类,但是registerBeanDefinitions方法的触发点还未揭秘。了解过spring源码的同学想必对refresh这个方法应该不陌生。咱们从这里开始挖掘一波~

refresh中有一个方法invokeBeanFactoryPostProcessors

这个是触发点的入口,一步步点进去,直到PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors方法,这里面会执行一些定义好的BeanFactoryPostProcessor中的postProcessBeanDefinitionRegistry方法,
咱们要关注的就是ConfigurationClassPostProcessor这个类。这个类其实干了很多活,包括前面提到的对于@Import这个注解的处理等等。

归纳成为三步,每一步都内嵌在前一步中:

  1. this.reader.loadBeanDefinitions(configClasses);
  2. loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);控制是否跳过一些bean的处理,例如咱们有时候会配置@ConditionalOnBean @ConditionalOnClass等等条件,若不满足,则直接跳过这些bean的处理。
  3. loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
1    private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
2        registrars.forEach((registrar, metadata) ->
3                registrar.registerBeanDefinitions(metadata, this.registry));
4    }
复制代码

这一块就是咱们的registerBeanDefinitions方法触发的地方了。也与前面一块的讲解也就串起来了。


总结

解决问题的同时也是对自己成长的一种促进,这次源码分析补充了前面很多强行看源码时的一些疑惑点。毕竟spring盘子太大了,不一定所有的使用注意事项都会在官方文档加以注释,碰见了搜索引擎解决不了的问题还是得自己手撸源码------>O(∩_∩)O哈哈~。

End