天天看點

原來 spring.xml 配置的 destroy-method 需要用到向虛拟機注冊鈎子來實作!

原來 spring.xml 配置的 destroy-method 需要用到向虛拟機注冊鈎子來實作!

作者:小傅哥

部落格:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收獲!😄

一、前言

有什麼方式,能給代碼留條活路?

原來 spring.xml 配置的 destroy-method 需要用到向虛拟機注冊鈎子來實作!

有人說:人人都是産品經理,那你知道嗎,人人也都可以是碼農程式員!就像:

  • 程式設計就是;定義屬性、建立方法、調用展示
  • Java 和 PHP 就像男人和女人,前者在乎架構化子產品後,後者在乎那個顔色我喜歡
  • 用心寫,但不要不做格式化
  • 初次和産品對接的三個寶:磚頭、鐵鍬、菜刀,分别保證有用、可用、好用
  • 從一行代碼到一噸代碼,開發越來越難,壁壘也越來越高

其實學會寫代碼并不難,但學會寫好代碼卻很難。從易閱讀上來說你的代碼要有準确的命名和清晰的注釋、從易使用上來說你的代碼要具備設計模式的包裝讓對外的服務調用更簡單、從易擴充上來說你的代碼要做好業務和功能的實作分層。在易閱讀、易使用、易擴充以及更多編碼規範的限制下,還需要在開發完成上線後的傳遞結果上滿足;高可用、高性能、高并發,與此同時你還會接到現有項目中層出不窮來自産品經理新增的需求。

🎙怎麼辦?知道你在碼磚,不知道你在蓋哪個豬圈!

就算碼的磚是蓋的豬圈,也得因為豬多擴面積、改水槽、加飼料呀,是以根本沒法保證你寫完的代碼就不會加需求。那麼新加的需求如果是以破壞了你原有的封裝了非常完美500行的

ifelse

咋辦,拆了重蓋嗎?

兄嘚,給代碼留條活路吧!你的代碼用上了定義接口嗎、接口繼承接口嗎、接口由抽象類實作嗎、類繼承的類實作了接口方法嗎,而這些操作都是為了讓你的程式邏輯做到分層、分區、分塊,把核心邏輯層和業務封裝層做好隔離,當有業務變化時候,隻需要做在業務層完成裝配,而底層的核心邏輯服務并不需要頻繁變化,它們所增加的接口也更原子化,不具備業務語意。是以這樣的實作方式才能給你的代碼留條活路。如果還不是太了解,可以多看看《重學Java設計模式》和現在編寫的《手撸Spring》,這裡面都有大量的設計模式應用實踐

二、目标

當我們的類建立的 Bean 對象,交給 Spring 容器管理以後,這個類對象就可以被賦予更多的使用能力。就像我們在上一章節已經給類對象添加了修改注冊Bean定義未執行個體化前的屬性資訊修改和執行個體化過程中的前置和後置處理,這些額外能力的實作,都可以讓我們對現有工程中的類對象做相應的擴充處理。

那麼除此之外我們還希望可以在 Bean 初始化過程,執行一些操作。比如幫我們做一些資料的加載執行,連結注冊中心暴漏RPC接口以及在Web程式關閉時執行連結斷開,記憶體銷毀等操作。如果說沒有Spring我們也可以通過構造函數、靜态方法以及手動調用的方式實作,但這樣的處理方式終究不如把諸如此類的操作都交給 Spring 容器來管理更加合适。 是以你會看到到 spring.xml 中有如下操作:

原來 spring.xml 配置的 destroy-method 需要用到向虛拟機注冊鈎子來實作!
  • 需要滿足使用者可以在 xml 中配置初始化和銷毀的方法,也可以通過實作類的方式處理,比如我們在使用 Spring 時用到的 InitializingBean, DisposableBean 兩個接口。

    -其實還可以有一種是注解的方式處理初始化操作,不過目前還沒有實作到注解的邏輯,後續再完善此類功能。

三、設計

