Day605.Bean生命周期错误问题 -Spring编程常见错误

Posted 阿昌喜欢吃黄桃

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day605.Bean生命周期错误问题 -Spring编程常见错误相关的知识,希望对你有一定的参考价值。

SpringBean生命周期错误问题

当我们在使用Spring框架时,必然会涉及到@Autowired@Bean@Component等这些对SpringIoc容器进行注入bean的注解方案。

当我们不了解Spring的生命周期时,在一些特定的场景,需要达成一定对应需求的时候就会出现问题,如:注入的时机不对导致的空指针


一、构造器内抛空指针异常

现在给出如下代码案例所需要实现的需求:我们需要在LightMgrService类的构造器中,去执行LightMgrService类中一个成员属性的方法

@Component
public class LightMgrService 
  @Autowired
  private LightService lightService;
  public LightMgrService() 
    lightService.check();
  

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

@Service
public class LightService 
    public void start() 
        System.out.println("turn on all lights");
    
    public void shutdown() 
        System.out.println("turn off all lights");
    
    public void check() 
        System.out.println("check all lights");
    

从整个案例代码实现来看,我们的期待是在 LightMgrService 初始化过程中,LightService 因为标记为 @Autowired,所以能被自动装配好;然后在 LightMgrService 的构造器执行中,LightService 的 shutdown() 方法能被自动调用;最终打印出 check all lights

当我们去启动SpringIoc容器的使用,所出现的结果并不是我们所想的结果,而是出现了空指针异常


那为什么会这样子呢????


首先需要知道这个问题为什么出现,我们肯定需要对Spring的生命周期执行流程进一步的了解

那如上图,整体的流程分为三个步骤

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

针对上面的问题,我们应该关注的是第三部,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. 实例化 Bean
    instanceWrapper = createBeanInstance(beanName, mbd, args);
  
  final Object bean = instanceWrapper.getWrappedInstance();
    //省略非关键代码
    Object exposedObject = bean;
    try 
    	//2. 注入 Bean 依赖
       populateBean(beanName, mbd, instanceWrapper);
       //3. 初始化
       exposedObject = initializeBean(beanName, exposedObject, mbd);
    
    catch (Throwable ex) 
    //省略非关键代码

上述代码完整地展示了 Bean 初始化的三个关键步骤。

按执行顺序分别是createBeanInstancepopulateBean,以及 initializeBean

分别对应

  • 实例化 Bean
  • 注入 Bean 依赖
  • 初始化 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);
      //当前的语言并非 Kotlin ,所以最后会走到 ctor.newInstance(args)
      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);
   
   //省略非关键代码

这里因为当前的语言并非 Kotlin,所以最终将调用 ctor.newInstance() 方法实例化用户定制类 LightMgrService

默认构造器显然是在类实例化的时候被自动调用的,Spring 也无法控制。而此时负责自动装配的 populateBean 方法还没有被执行,LightMgrService 的属性 LightService 还是 null,因而得到空指针异常也在情理之中

那么如何解决上面的问题呢???


问题的根源,就是在于使用 @Autowired 直接标记在成员属性上而引发的装配行为是发生在构造器执行之后的。

所以这里我们可以通过下面这种修订方法来纠正这个问题:

@Component
public class LightMgrService 
    private LightService lightService;
    //通过构造器注入,让他在进行默认构造器类实例化的时候就去ioc容器中寻找个bean,并进行注入
    public LightMgrService(LightService lightService) 
        this.lightService = lightService;
        lightService.check();
    

另外,除了这种纠正方式,有没有别的方式?


实际上,Spring 在类属性完成注入之后,会回调用户定制的初始化方法。

即在 populateBean 方法之后,会调用 initializeBean 方法,那代表我们可以在装配结束后,在进行执行我们需要的方法,来看一下它的关键代码:

protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) 
   //省略非关键代码 
   if (mbd == null || !mbd.isSynthetic()) 
   	//处理标有@PostConstruct的方法
      wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
   
   try 
   	//处理实现InitializingBean接口并实现afterPropertiesSet方法
      invokeInitMethods(beanName, wrappedBean, mbd);
   
   //省略非关键代码 

  • applyBeanPostProcessorsBeforeInitialization 与 @PostConstruct

applyBeanPostProcessorsBeforeInitialization 方法最终执行到后置处理器 InitDestroyAnnotationBeanPostProcessor 的 buildLifecycleMetadata 方法(CommonAnnotationBeanPostProcessor 的父类):

private LifecycleMetadata buildLifecycleMetadata(final Class<?> clazz) 
   //省略非关键代码 
   do 
      //省略非关键代码
      final List<LifecycleElement> currDestroyMethods = new ArrayList<>();
      ReflectionUtils.doWithLocalMethods(targetClass, method -> 
      //此处的 this.initAnnotationType 值,即为 PostConstruct.class
         if (this.initAnnotationType != null && method.isAnnotationPresent(this.initAnnotationType)) 
            LifecycleElement element = new LifecycleElement(method);
            currInitMethods.add(element);
  //非关键代码          

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

  • invokeInitMethods 与 InitializingBean 接口

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

protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd)
      throws Throwable 
   boolean isInitializingBean = (bean instanceof InitializingBean);
   if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) 
      // 省略非关键代码 
      else 
         ((InitializingBean) bean).afterPropertiesSet();
      
   
   // 省略非关键代码 
 

知道了上面的关系后,那我们就有了解决方案:

  • 实现InitializingBean接口重写afterPropertiesSet()方法,在里面执行我们需要的方法,因为此时已经执行过populateBean (),代表已经装配完成,所以就不会null,如下举例:
    @Component
    public class LightMgrService implements InitializingBean 
        @Autowired
        private LightService lightService;
      
        @Override
        public void afterPropertiesSet() throws Exception 
            lightService.check();
        
    
    
  • 创建一个方法,并指明上面被@PostConstruct注解修饰,添加 init 方法,并且使用 PostConstruct 注解进行修饰:如下举例
    @Component
    public class LightMgrService 
      @Autowired
      private LightService lightService;
      @PostConstruct
      public void init() 
           lightService.check();
      
    
    

二、意外触发 shutdown 方法

沿用之前的场景。这里我们可以简单复习一下 LightService 的实现,它包含了 shutdown 方法,负责关闭所有的灯,关键代码如下:

@Service
public class LightService 
  //省略其他非关键代码
  public void shutdown()
    System.out.println("shutting down all lights");
  
  //省略其他非关键代码

随着业务的需求变化,我们可能会去掉 @Service 注解,而是使用另外一种产生 Bean 的方式:创建一个配置类 BeanConfiguration(标记 @Configuration)来创建一堆 Bean,其中就包含了创建 LightService 类型的 Bean,并将其注册到 Spring 容器:

@Configuration
public class BeanConfiguration 
    @Bean
    public LightService getTransmission()
        return new LightService();
    

Spring 启动完成后立马关闭当前 Spring 上下文。这样等同于模拟系统的启停:

@SpringBootApplication
public class Application 
    public static void main(String[] args) 
        ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
        context.close();
    

以上代码没有其他任何方法的调用,仅仅是将所有符合约定的类初始化并加载到 Spring 容器,完成后再关闭当前的 Spring 容器。

按照预期,这段代码运行后不会有任何的 log 输出,毕竟我们只是改变了 Bean 的产生方式。

但实际运行这段代码后,我们可以看到控制台上打印了 shutting down all lights。

显然 shutdown 方法未按照预期被执行了,这导致一个很有意思的 bug:在使用新的 Bean 生成方式之前,每一次服务被重启时,宿舍里所有的灯都不会被关闭。但是修改后,只有服务重启,灯都被意外关闭了。如何理解这个 bug?


  • @Configuration + @Bean

