spring擴展點之三:Spring 的監聽事件 ApplicationListener 和 ApplicationEvent 用法,在spring啓動後作些事情

spring擴展點之三:Spring 的監聽事件 ApplicationListener 和 ApplicationEvent 用法,在spring啓動後作些事情html

服務網關zuul之七:zuul中的動態刷新路由配置java

 《觀察者模式與監聽模式web

 《JDK自帶的觀察者模式spring

JDK自帶的監聽器模式sql

ApplicationEvent事件機制源碼分析設計模式

背景

在開發工做中,用到spring cloud的zuul,zuul中的動態刷新zuul的路由信息中用到了事件監聽,事件監聽也是設計模式中 發佈-訂閱模式、觀察者模式的一種實現。緩存

在spring-cloud-netflix-core-1.4.4.RELEASE.jar中org.springframework.cloud.netflix.zuul.RoutesRefreshedEvent.javamvc

package org.springframework.cloud.netflix.zuul;

import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.context.ApplicationEvent;

/**
 * @author Dave Syer
 */
@SuppressWarnings("serial")
public class RoutesRefreshedEvent extends ApplicationEvent {

    private RouteLocator locator;

    public RoutesRefreshedEvent(RouteLocator locator) {
        super(locator);
        this.locator = locator;
    }

    public RouteLocator getLocator() {
        return this.locator;
    }

}

觀察者模式:簡單的來說就是你在作事情的時候身邊有人在盯着你,當你作的某一件事情是旁邊觀察的人感興趣的事情的時候,他會根據這個事情作一些其餘的事,可是盯着你看的人必需要到你這裏來登記,不然你沒法通知到他(或者說他沒有資格來盯着你作事情)。app

正文

要想順利的建立監聽器,並起做用,這個過程當中須要這樣幾個角色:
一、事件(event)能夠封裝和傳遞監聽器中要處理的參數,如對象或字符串,並做爲監聽器中監聽的目標。
二、監聽器(listener)具體根據事件發生的業務處理模塊,這裏能夠接收處理事件中封裝的對象或字符串。
三、事件發佈者(publisher)事件發生的觸發者。異步

在Spring中的,若是一個Bean實現了ApplicationListener接口,而且已經發布到容器中去,每次ApplicationContext發佈一個ApplicationEvent事件,這個Bean就會接到通知。Spring事件機制是觀察者模式的實現。

Spring中提供的標準事件:

  • ContextRefreshEvent,當ApplicationContext容器初始化完成或者被刷新的時候,就會發布該事件。好比調用ConfigurableApplicationContext接口中的refresh()方法。此處的容器初始化指的是全部的Bean都被成功裝載,後處理(post-processor)Bean被檢測到而且激活,全部單例Bean都被預實例化,ApplicationContext容器已經可使用。只要上下文沒有被關閉,刷新能夠被屢次觸發。XMLWebApplicationContext支持熱刷新,GenericApplicationContext不支持熱刷新。

  • ContextStartedEvent,當ApplicationContext啓動的時候發佈事件,即調用ConfigurableApplicationContext接口的start方法的時候。這裏的啓動是指,全部的被容器管理生命週期的Bean接受到一個明確的啓動信號。在常常須要中止後從新啓動的場合比較適用。

  • ContextStoppedEvent,當ApplicationContext容器中止的時候發佈事件,即調用ConfigurableApplicationContext的close方法的時候。這裏的中止是指,全部被容器管理生命週期的Bean接到一個明確的中止信號。

  • ContextClosedEvent,當ApplicationContext關閉的時候發佈事件,即調用ConfigurableApplicationContext的close方法的時候,關閉指的是全部的單例Bean都被銷燬。關閉上下後,不能從新刷新或者從新啓動。

  • RequestHandledEvent,只能用於DispatcherServlet的web應用,Spring處理用戶請求結束後,系統會觸發該事件。

實現

ApplicationEvent,容器事件,必須被ApplicationContext發佈。

ApplicationListener,監聽器,可由容器中任何監聽器Bean擔任。

實現了ApplicationListener接口以後,須要實現方法onApplicationEvent(),在容器將全部的Bean都初始化完成以後,就會執行該方法。

觀察者模式

觀察者模式,Observer Pattern也叫做發佈訂閱模式Publish/Subscribe。定義對象間一對多的依賴關係,使得每當一個對象改變狀態,則全部依賴與它的對象都會獲得通知,並被自動更新。