可能面對像 Spring 這樣龐大的架構,對外暴露的接口定義使用或者xml配置,完成的一系列擴充性操作,都讓 Spring 架構看上去很神秘。其實對于這樣在 Bean 容器初始化過程中額外添加的處理操作,無非就是預先執行了一個定義好的接口方法或者是反射調用類中xml中配置的方法,最終你隻要按照接口定義實作,就會有 Spring 容器在處理的過程中進行調用而已。整體設計結構如下圖:

原來 spring.xml 配置的 destroy-method 需要用到向虛拟機注冊鈎子來實作!
  • 在 spring.xml 配置中添加

    init-method、destroy-method

    兩個注解,在配置檔案加載的過程中,把注解配置一并定義到 BeanDefinition 的屬性當中。這樣在 initializeBean 初始化操作的工程中,就可以通過反射的方式來調用配置在 Bean 定義屬性當中的方法資訊了。另外如果是接口實作的方式,那麼直接可以通過 Bean 對象調用對應接口定義的方法即可,

    ((InitializingBean) bean).afterPropertiesSet()

    ,兩種方式達到的效果是一樣的。
  • 除了在初始化做的操作外,

    destroy-method

    DisposableBean

    接口的定義,都會在 Bean 對象初始化完成階段,執行注冊銷毀方法的資訊到 DefaultSingletonBeanRegistry 類中的 disposableBeans 屬性裡,這是為了後續統一進行操作。這裡還有一段擴充卡的使用,因為反射調用和接口直接調用,是兩種方式。是以需要使用擴充卡進行包裝,下文代碼講解中參考 DisposableBeanAdapter 的具體實作

    -關于銷毀方法需要在虛拟機執行關閉之前進行操作,是以這裡需要用到一個注冊鈎子的操作,如:

    Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("close!")));

    這段代碼你可以執行測試,另外你可以使用手動調用 ApplicationContext.close 方法關閉容器。

四、實作

1. 工程結構

small-spring-step-07
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.springframework
    │           ├── beans
    │           │   ├── factory
    │           │   │   ├── factory
    │           │   │   │   ├── AutowireCapableBeanFactory.java
    │           │   │   │   ├── BeanDefinition.java
    │           │   │   │   ├── BeanFactoryPostProcessor.java
    │           │   │   │   ├── BeanPostProcessor.java
    │           │   │   │   ├── BeanReference.java
    │           │   │   │   ├── ConfigurableBeanFactory.java
    │           │   │   │   └── SingletonBeanRegistry.java
    │           │   │   ├── support
    │           │   │   │   ├── AbstractAutowireCapableBeanFactory.java
    │           │   │   │   ├── AbstractBeanDefinitionReader.java
    │           │   │   │   ├── AbstractBeanFactory.java
    │           │   │   │   ├── BeanDefinitionReader.java
    │           │   │   │   ├── BeanDefinitionRegistry.java
    │           │   │   │   ├── CglibSubclassingInstantiationStrategy.java
    │           │   │   │   ├── DefaultListableBeanFactory.java
    │           │   │   │   ├── DefaultSingletonBeanRegistry.java
    │           │   │   │   ├── DisposableBeanAdapter.java
    │           │   │   │   ├── InstantiationStrategy.java
    │           │   │   │   └── SimpleInstantiationStrategy.java  
    │           │   │   ├── support
    │           │   │   │   └── XmlBeanDefinitionReader.java
    │           │   │   ├── BeanFactory.java
    │           │   │   ├── ConfigurableListableBeanFactory.java
    │           │   │   ├── DisposableBean.java
    │           │   │   ├── HierarchicalBeanFactory.java
    │           │   │   ├── InitializingBean.java
    │           │   │   └── ListableBeanFactory.java
    │           │   ├── BeansException.java
    │           │   ├── PropertyValue.java
    │           │   └── PropertyValues.java 
    │           ├── context
    │           │   ├── support
    │           │   │   ├── AbstractApplicationContext.java 
    │           │   │   ├── AbstractRefreshableApplicationContext.java 
    │           │   │   ├── AbstractXmlApplicationContext.java 
    │           │   │   └── ClassPathXmlApplicationContext.java 
    │           │   ├── ApplicationContext.java 
    │           │   └── ConfigurableApplicationContext.java
    │           ├── core.io
    │           │   ├── ClassPathResource.java 
    │           │   ├── DefaultResourceLoader.java 
    │           │   ├── FileSystemResource.java 
    │           │   ├── Resource.java 
    │           │   ├── ResourceLoader.java 
    │           │   └── UrlResource.java
    │           └── utils
    │               └── ClassUtils.java
    └── test
        └── java
            └── cn.bugstack.springframework.test
                ├── bean
                │   ├── UserDao.java
                │   └── UserService.java
                └── ApiTest.java
           

