第二十七章:SpringBoot使用ApplicationEvent&Listener完成業務解耦

ApplicationEvent以及Listener是Spring爲咱們提供的一個事件監聽、訂閱的實現,內部實現原理是觀察者設計模式,設計初衷也是爲了系統業務邏輯之間的解耦,提升可擴展性以及可維護性。事件發佈者並不須要考慮誰去監聽,監聽具體的實現內容是什麼,發佈者的工做只是爲了發佈事件而已。java

咱們平時平常生活中也是常常會有這種狀況存在,如:咱們在平時拔河比賽中,裁判員給咱們吹響了開始的信號,也就是給咱們發佈了一個開始的事件,而拔河雙方人員都在監聽着這個事件,一旦事件發佈後雙方人員就開始往本身方使勁。而裁判並不關心你比賽的過程,只是給你發佈事件你執行就能夠了。git

本章目標

咱們本章在SpringBoot平臺上經過ApplicationEvents以及Listener來完成簡單的註冊事件流程。web

構建項目

咱們本章只是簡單的講解如何使用ApplicationEvent以及Listener來完成業務邏輯的解耦,不涉及到數據交互因此依賴須要引入的也比較少,項目pom.xml配置文件以下所示:spring

.....//省略
<dependencies>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.16</version>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
.....//省略

其中lombok依賴你們有興趣能夠去深研究下,這是一個很好的工具,它能夠結合Idea開發工具完成對實體的動態添加構造函數、Getter/Setter方法、toString方法等。數據庫

建立UserRegisterEvent事件

咱們先來建立一個事件,監聽都是圍繞着事件來掛起的。事件代碼以下所示:設計模式

package com.yuqiyu.chapter27.event;

import com.yuqiyu.chapter27.bean.UserBean;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;

/**
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2017/7/21
 * Time:10:08
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Getter
public class UserRegisterEvent extends ApplicationEvent
{
    //註冊用戶對象
    private UserBean user;

    /**
     * 重寫構造函數
     * @param source 發生事件的對象
     * @param user 註冊用戶對象
     */
    public UserRegisterEvent(Object source,UserBean user) {
        super(source);
        this.user = user;
    }
}

咱們自定義事件UserRegisterEvent繼承了ApplicationEvent,繼承後必須重載構造函數,構造函數的參數能夠任意指定,其中source參數指的是發生事件的對象,通常咱們在發佈事件時使用的是this關鍵字代替本類對象,而user參數是咱們自定義的註冊用戶對象,該對象能夠在監聽內被獲取。app

在Spring內部中有多種方式實現監聽如:@EventListener註解、實現ApplicationListener泛型接口、實現SmartApplicationListener接口等,咱們下面來說解下這三種方式分別如何實現。框架

建立UserBean

咱們簡單建立一個用戶實體,並添加兩個字段:用戶名、密碼。實體代碼以下所示:less

package com.yuqiyu.chapter27.bean;
import lombok.Data;
/**
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2017/7/21
 * Time:10:05
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Data
public class UserBean
{
    //用戶名
    private String name;
    //密碼
    private String password;
}

建立UserService

UserService內添加一個註冊方法,該方法只是實現註冊事件發佈功能,代碼以下所示:異步

package com.yuqiyu.chapter27.service;

import com.yuqiyu.chapter27.bean.UserBean;
import com.yuqiyu.chapter27.event.UserRegisterEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;

/**
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2017/7/21
 * Time:10:11
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Service
public class UserService
{
    @Autowired
    ApplicationContext applicationContext;

    /**
     * 用戶註冊方法
     * @param user
     */
    public void register(UserBean user)
    {
        //../省略其餘邏輯

        //發佈UserRegisterEvent事件
        applicationContext.publishEvent(new UserRegisterEvent(this,user));
    }
}

事件發佈是由ApplicationContext對象管控的,咱們發佈事件前須要注入ApplicationContext對象調用publishEvent方法完成事件發佈。

建立UserController

建立一個@RestController控制器,對應添加一個註冊方法簡單實現,代碼以下所示:

package com.yuqiyu.chapter27.controller;

import com.yuqiyu.chapter27.bean.UserBean;
import com.yuqiyu.chapter27.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 用戶控制器
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2017/7/21
 * Time:10:05
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@RestController
public class UserController
{
    //用戶業務邏輯實現
    @Autowired
    private UserService userService;

    /**
     * 註冊控制方法
     * @param user 用戶對象
     * @return
     */
    @RequestMapping(value = "/register")
    public String register
            (
                    UserBean user
            )
    {
        //調用註冊業務邏輯
        userService.register(user);
        return "註冊成功.";
    }
}

