Spring常见问题解决 - 为啥shutdown函数在程序关闭时候被自动调用?
一. 函数在程序关闭时候被自动调用问题
1.1 案例
假设我们有一个天气预报业务,里面有个shutdown()
函数:目的是如果调用了它,那么所有调用了天气业务的客户端都不能在使用这个功能。
往往我们都是通过以下方式去写这么个Bean
的。
@Component
public class WeatherService {
public void shutdown() {
System.out.println("通知所有调用方,天气预报业务下线!");
}
}
假设我们有10太客户端,都使用了这个天气预报业务,那么我们模拟其中一台机器宕机:
@SpringBootApplication()
public class Main8080 {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Main8080.class, args);
context.close();
}
}
程序运行结果如下:
这样是没问题的。但是倘若将上述Bean
的定义方式改为:记得去掉@Component
注解
public class WeatherService {
public void shutdown() {
System.out.println("通知所有调用方,天气预报业务下线!");
}
}
@Configuration
public class MyConfig {
@Bean
public WeatherService getWeatherService(){
return new WeatherService();
}
}
程序运行结果如下:
我去,一台机器挂了,竟然触发了我们自定义的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.
* <p>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).
* <p>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.
* <p>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;
}
我们来借用有道翻译,来瞅瞅其中这么一段话:
但是恕我直言,这个翻译真的不咋滴,用我个人的话就是:通过@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);
}
}
那么这段代码究竟是哪里调用的呢?我们来说下老生常谈的东西:Bean
的创建。Bean
的创建我们都知道有三个步骤:
- 构建实例。
- 属性注入。
- 初始化。
而在这之后其实还有一个步骤就和本案例有关了:
registerDisposableBeanIfNecessary(beanName, bean, mbd);
这段代码主要执行的是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);
}
}
请记住最后的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();
}
可见这里复用了this.disposableBeans
,里面保存的是Bean在创建过程中,注册的销毁方法。然后一个个去执行。因此我们程序关闭的时候,我们自定义的shutdown
函数被执行了。
1.3 解决
方案一:将对应的destroyMethod
属性设置为空。避免其被赋值为默认的AbstractBeanDefinition.INFER_METHOD
。
@Configuration
public class MyConfig {
@Bean(destroyMethod = "")
public WeatherService getWeatherService(){
return new WeatherService();
}
}
方案二:养成良好的编码习惯,避免命名函数或者字段的时候,使用这种带有特殊含义的名称。 这里就和我的另一篇文章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));
}
// ...
}
}
}
我们看下requiresDestruction()
函数:
protected boolean requiresDestruction(Object bean, RootBeanDefinition mbd) {
return (bean.getClass() != NullBean.class &&
(DisposableBeanAdapter.hasDestroyMethod(bean, mbd) || (hasDestructionAwareBeanPostProcessors() &&
DisposableBeanAdapter.hasApplicableProcessors(bean, getBeanPostProcessors()))));
}
- 如果我们使用
@Bean
方式来注入Bean
,并且不显式地指定对应的destroyMethod
方法。 - 那么会自动将该类下与
close
和shutdown
同名的无参方法视为这个Bean
在生命周期结束时候执行的销毁方法。 - 因此上面
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);
}
那么,如果我们想要这样的Bean
在程序关闭的时候也执行对应的shutdown
函数咋办?
@Component
public class WeatherService {
public void shutdown() {
System.out.println("通知所有调用方,天气预报业务下线!");
}
}
我们关注下上文的hasDestroyMethod
函数中这么一行代码:
if (bean instanceof DisposableBean || bean instanceof AutoCloseable) {
return true;
}
如果Bean
是AutoCloseable
类型的也可以让其执行shutdown
。那么我们只需要对程序做出以下更改:
@Component
public class WeatherService implements Closeable {
public void shutdown() {
System.out.println("通知所有调用方,天气预报业务下线!");
}
@Override
public void close() throws IOException {
shutdown();
}
}
可以得到相同的结果:
这是因为Closeable
是AutoCloseable
的一个子类:
public interface Closeable extends AutoCloseable {}
2.1 总结
- 如果使用默认的
@Bean()
引入某个类,注意,这里没有写什么参数。那么如果这个类中有和close、shutdown
同名的函数。在程序关闭的时候会被自动执行。 - 其他通过
@Component、@Service
等方式注入的类,一般不会有这样的情况。如果希望实现同等效果,可以实现Closeable
接口,完成close()
方法的具体实现。 - 因为程序关闭的时候,判断是否有销毁函数,是根据这个
Bean
的类型来的。if (bean instanceof DisposableBean || bean instanceof AutoCloseable)
。除此之外,就看这个Bean
的destroyMethodName
是否为AbstractBeanDefinition.INFER_METHOD
。
那是因为:
public @interface Bean {
String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;
}