Spring之@EventListener

一.快問快答
1.爲什麼需要使用時間這種模式?
上面將註冊的主要邏輯(用戶信息落庫)和次要的業務邏輯(發送郵件)通過事件的方式解耦了。
次要的業務做成了可插拔的方式,比如不想發送郵件了,只需要將郵件監聽器上面的@Component註釋就可以了,非常方便擴展。
2.spring中實現事件
1.面相接口的方式(ApplicationEvent)
2.面相@EventListener註解的方式
3.spring排序:
方式1:實現org.springframework.core.Ordered接口:Ordered:需要實現一個getOrder方法,返回順序值,值越小,順序越高
方式2:實現org.springframework.core.PriorityOrdered接口:PriorityOrdered接口繼承了方式一種的Ordered接口,所以如果你實現PriorityOrdered接口,也需要實現getOrder方法。
方式3:類上使用@org.springframework.core.annotation.Order註解
4.監聽器異步模式:
監聽器最終是通過ApplicationEventMulticaster內部的實現來調用的,所以我們關注的重點就是這個類,這個類默認有個實現類SimpleApplicationEventMulticaster,這個類是支持監聽器異步調用的。
SimpleApplicationEventMulticaster中的taskExecutor

下面我們就一個個來介紹。

爲什麼需要使用時間這種模式?

先來看一個業務場景:

產品經理:路人,這兩天你幫我實現一個註冊的功能

我:註冊功能比較簡單,將用戶信息入庫就可以了,僞代碼如下:

public void registerUser(UserModel user){
    //插入用戶信息到db,完成註冊
    this.insertUser(user);
}

過了幾天,產品經理:路人,註冊成功之後,給用戶發送一封註冊成功的郵件

我:修改了一下上面註冊代碼,如下:

public void registerUser(UserModel user){
    //插入用戶信息到db,完成註冊
    this.insertUser(user);
    //發送郵件
    this.sendEmailToUser(user);
}

由於修改了註冊接口,所以所有調用這個方法的地方都需要重新測試一遍,讓測試的兄弟們幫忙跑了一遍。

又過了幾天,產品經理:路人,註冊成功之後,給用戶發一下優惠券

我:好的,又調整了一下代碼

public void registerUser(UserModel user){
    //插入用戶信息到db,完成註冊
    this.insertUser(user);
    //發送郵件
    this.sendEmailToUser(user);
    //發送優惠券
    this.sendCouponToUser(user);
}

我:測試的兄弟們,辛苦一下大家,註冊接口又修改了,幫忙再過一遍。

過了一段時間,公司效益太好,產品經理:路人,註冊的時候,取消給用戶發送優惠券的功能。

我:又跑去調整了一下上面代碼,將發送優惠券的功能幹掉了,如下

public void registerUser(UserModel user){
    //插入用戶信息到db,完成註冊
    this.insertUser(user);
    //發送郵件
    this.sendEmailToUser(user);
}

由於調整了代碼,而註冊功能又屬於核心業務,所以需要讓測試再次幫忙過一遍,又要麻煩測試來一遍了。

突然有一天,產品經理:路人,註冊接口怎麼這麼慢啊,並且還經常失敗?你這讓公司要損失多少用戶啊

我:趕緊跑去查看了一下運行日誌,發現註冊的時候給用戶發送郵件不穩定,依賴於第三方郵件服務器,耗時比較長,並且容易失敗。

跑去給產品經理說:由於郵件服務器不穩定的原因,導致註冊不穩定。

產品經理:郵件你可以不發,但是你得確保註冊功能必須可以用啊。

我想了想,將上面代碼改成了下面這樣,發送郵件放在了子線程中執行:

public void registerUser(UserModel user){
    //插入用戶信息到db,完成註冊
    this.insertUser(user);
    //發送郵件,放在子線程中執行,郵件的發送結果對註冊邏輯不會有干擾作用
    new Thread(()->{
        this.sendEmailToUser(user);
    }).start();
}

又過了幾天,產品經理又跑來了說:路人,最近效益不好,需要刺激用戶消費,註冊的時候繼續發送優惠券。

我:倒,這是玩我麼,反反覆覆讓我調整註冊的代碼,讓我改還好,讓測試也反反覆覆來回搞,這是要玩死我們啊。

花了點時間,好好覆盤整理了一下:發現問題不在於產品經理,從業務上來看,產品提的這些需求都是需求合理的,而結果代碼反覆調整、測試反覆測試,以及一些次要的功能導致註冊接口不穩定,這些問題歸根到底,主要還是我的設計不合理導致的,將註冊功能中的一些次要的功能耦合到註冊的方法中了,並且這些功能可能會經常調整,導致了註冊接口的不穩定性。

