Spring Bean生命周期你除了会背八股文面试,真的会用了吗?

Posted JavaEdge.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Bean生命周期你除了会背八股文面试,真的会用了吗?相关的知识,希望对你有一定的参考价值。

Spring Bean 的初始化过程及销毁过程中的一些问题。

  • 有些bug可在 Spring 异常提示下快速解决,但却不理解背后原理
  • 一些错误,不易在开发环境下被发现,从而在产线上造成较为严重后果

1 使用构造器参数实现隐式注入

类初始化时的常见 bug。构建宿舍管理系统时,有 LightMgrService 来管理 LightService,控制宿舍灯的开启和关闭。
现在期望在 LightMgrService 初始化时自动调用 LightService#check检查所有宿舍灯的电路是否正常:

我们在 LightMgrService 的默认构造器中调用了通过 @Autoware 注入的成员变量 LightService#check:

  • LightService 对象的原始类

预期现象:

  • 在 LightMgrService 初始化过程中,LightService 因被**@Autowired**标记,所以能被自动装配
  • 在 LightMgrService 构造器执行中,LightService#check() 能被自动调用
  • 打印 check all lights

然而事与愿违,我们得到的只会是 NPE:

1.1 源码解析

根因在于对Spring类初始化过程没有足够的了解。下面这张时序图描述了 Spring 启动时的一些关键结点:

  1. 将一些必要系统类,比如Bean后置处理器,注册到Spring容器,包括CommonAnnotationBeanPostProcessor
  2. 将这些后置处理器实例化,并注册到Spring容器
  3. 实例化所有用户定制类,调用后置处理器进行辅助装配、类初始化等等。

CommonAnnotationBeanPostProcessor 后置处理类是何时被 Spring 加载和实例化的呢?

  • 很多必要系统类,比如Bean后置处理器(CommonAnnotationBeanPostProcessor、AutowiredAnnotationBeanPostProcessor 等),都是被 Spring 统一加载和管理
  • 通过Bean后置处理器,Spring能灵活地在不同场景调用不同后置处理器,比如 @PostConstruct,它的处理逻辑就要用到 CommonAnnotationBeanPostProcessor(继承自 InitDestroyAnnotationBeanPostProcessor)

Spring 初始化单例类的一般过程:

  • getBean()
  • doGetBean()
  • getSingleton()

若发现 Bean 不存在,则调用

createBean()=doCreateBean() 

进行实例化。

doCreateBean()

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
		throws BeanCreationException {
    // ...
	if (instanceWrapper == null) {
		// 1.
		instanceWrapper = createBeanInstance(beanName, mbd, args);
	}
	final Object bean = instanceWrapper.getWrappedInstance();

    // ...
    Object exposedObject = bean;
    try {
       // 2.
       populateBean(beanName, mbd, instanceWrapper);
       // 3.
       exposedObject = initializeBean(beanName, exposedObject, mbd);
    }
    catch (Throwable ex) {
    // ...
}

Bean 初始化关键步骤:

  1. 实例化 Bean
  2. 注入 Bean 依赖
  3. 初始化 Bean (例如执行 @PostConstruct 标记的方法 )

实例化Bean的createBeanInstance通过依次调用:

  • DefaultListableBeanFactory.instantiateBean()
  • SimpleInstantiationStrategy.instantiate()

最终执行到 BeanUtils.instantiateClass():

public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws BeanInstantiationException {
   Assert.notNull(ctor, "Constructor must not be null");
   try {
      ReflectionUtils.makeAccessible(ctor);
      return (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass()) ?
            KotlinDelegate.instantiateClass(ctor, args) : ctor.newInstance(args));
   }
   catch (InstantiationException ex) {
      throw new BeanInstantiationException(ctor, "Is it an abstract class?", ex);
   }
   // ...
}

最终调用 ctor.newInstance() 实例化用户定制类LightMgrService,而默认构造器在类实例化时被自动调用,Spring 也无法控制。

而此时负责自动装配的 populateBean 方法还没有执行,LightMgrService 的属性 LightService 还是 null,导致NPE。

修正

问题在于使用 @Autowired 直接标记在成员属性引发的装配行为发生在构造器执行后。
所以可通过如下方案解决:

构造器注入

当使用上述代码,构造器参数 LightService 会被自动注入LightService 的 Bean,从而在构造器执行时,避免NPE。

Spring 在类属性完成注入之后,会回调我们定义的初始化方法。即在 populateBean 方法之后,会调用

AbstractAutowireCapableBeanFactory#initializeBean

  • applyBeanPostProcessorsBeforeInitialization处理 @PostConstruct
  • invokeInitMethods处理InitializingBean 接口

两种不同的初始化方案的逻辑

applyBeanPostProcessorsBeforeInitialization与 @PostConstruct

applyBeanPostProcessorsBeforeInitialization 方法最终执行到
InitDestroyAnnotationBeanPostProcessor#buildLifecycleMetadata:

