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

前言

最近项目在整合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调用时进行属性的注入。具体调用方法如下图:

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

  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这个接口的注释

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


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

源码分析

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

  1. @ConfigurationProperties与@EnableConfigurationProperties进行配合使用。
  2. @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中。

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());
    private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
        registrars.forEach((registrar, metadata) ->
                registrar.registerBeanDefinitions(metadata, this.registry));
    }

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


总结

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

End

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • 本来是准备看一看Spring源码的。然后在知乎上看到来一个帖子,说有一群**自己连Spring官方文档都没有完全读...
    此鱼不得水阅读 6,926评论 4 21
  • 参考W3C Spring教程 Spring致力于J2EE应用的各种解决方案,而不仅仅专注于某一层解决方案。可以说S...
    王侦阅读 1,152评论 0 6
  • 文章作者:Tyan博客:noahsnail.com 3.4 Dependencies A typical ente...
    SnailTyan阅读 4,128评论 2 7
  • 今天分享海灵格爷爷的,我允许 我允许任何事情的发生 我允许,事情是如此的开始 如此的发展,如此的结局 因为我知道,...
    静待那时花开阅读 97评论 0 1
  • 心灵作家张德芬曾说: 每个人都要分清楚三件事儿:自己的事儿,别人的事儿,老天的事儿。 在这三件事儿里,我们只负责自...
    平凡的男人_26b2阅读 156评论 0 3