转载

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

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        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已经在容器中持有,所以这个方向基本被排除了。

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

重定位

重新定位问题,暂时定位在 populateBean 属性注入时初始化ShiroProperties 失败。后面debug查看了一下 populateBean 时的TestController的BeanDefinition,发现ShiroProperties 这个属性根本没有在依赖注入的范围内。所以压根就没初始化。

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

属性中。

@Autowired注入流程

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

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

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

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

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

shiroProperties
propertyValues

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

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

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

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

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

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

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

从上图可以观察到正常情况下所有的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 方法,并将解析出来的所有类注册到容器中。

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

这里一共是将两个类注入到了容器中。他们都实现了ImportBeanDefinitionRegistrar接口,这个接口只有一个方法 registerBeanDefinitions ,看方法名也就略知一二了吧。

ConfigurationPropertiesBeanRegistrar

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

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

一共分为两步:

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

ConfigurationPropertiesBindingPostProcessorRegistrar

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

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

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

  • ConfigurationBeanFactoryMetadata:将beanFactory中所有的beandefinition都保存一份到 beansFactoryMetadata 集合中。
  • ConfigurationPropertiesBindingPostProcessor:这个类是处理ConfigurationProperties的重点类,它将会帮我们解析配置文件里面的配置并赋值到bean中。
Autowired无法正常注入的疑难杂症
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

原文  https://juejin.im/post/5ced6767e51d45108c59a4c8
正文到此结束
Loading...