其實上面代碼可以這麼做:

找3個人:註冊器、路人A、路人B。

註冊器:負責將用戶信息落庫,落庫成功之後,喊一聲:用戶XXX註冊成功了。

路人A和路人B,豎起耳朵,當聽到有人喊:XXX註冊成功 的聲音之後,立即行動做出下面反應:

路人A:負責給XXX發送一封註冊郵件

路人B:負責給XXX發送優惠券

我們來看一下:

註冊器只負責將用戶信息落庫,及廣播一條用戶註冊成功的消息。

A和B相當於一個監聽者,只負責監聽用戶註冊成功的消息,當聽到有這個消息產生的時候,A和B就去做自己的事情。

這裏面註冊器是感知不到A/B存在的,A和B也不用感知註冊器的存在,A/B只用關注是否有人廣播:XXX註冊成功了的消息,當AB聽到有人廣播註冊成功的消息,他們才做出反應,其他時間閒着休息。

這種方式就非常好:

當不想給用戶發送優惠券的時候,只需要將B去掉就行了,此時基本上也不用測試,註冊一下B的代碼就行了。

若註冊成功之後需要更多業務,比如還需要給用戶增加積分,只需新增一個監聽者C,監聽到註冊成功消息後,負責給用戶添加積分,此時根本不用去調整註冊的代碼,開發者和測試人員只需要確保監聽者C中的正確性就可以了。

上面這種模式就是事件模式。

事件模式中的幾個概念

事件源:事件的觸發者,比如上面的註冊器就是事件源。

事件:描述發生了什麼事情的對象,比如上面的:xxx註冊成功的事件

事件監聽器:監聽到事件發生的時候,做一些處理,比如上面的:路人A、路人B

下面我們使用事件模式實現用戶註冊的業務

我們先來定義和事件相關的幾個類。

事件對象

表示所有事件的父類,內部有個source字段,表示事件源;我們自定義的事件需要繼承這個類。

package com.javacode2018.lesson003.demo1.test0.event;
 
/**
 * 事件對象
 */
public abstract class AbstractEvent {
 
    //事件源
    protected Object source;
 
    public AbstractEvent(Object source) {
        this.source = source;
    }
 
    public Object getSource() {
        return source;
    }
 
    public void setSource(Object source) {
        this.source = source;
    }
}

事件監聽器

我們使用一個接口來表示事件監聽器,是個泛型接口,後面的類型E表示當前監聽器需要監聽的事件類型,此接口中只有一個方法,用來實現處理事件的業務;其定義的監聽器需要實現這個接口。

package com.javacode2018.lesson003.demo1.test0.event;
 
/**
 * 事件監聽器
 *
 * @param <E> 當前監聽器感興趣的事件類型
 */
public interface EventListener<E extends AbstractEvent> {
    /**
     * 此方法負責處理事件
     *
     * @param event 要響應的事件對象
     */
    void onEvent(E event);
}

事件廣播器

  • 負責事件監聽器的管理(註冊監聽器&移除監聽器,將事件和監聽器關聯起來)

  • 負責事件的廣播(將事件廣播給所有的監聽器,對該事件感興趣的監聽器會處理該事件)

package com.javacode2018.lesson003.demo1.test0.event;
 
/**
 * 事件廣播器:
 * 1.負責事件監聽器的管理(註冊監聽器&移除監聽器,將事件和監聽器關聯起來)
 * 2.負責事件的廣播(將事件廣播給所有的監聽器,對該事件感興趣的監聽器會處理該事件)
 */
public interface EventMulticaster {
 
    /**
     * 廣播事件給所有的監聽器,對該事件感興趣的監聽器會處理該事件
     *
     * @param event
     */
    void multicastEvent(AbstractEvent event);
 
    /**
     * 添加一個事件監聽器(監聽器中包含了監聽器中能夠處理的事件)
     *
     * @param listener 需要添加監聽器
     */
    void addEventListener(EventListener<?> listener);
 
 
    /**
     * 將事件監聽器移除
     *
     * @param listener 需要移除的監聽器
     */
    void removeEventListener(EventListener<?> listener);
}

事件廣播默認實現

package com.javacode2018.lesson003.demo1.test0.event;
 
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
 
/**
 * 事件廣播器簡單實現
 */
public class SimpleEventMulticaster implements EventMulticaster {
 
