问题描述
今日在开发过程中遇到一个spring configuration类注入失败的问题,具体代码如下:
@Configuration
@Import({TestBean1.class})
public class HelloConfiguration {
@Resource
private ApplicationContext applicationContext;
@Bean
public TestBean2 testBean2() {
TestBean1 testBean2 = applicationContext.getBean(TestBean1.class);
return new TestBean2();
}
@Bean
public TestBeanDefinitionRegistryPostProcessor testBeanDefinitionRegistryPostProcessor() {
return new TestBeanDefinitionRegistryPostProcessor();
}
}
执行过程中抛出如下异常NullPointerException:
Caused by: java.lang.NullPointerException
at com.example.demo.config.HelloConfiguration.testBean2(HelloConfiguration.java:27) ~[classes/:?]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_211]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_211]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_211]
at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_211]
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ~[spring-beans-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:579) ~[spring-beans-5.0.4.RELEASE.jar:5.0.4.RELEASE]
... 18 more
排查过程
通过发生npe的位置我们可以推测applicationContext对象没有注入成功。但是代码中我们的确是通过@Resource注解注入了该对象,那么到底是注入失败还是压根没有注入呢?要弄清缘由需要剖析一下spring的内部机制。我们用@Resource注解注入对象的时候,spring是如何处理的呢?
创建HelloConfiguration对象肯定需要AbstractAutowireCapableBeanFactory的doCreateBean方法,
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
throws BeanCreationException {
// Instantiate the bean.
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
// 这里实例化对象
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
Class<?> beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null);
mbd.resolvedTargetType = beanType;
// Allow post-processors to modify the merged bean definition.
synchronized (mbd.postProcessingLock) {
if (!mbd.postProcessed) {
try {
// 调用后置处理器
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Post-processing of merged bean definition failed", ex);
}
mbd.postProcessed = true;
}
}
@Resource是通过CommonAnnotationBeanPostProcessor后置处理器来注入的
通过debug发现在创建HelloConfiguration对象对象时,后置处理器中没有CommonAnnotationBeanPostProcessor。说明spring此时还没有加载到该对象。为什么呢?我们看下HelloConfiguration的代码,下面还创建了一个TestBeanDefinitionRegistryPostProcessor对象,是BeanDefinitionRegistryPostProcessor的实现类,而BeanDefinitionRegistryPostProcessor继承了BeanFactoryPostProcessor接口。如果对spring源码有所了解的话应该知道spring容器在启动的时候是先加载BeanFactoryPostProcessor,然后再加载BeanPostProcessor,而CommonAnnotationBeanPostProcessor正是BeanPostProcessor的实现。所以到这里我们可以推测HelloConfiguration的加载过程:
- 将configuration中的bean加载到bdf中
- 优先实例化BeanDefinitionRegistryPostProcessor对象,因为BeanDefinitionRegistryPostProcessor的factoryBean是HelloConfiguration,所以这里需要实例化HelloConfiguration。但是由于CommonAnnotationBeanPostProcessor还没有加载到spring中,所以无法注入applicationContext对象。
- 加载HelloConfiguration的时候会去调用所有@Bean方法,此时方法中使用了applicationContext,于是NPE了。
解决问题
问题的根源是由于BeanDefinitionRegistryPostProcessor的创建导致了HelloConfiguration实例化被前置了。
解决注入问题的一个方式是创建BeanDefinitionRegistryPostProcessor不去提前触发HelloConfiguration对象的实例化。代码如下,仅仅加上一个static修饰符:
@Configuration
@Import({TestBean1.class})
public class HelloConfiguration {
@Resource
private ApplicationContext applicationContext;
@Bean
public TestBean2 testBean2() {
TestBean1 testBean2 = applicationContext.getBean(TestBean1.class);
return new TestBean2();
}
@Bean
public static TestBeanDefinitionRegistryPostProcessor testBeanDefinitionRegistryPostProcessor() {
return new TestBeanDefinitionRegistryPostProcessor();
}
}
看到这里有人会问,为什么加了static问题就解决了?
答案是如果spring发现@Bean的方法是static的时候,会在AbstractObjectDefinition中标记到FactoryMethodName。spring在创建bean的时候如果识别到FactoryMethodName就会直接调用static方法,而不必去实例化HelloConfiguration对象。