1. 程式人生 > >🙈羞,Spring Bean 初始化/銷燬竟然有這麼多姿勢

🙈羞,Spring Bean 初始化/銷燬竟然有這麼多姿勢

文章來源:http://1t.click/bfHN

一、前言

日常開發過程有時需要在應用啟動之後載入某些資源,或者在應用關閉之前釋放資源。Spring 框架提供相關功能,圍繞 Spring Bean 生命週期,可以在 Bean 建立過程初始化資源,以及銷燬 Bean 過程釋放資源。Spring 提供多種不同的方式初始化/銷燬 Bean,如果同時使用這幾種方式,Spring 如何處理這幾者之間的順序?

有沒有覺得標題很熟悉,沒錯標題模仿二哥 「@沉默王二」 文章羞,Java 字串拼接竟然有這麼多姿勢。

二、姿勢剖析

首先我們先來回顧一下 Spring 初始化/銷燬 Bean 幾種方式,分別為:

  • init-method/destroy-method
  • InitializingBean/DisposableBean
  • @PostConstruct/@PreDestroy
  • ContextStartedEvent/ContextClosedEvent

PS: 其實還有一種方式,就是繼承 Spring Lifecycle 介面。不過這種方式比較繁瑣,這裡就不再分析。

2.1、init-method/destroy-method

這種方式在配置檔案檔案指定初始化/銷燬方法。XML 配置如下

<bean id="demoService" class="com.dubbo.example.provider.DemoServiceImpl"  destroy-method="close"  init-method="initMethod"/>

或者也可以使用註解方式配置:

@Configurable
public class AppConfig {

    @Bean(initMethod = "init", destroyMethod = "destroy")
    public HelloService hello() {
        return new HelloService();
    }
}

還記得剛開始接觸學習 Spring 框架,使用就是這種方式。

2.2、InitializingBean/DisposableBean

這種方式需要繼承 Spring 介面 InitializingBean/DisposableBean

,其中 InitializingBean 用於初始化動作,而 DisposableBean 用於銷燬之前清理動作。使用方式如下:

@Service
public class HelloService implements InitializingBean, DisposableBean {
    
    @Override
    public void destroy() throws Exception {
        System.out.println("hello destroy...");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("hello init....");
    }
}

2.3、@PostConstruct/@PreDestroy

這種方式相對於上面兩種方式來說,使用方式最簡單,只需要在相應的方法上使用註解即可。使用方式如下:

@Service
public class HelloService {


    @PostConstruct
    public void init() {
        System.out.println("hello @PostConstruct");
    }

    @PreDestroy
    public void PreDestroy() {
        System.out.println("hello @PreDestroy");
    }
}

這裡踩過一個坑,如果使用 JDK9 之後版本 ,@PostConstruct/@PreDestroy 需要使用 maven 單獨引入 javax.annotation-api,否者註解不會生效。

2.4、ContextStartedEvent/ContextClosedEvent

這種方式使用 Spring 事件機制,日常業務開發比較少見,常用與框架整合中。Spring 啟動之後將會發送 ContextStartedEvent 事件,而關閉之前將會發送 ContextClosedEvent 事件。我們需要繼承 Spring ApplicationListener 才能監聽以上兩種事件。

@Service
public class HelloListener implements ApplicationListener {

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if(event instanceof ContextClosedEvent){
            System.out.println("hello ContextClosedEvent");
        }else if(event instanceof ContextStartedEvent){
            System.out.println("hello ContextStartedEvent");
        }

    }
}

也可以使用 @EventListener註解,使用方式如下:

public class HelloListenerV2 {
    
    @EventListener(value = {ContextClosedEvent.class, ContextStartedEvent.class})
    public void receiveEvents(ApplicationEvent event) {
        if (event instanceof ContextClosedEvent) {
            System.out.println("hello ContextClosedEvent");
        } else if (event instanceof ContextStartedEvent) {
            System.out.println("hello ContextStartedEvent");
        }
    }
}

PS:只有呼叫 ApplicationContext#start 才會傳送 ContextStartedEvent。若不想這麼麻煩,可以監聽 ContextRefreshedEvent 事件代替。一旦 Spring 容器初始化完成,就會發送 ContextRefreshedEvent

三、綜合使用

回顧完上面幾種方式,這裡我們綜合使用上面的四種方式,來看下 Spring 內部的處理順序。在看結果之前,各位讀者大人可以猜測下這幾種方式的執行順序。

public class HelloService implements InitializingBean, DisposableBean {