工程源碼:

公衆号「bugstack蟲洞棧」,回複:Spring 專欄,擷取完整源碼

Spring 應用上下文和對Bean對象擴充機制的類關系,如圖 8-4

原來 spring.xml 配置的 destroy-method 需要用到向虛拟機注冊鈎子來實作!
  • 以上整個類圖結構描述出來的就是本次新增 Bean 執行個體化過程中的初始化方法和銷毀方法。
  • 因為我們一共實作了兩種方式的初始化和銷毀方法,xml配置和定義接口,是以這裡既有 InitializingBean、DisposableBean 也有需要 XmlBeanDefinitionReader 加載 spring.xml 配置資訊到 BeanDefinition 中。
  • 另外接口 ConfigurableBeanFactory 定義了 destroySingletons 銷毀方法,并由 AbstractBeanFactory 繼承的父類 DefaultSingletonBeanRegistry 實作 ConfigurableBeanFactory 接口定義的 destroySingletons 方法。這種方式的設計可能數程式員是沒有用過的,都是用的誰實作接口誰完成實作類,而不是把實作接口的操作又交給繼承的父類處理。是以這塊還是蠻有意思的,是一種不錯的隔離分層服務的設計方式
  • 最後就是關于向虛拟機注冊鈎子,保證在虛拟機關閉之前,執行銷毀操作。

    Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("close!")));

2. 定義初始化和銷毀方法的接口

cn.bugstack.springframework.beans.factory.InitializingBean

public interface InitializingBean {

    /**
     * Bean 處理了屬性填充後調用
     * 
     * @throws Exception
     */
    void afterPropertiesSet() throws Exception;

}
           

cn.bugstack.springframework.beans.factory.DisposableBean

public interface DisposableBean {

    void destroy() throws Exception;

}
           
  • InitializingBean、DisposableBean,兩個接口方法還是比較常用的,在一些需要結合 Spring 實作的元件中,經常會使用這兩個方法來做一些參數的初始化和銷毀操作。比如接口暴漏、資料庫資料讀取、配置檔案加載等等。

3. Bean屬性定義新增初始化和銷毀

cn.bugstack.springframework.beans.factory.config.BeanDefinition

public class BeanDefinition {

    private Class beanClass;

    private PropertyValues propertyValues;

    private String initMethodName;
    
    private String destroyMethodName;
    
    // ...get/set
}
           
  • 在 BeanDefinition 新增加了兩個屬性:initMethodName、destroyMethodName,這兩個屬性是為了在 spring.xml 配置的 Bean 對象中,可以配置

    init-method="initDataMethod" destroy-method="destroyDataMethod"

    操作,最終實作接口的效果是一樣的。隻不過一個是接口方法的直接調用,另外是一個在配置檔案中讀取到方法反射調用

4. 執行 Bean 對象的初始化方法

