Spring常见错误 - Bean构造注入报空指针异常
- 前言
- 一. 构造器内报NPE
- 1.1 案例
- 1.2 原理分析
- 1.2.1 空指针发生在哪一个阶段?
- 1.2.2 studentService字段为何是Null?
- 1.3 解决
- 二. Bean加载的初始化阶段
- 2.1 applyBeanPostProcessorsBeforeInitialization
- 2.2 invokeInitMethods
- 2.3 总结
前言
推荐大家先看下Spring源码系列:Bean的加载。那么本文的案例就很容易懂其原理了。
一. 构造器内报NPE
我们来看下案例:
1.1 案例
我们随便自定义一个类,并希望在创建HelloServiceBean
的时候,完成say()
操作,打印出Student
的字样。那么我们一般首先想到的就是在构造函数中完成对应的逻辑执行。
@Component
public class HelloService {
@Autowired
private StudentService studentService;
public HelloService() {
studentService.say();
}
}
项目启动后:
可以见到抛了空指针异常。从代码上看,看来是注入的studentService
出了问题,此刻还是null
。那么为什么会这样呢?这就要看Bean
加载的一个生命周期了。
1.2 原理分析
这一切还得从Bean
的创建来说起,相关函数在AbstractAutowireCapableBeanFactory.doCreateBean()
:
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException {
// Instantiate the bean.
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
// 1.实例的创建
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
// ...
Object exposedObject = bean;
try {
// 2.属性注入
populateBean(beanName, mbd, instanceWrapper);
// 3.Bean的初始化
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
catch (Throwable ex) {
// ..
}
// ..
return exposedObject;
}
Bean
的创建分为三大步骤:
- 实例构造:
createBeanInstance()
。 - 依赖注入:
populateBean()
。 - 初始化:
initializeBean()
。
1.2.1 空指针发生在哪一个阶段?
那么这里我们首先围绕第一个阶段:createBeanInstance()
函数来展开:
来看下它的源码:
protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
// 1.根据Class属性解析Class
Class<?> beanClass = resolveBeanClass(mbd, beanName);
// ...
// 2.是否存在Bean的回调函数
Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
if (instanceSupplier != null) {
return obtainFromSupplier(instanceSupplier, beanName);
}
// 3.是否有工厂方法
if (mbd.getFactoryMethodName() != null) {
return instantiateUsingFactoryMethod(beanName, mbd, args);
}
boolean resolved = false;// 构造函数是否被解析过
boolean autowireNecessary = false;// 构造函数里面的参数是否解析过
// 4.锁定构造函数
if (args == null) {
synchronized (mbd.constructorArgumentLock) {
if (mbd.resolvedConstructorOrFactoryMethod != null) {
resolved = true;
autowireNecessary = mbd.constructorArgumentsResolved;
}
}
}
// 5.若构造函数已经解析过了,那么就使用它
if (resolved) {
if (autowireNecessary) {
return autowireConstructor(beanName, mbd, null, null);
}
else {
return instantiateBean(beanName, mbd);
}
}
// 6.否则就根据参数来解析构造函数,这里是null
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
return autowireConstructor(beanName, mbd, ctors, args);
}
// 7.构造函数注入
ctors = mbd.getPreferredConstructors();
if (ctors != null) {
return autowireConstructor(beanName, mbd, ctors, null);
}
// 8.否则使用默认的构造函数
return instantiateBean(beanName, mbd);
}
总结如下:
- 先看这个
Bean
是否有对应的回调函数或者工厂方法。有的话直接调用返回。 - 解析这个
Bean
的构造函数。如果解析过了,那么直接调用。 - 再看这个
Bean
是否有构造函数注入。若都无,则调用默认的构造函数。
而代码Debug
中:可以看到,没有解析到相关的构造函数,那么此时就会调用默认的构造。
而底层逻辑就是根据两种情况执行两种不同的实例创建策略:
- 一般的
Bean
通过反射进行实例的创建:BeanUtils.instantiateClass(constructorToUse)
。 - 若有需要覆盖或者动态替换的方法,即
lookup
和replaced
方法。则进行cglib
动态代理:instantiateWithMethodInjection(bd, beanName, owner);
对于本案例来说,就是简单的调用了我们自己创建的构造函数罢了。如图:
在这里,我们知道空指针异常发生在HelloService
的实例构造阶段。
1.2.2 studentService字段为何是Null?
思考过程:
- 对于
HelloService
类来说,其属性注入(studentService
字段的装配)阶段发生在实例构造阶段之后。 - 而
studentService
字段通过@Autowired
注解来完成自动装配,在属性注入阶段,即在populateBean()
函数中实现。但此时populateBean()
还没有被执行。 - 因此
studentService
在实例构造的时候值为null
。因此无法调用其相关函数,会NPE
。
1.3 解决
总的来说就是使用 @Autowired
直接标记在成员属性上而引发的自动装配操作是在当前类构造器执行之后发生的。 因此我们可以不用@Autowired
注解,改为构造函数注入的方式:
@Component
public class HelloService {
private StudentService studentService;
public HelloService(StudentService studentService) {
this.studentService = studentService;
studentService.say();
}
}
执行结果:
其实,本案例的写法是非常少见的,但是这个思想却比较常见,即:希望某个Bean在创建的时候执行某段逻辑。
只不过1.1案例中,采取的是构造函数来执行某段逻辑的方式。但由于对SpringBean
生命周期加载顺序的不了解,导致了空指针异常。其实还有别的方法可以代替这种构造函数的写法。
二. Bean加载的初始化阶段
上文提到了,Bean
的创建一共有三个步骤,第三个步骤就是最终的收尾工作,初始化阶段,我们来看下函数:
exposedObject = initializeBean(beanName, exposedObject, mbd);
protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
// ...
// 1.执行某种后置处理器
Object wrappedBean = bean;
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}
try {
// 2.执行用户自定义的init方法。
invokeInitMethods(beanName, wrappedBean, mbd);
}
// ...
return wrappedBean;
}
这段代码有两个比较重要的分支:
-
applyBeanPostProcessorsBeforeInitialization
:执行某种后置处理器。 -
invokeInitMethods
:执行用户自定义的init
方法。
2.1 applyBeanPostProcessorsBeforeInitialization
这名字看起来很长。。但是吧,这个名字里面有一个非常突出的名称:BeanPostProcessors
。我们知道BeanPostProcessors
是一个接口:它的主要功能是在Bean
的初始化阶段的前后做一些自定义操作。
我们来追溯下它的执行:
public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
throws BeansException {
Object result = existingBean;
for (BeanPostProcessor processor : getBeanPostProcessors()) {
Object current = processor.postProcessBeforeInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}
这里执行的是InitDestroyAnnotationBeanPostProcessor
下的具体实现:
public class InitDestroyAnnotationBeanPostProcessor
implements DestructionAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor, PriorityOrdered, Serializable {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 根据类信息找到相关的元数据信息
LifecycleMetadata metadata = findLifecycleMetadata(bean.getClass());
try {
// 执行相关的初始化函数
metadata.invokeInitMethods(bean, beanName);
}
// ..
return bean;
}
}
而findLifecycleMetadata
的相关逻辑和buildLifecycleMetadata
息息相关。
private LifecycleMetadata findLifecycleMetadata(Class<?> clazz) {
if (this.lifecycleMetadataCache == null) {
// Happens after deserialization, during destruction...
return buildLifecycleMetadata(clazz);
}
// Quick check on the concurrent map first, with minimal locking.
LifecycleMetadata metadata = this.lifecycleMetadataCache.get(clazz);
if (metadata == null) {
synchronized (this.lifecycleMetadataCache) {
metadata = this.lifecycleMetadataCache.get(clazz);
if (metadata == null) {
metadata = buildLifecycleMetadata(clazz);
this.lifecycleMetadataCache.put(clazz, metadata);
}
return metadata;
}
}
return metadata;
}
buildLifecycleMetadata
:
private LifecycleMetadata buildLifecycleMetadata(final Class<?> clazz) {
if (!AnnotationUtils.isCandidateClass(clazz, Arrays.asList(this.initAnnotationType, this.destroyAnnotationType))) {
return this.emptyLifecycleMetadata;
}
List<LifecycleElement> initMethods = new ArrayList<>();
List<LifecycleElement> destroyMethods = new ArrayList<>();
Class<?> targetClass = clazz;
do {
final List<LifecycleElement> currInitMethods = new ArrayList<>();
final List<LifecycleElement> currDestroyMethods = new ArrayList<>();
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
if (this.initAnnotationType != null && method.isAnnotationPresent(this.initAnnotationType)) {
LifecycleElement element = new LifecycleElement(method);
currInitMethods.add(element);
if (logger.isTraceEnabled()) {
logger.trace("Found init method on class [" + clazz.getName() + "]: " + method);
}
}
if (this.destroyAnnotationType != null && method.isAnnotationPresent(this.destroyAnnotationType)) {
currDestroyMethods.add(new LifecycleElement(method));
if (logger.isTraceEnabled()) {
logger.trace("Found destroy method on class [" + clazz.getName() + "]: " + method);
}
}
});
initMethods.addAll(0, currInitMethods);
destroyMethods.addAll(currDestroyMethods);
targetClass = targetClass.getSuperclass();
}
// ...
}
我们可以看出,buildLifecycleMetadata()
函数主要是寻找两类函数,找到了就将他们加入到结果集并返回。
initAnnotationType
:初始化方法,相关类型如下:destroyAnnotationType
:销毁方法,相关类型如下:- 取到了之后,则交给外层的逻辑
metadata.invokeInitMethods(bean, beanName);
去调用即可。总结下就是: - 每个Bean在初始化阶段,可能都会去执行
applyBeanPostProcessorsBeforeInitialization
函数,即后置处理器。 applyBeanPostProcessorsBeforeInitialization
函数主要去寻找这个类中寻找两类方法。@PostConstruct
注解修饰的initMethods
方法、@PreDestroy
注解修饰的destroyMethods
方法。- 去执行对应的
initMethods
方法,完成后置处理。
那么对于本篇文章而言,我们可以通过 @PostConstruct
注解来替代隐式构造函数注入:
@Component
public class HelloService {
@Autowired
private StudentService studentService;
@PostConstruct
public void init() {
studentService.say();
}
@PreDestroy
public void destroy() {
System.out.println("Bye Bte");
}
程序跑起来然后关闭:
可见同样能达到创建Bean
的阶段中,执行某段逻辑的效果。那么我们再来看下第二种方案。
2.2 invokeInitMethods
invokeInitMethods
的执行,总的来说就是判断当前Bean
是否实现了InitializingBean
接口,若实现了,则执行对应的afterPropertiesSet()
。
protected void invokeInitMethods(String beanName, Object bean, @Nullable RootBeanDefinition mbd)
throws Throwable {
boolean isInitializingBean = (bean instanceof InitializingBean);
if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
// ...
if (System.getSecurityManager() != null) {
// ...
}
else {
((InitializingBean) bean).afterPropertiesSet();
}
}
// ...
}
那么我们就可以通过这样的方式来完成同样的效果:
@Component
public class HelloService implements InitializingBean {
@Autowired
private StudentService studentService;
@Override
public void afterPropertiesSet() throws Exception {
studentService.say();
}
}
结果如下:
2.3 总结
总结下本篇文章哈:
- 如果某个
Bean
中的某个字段A
,通过@Autowired
注解进行自动装配。同时在该Bean
中还显式声明了构造函数,并调用这个A
对象的某个方法。那么这种情况会出现NPE
。 - 终极原因是因为,
Spring
中一个Bean
的创建,其属性注入阶段(字段A
的赋值)在实例构造阶段(Bean
的构造函数调用)之后。 - 要想避免这种错误。可以通过构造注入的方式来完成。也可以通过
@PostConstruct
注解修饰对应的初始化逻辑。或者是实现InitializingBean
接口,在afterPropertiesSet()
函数中完成对应逻辑。