    @PostConstruct
    public void init() {
        System.out.println("hello @PostConstruct");
    }

    @PreDestroy
    public void PreDestroy() {
        System.out.println("hello @PreDestroy");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("bye DisposableBean...");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("hello InitializingBean....");
    }

    public void xmlinit(){
        System.out.println("hello xml-init...");
    }

    public void xmlDestory(){
        System.out.println("bye xmlDestory...");
    }

    @EventListener(value = {ContextClosedEvent.class, ContextStartedEvent.class})
    public void receiveEvents(ApplicationEvent event) {
        if (event instanceof ContextClosedEvent) {
            System.out.println("bye ContextClosedEvent");
        } else if (event instanceof ContextStartedEvent) {
            System.out.println("hello ContextStartedEvent");
        }
    }

}

xml 配置方式如下:

    <context:annotation-config />
    <context:component-scan base-package="com.dubbo.example.demo"/>
    
    <bean class="com.dubbo.example.demo.HelloService" init-method="xmlinit" destroy-method="xmlDestory"/>

應用啟動方法如下:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/dubbo-provider.xml");
context.start();
context.close();

程式輸出結果如下所示:

最後採用圖示說明總結以上結果:

四、原始碼解析

不知道各位讀者有沒有猜對這幾種方式的執行順序,下面我們就從原始碼角度解析 Spring 內部處理的順序。

4.1、初始化過程

使用 ClassPathXmlApplicationContext 啟動 Spring 容器,將會呼叫 refresh 方法初始化容器。初始化過程將會建立 Bean 。最後當一切準備完畢,將會發送 ContextRefreshedEvent。當容器初始化完畢,呼叫 context.start() 就傳送 ContextStartedEvent 事件。

refresh 方法原始碼如下:

public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
            //... 忽略無關程式碼

            // 初始化所有非延遲初始化的 Bean
            finishBeanFactoryInitialization(beanFactory);

            // 傳送 ContextRefreshedEvent
            finishRefresh();

            //... 忽略無關程式碼
    }
}

一路跟蹤 finishBeanFactoryInitialization 原始碼,直到 AbstractAutowireCapableBeanFactory#initializeBean,原始碼如下:

protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
        // 呼叫 BeanPostProcessor#postProcessBeforeInitialization 方法
        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    }

    try {
        // 初始化 Bean
        invokeInitMethods(beanName, wrappedBean, mbd);
    }
    catch (Throwable ex) {
        throw new BeanCreationException(
                (mbd != null ? mbd.getResourceDescription() : null),
                beanName, "Invocation of init method failed", ex);
    }
}

BeanPostProcessor 將會起著攔截器的作用,一旦 Bean 符合條件,將會執行一些處理。這裡帶有 @PostConstruct 註解的 Bean 都將會被 CommonAnnotationBeanPostProcessor 類攔截,內部將會觸發 @PostConstruct 標註的方法。

接著執行 invokeInitMethods ,方法如下:

protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd)
        throws Throwable {

    boolean isInitializingBean = (bean instanceof InitializingBean);
    if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
        // 省略無關程式碼
        // 如果是 Bean 繼承 InitializingBean,將會執行  afterPropertiesSet 方法
        ((InitializingBean) bean).afterPropertiesSet();
    }

    if (mbd != null) {
        String initMethodName = mbd.getInitMethodName();
        if (initMethodName != null && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
                !mbd.isExternallyManagedInitMethod(initMethodName)) {
            // 執行 XML 定義 init-method
            invokeCustomInitMethod(beanName, bean, mbd);
        }
    }
}

如果 Bean 繼承 InitializingBean 介面,將會執行 afterPropertiesSet 方法,另外如果在 XML 中指定了 init-method ,也將會觸發。

上面原始碼其實都是圍繞著 Bean 建立的過程,當所有 Bean 建立完成之後,呼叫 context#start 將會發送 ContextStartedEvent 。這裡原始碼比較簡單,如下:

public void start() {
    getLifecycleProcessor().start();
    publishEvent(new ContextStartedEvent(this));
}

4.2、銷燬過程

呼叫 ClassPathXmlApplicationContext#close 方法將會關閉容器,具體邏輯將會在 doClose 方法執行。

doClose 這個方法首先發送 ContextClosedEvent,然再後開始銷燬 Bean

靈魂拷問:如果我們顛倒上面兩者順序,結果會一樣嗎?

doClose 原始碼如下:

