面向事件编程
1.前言
当一个项目启动的时候,我们首先需要面向下面的问题:
- 实现策略分离
- 组件之间依赖绑定
- 生命期控制
- 需要发布一些事情告诉感兴趣的组件
- (optional)组件隔离
第一个问题通过面向接口编程的方式,可以让服务使用者不必关心服务是如何实现的,第二和第三个问题则是一个良好的IOC容器能够解决的,譬如Spring,第四个问题当然是一个事件通知机制,Spring对事件做了支持,第五个问题是OSGI能够处理的问题,一般没有那么严格高要求隔离化,这里不深入探讨。在面向这些问题的时候,我们基本上会选择Spring来解决,事实上Spring也做的足够好了,唯一的遗憾可能就是Spring现在实在是不能称之为一个“轻量级”的容器,即使只使用基础包也足有上M之多。
在开发BlackStar的时候同样的面对这些问题,而这里,我们使用一种不同的方式来解决这些问题,即所谓的面向事件编程,使用事件机制来解决如上的问题。
2.BlackStar背景介绍
在开始之前,我们先了解一下BlackStar的问题背景,BlackStar大概地分成如下几个部分:
2.1 基础组件:
- 调度服务:很多其他组件需要定时执行的时候需要使用到调度服务,譬如JVM监控需要每分钟执行一次,该组件使用Quartz实现
- Web服务:很多其他组件需要以Web的方式提供服务出去,譬如JMX Proxy需要以Hessian的方式提供JMX接入,UI组件需要展示页面信息,该组件使用Jetty实现
- 本地JVM JMX管理服务:负责自动识别本地的JVM、并负责与本地JVM的连接,其他组件需要依赖于这个服务,譬如JMX Proxy需要获得所有的本地JVM JMX以进行Proxy、JVM Monitor需要获得本地JVM JMX的变更情况以对本地JVM进行监控,这个服务需要依赖于调度服务以进行周期性地检测本地JVM的变更
2.2插件组件:
- JMX Proxy:负责将本地的JVM JMX服务Proxy给外部,外部可以使用JConsole接入,需要依赖于本地JVM JMX管理服务
- JVM Monitor:负责监控本地JVM的数据(CPU、PermSpace、TenuredSpace、Thread等),需要依赖于本地JVM JMX管理服务和调度服务(定期检测)
- Performance Stat:性能统计服务,需要依赖于调度服务以定时执行
- UI:将统计数据以Web的方式提供给外部,需要依赖于Web服务和本地JVM JMX管理服务
3.BlackStar内部组成结构
那么BlackStar是如何解决前言中提出的问题的呢?我们首先看下面的图,所有的组件都会以EventListener的方式存在,并注册到EventMediator上;基础组件部分提供一些服务事件出去,并监听这些事件;而服务使用者当需要使用到服务的时候发布相应的服务事件出去,基础组件收到事件后进行处理;EventMediator充当一个统一的中介者。当然,系统会有统一的生命周期事件(Startup、AfterStartup、BeforeShutdown、Shutdown),由系统统一发布。
4.具体实现
我们来看看具体是如何实现的
4.1核心的Event部分:
整个Event机制实现其实非常简单
4.1.1事件监听者EventListener,如果需要接收事件,实现这个接口
public interface EventListener<T extends Event>
{
/**
* 需要监听的事件
* @return
*/
Class[] events();
/**
* 事件处理
* @param event
* @throws Exception
*/
void onEvent(T event) throws Exception;
}
4.1.2事件中间者,负责Listener注册和事件分发(EventMediator):
public abstract class EventMediator implements EventDispatcher
{
private static EventMediator INSTANCE = new DefaultEventMediator();
public static void setInstance(EventMediator instance)
{
if (instance == null)
{
throw new IllegalArgumentException("instance can't be null");
}
INSTANCE = instance;
}
public abstract void dispatch(Event event);
public abstract void register(EventListener eventListener);
/**
* 注册Listener
* @param eventListener
*/
public static void registerListener(EventListener eventListener)
{
INSTANCE.register(eventListener);
}
/**
* 分发事件被关注的监听者
* @param event
*/
public static void dispatchEvent(Event event)
{
INSTANCE.dispatch(event);
}
}
4.1.2事件分发,如果需要事件分发,可以继承自EventDispatcherSupport
public interface EventDispatcher
{
/**
* 事件分发
* @param event
*/
void dispatch(Event event);
}
public class EventDispatcherSupport implements EventDispatcher
{
public void dispatch(Event event)
{
EventMediator.dispatchEvent(event);
}
}
4.2系统启动与系统事件
4.2.1
现在我们的系统准备开始启动了,非常简单,就是从配置中读取所有的Listener,注册,并发送StartupEvent和AfterStartupEvent
public static void main(String[] args) throws Exception
{
String listeners = AgentConfig.getProperty("listeners");
String[] listenerArray = listeners.split(",");
for (String listener : listenerArray)
{
final EventListener eventListener = (EventListener) Class.forName(
listener.trim()).newInstance();
EventMediator.registerListener(eventListener);
LOGGER.info("Regist EventListner[" + listener + "]");
}
EventMediator.registerListener(new ShutdownHook());
LOGGER.info("Dispatch StartupEvent");
EventMediator.dispatchEvent(new StartupEvent());
LOGGER.info("Dispatch AfterStartupEvent");
EventMediator.dispatchEvent(new AfterStartupEvent());
LOGGER.info("Startup Success");
}
4.2.2 从上面我们可以看到一个特殊的Listener——ShutdownHook,其负责向JVM注册ShutdownHook事件,在JVM关闭的时候向发布BeforeShutdownEvent和ShutdownEvent
public class ShutdownHook extends EventDispatcherSupport implements
EventListener<StartupEvent>
{
private final static Log LOGGER = LogFactory.getLog(ShutdownHook.class);
private static boolean isShutdown = false;;
public Class[] events()
{
return new Class[]
{ StartupEvent.class };
}
public void onEvent(StartupEvent event) throws Exception
{
Runtime.getRuntime().addShutdownHook(new Thread()
{
public void run()
{
synchronized (ShutdownHook.class)
{
if (!isShutdown)
{
LOGGER.info("Dispatch BeforeShutdown");
dispatch(new BeforeShutdownEvent());
LOGGER.info("Dispatch Shutdown");
dispatch(new ShutdownEvent());
LOGGER.info("Shutdown Success");
isShutdown = true;
}
else
{
LOGGER.error("Duplicate Shutdown");
}
}
}
});
}
}
4.2.3系统事件
从上面我们可以看到StartupEvent、AfterStartupEvent、BeforeShutdownEvent、ShutdownEvent,如果我们的Listener需要进行这些声明期的控制,譬如初始化、销毁对象之类的,可以注册这些事件,譬如如上的ShutdownHook,在其他Listener都初始化完成之后才去注册ShutdownHook
4.3如何使用一个服务
现在,系统已经把我们的Listener注册进去,并发送了启动事件让我们可以从容进行初始化的工作,现在我们的服务需要依赖于其他的服务,该如何做呢?我们以Monitor组件为例,Monitor组件需要定时执行程序。
首先,调度基础服务定义了调度事件,如下
public class ScheduleEvent extends Event
{
private ScheduleTask task;
private String cronExpression;
public ScheduleEvent(String name, String cronExpression, ScheduleTask task)
{
super(name);
this.cronExpression = cronExpression;
this.task = task;
}
public ScheduleEvent(String cronExpression, ScheduleTask task)
{
this(null, cronExpression, task);
}
public String getCronExpression()
{
return cronExpression;
}
public ScheduleTask getTask()
{
return task;
}
}
我们的服务需要调度,则只需要发布这个事件即可以,调度服务自动会处理这个事件,如下
final Monitor monitor = monitorInfo.getMonitorTask();
monitor.init(proxy);
String jobName = "jvm" + proxy.getId() + "_"
+ monitor.getName();
//分布定时调度
dispatch(new ScheduleEvent(jobName, monitorInfo
.getCronExpression(), new ScheduleTask()
{
public void schedule()
{
monitor.onTimeout();
}
}));
4.4服务如何处理服务事件
在上面中,服务使用者发布了一个调度事件,则调度服务提供者接受到了这个事件,可以开始处理
public void onEvent(Event event) throws Exception
{
……
else if (event instanceof ScheduleEvent)
{
if (!scheduler.isShutdown())
{
ScheduleEvent se = (ScheduleEvent) event;
if (se.getId() == null)
{
schedule(se.getCronExpression(), se.getTask());
} else
{
schedule(se.getId(), se.getCronExpression(), se.getTask());
}
}
……
}
4.5数据查询如何做
数据查询是面向事件编程最难处理也处理地最不好的部分,不过我们还是补充上吧。提供数据查询服务的服务提供方需要将自己的接口发布出来,而服务使用者接收到这个发布声明后,将其作为自己的一个属性
如下,服务发布方
public synchronized void startup()
{
……
dispatch(new JMXProxyManagerStartupEvent(this));
}
服务使用方
public class JMXProxyUtils implements
EventListener<JMXProxyManagerStartupEvent>
{
private static JMXProxyManager proxyManager;
public Class[] events()
{
return new Class[]
{ JMXProxyManagerStartupEvent.class };
}
public void onEvent(JMXProxyManagerStartupEvent event) throws Exception
{
proxyManager = event.getProxyManager();
}
……
}
4.6变更事件通知
变更事件通知是面向事件的老本行,处理方式与其他一致,譬如当识别到本地的一个JVM时,则需要创建它,并将其发布出去,通知其他组件有一个新的JVM实例产生了
protected synchronized boolean startupLocalJVM(LocalJVM localJVM)
{
if (!localJVM.start())
{
return false;
}
jvms.put(localJVM.getPid(), localJVM);
dispatch(new JMXProxyStartupEvent(localJVM));
return true;
}
其他组件接收到这个事件,则可以进行自己的处理
public void onEvent(Event event) throws Exception
{
if (event instanceof JMXProxyStartupEvent)
{
startup(((JMXProxyStartupEvent) event).getJMXProxy());
} else if (event instanceof JMXProxyShutdownEvent)
{
shutdown(((JMXProxyShutdownEvent) event).getJMXProxy());
}
}
5.结束语
一般而言,面向接口编程混合事件编程是一种通用的处理问题的方式,但缺陷在于需要有一个良好的生命周期机制和一个依赖管理组件,解决这两个问题一般依赖于一个设计良好的IOC容器。而面向事件编程是一种相对没有那么自然的处理问题的方式,当我们不希望引入一个复杂的IOC容器来解决我们的问题的时候,应该算是一个不错的选择。当然,其固有的缺陷也是相当明显,当项目比较庞杂服务众多的时候,定义事件都会让人不胜其扰,组件之间查询类的依赖比较多的时候,基本上也不是一个好的选择。一般这种纯粹面向事件的方式比较适应于小规模的、核心服务相对比较稳定而且组件之间比较少依赖于查询(没有返回结果,允许异步化处理的类型)的独立应用。