第二十七章:SpringBoot使用ApplicationEvent&Listener完成業(yè)務(wù)解耦

ApplicationEvent以及Listener是Spring為我們提供的一個(gè)事件監(jiān)聽忧风、訂閱的實(shí)現(xiàn)默色,內(nèi)部實(shí)現(xiàn)原理是觀察者設(shè)計(jì)模式,設(shè)計(jì)初衷也是為了系統(tǒng)業(yè)務(wù)邏輯之間的解耦狮腿,提高可擴(kuò)展性以及可維護(hù)性腿宰。事件發(fā)布者并不需要考慮誰去監(jiān)聽,監(jiān)聽具體的實(shí)現(xiàn)內(nèi)容是什么缘厢,發(fā)布者的工作只是為了發(fā)布事件而已吃度。

我們平時(shí)日常生活中也是經(jīng)常會(huì)有這種情況存在,如:我們在平時(shí)拔河比賽中贴硫,裁判員給我們吹響了開始的信號椿每,也就是給我們發(fā)布了一個(gè)開始的事件,而拔河雙方人員都在監(jiān)聽著這個(gè)事件夜畴,一旦事件發(fā)布后雙方人員就開始往自己方使勁拖刃。而裁判并不關(guān)心你比賽的過程,只是給你發(fā)布事件你執(zhí)行就可以了贪绘。

本章目標(biāo)

我們本章在SpringBoot平臺(tái)上通過ApplicationEvents以及Listener來完成簡單的注冊事件流程。

推薦閱讀

SpringBoot 企業(yè)級核心技術(shù)學(xué)習(xí)專題

專題 專題名稱 專題描述
001 Spring Boot 核心技術(shù) 講解SpringBoot一些企業(yè)級層面的核心組件
002 Spring Boot 核心技術(shù)章節(jié)源碼 Spring Boot 核心技術(shù)簡書每一篇文章碼云對應(yīng)源碼
003 Spring Cloud 核心技術(shù) 對Spring Cloud核心技術(shù)全面講解
004 Spring Cloud 核心技術(shù)章節(jié)源碼 Spring Cloud 核心技術(shù)簡書每一篇文章對應(yīng)源碼
005 QueryDSL 核心技術(shù) 全面講解QueryDSL核心技術(shù)以及基于SpringBoot整合SpringDataJPA
006 SpringDataJPA 核心技術(shù) 全面講解SpringDataJPA核心技術(shù)

構(gòu)建項(xiàng)目

我們本章只是簡單的講解如何使用ApplicationEvent以及Listener來完成業(yè)務(wù)邏輯的解耦央碟,不涉及到數(shù)據(jù)交互所以依賴需要引入的也比較少税灌,項(xiàng)目pom.xml配置文件如下所示:

.....//省略
<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>
.....//省略

其中l(wèi)ombok依賴大家有興趣可以去深研究下均函,這是一個(gè)很好的工具,它可以結(jié)合Idea開發(fā)工具完成對實(shí)體的動(dòng)態(tài)添加構(gòu)造函數(shù)菱涤、Getter/Setter方法苞也、toString方法等。

創(chuàng)建UserRegisterEvent事件

我們先來創(chuàng)建一個(gè)事件粘秆,監(jiān)聽都是圍繞著事件來掛起的如迟。事件代碼如下所示:

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;

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

我們自定義事件UserRegisterEvent繼承了ApplicationEvent,繼承后必須重載構(gòu)造函數(shù)攻走,構(gòu)造函數(shù)的參數(shù)可以任意指定殷勘,其中source參數(shù)指的是發(fā)生事件的對象,一般我們在發(fā)布事件時(shí)使用的是this關(guān)鍵字代替本類對象昔搂,而user參數(shù)是我們自定義的注冊用戶對象玲销,該對象可以在監(jiān)聽內(nèi)被獲取。

在Spring內(nèi)部中有多種方式實(shí)現(xiàn)監(jiān)聽如:@EventListener注解摘符、實(shí)現(xiàn)ApplicationListener泛型接口、實(shí)現(xiàn)SmartApplicationListener接口等,我們下面來講解下這三種方式分別如何實(shí)現(xiàn)茄猫。

創(chuàng)建UserBean

我們簡單創(chuàng)建一個(gè)用戶實(shí)體启涯,并添加兩個(gè)字段:用戶名、密碼带族。實(shí)體代碼如下所示:

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;
}