protected void doClose() {
    if (this.active.get() && this.closed.compareAndSet(false, true)) {
        // 省略無關程式碼

        try {
            // Publish shutdown event.
            publishEvent(new ContextClosedEvent(this));
        }
        catch (Throwable ex) {
            logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
        }


        // 銷燬 Bean
        destroyBeans();

        // 省略無關程式碼
    }
}

destroyBeans 最終將會執行 DisposableBeanAdapter#destroy@PreDestroyDisposableBeandestroy-method 三者定義的方法都將會在內部被執行。

首先執行 DestructionAwareBeanPostProcessor#postProcessBeforeDestruction,這裡方法類似與上面 BeanPostProcessor

@PreDestroy 註解將會被 CommonAnnotationBeanPostProcessor 攔截,這裡類同時也繼承了 DestructionAwareBeanPostProcessor

最後如果 BeanDisposableBean 的子類,將會執行 destroy 方法,如果在 xml 定義了 destroy-method 方法,該方法也會被執行。

public void destroy() {
    if (!CollectionUtils.isEmpty(this.beanPostProcessors)) {
        for (DestructionAwareBeanPostProcessor processor : this.beanPostProcessors) {
            processor.postProcessBeforeDestruction(this.bean, this.beanName);
        }
    }

    if (this.invokeDisposableBean) {
        // 省略無關程式碼
        // 如果 Bean 繼承 DisposableBean,執行 destroy 方法
        ((DisposableBean) bean).destroy();
        
    }

    if (this.destroyMethod != null) {
        // 執行 xml 指定的  destroy-method 方法
        invokeCustomDestroyMethod(this.destroyMethod);
    }
    else if (this.destroyMethodName != null) {
        Method methodToCall = determineDestroyMethod();
        if (methodToCall != null) {
            invokeCustomDestroyMethod(methodToCall);
        }
    }
}

五、總結

init-method/destroy-method 這種方式需要使用 XML 配置檔案或單獨註解配置類,相對來說比較繁瑣。而InitializingBean/DisposableBean 這種方式需要單獨繼承 Spring 的介面實現相關方法。@PostConstruct/@PreDestroy 這種註解方式使用方式簡單,程式碼清晰,比較推薦使用這種方式。

另外 ContextStartedEvent/ContextClosedEvent 這種方式比較適合在一些整合框架使用,比如 Dubbo 2.6.X 優雅停機就是用改機制。

六、Spring 歷史文章推薦

1、Spring 註解程式設計之註解屬性別名與覆蓋
2、Spring 註解程式設計之 AnnotationMetadata
3、Spring 註解程式設計之模式註解
4、緣起 Dubbo ,講講 Spring XML Schema 擴充套件機制

歡迎關注我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的部落格:studyidea.cn

相關推薦

&amp;#128584;Spring Bean 初始/銷燬竟然這麼姿勢

文章來源:http://1t.click/bfHN 一、前言 日常開發過程有時需要在應用啟動之後載入某些資源,或者在應用關閉之前釋放資源。Spring 框架提供相關功能,圍繞 Spring Bean 生命週期,可以在 Bean 建立過程初始化資源,以及銷燬 Bean 過程釋放資源。Spring 提供多

Java 字串拼接竟然這麼姿勢

二哥,我今年大二,看你分享的《阿里巴巴 Java 開發手冊》上有一段內容說:“迴圈體內,拼接字串最好使用 StringBuilder 的 append 方法,而不是 + 號操作符。”到底為什麼啊,我平常一直就用的‘+’號操作符啊!二哥有空的時候能否寫一篇文章分析一下呢? 就在昨天,一位叫小菜的讀者微信我說

spring(bean初始銷燬、注入作用域)

IOC:控制反轉,也稱為依賴注入(DI)。這是一個過程。通常我們需要一個物件的時候,是主動建立物件,並且主動傳入到需要使用該物件的地方。而IOC則是由容器建立物件,注入到我們需要使用該物件的位置,兩者相比,一個主動,一個被動,被動的則是稱為依賴注入(控制反轉,由主動建立改為被動注入)。IO

Spring:FactoryBean介面】實現FactoryBean介面Spring初始bean時有何不同

問題描述: 最近想要再次熟悉一下阿里中介軟體HSF的用法,在消費HSF時需要在Spring的配置檔案中進行如下配置: <bean id="myClassB" class="com.taobao.hsf.app.spring.util.HSFSpri

Spring bean初始與銷毀的幾種方式和區別

pack ack 構造 rop struct service() throws esc println 1. <bean> 元素的 init-method/destroy-method屬性指定初始化之後 /銷毀之前調用的操作方法 2. 指定方法上加上@PostC