cn.bugstack.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory {

    private InstantiationStrategy instantiationStrategy = new CglibSubclassingInstantiationStrategy();

    @Override
    protected Object createBean(String beanName, BeanDefinition beanDefinition, Object[] args) throws BeansException {
        Object bean = null;
        try {
            bean = createBeanInstance(beanDefinition, beanName, args);
            // 給 Bean 填充屬性
            applyPropertyValues(beanName, bean, beanDefinition);
            // 執行 Bean 的初始化方法和 BeanPostProcessor 的前置和後置處理方法
            bean = initializeBean(beanName, bean, beanDefinition);
        } catch (Exception e) {
            throw new BeansException("Instantiation of bean failed", e);
        }

        // ...

        addSingleton(beanName, bean);
        return bean;
    }

    private Object initializeBean(String beanName, Object bean, BeanDefinition beanDefinition) {
        // 1. 執行 BeanPostProcessor Before 處理
        Object wrappedBean = applyBeanPostProcessorsBeforeInitialization(bean, beanName);

        // 執行 Bean 對象的初始化方法
        try {
            invokeInitMethods(beanName, wrappedBean, beanDefinition);
        } catch (Exception e) {
            throw new BeansException("Invocation of init method of bean[" + beanName + "] failed", e);
        }

        // 2. 執行 BeanPostProcessor After 處理
        wrappedBean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
        return wrappedBean;
    }

    private void invokeInitMethods(String beanName, Object bean, BeanDefinition beanDefinition) throws Exception {
        // 1. 實作接口 InitializingBean
        if (bean instanceof InitializingBean) {
            ((InitializingBean) bean).afterPropertiesSet();
        }

        // 2. 配置資訊 init-method {判斷是為了避免二次執行銷毀}
        String initMethodName = beanDefinition.getInitMethodName();
        if (StrUtil.isNotEmpty(initMethodName)) {
            Method initMethod = beanDefinition.getBeanClass().getMethod(initMethodName);
            if (null == initMethod) {
                throw new BeansException("Could not find an init method named '" + initMethodName + "' on bean with name '" + beanName + "'");
            }
            initMethod.invoke(bean);
        }
    }

}
           
  • 抽象類 AbstractAutowireCapableBeanFactory 中的 createBean 是用來建立 Bean 對象的方法,在這個方法中我們之前已經擴充了 BeanFactoryPostProcessor、BeanPostProcessor 操作,這裡我們繼續完善執行 Bean 對象的初始化方法的處理動作。
  • 在方法 invokeInitMethods 中,主要分為兩塊來執行實作了 InitializingBean 接口的操作,處理 afterPropertiesSet 方法。另外一個是判斷配置資訊 init-method 是否存在,執行反射調用 initMethod.invoke(bean)。這兩種方式都可以在 Bean 對象初始化過程中進行處理加載 Bean 對象中的初始化操作,讓使用者可以額外新增加自己想要的動作。

5. 定義銷毀方法擴充卡(接口和配置)

cn.bugstack.springframework.beans.factory.support.DisposableBeanAdapter

public class DisposableBeanAdapter implements DisposableBean {

    private final Object bean;
    private final String beanName;
    private String destroyMethodName;

    public DisposableBeanAdapter(Object bean, String beanName, BeanDefinition beanDefinition) {
        this.bean = bean;
        this.beanName = beanName;
        this.destroyMethodName = beanDefinition.getDestroyMethodName();
    }

    @Override
    public void destroy() throws Exception {
        // 1. 實作接口 DisposableBean
        if (bean instanceof DisposableBean) {
            ((DisposableBean) bean).destroy();
        }

        // 2. 配置資訊 destroy-method {判斷是為了避免二次執行銷毀}
        if (StrUtil.isNotEmpty(destroyMethodName) && !(bean instanceof DisposableBean && "destroy".equals(this.destroyMethodName))) {
            Method destroyMethod = bean.getClass().getMethod(destroyMethodName);
            if (null == destroyMethod) {
                throw new BeansException("Couldn't find a destroy method named '" + destroyMethodName + "' on bean with name '" + beanName + "'");
            }
            destroyMethod.invoke(bean);
        }
        
    }

}
           
  • 可能你會想這裡怎麼有一個擴充卡的類呢,因為銷毀方法有兩種甚至多種方式,目前有

    實作接口 DisposableBean

    配置資訊 destroy-method

    ,兩種方式。而這兩種方式的銷毀動作是由 AbstractApplicationContext 在注冊虛拟機鈎子後看,虛拟機關閉前執行的操作動作。
  • 那麼在銷毀執行時不太希望還得關注都銷毀那些類型的方法,它的使用上更希望是有一個統一的接口進行銷毀,是以這裡就新增了适配類,做統一處理。