    private Map<Class<?>, List<EventListener>> eventObjectEventListenerMap = new ConcurrentHashMap<>();
 
    @Override
    public void multicastEvent(AbstractEvent event) {
        List<EventListener> eventListeners = this.eventObjectEventListenerMap.get(event.getClass());
        if (eventListeners != null) {
            for (EventListener eventListener : eventListeners) {
                eventListener.onEvent(event);
            }
        }
    }
 
    @Override
    public void addEventListener(EventListener<?> listener) {
        Class<?> eventType = this.getEventType(listener);
        List<EventListener> eventListeners = this.eventObjectEventListenerMap.get(eventType);
        if (eventListeners == null) {
            eventListeners = new ArrayList<>();
            this.eventObjectEventListenerMap.put(eventType, eventListeners);
        }
        eventListeners.add(listener);
    }
 
    @Override
    public void removeEventListener(EventListener<?> listener) {
        Class<?> eventType = this.getEventType(listener);
        List<EventListener> eventListeners = this.eventObjectEventListenerMap.get(eventType);
        if (eventListeners != null) {
            eventListeners.remove(listener);
        }
    }
 
    /**
     * 獲取事件監聽器需要監聽的事件類型
     *
     * @param listener
     * @return
     */
    protected Class<?> getEventType(EventListener listener) {
        ParameterizedType parameterizedType = (ParameterizedType) listener.getClass().getGenericInterfaces()[0];
        Type eventType = parameterizedType.getActualTypeArguments()[0];
        return (Class<?>) eventType;
    }
 
}

上面3個類支撐了整個時間模型,下面我們使用上面三個類來實現註冊的功能,目標是:高內聚低耦合,讓註冊邏輯方便擴展。

自定義用戶註冊成功事件類

繼承了AbstractEvent

package com.javacode2018.lesson003.demo1.test0.userregister;
 
import com.javacode2018.lesson003.demo1.test0.event.AbstractEvent;
 
/**
 * 用戶註冊成功事件
 */
public class UserRegisterSuccessEvent extends AbstractEvent {
    //用戶名
    private String userName;
 
    /**
     * 創建用戶註冊成功事件對象
     *
     * @param source   事件源
     * @param userName 當前註冊的用戶名
     */
    public UserRegisterSuccessEvent(Object source, String userName) {
        super(source);
        this.userName = userName;
    }
 
    public String getUserName() {
        return userName;
    }
 
    public void setUserName(String userName) {
        this.userName = userName;
    }
}

用戶註冊服務

負責實現用戶註冊邏輯

package com.javacode2018.lesson003.demo1.test0.userregister;
 
import com.javacode2018.lesson003.demo1.test0.event.EventMulticaster;
 
/**
 * 用戶註冊服務
 */
public class UserRegisterService {
    //事件發佈者
    private EventMulticaster eventMulticaster; //@0
 
    /**
     * 註冊用戶
     *
     * @param userName 用戶名
     */
    public void registerUser(String userName) { //@1
        //用戶註冊(將用戶信息入庫等操作)
        System.out.println(String.format("用戶【%s】註冊成功", userName)); //@2
        //廣播事件
        this.eventMulticaster.multicastEvent(new UserRegisterSuccessEvent(this, userName)); //@3
    }
 
    public EventMulticaster getEventMulticaster() {
        return eventMulticaster;
    }
 
    public void setEventMulticaster(EventMulticaster eventMulticaster) {
        this.eventMulticaster = eventMulticaster;
    }
}

@0:事件發佈者

@1:registerUser這個方法負責用戶註冊,內部主要做了2個事情

@2:模擬將用戶信息落庫

@3:使用事件發佈者eventPublisher發佈用戶註冊成功的消息:

下面我們使用spring來將上面的對象組裝起來

package com.javacode2018.lesson003.demo1.test0.userregister;
 
import com.javacode2018.lesson003.demo1.test0.event.EventListener;
import com.javacode2018.lesson003.demo1.test0.event.EventMulticaster;
import com.javacode2018.lesson003.demo1.test0.event.SimpleEventMulticaster;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
 
import java.util.List;
 
@Configuration
@ComponentScan
public class MainConfig0 {
 
    /**
     * 註冊一個bean:事件發佈者
     *
     * @param eventListeners
     * @return
     */
    @Bean
    @Autowired(required = false)
    public EventMulticaster eventMulticaster(List<EventListener> eventListeners) { //@1
        EventMulticaster eventPublisher = new SimpleEventMulticaster();
        if (eventListeners != null) {
            eventListeners.forEach(eventPublisher::addEventListener);
        }
        return eventPublisher;
    }
 
