• Spring常见问题解决 - 为啥shutdown()函数在程序关闭时候被自动调用?


    一. 函数在程序关闭时候被自动调用问题

    1.1 案例

    假设我们有一个天气预报业务,里面有个shutdown()函数:目的是如果调用了它,那么所有调用了天气业务的客户端都不能在使用这个功能。

    往往我们都是通过以下方式去写这么个Bean的。

    @Component
    public class WeatherService {
        public void shutdown() {
            System.out.println("通知所有调用方,天气预报业务下线!");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    假设我们有10太客户端,都使用了这个天气预报业务,那么我们模拟其中一台机器宕机:

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

    程序运行结果如下:
    在这里插入图片描述
    这样是没问题的。但是倘若将上述Bean的定义方式改为:记得去掉@Component注解

    public class WeatherService {
        public void shutdown() {
            System.out.println("通知所有调用方,天气预报业务下线!");
        }
    }
    
    @Configuration
    public class MyConfig {
        @Bean
        public WeatherService getWeatherService(){
            return new WeatherService();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    程序运行结果如下:
    在这里插入图片描述
    我去,一台机器挂了,竟然触发了我们自定义的shutdown函数,导致所有的客户端,一共10台,都无法再享用天气预报业务了。很明显,这不是我们想要的结果。因为我们自定义的shutdown函数,其调用时机应该由我们自己在业务上控制,而不是程序关闭的时候自动被执行了。

    1.2 原理分析

    首先,上述案例中,我们看到了两种Bean的定义方式:

    • @Component直接修饰。
    • @Configuration修饰的配置类中自定义@Bean

    而这种定义Bean方式的不同就导致了不同的结果。我们来分析下原因。

    因为咱们的案例中,对应的shutdown函数是被错误的执行的,而且发生时机在于程序的关闭。因此我们可以想到是否和Bean的生命周期有关?比如Bean的销毁:destroy操作。 那么我们来看下@Bean引入的Bean有什么骚操作,我们看下@Bean注解的源码,

    @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Bean {
    	// ...省略
    	/**
    	 * The optional name of a method to call on the bean instance upon closing the
    	 * application context, for example a {@code close()} method on a JDBC
    	 * {@code DataSource} implementation, or a Hibernate {@code SessionFactory} object.
    	 * The method must have no arguments but may throw any exception.
    	 * 

    As a convenience to the user, the container will attempt to infer a destroy * method against an object returned from the {@code @Bean} method. For example, given * an {@code @Bean} method returning an Apache Commons DBCP {@code BasicDataSource}, * the container will notice the {@code close()} method available on that object and * automatically register it as the {@code destroyMethod}. This 'destroy method * inference' is currently limited to detecting only public, no-arg methods named * 'close' or 'shutdown'. The method may be declared at any level of the inheritance * hierarchy and will be detected regardless of the return type of the {@code @Bean} * method (i.e., detection occurs reflectively against the bean instance itself at * creation time). *

    To disable destroy method inference for a particular {@code @Bean}, specify an * empty string as the value, e.g. {@code @Bean(destroyMethod="")}. Note that the * {@link org.springframework.beans.factory.DisposableBean} callback interface will * nevertheless get detected and the corresponding destroy method invoked: In other * words, {@code destroyMethod=""} only affects custom close/shutdown methods and * {@link java.io.Closeable}/{@link java.lang.AutoCloseable} declared close methods. *

    Note: Only invoked on beans whose lifecycle is under the full control of the * factory, which is always the case for singletons but not guaranteed for any * other scope. * @see org.springframework.beans.factory.DisposableBean * @see org.springframework.context.ConfigurableApplicationContext#close() */ String destroyMethod() default AbstractBeanDefinition.INFER_METHOD; }

    • 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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    我们来借用有道翻译,来瞅瞅其中这么一段话:
    在这里插入图片描述
    但是恕我直言,这个翻译真的不咋滴,用我个人的话就是:通过@Bean方式修饰的类,如果这个类中含有名称为close或者shutdown的无参函数。那么容器就会将他们视为一种销毁方法。 (注意,光同名还不行,还要无参数)

    那么根据源码,我们知道,如果我们没有手动定义destroyMethod属性,那么这个destroyMethod()这个属性值就是AbstractBeanDefinition.INFER_METHOD。那么我们全局搜下这个引用,最终在DisposableBeanAdapter中找到了:

    class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
    	private String inferDestroyMethodIfNecessary(Object bean, RootBeanDefinition beanDefinition) {
    		String destroyMethodName = beanDefinition.getDestroyMethodName();
    		if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) ||
    				(destroyMethodName == null && bean instanceof AutoCloseable)) {
    			// Only perform destroy method inference or Closeable detection
    			// in case of the bean not explicitly implementing 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
    • 27

    那么这段代码究竟是哪里调用的呢?我们来说下老生常谈的东西:Bean的创建。Bean的创建我们都知道有三个步骤:

    • 构建实例。
    • 属性注入。
    • 初始化。

    而在这之后其实还有一个步骤就和本案例有关了:

    registerDisposableBeanIfNecessary(beanName, bean, mbd);
    
    • 1

    这段代码主要执行的是Disposable方法的注册。

    public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
    	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()) {
    				// 记录当前Bean使用了哪一种destory方法,这些方法将会在程序close的时候调用
    				// 在这里是AnnotationConfigApplicationContext.close()。
    				registerDisposableBean(beanName,
    						new DisposableBeanAdapter(bean, beanName, mbd, getBeanPostProcessors(), acc));
    			}
    			// ...
    		}
    	}
    }
    
    public void registerDisposableBean(String beanName, DisposableBean bean) {
    	synchronized (this.disposableBeans) {
    		this.disposableBeans.put(beanName, bean);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    请记住最后的this.disposableBeans。而咱们的AnnotationConfigApplicationContext.close()函数,一个个调用链走下去,最终到达的地方是:

    AnnotationConfigApplicationContext.close()
    ↓↓↓↓↓↓↓↓↓↓↓↓
    public void destroySingletons() {
    	if (logger.isTraceEnabled()) {
    		logger.trace("Destroying singletons in " + this);
    	}
    	synchronized (this.singletonObjects) {
    		this.singletonsCurrentlyInDestruction = true;
    	}
    
    	String[] disposableBeanNames;
    	synchronized (this.disposableBeans) {
    		disposableBeanNames = StringUtils.toStringArray(this.disposableBeans.keySet());
    	}
    	for (int i = disposableBeanNames.length - 1; i >= 0; i--) {
    		destroySingleton(disposableBeanNames[i]);
    	}
    
    	this.containedBeanMap.clear();
    	this.dependentBeanMap.clear();
    	this.dependenciesForBeanMap.clear();
    
    	clearSingletonCache();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    可见这里复用了this.disposableBeans,里面保存的是Bean在创建过程中,注册的销毁方法。然后一个个去执行。因此我们程序关闭的时候,我们自定义的shutdown函数被执行了。

    1.3 解决

    方案一:将对应的destroyMethod属性设置为空。避免其被赋值为默认的AbstractBeanDefinition.INFER_METHOD

    @Configuration
    public class MyConfig {
        @Bean(destroyMethod = "")
        public WeatherService getWeatherService(){
            return new WeatherService();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    方案二:养成良好的编码习惯,避免命名函数或者字段的时候,使用这种带有特殊含义的名称。 这里就和我的另一篇文章Spring常见问题解决 - @Value注解注入的值出错了?很像了。

    • 方法名不要随意使用shutdown
    • 自定义配置文件中的变量名不要使用username或者user.name等等。

    二. 为何@Component方式注入的Bean没有自动执行shutdown

    根据我们知道,对于Disposable方法的注册,是有一定条件需要满足的。

    public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
    	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()) {
    				// 记录当前Bean使用了哪一种destory方法,这些方法将会在程序close的时候调用
    				// 在这里是AnnotationConfigApplicationContext.close()。
    				registerDisposableBean(beanName,
    						new DisposableBeanAdapter(bean, beanName, mbd, getBeanPostProcessors(), acc));
    			}
    			// ...
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    我们看下requiresDestruction()函数:

    protected boolean requiresDestruction(Object bean, RootBeanDefinition mbd) {
    	return (bean.getClass() != NullBean.class &&
    			(DisposableBeanAdapter.hasDestroyMethod(bean, mbd) || (hasDestructionAwareBeanPostProcessors() &&
    					DisposableBeanAdapter.hasApplicableProcessors(bean, getBeanPostProcessors()))));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 如果我们使用@Bean方式来注入Bean,并且不显式地指定对应的destroyMethod方法。
    2. 那么会自动将该类下与closeshutdown同名的无参方法视为这个Bean在生命周期结束时候执行的销毁方法。
    3. 因此上面DisposableBeanAdapter.hasDestroyMethod这个判断为true

    我们再来看下hasDestroyMethod函数:

    public static boolean hasDestroyMethod(Object bean, RootBeanDefinition beanDefinition) {
    	if (bean instanceof DisposableBean || bean instanceof AutoCloseable) {
    		return true;
    	}
    	// 如果使用的是@Bean(),依据本文案例的话,这里的destroyMethodName就是AbstractBeanDefinition.INFER_METHOD
    	// 如果使用的是其他方式注入的,这里的destroyMethodName就是null
    	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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    那么,如果我们想要这样的Bean在程序关闭的时候也执行对应的shutdown函数咋办?

    @Component
    public class WeatherService {
        public void shutdown() {
            System.out.println("通知所有调用方,天气预报业务下线!");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们关注下上文的hasDestroyMethod函数中这么一行代码:

    if (bean instanceof DisposableBean || bean instanceof AutoCloseable) {
    	return true;
    }
    
    • 1
    • 2
    • 3

    如果BeanAutoCloseable类型的也可以让其执行shutdown。那么我们只需要对程序做出以下更改:

    @Component
    public class WeatherService implements Closeable {
        public void shutdown() {
            System.out.println("通知所有调用方,天气预报业务下线!");
        }
    
        @Override
        public void close() throws IOException {
    		shutdown();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    可以得到相同的结果:
    在这里插入图片描述
    这是因为CloseableAutoCloseable的一个子类:

    public interface Closeable extends AutoCloseable {}
    
    • 1

    2.1 总结

    1. 如果使用默认的@Bean()引入某个类,注意,这里没有写什么参数。那么如果这个类中有和close、shutdown同名的函数。在程序关闭的时候会被自动执行。
    2. 其他通过@Component、@Service等方式注入的类,一般不会有这样的情况。如果希望实现同等效果,可以实现Closeable接口,完成close()方法的具体实现。
    3. 因为程序关闭的时候,判断是否有销毁函数,是根据这个Bean的类型来的。if (bean instanceof DisposableBean || bean instanceof AutoCloseable)。除此之外,就看这个BeandestroyMethodName是否为AbstractBeanDefinition.INFER_METHOD

    那是因为:

    public @interface Bean {
    	String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;
    }
    
    • 1
    • 2
    • 3
  • 相关阅读:
    【Java】封装的实现,访问限定符、包
    ubuntu 20.04 设置 authorized_keys 让 VS Code ssh 远程免密连接
    RTOS的基本概念与线程基础知识
    Learn Prompt- Midjourney案例:Logo设计
    C语言变量的定义和赋值
    JSON详解
    MySQL的事务使用
    登录(认证)常见安全问题与测试方法汇总
    基变换与矩阵对角化
    使用ollama + AnythingLLM快速且简单的在本地部署llama3
  • 原文地址:https://blog.csdn.net/Zong_0915/article/details/126440729