6. 建立Bean時注冊銷毀方法對象

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory {

    private InstantiationStrategy instantiationStrategy = new CglibSubclassingInstantiationStrategy();

    @Override
    protected Object createBean(String beanName, BeanDefinition beanDefinition, Object[] args) throws BeansException {
        Object bean = null;
        try {
            bean = createBeanInstance(beanDefinition, beanName, args);
            // 給 Bean 填充屬性
            applyPropertyValues(beanName, bean, beanDefinition);
            // 執行 Bean 的初始化方法和 BeanPostProcessor 的前置和後置處理方法
            bean = initializeBean(beanName, bean, beanDefinition);
        } catch (Exception e) {
            throw new BeansException("Instantiation of bean failed", e);
        }

        // 注冊實作了 DisposableBean 接口的 Bean 對象
        registerDisposableBeanIfNecessary(beanName, bean, beanDefinition);

        addSingleton(beanName, bean);
        return bean;
    }

    protected void registerDisposableBeanIfNecessary(String beanName, Object bean, BeanDefinition beanDefinition) {
        if (bean instanceof DisposableBean || StrUtil.isNotEmpty(beanDefinition.getDestroyMethodName())) {
            registerDisposableBean(beanName, new DisposableBeanAdapter(bean, beanName, beanDefinition));
        }
    }

}
           
  • 在建立 Bean 對象的執行個體的時候,需要把銷毀方法儲存起來,友善後續執行銷毀動作進行調用。
  • 那麼這個銷毀方法的具體方法資訊,會被注冊到 DefaultSingletonBeanRegistry 中新增加的

    Map<String, DisposableBean> disposableBeans

    屬性中去,因為這個接口的方法最終可以被類 AbstractApplicationContext 的 close 方法通過

    getBeanFactory().destroySingletons()

    調用。
  • 在注冊銷毀方法的時候,會根據是接口類型和配置類型統一交給 DisposableBeanAdapter 銷毀擴充卡類來做統一處理。實作了某個接口的類可以被 instanceof 判斷或者強轉後調用接口方法

7. 虛拟機關閉鈎子注冊調用銷毀方法

cn.bugstack.springframework.context.ConfigurableApplicationContext

public interface ConfigurableApplicationContext extends ApplicationContext {

    void refresh() throws BeansException;

    void registerShutdownHook();

    void close();

}
           
  • 首先我們需要在 ConfigurableApplicationContext 接口中定義注冊虛拟機鈎子的方法

    registerShutdownHook

    和手動執行關閉的方法

    close

cn.bugstack.springframework.context.support.AbstractApplicationContext

public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {

    // ...

    @Override
    public void registerShutdownHook() {
        Runtime.getRuntime().addShutdownHook(new Thread(this::close));
    }

    @Override
    public void close() {
        getBeanFactory().destroySingletons();
    }

}
           
  • 這裡主要展現了關于注冊鈎子和關閉的方法實作,上文提到過的

    Runtime.getRuntime().addShutdownHook

    ,可以嘗試驗證。在一些中間件和監控系統的設計中也可以用得到,比如監測伺服器當機,執行備機啟動操作。

五、測試

1. 事先準備

cn.bugstack.springframework.test.bean.UserDao

public class UserDao {

    private static Map<String, String> hashMap = new HashMap<>();

    public void initDataMethod(){
        System.out.println("執行:init-method");
        hashMap.put("10001", "小傅哥");
        hashMap.put("10002", "八杯水");
        hashMap.put("10003", "阿毛");
    }

    public void destroyDataMethod(){
        System.out.println("執行:destroy-method");
        hashMap.clear();
    }

    public String queryUserName(String uId) {
        return hashMap.get(uId);
    }

}
           

cn.bugstack.springframework.test.bean.UserService

public class UserService implements InitializingBean, DisposableBean {

    private String uId;
    private String company;
    private String location;
    private UserDao userDao;

    @Override
    public void destroy() throws Exception {
        System.out.println("執行:UserService.destroy");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("執行:UserService.afterPropertiesSet");
    }

