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

30 篇文章 3 订阅

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

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()))));
}
  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);
}

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

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

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

if (bean instanceof DisposableBean || bean instanceof AutoCloseable) {
	return true;
}

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

@Component
public class WeatherService implements Closeable {
    public void shutdown() {
        System.out.println("通知所有调用方,天气预报业务下线!");
    }

    @Override
    public void close() throws IOException {
		shutdown();
    }
}

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

public interface Closeable extends AutoCloseable {}

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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值