• Spring编程常见错误50例-Spring Bean生命周期常见错误


    构造器内抛空指针异常

    问题

    下面代码中,LightMgrService管理LightService控制宿舍灯的开启和关闭,让LightMgrService初始化时能够自动调用LightServicecheck方法来检查所有宿舍灯的电路是否正常

    @Component
    public class LightMgrService {
        @Autowired
        private LightService lightService;
    
        public LightMgrService() {
            lightService.check();
        }
    }
    
    @Service
    public class LightService {
        ...
        public void check() {
            System.out.println("check all lights");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    但是当启动项目后出现NullPointerException

    原因

    先了解Spring类初始化过程:

    在这里插入图片描述

    • 将一些必要的系统类(如Bean的后置处理器类)注册到Spring容器

    • 将这些后置处理器实例化,并注册到Spring的容器中

    • 实例化所有用户定制类,调用后置处理器进行辅助装配、类初始化等

      • 即Spring初始化单例类的一般过程,基本都是getBean()- >doGetBean()->getSingleton(),如Bean不存在则调用createBean()->doCreateBean()进行实例化,下面为doGetBean方法:
      // AbstractAutowireCapableBeanFactory#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);
          }
          final Object bean = instanceWrapper.getWrappedInstance();
          
          ...
          Object exposedObject = bean;
          try {
              // 注入Bean依赖
              populateBean(beanName, mbd, instanceWrapper);
              // 初始化Bean,如执行@PostConstruct标记的方法
              exposedObject = initializeBean(beanName, exposedObject, mbd);
          }
          catch (Throwable ex) {
              ...
          }
          ...
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 用来实例化Bean的createBeanInstance方法最终执行到instantiateClass方法:
      // 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);
              if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass())) {
                  return KotlinDelegate.instantiateClass(ctor, args);
              }
              else {
                  Class<?>[] parameterTypes = ctor.getParameterTypes();
                  Assert.isTrue(args.length <= parameterTypes.length, "Can't specify more arguments than constructor parameters");
                  Object[] argsWithDefaultValues = new Object[args.length];
                  for (int i = 0 ; i < args.length; i++) {
                      if (args[i] == null) {
                          Class<?> parameterType = parameterTypes[i];
                          argsWithDefaultValues[i] = (parameterType.isPrimitive() ? DEFAULT_TYPE_VALUES.get(parameterType) : null);
                      }
                      else {
                          argsWithDefaultValues[i] = args[i];
                      }
                  }
                  // 最终执行该方法
                  return ctor.newInstance(argsWithDefaultValues);
              }
          }
          ...
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27

    instantiateClass方法进行分析后可看出,默认构造器是在类实例化的时候被自动调用,Spring无法控制,此时负责自动装配populateBean方法还没执行,所以LightMgrService的属性LightService还是null

    解决方式

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

    // 构造器参数LightService会被自动注入LightService的Bean,从而在构造器执行时不会出现空指针
    private LightService lightService;
    
    public LightMgrService(LightService lightService) {
        this.lightService = lightService;
        lightService.check();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 方案2:添加init方法,并使用PostConstruct注解进行修饰
    @Autowired
    private LightService lightService;
    
    @PostConstruct
    public void init() {
        lightService.check();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 方案3:实现InitializingBean接口,并在其afterPropertiesSet方法中执行初始化代码
    @Component
    public class LightMgrService implements InitializingBean {
        @Autowired
    	private LightService lightService;
    
        @Override
        public void afterPropertiesSet() throws Exception {
            lightService.check();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    对于方案2和3进行以下分析:

    • Spring在类属性完成注入后会回调用户定制的初始化方法,即在populateBean方法后会调用initializeBean方法:
    // AbstractAutowireCapableBeanFactory#initializeBean
    protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
        ...
        // applyBeanPostProcessorsBeforeInitialization和invokeInitMethods分别处理@PostConstruct注解和InitializingBean接口的逻辑
        if (mbd == null || !mbd.isSynthetic()) {
            wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
        }
    
        try {
            invokeInitMethods(beanName, wrappedBean, mbd);
        }
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 分析applyBeanPostProcessorsBeforeInitialization@PostConstruct
    // InitDestroyAnnotationBeanPostProcessor#buildLifecycleMetadata(applyBeanPostProcessorsBeforeInitialization最终执行到这)
    private LifecycleMetadata buildLifecycleMetadata(final Class<?> clazz) {
        ...
        do {
            final List<LifecycleElement> currInitMethods = new ArrayList<>();
            final List<LifecycleElement> currDestroyMethods = new ArrayList<>();
    
            ReflectionUtils.doWithLocalMethods(targetClass, method -> {
                // 此处的this.initAnnotationType值为PostConstruct.class
                // Spring将遍历查找被PostConstruct.class注解过的方法,返回到上层并最终调用此方法
                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);
                    }
                }
                ...
            });
    
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 分析invokeInitMethodsInitializingBean接口
    // AbstractAutowireCapableBeanFactory#invokeInitMethods
    protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd)
        throws Throwable {
    	// 判断当前Bean是否实现了InitializingBean接口,实现了该接口才会调用该Bean的接口实现方法afterPropertiesSet()
        boolean isInitializingBean = (bean instanceof InitializingBean);
        if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
            ...
            else {
                ((InitializingBean) bean).afterPropertiesSet();
            }
        }
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    意外触发shutdown方法

    问题

    将上面的案例进行改造,去除LightService@Service注解,使用配置类BeanConfiguration来创建并注册LightService类型的Bean(配置类就是用于创建一堆的Bean):

    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");
        }
    }
    
    @Configuration
    public class BeanConfiguration {
        @Bean
        public LightService getTransmission(){
            return new LightService();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    同时在修改启动类,让Spring启动后立马关闭当前Spring的上下文:

    @SpringBootApplication
    public class Application {
        public static void main(String[] args) {
            ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
            context.close();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    按照预期代码运行后不会有任何输出,但实际却打印了shutting down all lights,即执行了shutDown方法

    原因

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

    • 使用@Bean的方法所注册的Bean对象,如果用户不设置destroyMethod属性,其属性值为AbstractBeanDefinition.INFER_METHOD(方案2的依据)
    • Spring会检查当前Bean对象的原始类中是否有名为shutdownclose方法
    • 如果有此方法会被Spring记录下来,并在容器被销毁时自动执行
    // DisposableBeanAdapter#inferDestroyMethodIfNecessary
    private String inferDestroyMethodIfNecessary(Object bean, RootBeanDefinition beanDefinition) {
        String destroyMethodName = beanDefinition.getDestroyMethodName();
        // 如果destroyMethodName=INFER_METHOD
        if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) ||
            (destroyMethodName == null && bean instanceof AutoCloseable)) {
            // 且没实现DisposableBean接口
            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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    inferDestroyMethodIfNecessary方法的调用链如下:

    doCreateBean- >registerDisposableBeanIfNecessary->registerDisposableBean(new DisposableBeanAdapter)->inferDestroyMethodIfNecessary

    • 分析doCreateBean方法,它包含了Bean的整个生命周期几乎所有的关键点-Bean实例的创建、Bean对象依赖的注入、定制类初始化方法的回调、Disposable方法的注册
    // AbstractAutowireCapableBeanFactory#doCreateBean
    protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
        throws BeanCreationException {
        ...
        if (instanceWrapper == null) {
            instanceWrapper = createBeanInstance(beanName, mbd, args);
        }
        ...
        // Initialize the bean instance.
        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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 分析registerDisposableBean方法:disposableBeans将暂存DisposableBeanAdapter实例,直到AnnotationConfigApplicationContextclose方法被调用
    // DefaultSingletonBeanRegistry#registerDisposableBean
    public void registerDisposableBean(String beanName, DisposableBean bean) {
        synchronized (this.disposableBeans) {
            this.disposableBeans.put(beanName, bean);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • AnnotationConfigApplicationContextclose方法被调用时(即Spring容器被销毁时),最终会调用destroySingleton方法:遍历disposableBeans 属性获取DisposableBean,并调用其close方法或shutdown方法
    // DefaultSingletonBeanRegistry#destroySingleton
    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    为什么@Service注入的LightServiceshutdown方法没有被调用?因为该注解标记的LightService没实现 AutoCloseableDisposableBean,也没添加DisposableBeanAdapter

    • 想要执行shutdown方法须添加DisposableBeanAdapter,而添加是有条件的:
    // AbstractBeanFactory#registerDisposableBeanIfNecessary
    protected void registerDisposableBeanIfNecessary(String beanName, Object bean, RootBeanDefinition mbd) {
        AccessControlContext acc = (System.getSecurityManager() != null ? getAccessControlContext() : null);
        // ①确定是否是单例 ②确定是否在需要在关闭时销毁(这一步返回了false)
        if (!mbd.isPrototype() && requiresDestruction(bean, mbd)) {
            if (mbd.isSingleton()) {
                registerDisposableBean(beanName,
                                       new DisposableBeanAdapter(bean, beanName, mbd, getBeanPostProcessors(), acc));
            }
            else {
               ...
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 由于该案例都是单例,所以仅需考虑是否在需要在关闭时销毁:
    // AbstractBeanFactory#requiresDestruction
    protected boolean requiresDestruction(Object bean, RootBeanDefinition mbd) {
        return (bean.getClass() != NullBean.class &&
                // 关键方法
                (DisposableBeanAdapter.hasDestroyMethod(bean, mbd) || 
                 (hasDestructionAwareBeanPostProcessors() &&
                  DisposableBeanAdapter.hasApplicableProcessors(bean, getBeanPostProcessors()))));
    }
    
    // DisposableBeanAdapter#hasDestroyMethod
    public static boolean hasDestroyMethod(Object bean, RootBeanDefinition beanDefinition) {
        // *使用@Service产生Bean时,类并没有实现AutoCloseable、DisposableBean
        if (bean instanceof DisposableBean || bean instanceof AutoCloseable) {
            return true;
        }
        String destroyMethodName = beanDefinition.getDestroyMethodName();
        // *使用@Service产生Bean时destroyMethodName是null,而使用使@Bean默认值为AbstractBeanDefinition.INFER_METHOD
        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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    解决方式
    • 方案1:避免在Java类中定义带有特殊意义动词的方法,即不使用shutdown进行命名
    • 方案2:将Bean注解内destroyMethod属性设置为空的方式
    @Configuration
    public class BeanConfiguration {
       @Bean(destroyMethod="")
        public LightService getTransmission(){
            return new LightService();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    参考

    极客时间-Spring 编程常见错误 50 例

    https://github.com/jiafu1115/springissue/tree/master/src/main/java/com/spring/puzzle/class4

  • 相关阅读:
    【Git报错】SSL certificate problem: unable to get local issuer certificate
    React Native 入门(三)——js与native互相通信
    【毕业设计】基于java+swing+GUI的雷电游戏GUI设计与实现(毕业论文+程序源码)——雷电游戏
    备战9月,美团50道软件测试经典面试题及答案汇总
    雾天行人车辆检测
    淘宝/天猫API:item_cat_get-获得淘宝商品类目
    DialogFragment方便地完成自定义弹窗
    前端实现在线预览Word文件
    Linux:Ubuntu20,22,Debian10,Fedora36,CentOS9 将硬件时钟设为本地 笔记221111
    搭建ftp服务器注意事项
  • 原文地址:https://blog.csdn.net/qq_41398418/article/details/133295370