    /**
     * 註冊一個bean:用戶註冊服務
     *
     * @param eventMulticaster
     * @return
     */
    @Bean
    public UserRegisterService userRegisterService(EventMulticaster eventMulticaster) { //@2
        UserRegisterService userRegisterService = new UserRegisterService();
        userRegisterService.setEventMulticaster(eventMulticaster);
        return userRegisterService;
    }
}

上面有2個方法,負責向spring容器中註冊2個bean。

@1:向spring容器中註冊了一個bean:事件發佈者,方法傳入了EventListener類型的List,這個地方會將容器中所有的事件監聽器注入進來,丟到EventMulticaster中。

@2:向spring容器中註冊了一個bean:用戶註冊服務

來個測試用例模擬用戶註冊

package com.javacode2018.lesson003.demo1;
 
import com.javacode2018.lesson003.demo1.test0.userregister.MainConfig0;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
public class EventTest {
 
    @Test
    public void test0() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig0.class);
        //獲取用戶註冊服務
        com.javacode2018.lesson003.demo1.test0.userregister.UserRegisterService userRegisterService =
                context.getBean(com.javacode2018.lesson003.demo1.test0.userregister.UserRegisterService.class);
        //模擬用戶註冊
        userRegisterService.registerUser("路人甲Java");
    }
 
}

運行輸出

用戶【路人甲Java】註冊成功

添加註冊成功發送郵件功能

下面添加一個註冊成功發送郵件的功能,只需要自定義一個監聽用戶註冊成功事件的監聽器就可以了,其他代碼不需要任何改動,如下

package com.javacode2018.lesson003.demo1.test0.userregister;
 
 
import com.javacode2018.lesson003.demo1.test0.event.EventListener;
import org.springframework.stereotype.Component;
 
/**
 * 用戶註冊成功事件監聽器->負責給用戶發送郵件
 */
@Component
public class SendEmailOnUserRegisterSuccessListener implements EventListener<UserRegisterSuccessEvent> {
    @Override
    public void onEvent(UserRegisterSuccessEvent event) {
        System.out.println(
                String.format("給用戶【%s】發送註冊成功郵件!", event.getUserName()));
    }
}

上面這個類使用了@Component,會被自動掃描註冊到spring容器。

再次運行測試用例輸出

  1.  用戶【路人甲Java】註冊成功
  2.  給用戶【路人甲Java】發送註冊成功郵件!

小結

上面將註冊的主要邏輯(用戶信息落庫)和次要的業務邏輯(發送郵件)通過事件的方式解耦了。次要的業務做成了可插拔的方式,比如不想發送郵件了,只需要將郵件監聽器上面的@Component註釋就可以了,非常方便擴展。

上面用到的和事件相關的幾個類,都是我們自己實現的,其實這些功能在spring中已經幫我們實現好了,用起來更容易一些,下面帶大家來體驗一下。

Spring中實現事件模式

事件相關的幾個類

Spring中事件相關的幾個類需要先了解一下,下面來個表格,將spring中事件相關的類和我們上面自定義的類做個對比,方便大家理解

 

 

這些類和我們自定義的類中代碼有點類似,有興趣的可以去看一下源碼,這裏就不列出來了。

硬編碼的方式使用spring事件3步驟

步驟1:定義事件

自定義事件,需要繼承ApplicationEvent類,

步驟2:定義監聽器

自定義事件監聽器,需要實現ApplicationListener接口,這個接口有個方法onApplicationEvent需要實現,用來處理感興趣的事件。

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
 
    /**
     * Handle an application event.
     * @param event the event to respond to
     */
    void onApplicationEvent(E event);
 
}

步驟3:創建事件廣播器

創建事件廣播器ApplicationEventMulticaster,這是個接口,你可以自己實現這個接口,也可以直接使用系統給我們提供的SimpleApplicationEventMulticaster,如下:

ApplicationEventMulticaster applicationEventMulticaster = new SimpleApplicationEventMulticaster();

步驟4:向廣播器中註冊事件監聽器

將事件監聽器註冊到廣播器ApplicationEventMulticaster中,如:

ApplicationEventMulticaster applicationEventMulticaster = new SimpleApplicationEventMulticaster();
applicationEventMulticaster.addApplicationListener(new SendEmailOnOrderCreateListener());

步驟5:通過廣播器發佈事件