@EventListener實現監聽

註解方式比較簡單,並不須要實現任何接口,具體代碼實現以下所示:

package com.yuqiyu.chapter27.listener;

import com.yuqiyu.chapter27.bean.UserBean;
import com.yuqiyu.chapter27.event.UserRegisterEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

/**
 * 使用@EventListener方法實現註冊事件監聽
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2017/7/21
 * Time:10:50
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Component
public class AnnotationRegisterListener {

    /**
     * 註冊監聽實現方法
     * @param userRegisterEvent 用戶註冊事件
     */
    @EventListener
    public void register(UserRegisterEvent userRegisterEvent)
    {
        //獲取註冊用戶對象
        UserBean user = userRegisterEvent.getUser();

        //../省略邏輯

        //輸出註冊用戶信息
        System.out.println("@EventListener註冊信息,用戶名:"+user.getName()+",密碼:"+user.getPassword());
    }
}

咱們只須要讓咱們的監聽類被Spring所管理便可,在咱們用戶註冊監聽實現方法上添加@EventListener註解,該註解會根據方法內配置的事件完成監聽。下面咱們啓動項目來測試下咱們事件發佈時是否被監聽者所感知。

測試事件監聽

使用SpringBootApplication方式啓動成功後,咱們來訪問下地址:http://127.0.0.1:8080/register?name=admin&password=123456,界面輸出內容確定是「註冊成功」,這個是沒有問題的,咱們直接查看控制檯輸出內容,以下所示:

2017-07-21 11:09:52.532  INFO 10460 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2017-07-21 11:09:52.532  INFO 10460 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2017-07-21 11:09:52.545  INFO 10460 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 13 ms
@EventListener註冊信息,用戶名:admin,密碼:123456

能夠看到咱們使用@EventListener註解配置的監聽已經生效了,當咱們在UserService內發佈了註冊事件時,監聽方法自動被調用而且輸出內信息到控制檯。

ApplicationListener實現監聽

這種方式也是Spring以前比較經常使用的監聽事件方式,在實現ApplicationListener接口時須要將監聽事件做爲泛型傳遞,監聽實現代碼以下所示:

package com.yuqiyu.chapter27.listener;

import com.yuqiyu.chapter27.bean.UserBean;
import com.yuqiyu.chapter27.event.UserRegisterEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
 * 原始方式實現
 * 用戶註冊監聽
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2017/7/21
 * Time:10:24
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Component
public class RegisterListener implements ApplicationListener<UserRegisterEvent>
{
    /**
     * 實現監聽
     * @param userRegisterEvent
     */
    @Override
    public void onApplicationEvent(UserRegisterEvent userRegisterEvent) {
        //獲取註冊用戶對象
        UserBean user = userRegisterEvent.getUser();

        //../省略邏輯

        //輸出註冊用戶信息
        System.out.println("註冊信息,用戶名:"+user.getName()+",密碼:"+user.getPassword());
    }
}

咱們實現接口後須要使用@Component註解來聲明該監聽須要被Spring注入管理,當有UserRegisterEvent事件發佈時監聽程序會自動調用onApplicationEvent方法而且將UserRegisterEvent對象做爲參數傳遞。
咱們UserService內的發佈事件不須要修改,咱們重啓下項目再次訪問以前的地址查看控制檯輸出的內容以下所示:

2017-07-21 13:03:35.399  INFO 4324 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2017-07-21 13:03:35.399  INFO 4324 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2017-07-21 13:03:35.411  INFO 4324 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 12 ms
註冊信息,用戶名:admin,密碼:123456

咱們看到了控制檯打印了咱們監聽內輸出用戶信息,事件發佈後就不會考慮具體哪一個監聽去處理業務,甚至能夠存在多個監聽同時須要處理業務邏輯。

咱們在註冊時若是不只僅是記錄註冊信息到數據庫,還須要發送郵件通知用戶,固然咱們能夠建立多個監聽同時監聽UserRegisterEvent事件,接下來咱們先來實現這個需求。

郵件通知監聽

咱們使用註解的方式來完成郵件發送監聽實現,代碼以下所示:

package com.yuqiyu.chapter27.listener;

import com.yuqiyu.chapter27.event.UserRegisterEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

/**
 * 註冊用戶事件發送郵件監聽
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2017/7/21
 * Time:13:08
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Component
public class RegisterUserEmailListener
{
    /**
     * 發送郵件監聽實現
     * @param userRegisterEvent 用戶註冊事件
     */
    @EventListener
    public void sendMail(UserRegisterEvent userRegisterEvent)
    {
        System.out.println("用戶註冊成功,發送郵件。");
    }
}