創(chuàng)建UserService

UserService內(nèi)添加一個(gè)注冊方法锁荔,該方法只是實(shí)現(xiàn)注冊事件發(fā)布功能,代碼如下所示:

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)
    {
        //../省略其他邏輯

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

事件發(fā)布是由ApplicationContext對象管控的炉菲,我們發(fā)布事件前需要注入ApplicationContext對象調(diào)用publishEvent方法完成事件發(fā)布堕战。

創(chuàng)建UserController

創(chuàng)建一個(gè)@RestController控制器,對應(yīng)添加一個(gè)注冊方法簡單實(shí)現(xiàn)拍霜,代碼如下所示:

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
{
    //用戶業(yè)務(wù)邏輯實(shí)現(xiàn)
    @Autowired
    private UserService userService;

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

@EventListener實(shí)現(xiàn)監(jiān)聽

注解方式比較簡單嘱丢,并不需要實(shí)現(xiàn)任何接口,具體代碼實(shí)現(xiàn)如下所示:

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方法實(shí)現(xiàn)注冊事件監(jiān)聽
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2017/7/21
 * Time:10:50
 * 碼云:http://git.oschina.net/jnyqy
 * ========================
 */
@Component
public class AnnotationRegisterListener {

    /**
     * 注冊監(jiān)聽實(shí)現(xiàn)方法
     * @param userRegisterEvent 用戶注冊事件
     */
    @EventListener
    public void register(UserRegisterEvent userRegisterEvent)
    {
        //獲取注冊用戶對象
        UserBean user = userRegisterEvent.getUser();

        //../省略邏輯

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

我們只需要讓我們的監(jiān)聽類被Spring所管理即可,在我們用戶注冊監(jiān)聽實(shí)現(xiàn)方法上添加@EventListener注解道偷,該注解會(huì)根據(jù)方法內(nèi)配置的事件完成監(jiān)聽缀旁。下面我們啟動(dòng)項(xiàng)目來測試下我們事件發(fā)布時(shí)是否被監(jiān)聽者所感知。

測試事件監(jiān)聽

使用SpringBootApplication方式啟動(dòng)成功后勺鸦,我們來訪問下地址:http://127.0.0.1:8080/register?name=admin&password=123456并巍,界面輸出內(nèi)容肯定是“注冊成功”,這個(gè)是沒有問題的换途,我們直接查看控制臺(tái)輸出內(nèi)容懊渡,如下所示:

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注解配置的監(jiān)聽已經(jīng)生效了剃执,當(dāng)我們在UserService內(nèi)發(fā)布了注冊事件時(shí)誓禁,監(jiān)聽方法自動(dòng)被調(diào)用并且輸出內(nèi)信息到控制臺(tái)。

ApplicationListener實(shí)現(xiàn)監(jiān)聽

這種方式也是Spring之前比較常用的監(jiān)聽事件方式肾档,在實(shí)現(xiàn)ApplicationListener接口時(shí)需要將監(jiān)聽事件作為泛型傳遞摹恰,監(jiān)聽實(shí)現(xiàn)代碼如下所示:

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;

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

        //../省略邏輯

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

我們實(shí)現(xiàn)接口后需要使用@Component注解來聲明該監(jiān)聽需要被Spring注入管理俗慈,當(dāng)有UserRegisterEvent事件發(fā)布時(shí)監(jiān)聽程序會(huì)自動(dòng)調(diào)用onApplicationEvent方法并且將UserRegisterEvent對象作為參數(shù)傳遞。
我們UserService內(nèi)的發(fā)布事件不需要修改速种,我們重啟下項(xiàng)目再次訪問之前的地址查看控制臺(tái)輸出的內(nèi)容如下所示:

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

我們看到了控制臺(tái)打印了我們監(jiān)聽內(nèi)輸出用戶信息配阵,事件發(fā)布后就不會(huì)考慮具體哪個(gè)監(jiān)聽去處理業(yè)務(wù)馏颂,甚至可以存在多個(gè)監(jiān)聽同時(shí)需要處理業(yè)務(wù)邏輯。

我們在注冊時(shí)如果不僅僅是記錄注冊信息到數(shù)據(jù)庫棋傍,還需要發(fā)送郵件通知用戶救拉,當(dāng)然我們可以創(chuàng)建多個(gè)監(jiān)聽同時(shí)監(jiān)聽UserRegisterEvent事件,接下來我們先來實(shí)現(xiàn)這個(gè)需求瘫拣。

郵件通知監(jiān)聽

我們使用注解的方式來完成郵件發(fā)送監(jiān)聽實(shí)現(xiàn)亿絮,代碼如下所示:

package com.yuqiyu.chapter27.listener;

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

/**
 * 注冊用戶事件發(fā)送郵件監(jiān)聽
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2017/7/21
 * Time:13:08
 * 碼云:http://git.oschina.net/jnyqy
 * ========================
 */
@Component
public class RegisterUserEmailListener
{
    /**
     * 發(fā)送郵件監(jiān)聽實(shí)現(xiàn)
     * @param userRegisterEvent 用戶注冊事件
     */
    @EventListener
    public void sendMail(UserRegisterEvent userRegisterEvent)
    {
        System.out.println("用戶注冊成功,發(fā)送郵件麸拄。");
    }
}

監(jiān)聽編寫完成后派昧,我們重啟項(xiàng)目,再次訪問注冊請求地址查看控制臺(tái)輸出內(nèi)容如下所示:

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
用戶注冊成功拢切,發(fā)送郵件蒂萎。
注冊信息,用戶名:admin淮椰,密碼:123456

我們看到控制臺(tái)輸出的內(nèi)容感到比較疑惑五慈,我注冊時(shí)用戶信息寫入數(shù)據(jù)庫應(yīng)該在發(fā)送郵件前面,為什么沒有在第一步執(zhí)行呢主穗?
好了泻拦,證明了一點(diǎn),事件監(jiān)聽是無序的忽媒,監(jiān)聽到的事件先后順序完全隨機(jī)出現(xiàn)的争拐。我們接下來使用SmartApplicationListener實(shí)現(xiàn)監(jiān)聽方式來實(shí)現(xiàn)該邏輯。

SmartApplicationListener實(shí)現(xiàn)有序監(jiān)聽

我們對注冊用戶以及發(fā)送郵件的監(jiān)聽重新編寫晦雨,注冊用戶寫入數(shù)據(jù)庫監(jiān)聽代碼如下所示:

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;

/**
 * 用戶注冊>>>保存用戶信息監(jiān)聽
 * ========================
 * 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時(shí)陆错,才會(huì)調(diào)用該監(jiān)聽內(nèi)的onApplicationEvent方法
     * @param aClass 接收到的監(jiān)聽事件類型
     * @return
     */
    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
        //只有UserRegisterEvent監(jiān)聽類型才會(huì)執(zhí)行下面邏輯
        return aClass == UserRegisterEvent.class;
    }

    /**
     *  該方法返回true&supportsEventType同樣返回true時(shí)灯抛,才會(huì)調(diào)用該監(jiān)聽內(nèi)的onApplicationEvent方法
     * @param aClass
     * @return
     */
    @Override
    public boolean supportsSourceType(Class<?> aClass) {
        //只有在UserService內(nèi)發(fā)布的UserRegisterEvent事件時(shí)才會(huì)執(zhí)行下面邏輯
        return aClass == UserService.class;
    }

    /**
     *  supportsEventType & supportsSourceType 兩個(gè)方法返回true時(shí)調(diào)用該方法執(zhí)行業(yè)務(wù)邏輯
     * @param applicationEvent 具體監(jiān)聽實(shí)例金赦,這里是UserRegisterEvent
     */
    @Override
    public void onApplicationEvent(ApplicationEvent applicationEvent) {

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

    /**
     * 同步情況下監(jiān)聽執(zhí)行的順序
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

SmartApplicationListener接口繼承了全局監(jiān)聽ApplicationListener夹抗,并且泛型對象使用的ApplicationEvent來作為全局監(jiān)聽绳慎,可以理解為使用SmartApplicationListener作為監(jiān)聽父接口的實(shí)現(xiàn),監(jiān)聽所有事件發(fā)布漠烧。

既然是監(jiān)聽所有的事件發(fā)布杏愤,那么SmartApplicationListener接口添加了兩個(gè)方法supportsEventType、supportsSourceType來作為區(qū)分是否是我們監(jiān)聽的事件已脓,只有這兩個(gè)方法同時(shí)返回true時(shí)才會(huì)執(zhí)行onApplicationEvent方法珊楼。

可以看到除了上面的方法,還提供了一個(gè)getOrder方法度液,這個(gè)方法就可以解決執(zhí)行監(jiān)聽的順序問題厕宗,return的數(shù)值越小證明優(yōu)先級越高,執(zhí)行順序越靠前堕担。

注冊成功發(fā)送郵件通知監(jiān)聽代碼如下所示:

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時(shí)已慢,才會(huì)調(diào)用該監(jiān)聽內(nèi)的onApplicationEvent方法
     * @param aClass 接收到的監(jiān)聽事件類型
     * @return
     */
    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
        //只有UserRegisterEvent監(jiān)聽類型才會(huì)執(zhí)行下面邏輯
        return aClass == UserRegisterEvent.class;
    }

    /**
     *  該方法返回true&supportsEventType同樣返回true時(shí),才會(huì)調(diào)用該監(jiān)聽內(nèi)的onApplicationEvent方法
     * @param aClass
     * @return
     */
    @Override
    public boolean supportsSourceType(Class<?> aClass) {
        //只有在UserService內(nèi)發(fā)布的UserRegisterEvent事件時(shí)才會(huì)執(zhí)行下面邏輯
        return aClass == UserService.class;
    }

    /**
     *  supportsEventType & supportsSourceType 兩個(gè)方法返回true時(shí)調(diào)用該方法執(zhí)行業(yè)務(wù)邏輯
     * @param applicationEvent 具體監(jiān)聽實(shí)例霹购,這里是UserRegisterEvent
     */
    @Override
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        //轉(zhuǎn)換事件類型
        UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent;
        //獲取注冊用戶對象信息
        UserBean user = userRegisterEvent.getUser();
        System.out.println("用戶:"+user.getName()+"佑惠,注冊成功,發(fā)送郵件通知齐疙。");
    }

    /**
     * 同步情況下監(jiān)聽執(zhí)行的順序
     * @return
     */
    @Override
    public int getOrder() {
        return 1;
    }
}

在getOrder方法內(nèi)我們返回的數(shù)值為“1”膜楷,這就證明了需要在保存注冊用戶信息監(jiān)聽后執(zhí)行,下面我們重啟項(xiàng)目訪問注冊地址查看控制臺(tái)輸出內(nèi)容如下所示:

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,注冊成功忆矛,發(fā)送郵件通知察蹲。

這次我們看到了輸出的順序就是正確的了,先保存信息然后再發(fā)送郵件通知催训。

如果說我們不希望在執(zhí)行監(jiān)聽時(shí)等待監(jiān)聽業(yè)務(wù)邏輯耗時(shí)洽议,發(fā)布監(jiān)聽后立即要對接口或者界面做出反映,我們該怎么做呢漫拭?

使用@Async實(shí)現(xiàn)異步監(jiān)聽

@Aysnc其實(shí)是Spring內(nèi)的一個(gè)組件亚兄,可以完成對類內(nèi)單個(gè)或者多個(gè)方法實(shí)現(xiàn)異步調(diào)用,這樣可以大大的節(jié)省等待耗時(shí)采驻。內(nèi)部實(shí)現(xiàn)機(jī)制是線程池任務(wù)ThreadPoolTaskExecutor审胚,通過線程池來對配置@Async的方法或者類做出執(zhí)行動(dòng)作匈勋。

線程任務(wù)池配置

我們創(chuàng)建一個(gè)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;

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

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

我們自定義的監(jiān)聽異步配置類實(shí)現(xiàn)了AsyncConfigurer接口并且實(shí)現(xiàn)內(nèi)getAsyncExecutor方法以提供線程任務(wù)池對象的獲取洽洁。
我們只需要在異步方法上添加@Async注解就可以實(shí)現(xiàn)方法的異步調(diào)用,為了證明這一點(diǎn)菲嘴,我們在發(fā)送郵件onApplicationEvent方法內(nèi)添加線程阻塞3秒饿自,修改后的代碼如下所示:

 /**
     * supportsEventType & supportsSourceType 兩個(gè)方法返回true時(shí)調(diào)用該方法執(zhí)行業(yè)務(wù)邏輯
     * @param applicationEvent 具體監(jiān)聽實(shí)例,這里是UserRegisterEvent
     */
    @Override
    @Async
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        try {
            Thread.sleep(3000);//靜靜的沉睡3秒鐘
        }catch (Exception e)
        {
            e.printStackTrace();
        }
        //轉(zhuǎn)換事件類型
        UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent;
        //獲取注冊用戶對象信息
        UserBean user = userRegisterEvent.getUser();
        System.out.println("用戶:"+user.getName()+"龄坪,注冊成功昭雌,發(fā)送郵件通知。");
    }