廣播事件,調用ApplicationEventMulticaster#multicastEvent方法廣播事件,此時廣播器中對這個事件感興趣的監聽器會處理這個事件。

applicationEventMulticaster.multicastEvent(new OrderCreateEvent(applicationEventMulticaster, 1L));

下面我們來個案例將這5個步驟串起來感受一下。

案例

實現功能:電商中訂單創建成功之後,給下單人發送一封郵件,發送郵件的功能放在監聽器中實現。

下面上代碼

來個事件類:訂單創建成功事件

package com.javacode2018.lesson003.demo1.test1;
 
import org.springframework.context.ApplicationEvent;
 
/**
 * 訂單創建事件
 */
public class OrderCreateEvent extends ApplicationEvent {
    //訂單id
    private Long orderId;
 
    /**
     * @param source  事件源
     * @param orderId 訂單id
     */
    public OrderCreateEvent(Object source, Long orderId) {
        super(source);
        this.orderId = orderId;
    }
 
    public Long getOrderId() {
        return orderId;
    }
 
    public void setOrderId(Long orderId) {
        this.orderId = orderId;
    }
}

來個監聽器:負責監聽訂單成功事件,發送郵件

package com.javacode2018.lesson003.demo1.test1;
 
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
 
/**
 * 訂單創建成功給用戶發送郵件
 */
@Component
public class SendEmailOnOrderCreateListener implements ApplicationListener<OrderCreateEvent> {
    @Override
    public void onApplicationEvent(OrderCreateEvent event) {
        System.out.println(String.format("訂單【%d】創建成功,給下單人發送郵件通知!", event.getOrderId()));
    }
}

測試用例

@Test
public void test2() throws InterruptedException {
    //創建事件廣播器
    ApplicationEventMulticaster applicationEventMulticaster = new SimpleApplicationEventMulticaster();
    //註冊事件監聽器
    applicationEventMulticaster.addApplicationListener(new SendEmailOnOrderCreateListener());
    //廣播事件訂單創建事件
    applicationEventMulticaster.multicastEvent(new OrderCreateEvent(applicationEventMulticaster, 1L));
}

運行輸出

訂單【1】創建成功,給下單人發送郵件通知!

ApplicationContext容器中事件的支持

上面演示了spring中事件的使用,那麼平時我們使用spring的時候就這麼使用?

非也非也,上面只是我給大家演示了一下原理。

通常情況下,我們會使用以ApplicationContext結尾的類作爲spring的容器來啓動應用,下面2個是比較常見的

AnnotationConfigApplicationContext
ClassPathXmlApplicationContext

 

 對這個圖我們來解釋一下:

1.AnnotationConfigApplicationContext和ClassPathXmlApplicationContext都繼承了AbstractApplicationContext
2.AbstractApplicationContext實現了ApplicationEventPublisher接口
3.AbstractApplicationContext內部有個ApplicationEventMulticaster類型的字段

上面第三條,說明了AbstractApplicationContext內部已經集成了事件廣播器ApplicationEventMulticaster,說明AbstractApplicationContext內部是具體事件相關功能的,這些功能是通過其內部的ApplicationEventMulticaster來實現的,也就是說將事件的功能委託給了內部的ApplicationEventMulticaster來實現。

ApplicationEventPublisher接口

上面類圖中多了一個新的接口ApplicationEventPublisher,來看一下源碼

@FunctionalInterface
public interface ApplicationEventPublisher {
 
    default void publishEvent(ApplicationEvent event) {
        publishEvent((Object) event);
    }
 
    void publishEvent(Object event);
 
}

這個接口用來發布事件的,內部定義2個方法都是用來發布事件的。

spring中不是有個ApplicationEventMulticaster接口麼,此處怎麼又來了一個發佈事件的接口?

這個接口的實現類中,比如AnnotationConfigApplicationContext內部將這2個方法委託給ApplicationEventMulticaster#multicastEvent進行處理了。

所以調用AbstractApplicationContext中的publishEvent方法,也實現廣播事件的效果,不過使用AbstractApplicationContext也只能通過調用publishEvent方法來廣播事件。

獲取ApplicationEventPublisher對象

如果我們想在普通的bean中獲取ApplicationEventPublisher對象,需要實現ApplicationEventPublisherAware接口

public interface ApplicationEventPublisherAware extends Aware {
    void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher);
}

spring容器會自動通過上面的setApplicationEventPublisher方法將ApplicationEventPublisher注入進來,此時我們就可以使用這個來發布事件了。

