在Spring中使用事件機制

1. 事件機制

事件機制的底層設計模式是觀察者模式,觀察者設計模式定義了對象間的一種一對多的組合關係,以便一個對象的狀態發生變化時,全部依賴於它的對象都獲得通知並自動刷新,它可以將觀察者和被觀察者之間進行解耦。本文不具體介紹觀察者模式,讀者能夠從其餘文章進行查閱。java

使用事件機制可以應對不少場景,它可以對系統業務邏輯之間的解耦。例如Swing中抽象出不少事件例如鼠標點擊、鍵盤鍵入等,它經過事件派發線程不斷從事件隊列中獲取事件並調用事件監聽器的事件處理方法來處理事件。spring

再舉一個實際業務中會存在的場景,咱們有一個方法TradeService進行用戶交易操做,此時有一個用戶支付訂單成功的方法paySuccess。當用戶支付成功時,會有不少操做,好比向庫存發送通知,以下述代碼(咱們對其進行簡化,僅傳入orderId):設計模式

public class TradeServiceImpl implements TradeService {

	/**
	 * 支付成功相關操做
	 * @param orderId 訂單號
	 */
	@Override
	public void paySuccess(Long orderId) {
		Preconditions.checkNotNull(orderId);
		
		// 1.修改訂單狀態爲支付成功

		// 2.告知倉庫準備發貨

	}

}


複製代碼

可是此時產品經理須要咱們在支付成功以後可以經過短信告訴用戶能夠進行抽獎,那咱們仍須要修改該類。每次添加新功能的時候都須要修改原有的類,難以維護,這違反了設計模式的單一職責原則開閉原則。咱們可使用事件機制,將支付操做和其餘支付以後的相關操做進行分離,經過事件來解耦。下文將圍繞該業務經過事件進行改造。 (注:實際上該應用場景由於一般是分佈式場景因此咱們須要經過消息中間件例如Kafka進行異步處理,本文因爲介紹Spring內置事件機制因此利用本地事件進行簡化)bash

2. Spring原生事件驅動模型

Spring提供了一些內置的事件供開發者使用:app

事件名稱 概述
ContextRefreshedEvent 該事件會在ApplicationContext被初始化或者更新時發佈。也能夠在調用ConfigurableApplicationContext 接口中的refresh()方法時被觸發。
ContextStartedEvent 當容器調用ConfigurableApplicationContext的Start()方法開始/從新開始容器時觸發該事件。
ContextStoppedEvent 當容器調用ConfigurableApplicationContext的Stop()方法中止容器時觸發該事件。
ContextClosedEvent 當ApplicationContext被關閉時觸發該事件。容器被關閉時,其管理的全部單例Bean都被銷燬。
RequestHandledEvent 在Web應用中,當一個http請求(request)結束觸發該事件。

2.1 同步方式

  • 建立事件

首先咱們先建立相關的事件,本場景下咱們須要圍繞支付成功進行相關操做,因此咱們須要建立支付成功事件PaySuccessEvent,相關代碼以下所示:異步

@Data
public class PaySuccessEvent extends ApplicationEvent {

	/**
	 * 訂單ID
	 */
	Long orderId;

	public PaySuccessEvent(Object source, Long orderId) {
		super(source);
		this.orderId = orderId;
	}
}

複製代碼
  • 發佈事件

發佈事件咱們能夠經過ApplicationContext或者ApplicationEventPublisher進行發佈,可是若是咱們只須要進行發佈事件,咱們只須要建立一個類實現ApplicationEventPublisherAware接口,經過回調方法,使其可以得到ApplicationEventPublish,該接口的publishEvent方法可以對事件進行發佈:async

public interface ApplicationEventPublisherAware extends Aware {

	/**
	 * Set the ApplicationEventPublisher that this object runs in.
	 * <p>Invoked after population of normal bean properties but before an init
	 * callback like InitializingBean's afterPropertiesSet or a custom init-method. * Invoked before ApplicationContextAware's setApplicationContext.
	 * @param applicationEventPublisher event publisher to be used by this object
	 */
	void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher);

}
複製代碼

咱們經常使用的ApplicationContext也是繼承自該接口:分佈式

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
		MessageSource, ApplicationEventPublisher, ResourcePatternResolver
複製代碼

若須要獲取ApplicationContext咱們只須要實現ApplicationContextAware接口經過其回調方法便可設置上下文環境。ide

咱們建立一個事件發佈的類:ui

public class EventPublisher implements ApplicationEventPublisherAware {

	public static ApplicationEventPublisher applicationEventPublisher;

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

	public static void publishEvent(ApplicationEvent applicationEvent) {
		applicationEventPublisher.publishEvent(applicationEvent);
	}
}

