《spring擴展點之三:Spring 的監聽事件 ApplicationListener 和 ApplicationEvent 用法,在spring啟動后做些事情》
背景
在開發工作中,用到spring cloud的zuul,zuul中的動態刷新zuul的路由信息中用到了事件監聽,事件監聽也是設計模式中 發布-訂閱模式、觀察者模式的一種實現。
在spring-cloud-netflix-core-1.4.4.RELEASE.jar中org.springframework.cloud.netflix.zuul.RoutesRefreshedEvent.java
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; } }
觀察者模式:簡單的來講就是你在做事情的時候身邊有人在盯着你,當你做的某一件事情是旁邊觀察的人感興趣的事情的時候,他會根據這個事情做一些其他的事,但是盯着你看的人必須要到你這里來登記,否則你無法通知到他(或者說他沒有資格來盯着你做事情)。
正文
要想順利的創建監聽器,並起作用,這個過程中需要這樣幾個角色:
1、事件(event)可以封裝和傳遞監聽器中要處理的參數,如對象或字符串,並作為監聽器中監聽的目標。
2、監聽器(listener)具體根據事件發生的業務處理模塊,這里可以接收處理事件中封裝的對象或字符串。
3、事件發布者(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接口。
訂閱發布模型
觀察者模式也叫作發布/訂閱模式。
一、非注解的監聽器的實現方式
非注解的監聽器的實現方式,這樣有利於了解一下注解實現的原理
什么是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.1、初始化處理
下面給出例子:
首先創建一個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); } } }
<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!
二、注解 實現事件監聽
好處:不用每次都去實現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
- public class ActionEvent extends ApplicationEvent{
- public ActionEvent(Object source) {
- super(source);
- System.out.println("This is ActionEvent");
- }
- }
2. 建立監聽類 ActionListener1.java、ActionListener2.java
- public class ActionListener1 implements ApplicationListener {
- public void onApplicationEvent(ApplicationEvent event) {
- if(event instanceof ActionEvent){
- System.out.println("ActionListener1: "+event.toString());
- }
- }
- }
- public class ActionListener2 implements ApplicationListener {
- public void onApplicationEvent(ApplicationEvent event) {
- if(event instanceof ActionEvent){
- System.out.println("ActionListener2: "+event.toString());
- }
- }
- }
3. 在 bean.xml 中注冊事件類和監聽類
- <bean id="loginaction" class="com.ayali.action.LoginAction"/>
- <bean id="listener1" class="com.ayali.action.ActionListener1"/>
- <bean id="listener2" class="com.ayali.action.ActionListener2"/>
4. 編寫 需要發布事件的 loginAction.java
- public class LoginAction implements ApplicationContextAware{
- private ApplicationContext applicationContext;
- public void setApplicationContext(ApplicationContext applicationContext)
- throws BeansException {
- this.applicationContext = applicationContext;
- }
- public void login(String username, String password){
- ActionEvent event = new ActionEvent(username);
- this.applicationContext.publishEvent(event);
- }
- }
5. 編寫測試方法
- public void testActionListener(){
- ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml");
- LoginAction loginAction = (LoginAction) ctx.getBean("loginaction");
- loginAction.login("jack", "123");
- }
輸出結果為:
- This is ActionEvent
- ActionListener1:com.ayali.action.ActionEvent[source=jack]
- ActionListener2:com.ayali.action.ActionEvent[source=jack]