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

image

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

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

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

类初始化时的常见 bug。构建宿舍管理系统时,有 LightMgrService 来管理 LightService,控制宿舍灯的开启和关闭。

现在期望在 LightMgrService 初始化时自动调用 LightService#check检查所有宿舍灯的电路是否正常:

image

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

  • LightService 对象的原始类
image

预期现象:

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

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

image

1.1 源码解析

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

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

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 直接标记在成员属性引发的装配行为发生在构造器执行后。

所以可通过如下方案解决:

构造器注入

image

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

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

AbstractAutowireCapableBeanFactory#initializeBean

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

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

applyBeanPostProcessorsBeforeInitialization与 @PostConstruct

applyBeanPostProcessorsBeforeInitialization 方法最终执行到

InitDestroyAnnotationBeanPostProcessor#buildLifecycleMetadata:

applyBeanPostProcessorsBeforeInitialization处理 @PostConstruct
invokeInitMethods处理InitializingBean 接口
两种不同的初始化方案的逻辑

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

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

invokeInitMethods 与 InitializingBean 接口

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

若是,则调用必要的回调。

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

image

还有两种方式:

init 方法 && @PostConstruct

image
image

实现 InitializingBean 接口,回调afterPropertiesSet()

image

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

2 意外触发 shutdown 方法

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

LightService#shutdown,负责关灯:

image

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

image

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

image

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

运行后,控制台打印:

image

显然 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;
}

总结

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

作者:JavaEdge.
原文链接:https://blog.csdn.net/qq_33589510/article/details/120275701

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

推荐阅读更多精彩内容