SpringBoot事件監聽機制及觀察者模式/發佈訂閱模式

本篇要點

  • 介紹觀察者模式和發佈訂閱模式的區別。
  • SpringBoot快速入門事件監聽。

什麼是觀察者模式?

觀察者模式是經典行爲型設計模式之一。html

在GoF的《設計模式》中,觀察者模式的定義:在對象之間定義一個一對多的依賴,當一個對象狀態改變的時候,全部依賴的對象都會自動收到通知。若是你以爲比較抽象,接下來這個例子應該會讓你有所感受:java

就拿用戶註冊功能爲例吧,假設用戶註冊成功以後,咱們將會發送郵件,優惠券等等操做,很容易就能寫出下面的邏輯:git

@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);
        // 發送郵件
        emailService.sendEmail(username);
        // 發送優惠券
        couponService.addCoupon(username);
        return "註冊成功!";
    }
}

這樣寫會有什麼問題呢?受王爭老師啓發:spring

  • 方法調用時,同步阻塞致使響應變慢,須要異步非阻塞的解決方案。設計模式

  • 註冊接口此時作的事情:註冊,發郵件,優惠券,違反單一職責的原則。固然,若是後續沒有拓展和修改的需求,這樣子倒能夠接受。springboot

  • 若是後續註冊的需求頻繁變動,相應就須要頻繁變動register方法,違反了開閉原則。併發

針對以上的問題,咱們想想解決的方案:app

1、異步非阻塞的效果能夠新開一個線程執行耗時的發送郵件任務,但頻繁地建立和銷燬線程比較耗時,而且併發線程數沒法控制,建立過多的線程會致使堆棧溢出。異步

2、使用線程池執行任務解決上述問題。ide

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

@Slf4j
@Service
public class SimpleCouponService {

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

這裏用戶註冊事件對【發送短信和優惠券】實際上是一對多的關係,可使用觀察者模式進行解耦:

/**
 * 主題接口
 * @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("向[{}]發送郵件", message);
    }
}
@Component
@Slf4j
public class CouponObserver implements Observer {

    @Override
    public void update(String message) {
        log.info("向[{}]發送優惠券",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";
    }
}

發佈訂閱模式是什麼?

觀察者模式和發佈訂閱模式是有一點點區別的,區別有如下幾點:

  • 前者:觀察者訂閱主題,主題也維護觀察者的記錄,然後者:發佈者和訂閱者不須要彼此瞭解,而是在消息隊列或代理的幫助下通訊,實現鬆耦合。
  • 前者主要以同步方式實現,即某個事件發生時,由Subject調用全部Observers的對應方法,後者則主要使用消息隊列異步實現。

圖源:https://hackernoon.com/observer-vs-pub-sub-pattern-50d3b27f838c

儘管二者存在差別,可是他們其實在概念上類似,網上說法不少,不須要過於糾結,重點在於咱們須要他們爲何出現,解決了什麼問題。

Spring事件監聽機制概述

SpringBoot中事件監聽機制則經過發佈-訂閱實現,主要包括如下三部分:

  • 事件 ApplicationEvent,繼承JDK的EventObject,可自定義事件。
  • 事件發佈者 ApplicationEventPublisher,負責事件發佈。
  • 事件監聽者 ApplicationListener,繼承JDK的EventListener,負責監聽指定的事件。

咱們經過SpringBoot的方式,可以很容易實現事件監聽,接下來咱們改造一下上面的案例:

SpringBoot事件監聽

定義註冊事件

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定義監聽器

/**
 * 註解方式 @EventListener
 * @author Summerday
 */
@Service
@Slf4j
public class CouponService {
    /**
     * 監聽用戶註冊事件,執行發放優惠券邏輯
     */
    @EventListener
    public void addCoupon(UserRegisterEvent event) {
        log.info("給用戶[{}]發放優惠券", event.getUsername());
    }
}

實現ApplicationListener的方式定義監聽器

/**
 * 實現ApplicationListener<Event>的方式
 * @author Summerday
 */
@Service
@Slf4j
public class EmailService implements ApplicationListener<UserRegisterEvent> {
    /**
     * 監聽用戶註冊事件, 異步發送執行發送郵件邏輯
     */
    @Override
    @Async
    public void onApplicationEvent(UserRegisterEvent event) {
        log.info("給用戶[{}]發送郵件", event.getUsername());
    }
}

註冊事件發佈者

@Service
@Slf4j
public class UserService implements ApplicationEventPublisherAware {

    // 注入事件發佈者
    private ApplicationEventPublisher applicationEventPublisher;

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

    /**
     * 發佈事件
     */
    public void register(String username) {
        log.info("執行用戶[{}]的註冊邏輯", 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=天喬巴夏,結果以下:

2020-12-21 00:59:46.679  INFO 12800 --- [nio-8081-exec-1] com.hyh.service.UserService              : 執行用戶[天喬巴夏]的註冊邏輯
2020-12-21 00:59:46.681  INFO 12800 --- [nio-8081-exec-1] com.hyh.service.CouponService            : 給用戶[天喬巴夏]發放優惠券
2020-12-21 00:59:46.689  INFO 12800 --- [         task-1] com.hyh.service.EmailService             : 給用戶[天喬巴夏]發送郵件

源碼下載

本文內容均爲對優秀博客及官方文檔總結而得,原文地址均已在文中參考閱讀處標註。最後,文中的代碼樣例已經所有上傳至Gitee:https://gitee.com/tqbx/springboot-samples-learn,另有其餘SpringBoot的整合哦。

參考閱讀

相關文章
相關標籤/搜索