監聽編寫完成後,咱們重啓項目,再次訪問註冊請求地址查看控制檯輸出內容以下所示:

2017-07-21 13:09:20.671  INFO 7808 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2017-07-21 13:09:20.671  INFO 7808 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2017-07-21 13:09:20.685  INFO 7808 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 14 ms
用戶註冊成功,發送郵件。
註冊信息,用戶名:admin,密碼:123456

咱們看到控制檯輸出的內容感到比較疑惑,我註冊時用戶信息寫入數據庫應該在發送郵件前面,爲何沒有在第一步執行呢?
好了,證實了一點,事件監聽是無序的,監聽到的事件前後順序徹底隨機出現的。咱們接下來使用SmartApplicationListener實現監聽方式來實現該邏輯。

SmartApplicationListener實現有序監聽

咱們對註冊用戶以及發送郵件的監聽從新編寫,註冊用戶寫入數據庫監聽代碼以下所示:

package com.yuqiyu.chapter27.listener;

import com.yuqiyu.chapter27.bean.UserBean;
import com.yuqiyu.chapter27.event.UserRegisterEvent;
import com.yuqiyu.chapter27.service.UserService;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.stereotype.Component;

/**
 * 用戶註冊>>>保存用戶信息監聽
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2017/7/21
 * Time:10:09
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Component
public class UserRegisterListener implements SmartApplicationListener
{
    /**
     *  該方法返回true&supportsSourceType一樣返回true時,纔會調用該監聽內的onApplicationEvent方法
     * @param aClass 接收到的監聽事件類型
     * @return
     */
    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
        //只有UserRegisterEvent監聽類型纔會執行下面邏輯
        return aClass == UserRegisterEvent.class;
    }

    /**
     *  該方法返回true&supportsEventType一樣返回true時,纔會調用該監聽內的onApplicationEvent方法
     * @param aClass
     * @return
     */
    @Override
    public boolean supportsSourceType(Class<?> aClass) {
        //只有在UserService內發佈的UserRegisterEvent事件時纔會執行下面邏輯
        return aClass == UserService.class;
    }

    /**
     *  supportsEventType & supportsSourceType 兩個方法返回true時調用該方法執行業務邏輯
     * @param applicationEvent 具體監聽實例,這裏是UserRegisterEvent
     */
    @Override
    public void onApplicationEvent(ApplicationEvent applicationEvent) {

        //轉換事件類型
        UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent;
        //獲取註冊用戶對象信息
        UserBean user = userRegisterEvent.getUser();
        //.../完成註冊業務邏輯
        System.out.println("註冊信息,用戶名:"+user.getName()+",密碼:"+user.getPassword());
    }

    /**
     * 同步狀況下監聽執行的順序
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

SmartApplicationListener接口繼承了全局監聽ApplicationListener,而且泛型對象使用的ApplicationEvent來做爲全局監聽,能夠理解爲使用SmartApplicationListener做爲監聽父接口的實現,監聽全部事件發佈。

既然是監聽全部的事件發佈,那麼SmartApplicationListener接口添加了兩個方法supportsEventType、supportsSourceType來做爲區分是不是咱們監聽的事件,只有這兩個方法同時返回true時纔會執行onApplicationEvent方法。

能夠看到除了上面的方法,還提供了一個getOrder方法,這個方法就能夠解決執行監聽的順序問題,return的數值越小證實優先級越高,執行順序越靠前。

註冊成功發送郵件通知監聽代碼以下所示:

package com.yuqiyu.chapter27.listener.order;

import com.yuqiyu.chapter27.bean.UserBean;
import com.yuqiyu.chapter27.event.UserRegisterEvent;
import com.yuqiyu.chapter27.service.UserService;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.stereotype.Component;

/**
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2017/7/21
 * Time:13:38
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Component
public class UserRegisterSendMailListener implements SmartApplicationListener
{
    /**
     *  該方法返回true&supportsSourceType一樣返回true時,纔會調用該監聽內的onApplicationEvent方法
     * @param aClass 接收到的監聽事件類型
     * @return
     */
    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
        //只有UserRegisterEvent監聽類型纔會執行下面邏輯
        return aClass == UserRegisterEvent.class;
    }

    /**
     *  該方法返回true&supportsEventType一樣返回true時,纔會調用該監聽內的onApplicationEvent方法
     * @param aClass
     * @return
     */
    @Override
    public boolean supportsSourceType(Class<?> aClass) {
        //只有在UserService內發佈的UserRegisterEvent事件時纔會執行下面邏輯
        return aClass == UserService.class;
    }

    /**
     *  supportsEventType & supportsSourceType 兩個方法返回true時調用該方法執行業務邏輯
     * @param applicationEvent 具體監聽實例,這裏是UserRegisterEvent
     */
    @Override
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        //轉換事件類型
        UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent;
        //獲取註冊用戶對象信息
        UserBean user = userRegisterEvent.getUser();
        System.out.println("用戶:"+user.getName()+",註冊成功,發送郵件通知。");
    }

    /**
     * 同步狀況下監聽執行的順序
     * @return
     */
    @Override
    public int getOrder() {
        return 1;
    }
}

