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

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

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

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

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

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

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

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

  • LightService 对象的原始类

1.png

预期现象:


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


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

image.png

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

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

构造器注入

1.png

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

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

AbstractAutowireCapableBeanFactory#initializeBean

image.png

  • 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()。

image.png

还有两种方式:

init 方法 && @PostConstruct

image.png

1.png

实现 InitializingBean 接口,回调afterPropertiesSet()

1.png

对于本案例,后两种方案并非最优。

但在一些场景下,这两种方案各有所长。

目录
相关文章
|
23天前
|
Java Spring
Spring面试题pro版-5
Spring面试题pro版-5
14 0
|
23天前
|
Java Spring
Spring面试题pro版-4
Spring面试题pro版-4
15 0
|
23天前
|
XML Java 数据格式
Spring面试题pro版-3
Spring面试题pro版-3
30 0
|
23天前
|
设计模式 安全 Java
Spring面试题系列-6
Spring面试题系列-6
22 1
|
23天前
|
XML 安全 Java
Spring面试题系列-5
Spring面试题系列-5
15 1
|
23天前
|
设计模式 Java 数据库
Spring面试题系列-4
Spring面试题系列-4
19 1
|
23天前
|
XML 缓存 Java
Spring面试题系列-2
Spring面试题系列-2
20 1
|
4天前
|
监控 Java 数据库连接
总结Spring Boot面试知识点
Spring Boot是一个基于Spring框架的开源项目,它简化了Spring应用的初始搭建以及开发过程。通过提供“约定优于配置”的方式,Spring Boot可以帮助开发者快速构建出生产级别的Spring应用。
12 0
|
12天前
|
消息中间件 安全 Java
在Spring Bean中,如何通过Java配置类定义Bean?
【4月更文挑战第30天】在Spring Bean中,如何通过Java配置类定义Bean?
20 1
|
15天前
|
前端开发 Java 数据格式
【Spring系列笔记】定义Bean的方式
在Spring Boot应用程序中,定义Bean是非常常见的操作,它是构建应用程序的基础。Spring Boot提供了多种方式来定义Bean,每种方式都有其适用的场景和优势。
32 2