一、前言
釋出訂閱模式也叫觀察者模式,利用該模式可以進行程式碼解耦,很多框架都用到該設計模式,比如Spring的事件機制,guava的EventBus(事件匯流排)等,不清楚觀察者模式的話可以檢視本人之前寫的部落格《設計模式之觀察者模式》。
為了更好的瞭解現有的事件框架實現原理,便手寫了一個簡單的事件釋出/訂閱框架供大家參考。
二、設計編碼
首先建立一個事件類繼承,所有的事件都繼承該類。
/**
* @author 2YSP
* @date 2022/4/16 16:00
*/
public class Event extends EventObject {
/**
* Constructs a prototypical Event.
*
* @param source The object on which the Event initially occurred.
* @throws IllegalArgumentException if source is null.
*/
public Event(Object source) {
super(source);
}
}
JDK要求所有事件都繼承EventObject,並通過source得到事件源。
然後定義事件監聽器介面EventListener
/**
* @author 2YSP
* @description: 事件監聽器
* @date 2022/4/10 14:45
*/
public interface EventListener<E extends Event> {
/**
* 觸發事件
* @param e
*/
void onEvent(E e);
}
核心部分就是需要一個類來管理所有的事件監聽器,分別具備以下三個方法:
registerListener():註冊一個事件監聽器
removeListener():移除事件監聽器
notifyListener():通知該事件觸發的所有監聽器
package cn.sp.event;
import com.google.common.collect.Lists;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
import javax.annotation.PostConstruct;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author 2YSP
* @date 2022/4/16 16:12
*/
@Component
public class EventManager implements ApplicationContextAware {
/**
* 事件map
*/
private static Map<Class<? extends Event>, List<EventListener>> map = new HashMap<>(64);
private static ApplicationContext applicationContext;
private static final String EVENT_METHOD = "onEvent";
/**
* 初始化事件快取map
*/
@PostConstruct
private void initEventMap() {
Map<String, EventListener> beanMap = applicationContext.getBeansOfType(EventListener.class);
if (beanMap == null) {
return;
}
beanMap.forEach((key, value) -> {
// 反射獲取onEvent方法的引數型別
Method[] methods = ReflectionUtils.getDeclaredMethods(value.getClass());
for (Method method : methods) {
if (method.getName().equals(EVENT_METHOD)) {
Parameter parameter = method.getParameters()[0];
// 引數必須為Event的子類
if (parameter.getType().getName().equals(Event.class.getName())) {
continue;
}
registerListener((Class<? extends Event>) parameter.getType(), value);
}
}
});
}
/**
* 註冊一個事件監聽器
*
* @param clazz
* @param eventListener
* @param <E>
*/
public <E extends Event> void registerListener(Class<? extends Event> clazz, EventListener<E> eventListener) {
List<EventListener> list = map.get(clazz);
if (CollectionUtils.isEmpty(list)) {
map.put(clazz, Lists.newArrayList(eventListener));
} else {
list.add(eventListener);
map.put(clazz, list);
}
}
/**
* 移除一個事件監聽器
*
* @param clazz
* @param <E>
*/
public <E extends Event> void removeListener(Class<E> clazz) {
map.remove(clazz);
}
/**
* 通知所有該事件的監聽器
*
* @param <E>
*/
public <E extends Event> void notifyListener(E e) {
List<EventListener> eventListeners = map.get(e.getClass());
if (CollectionUtils.isEmpty(eventListeners)) {
return;
}
eventListeners.forEach(eventListener -> {
// 同步執行
eventListener.onEvent(e);
});
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
EventManager.applicationContext = applicationContext;
}
}
通過initEventMap()方法在專案啟動後,利用反射註冊所有的事件監聽器,但是notifyListener()方法是序列執行,如果想要非同步執行增加一個標記註解@AsyncExecute就行了,優化後版本如下:
/**
* @author 2YSP
* @date 2022/4/16 17:35
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AsyncExecute {
}
EventManager
/**
* @author 2YSP
* @date 2022/4/16 16:12
*/
@Component
public class EventManager implements ApplicationContextAware {
/**
* 事件map
*/
private static Map<Class<? extends Event>, List<EventListener>> map = new HashMap<>(64);
private static ApplicationContext applicationContext;
private static final String EVENT_METHOD = "onEvent";
/**
* 事件執行執行緒池
*/
private static ExecutorService eventPool = new ThreadPoolExecutor(4,
8, 30L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(512), new ThreadFactoryBuilder().setNameFormat("event-pool-%d").build());
/**
* 初始化事件快取map
*/
@PostConstruct
private void initEventMap() {
Map<String, EventListener> beanMap = applicationContext.getBeansOfType(EventListener.class);
if (beanMap == null) {
return;
}
beanMap.forEach((key, value) -> {
// 反射獲取onEvent方法的引數型別
Method[] methods = ReflectionUtils.getDeclaredMethods(value.getClass());
for (Method method : methods) {
if (method.getName().equals(EVENT_METHOD)) {
Parameter parameter = method.getParameters()[0];
// 引數必須為Event的子類
if (parameter.getType().getName().equals(Event.class.getName())) {
continue;
}
registerListener((Class<? extends Event>) parameter.getType(), value);
}
}
});
}
/**
* 註冊一個事件監聽器
*
* @param clazz
* @param eventListener
* @param <E>
*/
public <E extends Event> void registerListener(Class<? extends Event> clazz, EventListener<E> eventListener) {
List<EventListener> list = map.get(clazz);
if (CollectionUtils.isEmpty(list)) {
map.put(clazz, Lists.newArrayList(eventListener));
} else {
list.add(eventListener);
map.put(clazz, list);
}
}
/**
* 移除一個事件監聽器
*
* @param clazz
* @param <E>
*/
public <E extends Event> void removeListener(Class<E> clazz) {
map.remove(clazz);
}
/**
* 通知所有該事件的監聽器
*
* @param <E>
*/
public <E extends Event> void notifyListener(E e) {
List<EventListener> eventListeners = map.get(e.getClass());
if (CollectionUtils.isEmpty(eventListeners)) {
return;
}
eventListeners.forEach(eventListener -> {
AsyncExecute asyncExecute = eventListener.getClass().getAnnotation(AsyncExecute.class);
if (asyncExecute == null) {
// 同步執行
eventListener.onEvent(e);
} else {
// 非同步執行
eventPool.execute(() -> eventListener.onEvent(e));
}
});
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
EventManager.applicationContext = applicationContext;
}
}
@AsyncExecute註解可用在類上,每次呼叫notifyListener()方法時通過反射判斷是否存在@AsyncExecute註解,如果存在則用執行緒池非同步執行,其實反射的效能不是很好,如果追求效能的話可以考慮在初始化時就將是否非同步執行的資訊維護到事件快取map中。
現在就差一個釋出事件的工具類EventPublisher
/**
* @author 2YSP
* @date 2022/4/16 16:07
*/
@Component
public class EventPublisher<E extends Event> {
@Resource
private EventManager eventManager;
public <E extends Event> void publish(E event) {
eventManager.notifyListener(event);
}
}
三、測試
測試的場景是訂單建立後,發生訂單建立事件,然後有兩個監聽器都監聽了該事件,區別是一個用了@AsyncExecute註解,一個沒有。
- 建立Order實體
public class Order {
private String orderNo;
public String getOrderNo() {
return orderNo;
}
public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}
}
2.建立訂單建立事件
public class OrderCreateEvent extends Event {
private Order order;
public OrderCreateEvent(Object source, Order order) {
super(source);
this.order = order;
}
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
}
3.建立事件監聽器
@Component
public class OrderCreateEventListener implements EventListener<OrderCreateEvent> {
@Override
public void onEvent(OrderCreateEvent orderCreateEvent) {
System.out.println(Thread.currentThread().getName() + "--監聽訂單建立事件。。。。。。。。。");
Order order = orderCreateEvent.getOrder();
System.out.println(order.getOrderNo());
}
}
@AsyncExecute
@Component
public class OrderCreateEventListener2 implements EventListener<OrderCreateEvent> {
@Override
public void onEvent(OrderCreateEvent orderCreateEvent) {
System.out.println(Thread.currentThread().getName() + "--監聽訂單建立事件2。。。。。。。。。");
Order order = orderCreateEvent.getOrder();
System.out.println(order.getOrderNo());
}
}
4.釋出事件
@RequestMapping("/order")
@RestController
public class OrderController {
@Resource
private OrderService orderService;
@PostMapping("")
public void create(@RequestBody Order order) {
orderService.create(order);
}
}
@Service
public class OrderService {
@Resource
private EventPublisher<OrderCreateEvent> publisher;
/**
* 建立訂單
*
* @param order
*/
public void create(Order order) {
// 傳送訂單建立事件
order.setOrderNo("sssss");
publisher.publish(new OrderCreateEvent(this, order));
}
}
測試程式碼編寫完畢,啟動專案請求訂單建立介面http://localhost:8080/order,控制檯輸出如下
http-nio-8080-exec-2--監聽訂單建立事件。。。。。。。。。
sssss
event-pool-0--監聽訂單建立事件2。。。。。。。。。
sssss
說明兩個事件監聽器都被觸發了,且執行緒名字不同,說明一個是主執行緒同步執行,另一個是執行緒池非同步,至此測試成功。
四、總結
寫完發現實現一個釋出/訂閱框架並不難,當然這個功能比較簡單,還有優化的空間,程式碼已經上傳到github,點選即可檢視。