在Spring项目中,我们在定义一个bean的时候,可能会随手写一个close或者shutdown方法去关闭一些资源。但是有时候这两个看起来很正常的方法名,即使我们不添加任何特殊配置,也可能会给我们带来潜在的bug。
通过一个简单的bean重现一下这个问题。
定义一个系统配置类,在某些条件下,我们会调用这个类的close方法去执行一些关闭资源的动作。
@Data
public 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);
}
@Bean
public SystemConfig systemConfig(){
SystemConfig systemConfig = new SystemConfig();
systemConfig.setConfig("config");
return systemConfig;
}
}
在很长一段时间内,这段代码都执行得很好。但是有一次系统意外停机时,bug发生了。
通过上图可以看到,close方法在没有任何主动调用的情况下,被Spring自动执行了。。。
先说结论:问题主要出现在@Bean注解的destroyMethod属性上。
我们点开@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方法的。
@Nullable
private 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。