下面我們重啟下項(xiàng)目健田,訪問注冊地址烛卧,查看界面反映是否也有延遲。
我們測試發(fā)現(xiàn)訪問界面時(shí)反映速度要不之前還要快一些妓局,我們?nèi)ゲ榭纯刂婆_(tái)時(shí)总放,可以看到注冊信息輸出后等待3秒后再才輸出郵件發(fā)送通知,而在這之前界面已經(jīng)做出了反映跟磨。

注意:如果存在多個(gè)監(jiān)聽同一個(gè)事件時(shí)间聊,并且存在異步與同步同時(shí)存在時(shí)則不存在執(zhí)行順序。

總結(jié)

我們在傳統(tǒng)項(xiàng)目中往往各個(gè)業(yè)務(wù)邏輯之間耦合性較強(qiáng)抵拘,因?yàn)槲覀冊趕ervice都是直接引用的關(guān)聯(lián)service或者jpa來作為協(xié)作處理邏輯哎榴,然而這種方式在后期更新、維護(hù)性難度都是大大提高了僵蛛。然而我們采用事件通知尚蝌、事件監(jiān)聽形式來處理邏輯時(shí)耦合性則是可以降到最小。

本章代碼已經(jīng)上傳到碼云:
SpringBoot配套源碼地址:https://gitee.com/hengboy/spring-boot-chapter
SpringCloud配套源碼地址:https://gitee.com/hengboy/spring-cloud-chapter