複製代碼

而後咱們對支付成功事件進行發佈,以下所示:

public class TradeServiceImpl implements TradeService {

	/**
	 * 支付成功相關操做
	 * @param orderId 訂單號
	 */
	@Override
	public void paySuccess(Long orderId) {
		Preconditions.checkNotNull(orderId);
		EventPublisher.publishEvent(new PaySuccessEvent(this, orderId));
	}

}
複製代碼
  • 事件監聽

咱們可使用EventListener註解或者實現ApplicationListener接口來對事件進行監聽。

  • 1. 使用EventListener註解

@Component
public class PaySuccessEventListener {

	/**
	 * 修改訂單狀態
	 * @param paySuccessEvent 支付成功事件
	 */
	@EventListener
	public void modifyOrderStatus(PaySuccessEvent paySuccessEvent)
	{
		System.out.println("修改訂單狀態成功!orderId:" + paySuccessEvent.getOrderId());
	}

	/**
	 * 發送短信告知用戶進行抽獎
	 * @param paySuccessEvent 支付成功事件
	 */
	@EventListener
	public void sendSMS(PaySuccessEvent paySuccessEvent)
	{
		System.out.println("發送短信成功!orderId:" + paySuccessEvent.getOrderId());
	}


}

複製代碼
  • 2. 實現ApplicationListener接口

public class PaySuccessListener implements ApplicationListener<PaySuccessEvent> {

	@Override
	public void onApplicationEvent(PaySuccessEvent event) {
		System.out.println("告知倉庫準備發貨成功!" + event.getOrderId());
	}

}
複製代碼

2.2 異步方式

Spring經過ApplicationEventMulticaster提供異步偵聽事件的方式,可是註冊 ApplicationEventMulticaster Bean 後全部的事件偵聽處理都會變成的異步形式,若是須要針對特定的事件偵聽採用異步方式的話:可使用@EnableAsync和@Async組合來實現。 咱們能夠經過@EnableAsync註解開啓異步方式,以後咱們在須要異步監聽的方法上加上@Async註解,這樣可以使得發佈事件時,發佈方能異步調用監聽器,主方法不會阻塞。如下是EnableAsync的部分註釋:

* <p>By default, Spring will be searching for an associated thread pool definition:
 * either a unique {@link org.springframework.core.task.TaskExecutor} bean in the context,
 * or an {@link java.util.concurrent.Executor} bean named "taskExecutor" otherwise. If
 * neither of the two is resolvable, a {@link org.springframework.core.task.SimpleAsyncTaskExecutor}
 * will be used to process async method invocations. Besides, annotated methods having a
 * {@code void} return type cannot transmit any exception back to the caller. By default,
 * such uncaught exceptions are only logged.
 
  * <p>To customize all this, implement {@link AsyncConfigurer} and provide:
 * <ul>
 * <li>your own {@link java.util.concurrent.Executor Executor} through the
 * {@link AsyncConfigurer#getAsyncExecutor getAsyncExecutor()} method, and</li>
 * <li>your own {@link org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler
 * AsyncUncaughtExceptionHandler} through the {@link AsyncConfigurer#getAsyncUncaughtExceptionHandler
 * getAsyncUncaughtExceptionHandler()}
 * method.</li>
 * </ul>
複製代碼

簡而言之就是Spring默認狀況下會先搜索TaskExecutor或者名稱爲taskExecutor的Executor類型的bean,若都不存在那麼Spring會用SimpleAsyncTaskExecutor去執行異步方法。此外咱們還能夠經過實現AsyncConfigurer接口去自定義異步配置。

咱們新建一個異步配置類AsyncConfig,使其具有異步能力:

@Configuration
@EnableAsync
@Slf4j
public class AsyncConfig implements AsyncConfigurer {

	@Override
	@Bean(name = "taskExecutor")
	public Executor getAsyncExecutor() {
		ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
		taskExecutor.setCorePoolSize(5);
		taskExecutor.setMaxPoolSize(10);
		taskExecutor.setQueueCapacity(25);
		taskExecutor.initialize();
		return taskExecutor;
	}

	@Override
	public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
		return new MyAsyncExceptionHandler();
	}

	/**
	 * 自定義異常處理類
	 */
	class MyAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

		//手動處理捕獲的異常
		@Override
		public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
			System.out.println("------catched exception!------");
			log.info("Exception message - " + throwable.getMessage());
			log.info("Method name - " + method.getName());
			for (Object param : obj) {
				log.info("Parameter value - " + param);
			}
		}
	}
}
複製代碼

在開啓異步化以後,咱們只須要在監聽方法上添加@Async註解就能夠實現事件的異步調用。

相關文章
相關標籤/搜索