Spring爲了簡化事件的使用,提供了2種使用方式

  1. 面相接口的方式

  2. 面相@EventListener註解的方式

面相接口的方式

案例

實現用戶註冊成功後發佈事件,然後在監聽器中發送郵件的功能。

用戶註冊事件

需要繼承ApplicationEvent

package com.javacode2018.lesson003.demo1.test2;
 
import org.springframework.context.ApplicationEvent;
 
/**
 * 用戶註冊事件
 */
public class UserRegisterEvent extends ApplicationEvent {
    //用戶名
    private String userName;
 
    public UserRegisterEvent(Object source, String userName) {
        super(source);
        this.userName = userName;
    }
 
    public String getUserName() {
        return userName;
    }
}

發送郵件監聽器

需實現ApplicationListener接口

package com.javacode2018.lesson003.demo1.test2;
 
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
 
/**
 * 用戶註冊成功發送郵件
 */
@Component
public class SendEmailListener implements ApplicationListener<UserRegisterEvent> {
 
    @Override
    public void onApplicationEvent(UserRegisterEvent event) {
        System.out.println(String.format("給用戶【%s】發送註冊成功郵件!", event.getUserName()));
 
    }
}

用戶註冊服務

內部提供用戶註冊的功能,併發布用戶註冊事件

package com.javacode2018.lesson003.demo1.test2;
 
 
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Component;
 
/**
 * 用戶註冊服務
 */
@Component
public class UserRegisterService implements ApplicationEventPublisherAware {
 
    private ApplicationEventPublisher applicationEventPublisher;
 
    /**
     * 負責用戶註冊及發佈事件的功能
     *
     * @param userName 用戶名
     */
    public void registerUser(String userName) {
        //用戶註冊(將用戶信息入庫等操作)
        System.out.println(String.format("用戶【%s】註冊成功", userName));
        //發佈註冊成功事件
        this.applicationEventPublisher.publishEvent(new UserRegisterEvent(this, userName));
    }
 
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { //@1
        this.applicationEventPublisher = applicationEventPublisher;
    }
}

注意上面實現了ApplicationEventPublisherAware接口,spring容器會通過@1ApplicationEventPublisher注入進來,然後我們就可以使用這個來發布事件了。

來個spring配置類
package com.javacode2018.lesson003.demo1.test2;
 
import org.springframework.context.annotation.ComponentScan;
 
@ComponentScan
public class MainConfig2 {
}

上測試用例

@Test
public void test2() throws InterruptedException {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(MainConfig2.class);
    context.refresh();
    //獲取用戶註冊服務
    com.javacode2018.lesson003.demo1.test2.UserRegisterService userRegisterService =
            context.getBean(com.javacode2018.lesson003.demo1.test2.UserRegisterService.class);
    //模擬用戶註冊
    userRegisterService.registerUser("路人甲Java");
}

運行輸出

用戶【路人甲Java】註冊成功
給用戶【路人甲Java】發送註冊成功郵件!

原理

spring容器在創建bean的過程中,會判斷bean是否爲ApplicationListener類型,進而會將其作爲監聽器註冊到AbstractApplicationContext#applicationEventMulticaster中,這塊的源碼在下面這個方法中,有興趣的可以看一下

org.springframework.context.support.ApplicationListenerDetector#postProcessAfterInitialization

小結

從上面這個案例中可以看出,事件類、監聽器類都是通過基於spring中的事件相關的一些接口來實現事件的功能,這種方式我們就稱作面相接口的方式。

面相@EventListener註解方式

用法

上面是通過接口的方式創建一個監聽器,spring還提供了通過@EventListener註解的方式來創建一個監聽器,直接將這個註解標註在一個bean的方法上,那麼這個方法就可以用來處理感興趣的事件,使用更簡單,如下,方法參數類型爲事件的類型:

@Component
public class UserRegisterListener {
    @EventListener
    public void sendMail(UserRegisterEvent event) {
        System.out.println(String.format("給用戶【%s】發送註冊成功郵件!", event.getUserName()));
    }
}

案例

註冊成功之後:來2個監聽器:一個負責發送郵件、一個負責發送優惠券。

其他代碼都不上了,和上面案例中的一樣,主要看監聽器的代碼,如下:

package com.javacode2018.lesson003.demo1.test3;
 
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
 
/**
 * 用戶註冊監聽器
 */
@Component
public class UserRegisterListener {
    @EventListener
    public void sendMail(UserRegisterEvent event) {
        System.out.println(String.format("給用戶【%s】發送註冊成功郵件!", event.getUserName()));
    }
 