觀察者模式的幾角色名稱:

  • Subject被觀察者,定義被觀察者必須實現的職責,它能動態的增長取消觀察者,它通常是抽象類或者是實現類,僅僅完成做爲被觀察者必須實現的職責:管理觀察者並通知觀察者。
  • Observer觀察者,觀察者接受到消息後,即進行更新操做,對接收到的信息進行處理。
  • ConcreteSubject具體的被觀察者,定義被觀察者本身的業務邏輯,同時定義對哪些事件進行通知。
  • ConcreteObserver具體的觀察者,每一個觀察者接收到消息後的處理反應是不一樣的,每一個觀察者都有本身的處理邏輯。

觀察者模式的優勢

  • 觀察者和被觀察者之間是抽象耦合,不論是增長觀察者仍是被觀察者都很是容易擴展。
  • 創建一套觸發機制。

觀察者模式的缺點

觀察者模式須要考慮開發效率和運行效率問題,一個被觀察者,多個觀察者,開發和調試比較複雜,Java消息的通知默認是順序執行的,一個觀察者卡殼,會影響總體的執行效率。這種狀況通常考慮異步的方式。

使用場景

  • 關聯行爲場景,關聯是可拆分的。
  • 事件多級觸發場景。
  • 跨系統的消息交換場景,如消息隊列的處理機制。

Java中的觀察者模式

java.util.Observable類和java.util.Observer接口。

訂閱發佈模型

觀察者模式也叫做發佈/訂閱模式。

1、非註解的監聽器的實現方式

非註解的監聽器的實現方式,這樣有利於瞭解一下註解實現的原理 

什麼是ApplicationContext? 
它是Spring的核心,Context咱們一般解釋爲上下文環境,可是理解成容器會更好些。 
ApplicationContext則是應用的容器。
Spring把Bean(object)放在容器中,須要用就經過get方法取出來。
ApplicationEven:是個抽象類,裏面只有一個構造函數和一個長整型的timestamp。
ApplicationListener:是一個接口,裏面只有一個onApplicationEvent方法。

package org.springframework.context;
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {

    /**
     * Handle an application event.
     * @param event the event to respond to
     */
    void onApplicationEvent(E event);

}

因此本身的類在實現該接口的時候,要實裝該方法。


若是在上下文中部署一個實現了ApplicationListener接口的bean,那麼每當在一個ApplicationEvent發佈到ApplicationContext時,這個bean獲得通知。其實這就是標準的Oberver設計模式。

 

2.一、初始化處理

