今天项目组遇到一个问题让我帮忙排查,虽然是一个小众问题,但是涉及到Spring的Bean初始化的相关知识点。整理了一下,分享给大家。
问题描述
注解了@Service的对象,其内部注解了@Autowired的对象没有被注入,方法在调用时为null。并且,排查时发现包括@PostConstruct方法没有被调用。(注:以下代码均为示例代码,非业务代码)
问题排查
思路: @Autowired在启动时,如果是正常的BeanFactory创建的对象,会抛出相应异常。
所以,初步判断为该对象的实例化时机存在问题。
问题跟踪: 创建构建函数,跟踪问题对象在哪里实例化。
从上图的调用堆栈中,可以非常明确的看到对象ProblemService的实例化调用的一个关系。
查看相关代码
找到问题
对象ProblemService被一个BeanFactoryPostProcessor在处理时通过Class.forName装载了,此时会执行装载的对象的静态代码块。
静态代码块,通过SpringBeans#ConfigurableListableBeanFactory从BeanFactory中加载对象。
那么,为什么加载的对象ProblemService的depInjectBean AutoWired没有生效,并且init方法没有被调用呢?这是因为BeanFactoryPostProcessor的机制引起的。
BeanFactoryPostProcessor
在说明BeanFactoryPostProcessor之前,我们需要对Spring的Bean加载处理机制有一定的了解。这里我简要的说明一下。
Spring的Bean加载时有 定义和初始化的区分。而BeanFactoryPostProcessor的发生时机在定义完成之后,注意,此时所有的Bean只是定义完成,并没有真正的初始化完成。
那么,BeanFactoryPostProcessor具体是做什么的呢?
定义:应用工厂的勾子处理,允许自定义修改应用上下文中的Bean定义的属性。但是,请不在要BeanFactoryPostProcessor时实例化任何一个Bean,否则会导致Bean过早实例早,出现不可预见的情况。
再强调一次:不在要BeanFactoryPostProcessor时实例化任何一个Bean,否则会导致Bean过早实例早,出现不可预见的情况。
原因是,BeanFactoryPostProcessor的发生时机在所有的Bean定义完成之后,实例化之前。也就是在发生时刻,在Spring容器中,所有的Bean已经定义好了,等待进行实例化。
如果此时,在BeanFactoryPostProcessor时机,对Bean进行了实例化,而没有完整的注入依赖关系。那么,对于Spring容器来说,在Bean实例化阶段时,会判断已经实例化而跳过这个Bean的处理。
所以,此时对于这个在BeanFactoryPostProcessor实例化的Bean,其保持着实例化时刻的所有定义。也就是没有注入的依赖一定是null,并且相关的注解,如@PostConstruct等失效。
问题总结
在上述的流程中,主要出现了二个不合理的应用:
- 在一个BeanFactoryPostProcessor中对会一些类进行装载,导致静态代码在非预期时被执行
对于开发和JVM来说,一些类在未被使用时并不需要装载和初始化。所以对开发框架的扩展或者封装,不应该破坏类应该在被需求时才进行装载和初始化,提前装载和初始化应该是有条件的!
- 在类的静态变量中,使用一个可以通过静态方法获取Spring容器中的Bean工具类
对于SpringBean对象的获取时机是要非常注意的。在这种写法中,如果这个类的方法在Spring容器未启动完成之前被调用,就会导致依赖Bean的空指针异常。如果,在静态方法中需要依赖于Spring容器中的Bean时,需要非常的注意方法的调用时机。如果不是非必须的,不应该用这种方法来实现!