Spring Bean初始之後/銷燬之前執行指定方法

關於在spring  容器初始化 bean 和銷燬前所做的操作定義方式有三種: 通過@PostConstruct 和 @PreDestroy 方法 實現初始化和銷燬bean之前進行的操作 通過 在xml中定義init-method 和  destory-metho

Spring Bean初始之後執行指定方法

轉載:https://blog.csdn.net/forever7107/article/details/76446544 常用的設定方式有以下三種: 通過實現 InitializingBean/DisposableBean 介面來定製初始化之後/銷燬之前的操作方法; 通過 <bean&g

【問題記錄】eclipse啟動web專案時spring初始兩次

背景:一個tomcat,一個eclipse,一個SSM框架的web專案。在eclipse中新建tomcat伺服器,預設配置,然後在伺服器配置中將Server Locations改成Use Tomcat

Spring bean初始原理詳解

一、  閒言 使用spring已經多年,卻從來沒有仔細研究過spring bean的初始化過程以及原理。知其然而不知其所以然,當面遇到比較深度的問題的時候,就無法解決或者需要花費大量問題方可解決。 二、  目的 本文主要想解決以下幾個問題,希望大家看完本文以後,能得出答案

Spring Bean 初始之InitializingBean, init-method 和 PostConstruct

概述從介面的名字上不難發現,InitializingBean 的作用就是在 bean 初始化後執行定製化的操作。Spring 容器中的 Bean 是有生命週期的,Spring 允許在 Bean 在初始化完成後以及 Bean 銷燬前執行特定的操作,常用的設定方式有以下三種:通過實現 InitializingBe

spring-bean初始銷燬之前之後的操作

使用註解定義Bean的初始化和銷燬     Spring初始化bean或銷燬bean時,有時需要作一些處理工作,因此spring可以在建立和拆卸bean的時候呼叫bean的兩個生命週期方法。 回顧配置檔案的寫法:<bean id=“foo” class=“...Foo”

spring容器初始bean之後或銷燬bean之前能做的操作

通過 <bean> 標籤 init-method  初始化bean之後呼叫的方法 destroy-method  銷燬bean之前呼叫的操作方法 <bean id="initQuartzJob" class="com.upinc

Spring管理的bean初始方法的三種方式以及@PostConstruct不起作用的原因

1:Spring 容器中的 Bean 是有生命週期的,spring 允許 Bean 在初始化完成後以及銷燬前執行特定的操作。下面是常用的三種指定特定操作的方法: 通過實現InitializingBean/DisposableBean 介面來定製初始化之後/銷燬之前的操作

Spring cglib 初始 ExceptionInInitializerErrornew Enhancer() 異常

ali ctc ant pan sta span pre get jar 解決辦法:更換 spring-cglib-repack-*.*.jar 包 java.lang.ExceptionInInitializerError at org.springfra

Spring啟動流程(四)之Bean初始前後的一些操作

【Spring原始碼分析】非懶載入的單例Bean初始化前後的一些操作   再看AbstractApplicationContext的refresh方法中的細節: Spring預設載入的兩個Bean,systemProperties和systemEnvironment,

spring ioc---定製bean初始銷燬時的回撥函式

約莫有4種方式定製bean初始化和銷燬時的回撥函式,如下表格. 方式 說明 使用beans標籤的屬性 beans標籤中,使用以下兩個屬性, `default-init-method`和`default-destroy-m

Spring】Springboot監聽器啟動之後初始工作

package com.laplace.laplace.common.starter.config; import java.io.IOException; import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springfram

SpringBean初始的三種方法

常用的設定方式有以下三種: 通過實現 InitializingBean/DisposableBean 介面來定製初始化之後/銷燬之前的操作方法; 通過 <bean> 元素的 init-method/destroy-method屬性指定初始化之後 /銷燬之前呼叫的

Spring何時初始bean

今天在看《spring原始碼深度解析》第五章關於bean載入的部分,跟蹤原始碼的過程中產生一個困惑。就是在我的程式碼中呼叫getBean以前,在載入xml配置檔案的時候對應的bean就已經進行了初始化。 程式碼如下: Main.java public cl

springbean初始過程

在傳統的Java應用中,Bean的生命週期非常簡單。Java的關鍵詞new用來例項化Bean(或許他是非序列化的)。這樣就夠用了。相反,Bean 的生命週期在Spring容器中更加細緻。理解Spring Bean的生命週期非常重要,因為你或許要利用Spring提供的機會來訂製