    // ...get/set
}
           
  • UserDao,修改了之前使用 static 靜态塊初始化資料的方式,改為提供 initDataMethod 和 destroyDataMethod 兩個更優雅的操作方式進行處理。
  • UserService,以實作接口 InitializingBean, DisposableBean 的兩個方法 destroy()、afterPropertiesSet(),處理相應的初始化和銷毀方法的動作。afterPropertiesSet,方法名字很好,在屬性設定後執行

2. 配置檔案

基礎配置,無BeanFactoryPostProcessor、BeanPostProcessor,實作類

<?xml version="1.0" encoding="UTF-8"?>
<beans>

    <bean id="userDao" class="cn.bugstack.springframework.test.bean.UserDao" init-method="initDataMethod" destroy-method="destroyDataMethod"/>

    <bean id="userService" class="cn.bugstack.springframework.test.bean.UserService">
        <property name="uId" value="10001"/>
        <property name="company" value="騰訊"/>
        <property name="location" value="深圳"/>
        <property name="userDao" ref="userDao"/>
    </bean>

</beans>
           
  • 配置檔案中主要是新增了,

    init-method="initDataMethod" destroy-method="destroyDataMethod"

    ,這樣兩個配置。從源碼的學習中可以知道,這兩個配置是為了加入到 BeanDefinition 定義類之後寫入到類 DefaultListableBeanFactory 中的 beanDefinitionMap 屬性中去。

3. 單元測試

@Test
public void test_xml() {
    // 1.初始化 BeanFactory
    ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring.xml");
    applicationContext.registerShutdownHook();      

    // 2. 擷取Bean對象調用方法
    UserService userService = applicationContext.getBean("userService", UserService.class);
    String result = userService.queryUserInfo();
    System.out.println("測試結果:" + result);
}
           
  • 測試方法中新增加了一個,注冊鈎子的動作。

    applicationContext.registerShutdownHook();

測試結果

執行:init-method
執行:UserService.afterPropertiesSet
測試結果:小傅哥,騰訊,深圳
執行:UserService.destroy
執行:destroy-method

Process finished with exit code 0
           
  • 從測試結果可以看到,我們的新增加的初始和銷毀方法已經可以如期輸出結果了。

六、總結

  • 本文主要完成了關于初始和銷毀在使用接口定義

    implements InitializingBean, DisposableBean

    和在spring.xml中配置

    init-method="initDataMethod" destroy-method="destroyDataMethod"

    的兩種具體在

    AbstractAutowireCapableBeanFactory

    完成初始方法和

    AbstractApplicationContext

    處理銷毀動作的具體實作過程。
  • 通過本文的實作内容,可以看到目前這個 Spring 架構對 Bean 的操作越來越完善了,可擴充性也不斷的增強。你既可以在Bean注冊完成執行個體化前進行 BeanFactoryPostProcessor 操作,也可以在Bean執行個體化過程中執行前置和後置操作,現在又可以執行Bean的初始化方法和銷毀方法。是以一個簡單的Bean對象,已經被賦予了各種擴充能力。
  • 在學習和動手實踐 Spring 架構學習的過程中,特别要注意的是它對接口和抽象類的把握和使用,尤其遇到類似,A繼承B實作C時,C的接口方法由A繼承的父類B實作,這樣的操作都蠻有意思的。也是可以複用到通常的業務系統開發中進行處理一些複雜邏輯的功能分層,做到程式的可擴充、易維護等特性。

七、系列推薦

  • 【經驗分享】碼農雲服務使用學習,部環境、開端口、配域名、弄SSL、搭部落格!
  • 方案設計:基于IDEA插件開發和位元組碼插樁技術,實作研發傳遞品質自動分析
  • 工作兩三年了,整不明白架構圖都畫啥?
  • 《SpringBoot 中間件設計和開發》| 對,小傅哥這次教你造火箭!
  • 工作3年,看啥資料能月薪30K?

公衆号:bugstack蟲洞棧 | 作者小傅哥多年從事一線網際網路 Java 開發的學習曆程技術彙總,旨在為大家提供一個清晰詳細的學習教程,側重點更傾向編寫Java核心内容。如果能為您提供幫助,請給予支援(關注、點贊、分享)!