事件驱动的作用与目标
假设这样的业务需求:游戏服务器希望在玩家升级时触发多种效果。例如玩家升级后,各种属性都会提高,开启新的系统玩法,学习新的技能……入门程序员写出来的代码可能是这样——
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();
}
}
至此,事件驱动器的全部代码就完成了。
事件驱动模拟的测试案例
@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]
到这里,关于事件驱动模型的实现就介绍完毕了。