作者個(gè)人 博客
使用開源框架 ApiBoot 助你成為Api接口服務(wù)架構(gòu)師

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末充尉,一起剝皮案震驚了整個(gè)濱河市飘言,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌驼侠,老刑警劉巖姿鸿,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異倒源,居然都是意外死亡苛预,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進(jìn)店門笋熬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來热某,“玉大人,你說我怎么就攤上這事∥舨觯” “怎么了筹吐?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長秘遏。 經(jīng)常有香客問我丘薛,道長,這世上最難降的妖魔是什么垄提? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任榔袋,我火速辦了婚禮,結(jié)果婚禮上铡俐,老公的妹妹穿的比我還像新娘。我一直安慰自己妥粟,他們只是感情好审丘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著勾给,像睡著了一般滩报。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上播急,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天脓钾,我揣著相機(jī)與錄音,去河邊找鬼桩警。 笑死可训,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的捶枢。 我是一名探鬼主播握截,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼烂叔!你這毒婦竟也來了谨胞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蒜鸡,失蹤者是張志新(化名)和其女友劉穎胯努,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體逢防,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡叶沛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了胞四。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片恬汁。...
    茶點(diǎn)故事閱讀 39,991評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出氓侧,到底是詐尸還是另有隱情脊另,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布约巷,位于F島的核電站偎痛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏独郎。R本人自食惡果不足惜踩麦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望氓癌。 院中可真熱鬧谓谦,春花似錦、人聲如沸贪婉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽疲迂。三九已至才顿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間尤蒿,已是汗流浹背郑气。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留腰池,地道東北人尾组。 一個(gè)月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像巩螃,于是被迫代替她去往敵國和親演怎。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理避乏,服務(wù)發(fā)現(xiàn)爷耀,斷路器,智...
    卡卡羅2017閱讀 134,657評論 18 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,811評論 6 342
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,139評論 25 707
  • 暑假一向是各大青春偶像愛情劇的“戰(zhàn)場”爹橱,各大電視劇卯足了勁搶占收視率萨螺。 小編就追了一部糖分滿滿的劇,狗糧大概可以繞...
    潮流一起說閱讀 250評論 0 0
  • 前言 特別討厭跟文字打交道的我,從來沒想過要寫自己什么慰技,甚至在去年她那么認(rèn)真開玩笑說給我寫一部自傳,我都覺得搞笑吻商,...
    青松還好閱讀 156評論 0 0