private LifecycleMetadata buildLifecycleMetadata(final Class<?> clazz) {
   // ...
   do {
      // ...
      final List<LifecycleElement> currDestroyMethods = new ArrayList<>();
      ReflectionUtils.doWithLocalMethods(targetClass, method -> {
      // initAnnotationType 即 PostConstruct.class
         if (this.initAnnotationType != null && method.isAnnotationPresent(this.initAnnotationType)) {
            LifecycleElement element = new LifecycleElement(method);
            currInitMethods.add(element);
  // ...      
}

在这个方法里,Spring 将遍历查找被 PostConstruct.class 注解过的方法,返回到上层,并最终调用此方法。

invokeInitMethods 与 InitializingBean 接口

给bean一个机会去响应现在它的所有属性都已设置,并有机会了解它拥有的bean工厂(这个对象)。 这意味着检查 bean 是否实现了 InitializingBean 或自定义了 init 方法。
若是,则调用必要的回调。

invokeInitMethods会判断当前 Bean 是否实现了 InitializingBean 接口,只有实现该接口时,Spring 才会调用该 Bean 的接口实现方法 afterPropertiesSet()。

还有两种方式:

init 方法 && @PostConstruct


实现 InitializingBean 接口,回调afterPropertiesSet()

对于本案例,后两种方案并非最优。
但在一些场景下,这两种方案各有所长。

2 意外触发 shutdown 方法

类销毁时,也容易写出一堆 bug。

LightService#shutdown,负责关灯:

之前的案例中,若宿管系统重启,灯是不会被关闭的。但随着业务变化,可能会去掉 @Service ,而使用另外一种产生 Bean 的方式:创建一个配置类 BeanConfiguration(标记 @Configuration)来创建一堆 Bean,其中就包含了创建 LightService 类型的 Bean,并将其注册到 Spring 容器:

让 Spring 启动完成后立马关闭当前 Spring 上下文,这就能模拟模拟宿管系统的启停:

以上代码没有其他任何方法的调用,仅是将所有符合约定的类初始化并加载到 Spring 容器,完成后再关闭当前 Spring 容器。
预期:运行后不会有任何log,只改变 Bean 的产生方式。

运行后,控制台打印:

显然 shutdown 方法未按照预期,被执行了,这就导致一个有意思的 bug:

  • 在使用新的 Bean 生成方式之前,每一次宿舍管理服务被重启时,宿舍里所有的灯都不会被关闭
  • 但修改后,只要服务重启,灯都被意外关闭

你能理解这个bug吗?

源码解析

发现:

  • 只有通过使用 Bean 注解注册到 Spring 容器的对象,才会在 Spring 容器被关闭时自动调用 shutdown
  • 使用 @Component将当前类自动注入到 Spring 容器时,shutdown 方法则不会被自动执行

可尝试到 Bean 注解类的代码中去寻找一些线索,可看到属性 destroyMethod。

使用 Bean 注解的方法所注册的 Bean 对象,如果用户不设置 destroyMethod 属性,则其属性值为 AbstractBeanDefinition.INFER_METHOD。
此时 Spring 会检查当前 Bean 对象的原始类中是否有名为 shutdown 或 close 的方法:

  • 有,此方法会被 Spring 记录下来,并在容器被销毁时自动执行
  • 没有,安然无事

查找 INFER_METHOD 枚举值的引用,很容易就找到了使用该枚举值的方法

DisposableBeanAdapter#inferDestroyMethodIfNecessary

private String inferDestroyMethodIfNecessary(Object bean, RootBeanDefinition beanDefinition) {
   String destroyMethodName = beanDefinition.getDestroyMethodName();
   if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) ||(destroyMethodName == null && bean instanceof AutoCloseable)) {
      if (!(bean instanceof DisposableBean)) {
         try {
            // 尝试查找 close 方法
            return bean.getClass().getMethod(CLOSE_METHOD_NAME).getName();
         }
         catch (NoSuchMethodException ex) {
            try {
               // 尝试查找 shutdown 方法
               return bean.getClass().getMethod(SHUTDOWN_METHOD_NAME).getName();
            }
            catch (NoSuchMethodException ex2) {
               // no candidate destroy method found
            }
         }
      }
      return null;
   }
   return (StringUtils.hasLength(destroyMethodName) ? destroyMethodName : null);
}

代码逻辑和
Bean 注解类中对于 destroyMethod 属性的注释:
完全一致。

  • destroyMethodName==INFER_METHOD&&当前类没有实现DisposableBean接口
    则先查找类的 close 方法:
    • 找不到
      就在抛出异常后继续查找 shutdown 方法
    • 找到
      则返回其方法名(close 或者 shutdown)

接着,继续逐级查找引用,最终得到的调用链从上到下为:

  • doCreateBean
  • registerDisposableBeanIfNecessary
  • registerDisposableBean(new DisposableBeanAdapter)
  • inferDestroyMethodIfNecessary

然后,我们追溯到了顶层的 doCreateBean:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
      throws BeanCreationException {
   // 实例化 bean
   if (instanceWrapper == null) {
      instanceWrapper = createBeanInstance(beanName, mbd, args);
   }
   // ...
   // 初始化 bean 实例.
   Object exposedObject = bean;
   try {
      populateBean(beanName, mbd, instanceWrapper);
      exposedObject = initializeBean(beanName, exposedObject, mbd);
   }
   // ...
   // Register bean as disposable.
   try {
      registerDisposableBeanIfNecessary(beanName, bean, mbd);
   }
   catch (BeanDefinitionValidationException ex) {
      throw new BeanCreationException(
            mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
   }

   return exposedObject;
}

doCreateBean 管理了Bean的整个生命周期中几乎所有的关键节点,直接负责了 Bean 对象的生老病死,其主要功能包括:

  • Bean 实例的创建
  • Bean 对象依赖的注入
  • 定制类初始化方法的回调
  • Disposable 方法的注册

接着,继续查看 registerDisposableBean:

public void registerDisposableBean(String beanName, DisposableBean bean) {
	synchronized (this.disposableBeans) {
		this.disposableBeans.put(beanName, bean);
	}
}

DisposableBeanAdapter 类(其属性destroyMethodName 记录了使用哪种 destory 方法)被实例化
并添加到 DefaultSingletonBeanRegistry#disposableBeans 属性内,disposableBeans 将暂存这些 DisposableBeanAdapter 实例,直到 AnnotationConfigApplicationContext#close被调用。

而当 AnnotationConfigApplicationContext#close被调用时,即当 Spring 容器被销毁时,最终会调用到 DefaultSingletonBeanRegistry#destroySingleton:

  • 遍历 disposableBeans 属性
  • 逐一获取 DisposableBean
  • 依次调用其 close 或 shutdown
public void destroySingleton(String beanName) {
   // Remove a registered singleton of the given name, if any.
   removeSingleton(beanName);
   // Destroy the corresponding DisposableBean instance.
   DisposableBean disposableBean;
   synchronized (this.disposableBeans) {
      disposableBean = (DisposableBean) this.disposableBeans.remove(beanName);
   }
   destroyBean(beanName, disposableBean);
}

案例调用了 LightService#shutdown 方法,将所有的灯关闭了。

修正

避免在Java类中定义一些带有特殊意义动词的方法来解决。

如果一定要定义名为 close 或者 shutdown 方法,可以将 Bean 注解内 destroyMethod 属性设置为空。如下:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanConfiguration {

    @Bean(destroyMethod="")
    public LightService getTransmission() {
        return new LightService();
    }
}

为什么 @Service 注入的 LightService,其 shutdown 方法不能被执行?想要执行,则必须要添加 DisposableBeanAdapter,而它的添加是有条件的:

protected void registerDisposableBeanIfNecessary(String beanName, Object bean, RootBeanDefinition mbd) {
   AccessControlContext acc = (System.getSecurityManager() != null ? getAccessControlContext() : null);
   if (!mbd.isPrototype() && requiresDestruction(bean, mbd)) {
      if (mbd.isSingleton()) {
         // Register a DisposableBean implementation that performs all destruction
         // work for the given bean: DestructionAwareBeanPostProcessors,
         // DisposableBean interface, custom destroy method.
         registerDisposableBean(beanName,
               new DisposableBeanAdapter(bean, beanName, mbd, getBeanPostProcessors(), acc));
      }
      else {
        //省略非关键代码
      }
   }
}

关键的语句在于:

!mbd.isPrototype() && requiresDestruction(bean, mbd)

案例代码修改前后,我们都是单例,所以区别仅在于是否满足requiresDestruction 条件。

DisposableBeanAdapter#hasDestroyMethod:

public static boolean hasDestroyMethod(Object bean, RootBeanDefinition beanDefinition) {
   if (bean instanceof DisposableBean || bean instanceof AutoCloseable) {
      return true;
   }
   String destroyMethodName = beanDefinition.getDestroyMethodName();
   if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName)) {
      return (ClassUtils.hasMethod(bean.getClass(), CLOSE_METHOD_NAME) ||
            ClassUtils.hasMethod(bean.getClass(), SHUTDOWN_METHOD_NAME));
   }
   return StringUtils.hasLength(destroyMethodName);
}
  • 如果使用 @Service 产生 Bean,则上述代码获取的destroyMethodName是 null
  • 使用 @Bean,默认值为AbstractBeanDefinition.INFER_METHOD,参考 Bean 定义:
public @interface Bean {
   //省略其他非关键代码
   String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;
}

@Service 标记的 LightService 也没有实现 AutoCloseable、DisposableBean,最终没有添加一个 DisposableBeanAdapter。所以最终我们定义的 shutdown 方法没有被调用。

总结

DefaultListableBeanFactory 类是 Spring Bean 的灵魂,核心就是其doCreateBean,掌控了 Bean 实例的创建、Bean 对象依赖的注入、定制类初始化方法的回调以及 Disposable 方法的注册等关键节点。

以上是关于Spring Bean生命周期你除了会背八股文面试,真的会用了吗?的主要内容,如果未能解决你的问题,请参考以下文章

一文读懂 Spring Bean 的生命周期

一文读懂 Spring Bean 的生命周期

如何记忆 Spring Bean 的生命周期

大厂高频面试题Spring Bean生命周期最详解

Java面试小短文Spring Bean生命周期的执行流程

Java面试小短文Spring Bean生命周期的执行流程