事件驅動的作用與目標
假設這樣的業務需求:遊戲服務器希望在玩家升級時觸發多種效果。例如玩家升級後,各種屬性都會提高,開啓新的系統玩法,學習新的技能……入門程序員寫出來的代碼可能是這樣——
private void handleRoleUpgrade(Object role){
if(meetUpgradeCondition(role)){//滿足升級條件
RoleManager.getInstance().upgradeAttribution(role);//屬性提升
SkillManager.getInstance().learnNewSkill(role);//學會新技能
//其他一堆業務
}
}
可以看出,玩家升級後,所有跟升級掛鉤的業務都要集中在一起,依次被處理。這樣寫出來的代碼耦合度非常高。一旦有新的業務加入,這裏就要繼續插代碼。爲了達到解耦的效果,我們引入了事件驅動模型。
當玩家觸發了升級這個動作,我們完全可以把“升級”這個動作包裝成一個事件,任何對這個事件感興趣的“觀察者”就可以捕捉並執行相應的邏輯。
我們希望我們的事件驅動模型能夠滿足以下幾個要求:
1. 當觸發某個動作時,將動作包裝成事件並進行分發,所有與之相關的監聽器自動感應事件的發生;
2. 同一個事件可以被多個監聽器響應;
3. 一個監聽器可以同時監聽多個事件;
4. 事件可以選擇同步執行,也可以選擇異步執行。
事件驅動的代碼實現
下面開始我們的編碼邏輯
1. 首先,我們定義”事件“這個抽象概念(GameEvent)。注意,事件有一個基類方法標識是否同步執行。
/**
* 監聽器監聽的事件抽象類
*/
public abstract class GameEvent {
/** 創建時間 */
private long createTime;
/** 事件類型 */
private final EventType eventType;
public GameEvent(EventType evtType) {
this.createTime = System.currentTimeMillis();
this.eventType = evtType;
}
public long getCreateTime() {
return this.createTime;
}
public EventType getEventType() {
return this.eventType;
}
/**
* 是否在消息主線程同步執行
* @return
*/
public boolean isSynchronized() {
return true;
}
}
2. 爲了區分各種事件,我們定義一個表示事件類型的枚舉器(EventType.java)public enum EventType {
/** 升級事件 */
LEVEL_UP;
}
3. 在遊戲業務裏,很多事件都是綁定角色的,所以定義一個跟玩家關係密切的玩家事件(PlayerEvent.java)/**
* 玩家事件抽象類
*/
public abstract class PlayerEvent extends GameEvent {
/** 玩家id */
private final long playerId;
public PlayerEvent(EventType evtType, long playerId) {
super(evtType);
this.playerId = playerId;
}
public long getPlayerId() {
return this.playerId;
}
}
4.定義一個註解(Listener.java),標識”監聽器“@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Listener {
}
5. 定義事件分發器(EventDispatcher),該分發器擁有以下作用:綁定事件與事件監聽者;
分發事件,若爲同步事件,則在當前的業務主線程執行,若爲異步事件,則放到獨立線池異步執行。
public class EventDispatcher {
private static EventDispatcher instance = new EventDispatcher();
private EventDispatcher() {
new NameableThreadFactory("event-dispatch").newThread(new EventWorker()).start();
};
public static EventDispatcher getInstance() {
return instance;
}
/** 事件類型與事件監聽器列表的映射關係 */
private final Map<EventType, Set<Object>> observers = new HashMap<>();
/** 異步執行的事件隊列 */
private LinkedBlockingQueue<GameEvent> eventQueue = new LinkedBlockingQueue<>();
/**
* 註冊事件監聽器
* @param evtType
* @param listener
*/
public void registerEvent(EventType evtType, Object listener) {
Set<Object> listeners = observers.get(evtType);
if(listeners == null){
listeners = new CopyOnWriteArraySet<>();
observers.put(evtType, listeners);
}
listeners.add(listener);
}
/**
* 分發事件
* @param event
*/
public void fireEvent(GameEvent event) {
if(event == null){
throw new NullPointerException("event cannot be null");
}
//如果事件是同步的,那麼就在消息主線程執行邏輯
if (event.isSynchronized()) {
triggerEvent(event);
} else {
//否則,就丟到事件線程異步執行
eventQueue.add(event);
}
}
private void triggerEvent(GameEvent event) {
EventType evtType = event.getEventType();
Set<Object> listeners = observers.get(evtType);
if(listeners != null){
listeners.forEach(listener->{
try{
ListenerManager.INSTANCE.fireEvent(listener, event);
}catch(Exception e){
LoggerUtils.error("triggerEvent failed", e);; //防止其中一個listener報異常而中斷其他邏輯
}
});
}
}
private class EventWorker implements Runnable {
@Override
public void run() {
while(true) {
try {
GameEvent event = eventQueue.take();
triggerEvent(event);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
6.分發器只是綁定了事件與其監聽器,並沒有說明,這個事件由其監聽器的哪個方法監聽。爲了達到方法級別的綁定,我們引入另一個註解(EventHandler.java)。/**
* 事件處理者
* @author kingston
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventHandler {
/** 綁定的事件類型列表 */
public EventType[] value();
}
也就是說,監聽器由Listener註解標識,該監聽器要監聽多個事件,那麼就在每一個事件的執行者方法加一個EventHandler註解。如果看過之前的文章,那篇關於使用Controller, RequestMapper來自動映射玩家消息與業務執行者。就會發現,Listerner與ListenerHandler所發揮的作用,跟之前是完全一樣的。
7. 由於一個監聽器可以同時監聽多個事件,爲了精確找到具體的業務執行者,我們必須將監聽器與事件類型作爲聯合主鍵,緩存這樣一個映射關係。key=listener_eventType, value=listenerMethod。所以,我們又加入一個工具類(ListenerManager.java)
public enum ListenerManager {
INSTANCE;
private Map<String, Method> map = new HashMap<>();
private final String SCAN_PATH = "com.kingston.game";
public void initalize() {
Set<Class<?>> listeners = ClassScanner.getClasses(SCAN_PATH, new ClassFilter() {
@Override
public boolean accept(Class<?> clazz) {
return clazz.getAnnotation(Listener.class) != null;
}
});
for (Class<?> listener: listeners) {
try {
Object handler = listener.newInstance();
Method[] methods = listener.getDeclaredMethods();
for (Method method:methods) {
EventHandler mapperAnnotation = method.getAnnotation(EventHandler.class);
if (mapperAnnotation != null) {
EventType[] eventTypes = mapperAnnotation.value();
for(EventType eventType: eventTypes) {
EventDispatcher.getInstance().registerEvent(eventType, handler);
map.put(getKey(handler, eventType), method);
}
}
}
}catch(Exception e) {
LoggerUtils.error("", e);
}
}
}
/**
* 分發給具體監聽器執行
* @param handler
* @param event
*/
public void fireEvent(Object handler,GameEvent event) {
try {
Method method = map.get(getKey(handler, event.getEventType()));
method.invoke(handler, event);
} catch (Exception e) {
LoggerUtils.error("", e);
}
}
private String getKey(Object handler, EventType eventType) {
return handler.getClass().getName() + "-" + eventType.toString();
}
}
至此,事件驅動器的全部代碼就完成了。事件驅動模擬的測試案例
我們直接將引子的場景作爲測試吧,當玩家升級時,就會觸發學習一個新技能。
1.申明事件類型,EventType.LEVEL_UP
2.申明事件監聽器(Listener)與執行事件的方法(ListenerHandler)
@Listener
public class SkillListener {
@EventHandler(value=EventType.LEVEL_UP)
public void onPlayerLevelup(EventPlayerLevelUp levelUpEvent) {
System.err.println(getClass().getSimpleName()+"捕捉到事件"+levelUpEvent);
}
}
3. 測試代碼,服務啓動時,直接拋出一個升級事件 //啓動socket服務
try{
new SocketServer().start();
}catch(Exception e) {
LoggerUtils.error("ServerStarter failed ", e);
}
Player player = PlayerManager.getInstance().get(10000L);
EventDispatcher.getInstance().fireEvent(new EventPlayerLevelUp(EventType.LEVEL_UP,
player.getId(), 2));
4. SkillListener直接捕捉到升級事件,輸出如下SkillListener捕捉到事件EventPlayerLevelUp [upLevel=2, playerId=2815129724291645440, EventType=LEVEL_UP]
到這裏,關於事件驅動模型的實現就介紹完畢了。
文章預告:下一篇主要介紹如何使用http服務構建後臺管理系統
手遊服務端開源框架系列完整的代碼請移步github ->>game_server