使用 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 
   //省略非关键代码 
   if (instanceWrapper == null) 
 	  	//Bean 实例的创建
      instanceWrapper = createBeanInstance(beanName, mbd, args);
   
   //省略非关键代码
   // Initialize the bean instance.
   Object exposedObject = bean;
   try 
	   //Bean 对象依赖的注入
      populateBean(beanName, mbd, instanceWrapper);
      //定制类初始化方法的回调
      exposedObject = initializeBean(beanName, exposedObject, mbd);
   
   //省略非关键代码 
   // Register bean as disposable.
   try 
  	 //Disposable 方法的注册
      registerDisposableBeanIfNecessary(beanName, bean, mbd);
   
   catch (BeanDefinitionValidationException ex) 
      throw new BeanCreationException(
            mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
   

   return exposedObject;

到这,我们就可以对 doCreateBean 方法做一个小小的总结了。可以说 doCreateBean 管理了 Bean 的整个生命周期中几乎所有的关键节点,直接负责了 Bean 对象的生老病死,其主要功能包括:

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

继续查看 registerDisposableBean 方法:

public void registerDisposableBean(String beanName, DisposableBean bean) 
   //省略其他非关键代码
   synchronized (this.disposableBeans) 
      this.disposableBeans.put(beanName, bean);
   
   //省略其他非关键代码

此方法将遍历 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 方法,将所有的灯关闭了。


那知道了这些,我们就知道了为什么我们上面的方法并没有被某个引用调用,但是为什么会被执行了!!!

那就是Spring自动根据对应特定的方法名进行执行了,那我们只需要避免在 Java 类中定义一些带有特殊意义动词的方法来解决,当然如果一定要定义名为 close 或者 shutdown 方法,也可以通过将 Bean 注解内 destroyMethod 属性设置为空的方式来解决这个问题。

第一种修改方式比较简单,所以这里只展示第二种修改方式,代码如下:

@Configuration
public class BeanConfiguration 
    @Bean(destroyMethod="")
    public LightService getTransmission()
        return new LightService();
    

以上是针对@Configuration + @Bean 的方案,Spring就会找对应特定的方法进行执行


不过说到这里,你也可能还是会疑惑,为什么 @Service 注入的 LightService,其 shutdown 方法不能被执行

想要执行,则必须要添加 DisposableBeanAdapter,而它的添加是有条件的:

protected void registerDisposableBeanIfNecessary(String beanName, Object bean, RootBeanDefinition mbd) 
   AccessControlContext acc = (System.getSecurityManager() != null ? getAccessControlContext() : null);
   //条件是:它是单例的 && requiresDestruction()返回true
   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 方法没有被调用。


那最后一个思考,当我们不在 @Configuration 注解类中使用 Bean 方法将其注入 Spring 容器,而是坚持使用 @Service 将其自动注入到容器,同时实现 Closeable 接口,代码如下: 使用@Service方案+实现Closeable 接口

@Service
public class LightService implements Closeable 
    public void close() 
        System.out.println("turn off all lights);
    
    //省略非关键代码

接口方法 close() 也会在 Spring 容器被销毁的时候自动执行么???


显示,是会的。那为什么呢?

=关键在意这里的判断,当这个bean实现了Closeable 接口,那就会instanceof AutoCloseable 就返回true,整个方法就会返回true了,所以他就会告诉spring容器说这个方法是有DestoryMethod的。

以上是关于Day605.Bean生命周期错误问题 -Spring编程常见错误的主要内容,如果未能解决你的问题,请参考以下文章

Day19_06_Vue教程之Vue实例的生命周期

activity的生命周期(day02)

day38 09-Spring类的完整生命周期及后处理Bean

Day829.Java线程的生命周期 -Java 并发编程实战

Day829.Java线程的生命周期 -Java 并发编程实战

Day353.类的加载过程(类的生命周期) -JVM