1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        SpringBoot事件監(jiān)聽機(jī)制及觀察者模式/發(fā)布訂閱模式

        共 6635字,需瀏覽 14分鐘

         ·

        2023-06-25 22:35

        走過路過不要錯過

        點擊藍(lán)字關(guān)注我們


        本篇要點

        • 介紹觀察者模式和發(fā)布訂閱模式的區(qū)別。

        • SpringBoot快速入門事件監(jiān)聽。

        什么是觀察者模式?

        觀察者模式是經(jīng)典行為型設(shè)計模式之一。

        在GoF的《設(shè)計模式》中,觀察者模式的定義:在對象之間定義一個一對多的依賴,當(dāng)一個對象狀態(tài)改變的時候,所有依賴的對象都會自動收到通知。如果你覺得比較抽象,接下來這個例子應(yīng)該會讓你有所感覺:

        就拿用戶注冊功能為例吧,假設(shè)用戶注冊成功之后,我們將會發(fā)送郵件,優(yōu)惠券等等操作,很容易就能寫出下面的邏輯:

        @RestController
        @RequestMapping("/user")
        public class SimpleUserController {

        @Autowired
        private SimpleEmailService emailService;

        @Autowired
        private SimpleCouponService couponService;

        @Autowired
        private SimpleUserService userService;

        @GetMapping("/register")
        public String register(String username) {
        // 注冊
        userService.register(username);
        // 發(fā)送郵件
        emailService.sendEmail(username);
        // 發(fā)送優(yōu)惠券
        couponService.addCoupon(username);
        return "注冊成功!";
        }
        }


        這樣寫會有什么問題呢?受王爭老師啟發(fā):

        • 方法調(diào)用時,同步阻塞導(dǎo)致響應(yīng)變慢,需要異步非阻塞的解決方案。

        • 注冊接口此時做的事情:注冊,發(fā)郵件,優(yōu)惠券,違反單一職責(zé)的原則。當(dāng)然,如果后續(xù)沒有拓展和修改的需求,這樣子倒可以接受。

        • 如果后續(xù)注冊的需求頻繁變更,相應(yīng)就需要頻繁變更register方法,違反了開閉原則。

        針對以上的問題,我們想一想解決的方案:

        一、異步非阻塞的效果可以新開一個線程執(zhí)行耗時的發(fā)送郵件任務(wù),但頻繁地創(chuàng)建和銷毀線程比較耗時,并且并發(fā)線程數(shù)無法控制,創(chuàng)建過多的線程會導(dǎo)致堆棧溢出。

        二、使用線程池執(zhí)行任務(wù)解決上述問題。

        @Service
        @Slf4j
        public class SimpleEmailService {
        // 啟動一個線程執(zhí)行耗時操作
        public void sendEmail(String username) {
        Thread thread = new Thread(()->{
        try {
        // 模擬發(fā)郵件耗時操作
        Thread.sleep(3000);
        } catch (InterruptedException e) {
        e.printStackTrace();
        }
        log.info("給用戶 [{}] 發(fā)送郵件...", username);
        });
        thread.start();
        }
        }

        @Slf4j
        @Service
        public class SimpleCouponService {

        ExecutorService executorService = Executors.newSingleThreadExecutor();
        // 線程池執(zhí)行任務(wù),減少資源消耗
        public void addCoupon(String username) {
        executorService.execute(() -> {
        try {
        Thread.sleep(3000);
        } catch (InterruptedException e) {
        e.printStackTrace();
        }
        log.info("給用戶 [{}] 發(fā)放優(yōu)惠券", username);
        });
        }
        }


        這里用戶注冊事件對【發(fā)送短信和優(yōu)惠券】其實是一對多的關(guān)系,可以使用觀察者模式進(jìn)行解耦:

        /**
        * 主題接口
        * @author Summerday
        */

        public interface Subject {

        void registerObserver(Observer observer);
        void removeObserver(Observer observer);
        void notifyObservers(String message);
        }

        /**
        * 觀察者接口
        * @author Summerday
        */

        public interface Observer {

        void update(String message);
        }

        @Component
        @Slf4j
        public class EmailObserver implements Observer {

        @Override
        public void update(String message) {
        log.info("向[{}]發(fā)送郵件", message);
        }
        }
        @Component
        @Slf4j
        public class CouponObserver implements Observer {

        @Override
        public void update(String message) {
        log.info("向[{}]發(fā)送優(yōu)惠券",message);
        }
        }

        @Component
        public class UserRegisterSubject implements Subject {

        @Autowired
        List<Observer> observers;

        @Override
        public void registerObserver(Observer observer) {
        observers.add(observer);
        }

        @Override
        public void removeObserver(Observer observer) {
        observers.remove(observer);
        }

        @Override
        public void notifyObservers(String username) {
        for (Observer observer : observers) {
        observer.update(username);
        }
        }
        }


        @RestController
        @RequestMapping("/")
        public class UserController {

        @Autowired
        UserRegisterSubject subject;

        @Autowired
        private SimpleUserService userService;


        @GetMapping("/reg")
        public String reg(String username) {
        userService.register(username);
        subject.notifyObservers(username);
        return "success";
        }
        }


        發(fā)布訂閱模式是什么?

        觀察者模式和發(fā)布訂閱模式是有一點點區(qū)別的,區(qū)別有以下幾點:

        • 前者:觀察者訂閱主題,主題也維護(hù)觀察者的記錄,而后者:發(fā)布者和訂閱者不需要彼此了解,而是在消息隊列或代理的幫助下通信,實現(xiàn)松耦合。

        • 前者主要以同步方式實現(xiàn),即某個事件發(fā)生時,由Subject調(diào)用所有Observers的對應(yīng)方法,后者則主要使用消息隊列異步實現(xiàn)。

        盡管兩者存在差異,但是他們其實在概念上相似,網(wǎng)上說法很多,不需要過于糾結(jié),重點在于我們需要他們?yōu)槭裁闯霈F(xiàn),解決了什么問題。

        Spring事件監(jiān)聽機(jī)制概述

        SpringBoot中事件監(jiān)聽機(jī)制則通過發(fā)布-訂閱實現(xiàn),主要包括以下三部分:

        • 事件 ApplicationEvent,繼承JDK的EventObject,可自定義事件。

        • 事件發(fā)布者 ApplicationEventPublisher,負(fù)責(zé)事件發(fā)布。

        • 事件監(jiān)聽者 ApplicationListener,繼承JDK的EventListener,負(fù)責(zé)監(jiān)聽指定的事件。

        我們通過SpringBoot的方式,能夠很容易實現(xiàn)事件監(jiān)聽,接下來我們改造一下上面的案例:

        SpringBoot事件監(jiān)聽

        定義注冊事件

        public class UserRegisterEvent extends ApplicationEvent {

        private String username;

        public UserRegisterEvent(Object source) {
        super(source);
        }

        public UserRegisterEvent(Object source, String username) {
        super(source);
        this.username = username;
        }

        public String getUsername() {
        return username;
        }
        }


        注解方式 @EventListener定義監(jiān)聽器

        /**
        * 注解方式 @EventListener
        * @author Summerday
        */

        @Service
        @Slf4j
        public class CouponService {
        /**
        * 監(jiān)聽用戶注冊事件,執(zhí)行發(fā)放優(yōu)惠券邏輯
        */

        @EventListener
        public void addCoupon(UserRegisterEvent event) {
        log.info("給用戶[{}]發(fā)放優(yōu)惠券", event.getUsername());
        }
        }


        實現(xiàn)ApplicationListener的方式定義監(jiān)聽器

        /**
        * 實現(xiàn)ApplicationListener<Event>的方式
        * @author Summerday
        */

        @Service
        @Slf4j
        public class EmailService implements ApplicationListener<UserRegisterEvent> {
        /**
        * 監(jiān)聽用戶注冊事件, 異步發(fā)送執(zhí)行發(fā)送郵件邏輯
        */

        @Override
        @Async
        public void onApplicationEvent(UserRegisterEvent event) {
        log.info("給用戶[{}]發(fā)送郵件", event.getUsername());
        }
        }


        注冊事件發(fā)布者

        @Service
        @Slf4j
        public class UserService implements ApplicationEventPublisherAware {

        // 注入事件發(fā)布者
        private ApplicationEventPublisher applicationEventPublisher;

        @Override
        public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
        }

        /**
        * 發(fā)布事件
        */

        public void register(String username) {
        log.info("執(zhí)行用戶[{}]的注冊邏輯", username);
        applicationEventPublisher.publishEvent(new UserRegisterEvent(this, username));
        }
        }


        定義接口

        @RestController
        @RequestMapping("/event")
        public class UserEventController {

        @Autowired
        private UserService userService;

        @GetMapping("/register")
        public String register(String username){
        userService.register(username);
        return "恭喜注冊成功!";
        }
        }


        主程序類

        @EnableAsync // 開啟異步
        @SpringBootApplication
        public class SpringBootEventListenerApplication {

        public static void main(String[] args) {

        SpringApplication.run(SpringBootEventListenerApplication.class, args);
        }

        }


        測試接口

        啟動程序,訪問接口:http://localhost:8081/event/register?username=Java爛豬皮,結(jié)果如下:


        2020-12-21 00:59:46.679  INFO 12800 --- [nio-8081-exec-1] com.hyh.service.UserService              : 執(zhí)行用戶[Java爛豬皮]的注冊邏輯
        2020-12-21 00:59:46.681 INFO 12800 --- [nio-8081-exec-1] com.hyh.service.CouponService : 給用戶[Java爛豬皮]發(fā)放優(yōu)惠券
        2020-12-21 00:59:46.689 INFO 12800 --- [task-1] com.hyh.service.EmailService : 給用戶[Java爛豬皮]發(fā)送郵件




        想進(jìn)大廠的小伙伴請注意,

        大廠面試的套路很神奇,

        早做準(zhǔn)備對大家更有好處,

        埋頭刷題效率低,

        看面經(jīng)會更有效率!

        小編準(zhǔn)備了一份大廠常問面經(jīng)匯總集

        剩下的就不會給大家一展出來了,以上資料按照一下操作即可獲得

        ——將文章進(jìn)行轉(zhuǎn)發(fā)評論,關(guān)注公眾號【Java烤豬皮】,關(guān)注后繼續(xù)后臺回復(fù)領(lǐng)取口令“ 666 ”即可免費領(lǐng)文章取中所提供的資料。




        往期精品推薦



        騰訊、阿里、滴滴后臺試題匯集總結(jié) — (含答案)

        面試:史上最全多線程序面試題!

        最新阿里內(nèi)推Java后端試題

        JVM難學(xué)?那是因為你沒有真正看完整這篇文章


        結(jié)束


        關(guān)注作者微信公眾號 — 《JAVA烤豬皮》


        了解了更多java后端架構(gòu)知識以及最新面試寶典



        看完本文記得給作者點贊+在看哦~~~大家的支持,是作者來源不斷出文的動力~

        瀏覽 53
        點贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            古代吻胸摸腿扒衣 | 成年人免费看 | 性video饥渴少妇china | 欧美性爱亚洲日韩 | 做爱小视频网址免费观看 | 91在线无码精品秘 入口楼乃 | 国产精品伦子伦免费 | 日韩视频-熊猫成人网 | 色就是操 | 男人舔女人私密处视频 |