    @EventListener
    public void sendCompon(UserRegisterEvent event) {
        System.out.println(String.format("給用戶【%s】發送優惠券!", event.getUserName()));
    }
}

這塊案例代碼

com.javacode2018.lesson003.demo1.EventTest#test3

運行結果

用戶【路人甲Java】註冊成功
給用戶【路人甲Java】發送優惠券!
給用戶【路人甲Java】發送註冊成功郵件!

原理

spring中處理@EventListener註解源碼位於下面的方法中

org.springframework.context.event.EventListenerMethodProcessor#afterSingletonsInstantiated

EventListenerMethodProcessor實現了SmartInitializingSingleton接口,SmartInitializingSingleton接口中的afterSingletonsInstantiated方法會在所有單例的bean創建完成之後被spring容器調用。

idea對註解的方式支持比較好

註解的方式實現監聽器,idea對這塊支持比較好,時間發佈的地方會顯示一個耳機,點擊這個耳機的時候,spring會幫我們列出這個事件有哪些監聽器

 

 點擊耳機列出了2個監聽器,可以快速定位到監聽器,如下

 

 同樣監聽器的地方也有一個廣播的圖標,如下圖

 

 

點擊上面這個廣播的圖標,可以快速導航到事件發佈的地方,相當方便。

監聽器支持排序功能

如果某個事件有多個監聽器,默認情況下,監聽器執行順序是無序的,不過我們可以爲監聽器指定順序。

通過接口實現監聽器的情況

如果自定義的監聽器是通過ApplicationListener接口實現的,那麼指定監聽器的順序有三種方式

方式1:實現org.springframework.core.Ordered接口

需要實現一個getOrder方法,返回順序值,值越小,順序越高

int getOrder();

方式2:實現org.springframework.core.PriorityOrdered接口

PriorityOrdered接口繼承了方式一中的Ordered接口,所以如果你實現PriorityOrdered接口,也需要實現getOrder方法。

方式3:類上使用@org.springframework.core.annotation.Order註解

看一下這個註解的源碼

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface Order {
 
    int value() default Ordered.LOWEST_PRECEDENCE;
 
}

value屬性用來指定順序

這幾種方式排序規則
PriorityOrdered#getOrder ASC,Ordered或@Order ASC

通過@EventListener實現事件監聽器的情況

可以在標註@EventListener的方法上面使用@Order(順序值)註解來標註順序,如:

@EventListener
@Order(1)
public void sendMail(com.javacode2018.lesson003.demo1.test3.UserRegisterEvent event) {
    System.out.println(String.format("給用戶【%s】發送註冊成功郵件!", event.getUserName()));
}

案例

package com.javacode2018.lesson003.demo1.test4;
 
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
 
/**
 * 用戶註冊監聽器
 */
@Component
public class UserRegisterListener {
    @EventListener
    @Order(1)
    public void sendMail(UserRegisterEvent event) {
        System.out.println(String.format("【%s】,給用戶【%s】發送註冊成功郵件!", Thread.currentThread(), event.getUserName()));
    }
 
    @EventListener
    @Order(0)
    public void sendCompon(UserRegisterEvent event) {
        System.out.println(String.format("【%s】,給用戶【%s】發送優惠券!", Thread.currentThread(), event.getUserName()));
    }
}

上面會先發送優惠券、然後再發送郵件。

上面輸出中順便將線程信息也輸出了。

對應測試用例
com.javacode2018.lesson003.demo1.EventTest#test4

運行輸出

【Thread[main,5,main]】,用戶【路人甲Java】註冊成功
【Thread[main,5,main]】,給用戶【路人甲Java】發送優惠券!
【Thread[main,5,main]】,給用戶【路人甲Java】發送註冊成功郵件!

從輸出中可以看出上面程序的執行都在主線程中執行的,說明監聽器中的邏輯和註冊邏輯在一個線程中執行的,此時如果監聽器中的邏輯比較耗時或者失敗,直接會導致註冊失敗,通常我們將一些非主要邏輯可以放在監聽器中執行,至於這些非主要邏輯成功或者失敗,最好不要對主要的邏輯產生影響,所以我們最好能將監聽器的運行和主業務隔離開,放在不同的線程中執行,主業務不用關注監聽器的結果,spring中支持這種功能,下面繼續看。

監聽器異步模式

先來看看到底如何實現?

監聽器最終是通過ApplicationEventMulticaster內部的實現來調用的,所以我們關注的重點就是這個類,這個類默認有個實現類SimpleApplicationEventMulticaster,這個類是支持監聽器異步調用的,裏面有個字段:

private Executor taskExecutor;

高併發比較熟悉的朋友對Executor這個接口是比較熟悉的,可以用來異步執行一些任務。

我們常用的線程池類java.util.concurrent.ThreadPoolExecutor就實現了Executor接口。

再來看一下SimpleApplicationEventMulticaster中事件監聽器的調用,最終會執行下面這個方法

@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
    ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
    Executor executor = getTaskExecutor();
    for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
        if (executor != null) { //@1
            executor.execute(() -> invokeListener(listener, event));
        }
        else {
            invokeListener(listener, event);
        }
    }
}

上面的invokeListener方法內部就是調用監聽器,從代碼@1可以看出,如果當前executor不爲空,監聽器就會被異步調用,所以如果需要異步只需要讓executor不爲空就可以了,但是默認情況下executor是空的,此時需要我們來給其設置一個值,下面我們需要看容器中是如何創建廣播器的,我們在那個地方去幹預。

通常我們使用的容器是AbstractApplicationContext類型的,需要看一下AbstractApplicationContext中廣播器是怎麼初始化的,就是下面這個方法,容器啓動的時候會被調用,用來初始化AbstractApplicationContext中的事件廣播器applicationEventMulticaster

public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster";
 
protected void initApplicationEventMulticaster() {
    ConfigurableListableBeanFactory beanFactory = getBeanFactory();
    if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
        this.applicationEventMulticaster =
            beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
    }
    else {
        this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
        beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
    }
}

上面邏輯解釋一下:判斷spring容器中是否有名稱爲applicationEventMulticaster的bean,如果有就將其作爲事件廣播器,否則創建一個SimpleApplicationEventMulticaster作爲廣播器,並將其註冊到spring容器中。

從上面可以得出結論:我們只需要自定義一個類型爲SimpleApplicationEventMulticaster名稱爲applicationEventMulticaster的bean就可以了,順便給executor設置一個值,就可以實現監聽器異步執行了。

具體實現如下

package com.javacode2018.lesson003.demo1.test5;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ApplicationEventMulticaster;
import org.springframework.context.event.SimpleApplicationEventMulticaster;
import org.springframework.scheduling.concurrent.ThreadPoolExecutorFactoryBean;
 
import java.util.concurrent.Executor;
 
@ComponentScan
@Configuration
public class MainConfig5 {
    @Bean
    public ApplicationEventMulticaster applicationEventMulticaster() { //@1
        //創建一個事件廣播器
        SimpleApplicationEventMulticaster result = new SimpleApplicationEventMulticaster();
        //給廣播器提供一個線程池,通過這個線程池來調用事件監聽器
        Executor executor = this.applicationEventMulticasterThreadPool().getObject();
        //設置異步執行器
        result.setTaskExecutor(executor);//@1
        return result;
    }
 
    @Bean
    public ThreadPoolExecutorFactoryBean applicationEventMulticasterThreadPool() {
        ThreadPoolExecutorFactoryBean result = new ThreadPoolExecutorFactoryBean();
        result.setThreadNamePrefix("applicationEventMulticasterThreadPool-");
        result.setCorePoolSize(5);
        return result;
    }
}

@1:定義了一個名稱爲applicationEventMulticaster的事件廣播器,內部設置了一個線程池用來異步調用監聽器

這段代碼對應的測試用例
com.javacode2018.lesson003.demo1.EventTest#test5

運行輸出

當前線程【Thread[main,5,main]】,用戶【路人甲Java】註冊成功
當前線程【Thread[applicationEventMulticasterThreadPool-2,5,main]】,給用戶【路人甲Java】發送註冊成功郵件!
當前線程【Thread[applicationEventMulticasterThreadPool-1,5,main]】,給用戶【路人甲Java】發放一些優惠券!

此時實現了監聽器異步執行的效果。

關於事件使用建議

  1. spring中事件是使用接口的方式還是使用註解的方式?具體使用哪種方式都可以,不過在公司內部最好大家都統一使用一種方式

  2. 異步事件的模式,通常將一些非主要的業務放在監聽器中執行,因爲監聽器中存在失敗的風險,所以使用的時候需要注意。如果只是爲了解耦,但是被解耦的次要業務也是必須要成功的,可以使用消息中間件的方式來解決這些問題。

  3. 事件的使用就到這裏,有問題的歡迎留言討論。

案例源碼包

 

本文章轉載:https://blog.csdn.net/flymoringbird/article/details/120481883
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章