在一些業務場景中,當容器初始化完成以後,須要處理一些操做,好比一些數據的加載、初始化緩存、特定任務的註冊等等。通常來講一個項目啓動時須要加載或者執行一些特殊的任務來初始化系統,一般的作法就是用servlet去初始化,可是 servlet在使用spring bean時不能直接注入,還須要在web.xml配置,比較麻煩(見http://www.cnblogs.com/duanxz/p/3772979.html)。這個時候咱們就可使用Spring提供的ApplicationListener來進行操做。
本文以在Spring boot下的使用爲例來進行說明。首先,須要實現ApplicationListener接口並實現onApplicationEvent方法。把須要處理的操做放在onApplicationEvent中進行處理:
而後,實例化ApplicationStartListener這個類,在Spring boot中經過一個配置類來進行實例化:
隨後,啓動Spring boot服務,打印出一下內容:
從打印的結果能夠看出,ApplicationStartListener的onApplicationEvent方法在容器啓動時已經被成功調用了。而此時初始化的容器爲root容器。

下面給出例子:
首先建立一個ApplicationEvent實現類:

import org.springframework.context.ApplicationEvent;  
  
public class EmailEvent extends ApplicationEvent {  
    /** 
     * <p>Description:</p> 
     */  
    private static final long serialVersionUID = 1L;  
    public String address;    
    public String text;  
      
    public EmailEvent(Object source) {  
        super(source);  
    }  
      
    public EmailEvent(Object source, String address, String text) {  
        super(source);  
        this.address = address;  
        this.text = text;  
    }  
      
    public void print(){  
        System.out.println("hello spring event!");  
    }  
  
}  

給出監聽器:

import org.springframework.context.ApplicationEvent;  
import org.springframework.context.ApplicationListener;  
  
public class EmailListener implements ApplicationListener {  
  
    public void onApplicationEvent(ApplicationEvent  event) {  
        if(event instanceof EmailEvent){  
            EmailEvent emailEvent = (EmailEvent)event;  
            emailEvent.print();  
            System.out.println("the source is:"+emailEvent.getSource());  
            System.out.println("the address is:"+emailEvent.address);  
            System.out.println("the email's context is:"+emailEvent.text);  
        }  
          
    }  
  
}  
applicationContext.xml文件配置:  
<bean id="emailListener" class="com.spring.event.EmailListener"></bean>  

測試類:  

import org.springframework.context.ApplicationContext;  
import org.springframework.context.support.ClassPathXmlApplicationContext;  
public class Test {  
    public static void main(String[] args) {  
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");  
          
        //HelloBean hello = (HelloBean) context.getBean("helloBean");  
        //hello.setApplicationContext(context);  
        EmailEvent event = new EmailEvent("hello","boylmx@163.com","this is a email text!");  
        context.publishEvent(event);  
        //System.out.println();  
    }  
}  

測試結果:
hello spring event!
the source is:hello
the address is:boylmx@163.com
the email's context is:this is a email text!

 

2、註解 實現事件監聽

好處:不用每次都去實現ApplicationListener,能夠在一個class中定義多個方法,用@EventListener來作方法級別的註解。例如:

package com.mu.listener;

import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import com.mu.event.MyTestEvent;

@Component
public class MyAnnotationListener {

    @EventListener
    public void listener1(MyTestEvent event) {
        System.out.println("註解監聽器1:" + event.getMsg());
    }
}

 

在實際工做中,事件監聽常常會用在發送通知,消息、郵件等狀況下,那麼這個時候每每是須要異步執行的,不能在業務的主線程裏面,那怎麼樣能夠實現異步處理呢?固然你能夠寫一個線程,單獨作這個事情,在此,我比較推薦的是用spring的@Async註解方式,一個簡單的註解,就能夠把某一個方法或者類下面的全部方法所有變成異步處理的方法,這樣,就能夠作處處理監聽事件的時候也不會阻塞主進程了。
新增監聽器listener2,在方法上加上@Async註解,可是此註解不能標註static修飾的方法

package com.mu.listener;

import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import com.mu.event.MyTestEvent;

@Component
public class MyAnnotationListener {

    @EventListener
    public void listener1(MyTestEvent event) {
        System.out.println("註解監聽器1:" + event.getMsg());
    }

    @EventListener
 @Async public void listener2(MyTestEvent event) {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("註解監聽器2:" + event.getMsg());
    }

}

 

想要啓動註解方式的異步處理辦法,還須要作一下配置

註解的應用範圍: 
類:表示這個類中的全部方法都是異步的 
方法:表示這個方法是異步的,若是類也註解了,則以這個方法的註解爲準 
配置:executor:指定一個缺省的executor給@Async使用。 

 

 

-------------------------------------------------------------------------------------------------------------------------

當spring 容器初始化完成後執行某個方法 防止onApplicationEvent方法被執行兩次

在作web項目開發中,尤爲是企業級應用開發的時候,每每會在工程啓動的時候作許多的前置檢查。

  好比檢查是否使用了咱們組禁止使用的Mysql的group_concat函數,若是使用了項目就不能啓動,並指出哪一個文件的xml文件使用了這個函數。

而在Spring的web項目中,咱們能夠介入Spring的啓動過程。咱們但願在Spring容器將全部的Bean都初始化完成以後,作一些操做,這個時候咱們就能夠實現一個接口:

package com.yk.test.executor.processor
public class InstantiationTracingBeanPostProcessor implements ApplicationListener<ContextRefreshedEvent> {
     @Override
     public void onApplicationEvent(ContextRefreshedEvent event) {
       //須要執行的邏輯代碼,當spring容器初始化完成後就會執行該方法。
  }
 }

同時在Spring的配置文件中,添加註入:

<bean class="com.yk.test.executor.processor.InstantiationTracingBeanPostProcessor"/>

可是這個時候,會存在一個問題,在web 項目中(spring mvc),系統會存在兩個容器,一個是root application context ,另外一個就是咱們本身的 projectName-servlet  context(做爲root application context的子容器)。

這種狀況下,就會形成onApplicationEvent方法被執行兩次。爲了不上面提到的問題,咱們能夠只在root application context初始化完成後調用邏輯代碼,其餘的容器的初始化完成,則不作任何處理,修改後代碼

以下:

@Override
   public void onApplicationEvent(ContextRefreshedEvent event) {
       if(event.getApplicationContext().getParent() == null){//root application context 沒有parent,他就是老大.
            //須要執行的邏輯代碼,當spring容器初始化完成後就會執行該方法。
       }
   }

 

Spring 的事件傳播機制 是基於觀察者模式(Observer)實現的,它能夠將 Spring Bean 的改變定義爲事件 ApplicationEvent,經過 ApplicationListener 監聽 ApplicationEvent 事件,一旦Spring Bean 使用 ApplicationContext.publishEvent( ApplicationEvent event )發佈事件後,Spring 容器會通知註冊在 bean.xml 中全部 ApplicationListener 接口的實現類,最後 ApplicationListener 接口實現類判斷是否響應剛發佈出來的 ApplicationEvent 事件。

因此,要使用 Spring 事件傳播機制須要如下四點:

1. 創建事件類,繼承 ApplicationEvent 父類

2. 創建監聽類,實現 ApplicationListener 接口

3. 在配置文件 bean.xml 中註冊寫好的全部 事件類 和 監聽類

4. 須要發佈事件的類 要實現 ApplicationContextAware 接口,並獲取 ApplicationContext 參數

隨後即可以開始使用 Spring 事件傳播機制爲咱們服務:(爲了講解流程的連貫性,續以上步驟來測試)

4.1 在本身編寫的須要發佈事件的 Action 類中實例化 1 中編寫好的事件類,並使用 ApplicationContext.publishEvent 發佈事件

5. 經過 Spring 調用 Action 方法,觀察輸出結果(本文使用 Junit 測試)

 

如下爲1-5步驟的源碼:

1. 創建事件類 ActionEvent.java 

 

[java]  view plain  copy
 
  1. public class ActionEvent extends ApplicationEvent{  
  2.   
  3.     public ActionEvent(Object source) {  
  4.         super(source);  
  5.         System.out.println("This is ActionEvent");  
  6.     }  
  7. }  

 

2. 創建監聽類 ActionListener1.java、ActionListener2.java

 

 

[java]  view plain  copy
 
  1. public class ActionListener1 implements ApplicationListener {  
  2.   
  3.     public void onApplicationEvent(ApplicationEvent event) {  
  4.         if(event instanceof ActionEvent){  
  5.             System.out.println("ActionListener1: "+event.toString());  
  6.         }  
  7.     }  
  8.   
  9. }  
[java]  view plain  copy
 
  1. public class ActionListener2 implements ApplicationListener {  
  2.   
  3.     public void onApplicationEvent(ApplicationEvent event) {  
  4.         if(event instanceof ActionEvent){  
  5.             System.out.println("ActionListener2: "+event.toString());  
  6.         }  
  7.     }  
  8.   
  9. }  

 

3. 在 bean.xml 中註冊事件類和監聽類

 

[java]  view plain  copy
 
  1. <bean id="loginaction" class="com.ayali.action.LoginAction"/>  
  2. <bean id="listener1" class="com.ayali.action.ActionListener1"/>  
  3. <bean id="listener2" class="com.ayali.action.ActionListener2"/>  

 

4. 編寫 須要發佈事件的 loginAction.java

 

 

[java]  view plain  copy
 
  1. public class LoginAction implements ApplicationContextAware{  
  2.   
  3.     private ApplicationContext applicationContext;  
  4.       
  5.     public void setApplicationContext(ApplicationContext applicationContext)  
  6.             throws BeansException {  
  7.         this.applicationContext = applicationContext;  
  8.     }  
  9.       
  10.     public void login(String username, String password){  
  11.         ActionEvent event = new ActionEvent(username);  
  12.         this.applicationContext.publishEvent(event);  
  13.     }  
  14.   
  15. }  

 

5. 編寫測試方法

 

 

[java]  view plain  copy
 
  1. public void testActionListener(){  
  2.     ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml");  
  3.     LoginAction loginAction = (LoginAction) ctx.getBean("loginaction");  
  4.     loginAction.login("jack", "123");  
  5. }  

 

輸出結果爲:

 

[html]  view plain  copy
 
    1. This is ActionEvent  
    2. ActionListener1:com.ayali.action.ActionEvent[source=jack]  
    3. ActionListener2:com.ayali.action.ActionEvent[source=jack]  
相關文章
相關標籤/搜索