没事不要乱写close和shutdown方法,搞不好线上就出个大bug

在Spring项目中,我们在定义一个bean的时候,可能会随手写一个close或者shutdown方法去关闭一些资源 。但是有时候这两个看起来很正常的方法名,即使我们不添加任何特殊配置,也可能会给我们带来潜在的bug 。
问题复现通过一个简单的bean重现一下这个问题 。
定义一个系统配置类,在某些条件下,我们会调用这个类的close方法去执行一些关闭资源的动作 。
@Datapublic class SystemConfig {private String config;private String type;//....省略其他属性//一个普通的close方法,没有做任何特殊配置public void close(){//在某些条件下,关闭一些系统资源,不仅局限于本系统System.out.println("开始关闭>>>");}}通过@Bean将这个类注入到Spring容器中:
@SpringBootApplication(scanBasePackages = "com.shishan.demo2023.*")public class Demo2023Application {public static void main(String[] args) {SpringApplication.run(Demo2023Application.class, args);}@Beanpublic SystemConfig systemConfig(){SystemConfig systemConfig = new SystemConfig();systemConfig.setConfig("config");return systemConfig;}}在很长一段时间内,这段代码都执行得很好 。但是有一次系统意外停机时,bug发生了 。

没事不要乱写close和shutdown方法,搞不好线上就出个大bug

文章插图
 
通过上图可以看到,close方法在没有任何主动调用的情况下,被Spring自动执行了 。。。
原理探究先说结论:问题主要出现在@Bean注解的destroyMethod属性上 。
没事不要乱写close和shutdown方法,搞不好线上就出个大bug

文章插图
 
我们点开@Bean注解,在destroyMethod方法上,可以看到一段注释 。
翻译过来的意思就是:
为了方便用户,容器将尝试针对从 @Bean方法返回的对象推断destroy方法 。例如,给定一个 @Bean方法返回一个Apache Commons DBCP BasicDataSource,容器将注意到该对象上可用的close() 方法,并自动将其注册为destroyMethod 。
简单来说,当使用@Bean注解时,如果destroyMethod属性没有设置值,Spring会自动检查通过@Bean方法注入的对象是否包含close方法或者shutdown方法,如果有,则将其注册为destroyMethod,并且在bean被销毁时自动调用该方法 。
通过搜索destroyMethod的默认值
AbstractBeanDefinition.INFER_METHOD的引用,我们可以在DisposableBeanAdapter类中的inferDestroyMethodIfNecessary方法找到Spring是如何判断close方法的 。
@Nullableprivate static String inferDestroyMethodIfNecessary(Object bean, RootBeanDefinition beanDefinition) {String destroyMethodName = beanDefinition.resolvedDestroyMethodName;if (destroyMethodName == null) {destroyMethodName = beanDefinition.getDestroyMethodName();boolean autoCloseable = (bean instanceof AutoCloseable);//如果destroyMethod没有定义,而且是默认值if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) ||(destroyMethodName == null && autoCloseable)) {destroyMethodName = null;if (!(bean instanceof DisposableBean)) {//并且没有实现DisposableBean接口if (autoCloseable) {destroyMethodName = CLOSE_METHOD_NAME;}else {try {//先找close方法destroyMethodName = bean.getClass().getMethod(CLOSE_METHOD_NAME).getName();}catch (NoSuchMethodException ex) {try {//如果close方法没找到,就尝试找shutdown方法destroyMethodName = bean.getClass().getMethod(SHUTDOWN_METHOD_NAME).getName();}catch (NoSuchMethodException ex2) {// no candidate destroy method found}}}}}beanDefinition.resolvedDestroyMethodName = (destroyMethodName != null ? destroyMethodName : "");}return (StringUtils.hasLength(destroyMethodName) ? destroyMethodName : null);}通过以上源码我们可以看出,Spring会尝试先找close方法,再找shutdown方法,如果找到了,就将其设置为destroyMethod,如果都没有找到,那就不做处理 。
总得来说,建议避免在JAVA类中定义一些带有特殊意义动词的方法,当然如果在线上运行的类已经定义了close或者shutdown方法另作他用,也可以通过将Bean注解内destroyMethod属性设置为显示指定其他方法的方式来解决这个问题 。
最后在实际项目中,用@Bean方式注入的,一般都是第三方包的类 。这些第三方包中的类由于没有强依赖Spring,所以无法直接使用@Component、@Service将类注入容器 。而且这些类在容器销毁的时候可能也有一些后置处理的需求,为了保持黑盒,Spring就采用这种默认的配置帮助我们执行一些后置处理 。如果我们作为第三方开发,建议能够了解这种机制,以免出现一些意想不到的bug 。

【没事不要乱写close和shutdown方法,搞不好线上就出个大bug】


    推荐阅读