在getOrder方法內咱們返回的數值爲「1」,這就證實了須要在保存註冊用戶信息監聽後執行,下面咱們重啓項目訪問註冊地址查看控制檯輸出內容以下所示:

2017-07-21 13:40:43.104  INFO 10128 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2017-07-21 13:40:43.104  INFO 10128 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2017-07-21 13:40:43.119  INFO 10128 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 15 ms
註冊信息,用戶名:admin,密碼:123456
用戶:admin,註冊成功,發送郵件通知。

此次咱們看到了輸出的順序就是正確的了,先保存信息而後再發送郵件通知。

若是說咱們不但願在執行監聽時等待監聽業務邏輯耗時,發佈監聽後當即要對接口或者界面作出反映,咱們該怎麼作呢?

使用@Async實現異步監聽

@Aysnc實際上是Spring內的一個組件,能夠完成對類內單個或者多個方法實現異步調用,這樣能夠大大的節省等待耗時。內部實現機制是線程池任務ThreadPoolTaskExecutor,經過線程池來對配置@Async的方法或者類作出執行動做。

線程任務池配置

咱們建立一個ListenerAsyncConfiguration,而且使用@EnableAsync註解開啓支持異步處理,具體代碼以下所示:

package com.yuqiyu.chapter27;

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

/**
 * 異步監聽配置
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2017/7/21
 * Time:14:04
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Configuration
@EnableAsync
public class ListenerAsyncConfiguration implements AsyncConfigurer
{
    /**
     * 獲取異步線程池執行對象
     * @return
     */
    @Override
    public Executor getAsyncExecutor() {
        //使用Spring內置線程池任務對象
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        //設置線程池參數
        taskExecutor.setCorePoolSize(5);
        taskExecutor.setMaxPoolSize(10);
        taskExecutor.setQueueCapacity(25);
        taskExecutor.initialize();
        return taskExecutor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return null;
    }
}

咱們自定義的監聽異步配置類實現了AsyncConfigurer接口而且實現內getAsyncExecutor方法以提供線程任務池對象的獲取。
咱們只須要在異步方法上添加@Async註解就能夠實現方法的異步調用,爲了證實這一點,咱們在發送郵件onApplicationEvent方法內添加線程阻塞3秒,修改後的代碼以下所示:

/**
     * supportsEventType & supportsSourceType 兩個方法返回true時調用該方法執行業務邏輯
     * @param applicationEvent 具體監聽實例,這裏是UserRegisterEvent
     */
    @Override
    @Async
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        try {
            Thread.sleep(3000);//靜靜的沉睡3秒鐘
        }catch (Exception e)
        {
            e.printStackTrace();
        }
        //轉換事件類型
        UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent;
        //獲取註冊用戶對象信息
        UserBean user = userRegisterEvent.getUser();
        System.out.println("用戶:"+user.getName()+",註冊成功,發送郵件通知。");
    }

下面咱們重啓下項目,訪問註冊地址,查看界面反映是否也有延遲。
咱們測試發現訪問界面時反映速度要不以前還要快一些,咱們去查看控制檯時,能夠看到註冊信息輸出後等待3秒後再才輸出郵件發送通知,而在這以前界面已經作出了反映。

注意:若是存在多個監聽同一個事件時,而且存在異步與同步同時存在時則不存在執行順序。

總結

咱們在傳統項目中每每各個業務邏輯之間耦合性較強,由於咱們在service都是直接引用的關聯service或者jpa來做爲協做處理邏輯,然而這種方式在後期更新、維護性難度都是大大提升了。然而咱們採用事件通知、事件監聽形式來處理邏輯時耦合性則是能夠降到最小。

本章代碼已經上傳到碼雲:
網頁地址:http://git.oschina.net/jnyqy/lessons
Git地址:https://git.oschina.net/jnyqy/lessons.git
SpringBoot相關係列文章請訪問:目錄:SpringBoot學習目錄
QueryDSL相關係列文章請訪問:QueryDSL通用查詢框架學習目錄
感謝閱讀!
歡迎加入QQ技術交流羣,共同進步。
QQ技術交流羣

相關文章
相關標籤/搜索