沒事不要亂寫close和shutdown方法,搞不好線上就出個大bug

程序員拾山 發佈 2022-12-04T08:36:05.200217+00:00

在Spring項目中,我們在定義一個bean的時候,可能會隨手寫一個close或者shutdown方法去關閉一些資源。

在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。

學習技術,分享技術,期待與大家共同進步,也感謝您的點讚與關注。

關鍵字: