英文原文:Reverse Ajax, Part 5: Event-driven web development
前言
这一文章系列展示了如何使用反向Ajax(Reverse Ajax)技术开发事件驱动的web应用,第1部分内容介绍了反向Ajax、轮询(polling)、流(streaming)、Comet和长轮询(long polling);第2部分内容说明了如何使用WebSocket来实现反向Ajax,并讨论了使用Comet和WebSocket的web服务器的局限性;第3部分内容说明了如果需要支持多种服务器,或是给用户提供部署在他们自己的服务器上的独立的web应用的话,那么实现自己的Comet或是WebSocket通信系统会有哪些难点,该部分内容还讨论了Socket.IO;第4部分内容谈到了Atmosphere和CometD——最知名的用于Java技术服务器的开源反向Ajax库。
到目前为止你已经了解了创建通过事件来通信的组件,在本系列的最后一部分内容中,我们把事件驱动开发的原则应用到实践中,构建一个示例性的事件驱动web应用。
你可以下载本文中使用的源代码。
前提条件
理想情况下,要充分体会本文的话,你应该对JavaScrpit和Java有一定的了解,并且要有一些web开发经验。若要运行本文中的例子,你还需要最新版本的Maven和JDK(参见参考资料)。
术语
你可能对事件驱动架构(event-driven architecture,EDA)、EventBus系统、消息系统、复杂事件处理(complex event processing,CEP)和信道(channel)这些说辞并不陌生,这些术语和概念已出现多年。随着技术的成熟,你可能会更频繁地听到这类说法。本节内容为这些概念提供一些简短的解释。
事件(event)
在系统中会发生的一些事情的出现,事件通常具有属性,比如说出现的时间(时间戳)、来源或位置(我们点击的组件),以及一些描述事件的数据。根据系统的不同,事件还可以有其他的一些属性选择。
事件驱动架构(Event-driven architecture,EDA)
也称作基于事件的编程,这是一种架构设计,在这种设计中,应用由通过发送和接收事件来通信和执行的组件构成。Swing的图形化用户界面(GUI)就是一个EDA例子,每个Swing组件都可以监听事件、对事件作出反应、发送其他事件等。EDA由几个部分组成:事件生产者、事件消费者、事件和处理软件。
1. 事件生产者(event producer)——该组件发出事件。在本文的例子中,表单的提交按钮就是一个事件生产者。
2. 事件消费者(event consumer)——监听特定事件的组件。例如,例子中的表单提交这种情况,浏览器监听表单的提交按钮上的点击操作,把表单数据发送给服务器。
3. 事件处理软件(event-processing software)——这是系统的核心,事件生产者发布事件,事件消费者注册自身以接收事件。根据软件的不同,处理过程可以很简单(只是把接收到的事件转发给消费者),或很复杂(CEP)。有了CEP,软件就可以支持各种各样的处理方式,比如说事件的汇集、过滤和转换等。
Esper就是这样的一个软件例子。事件处理软件不仅可以表现成一个独立的运行应用,其还可以是整合到你的应用中的库。
消息系统(messaging system)
一种事件驱动应用类型,在这种应用中,事件生产者把消息发布到信道中,事件消费者则通过信道进行订阅。事件生产者和消费者彼此之间没有链接,是完全独立开来的。在这种类型的事件驱动应用中,通常用到的术语是消息(message)而不是事件(event)。
信道(channel)
消息系统中分类事件的一种方式。其代表了事件生产者希望事件发送到的目的地。例如,在一个聊天室应用中,某个信道可能会是 /chatapplication/chatrooms/asdrt678,该信道标识了一个事件生产者可以发送消息的特定的聊天室,图形化的组件应该要订阅该信道,目的是显示最新到达的消息。
某些消息系统提供了两种类型的信道:队列(queue)和主题(topic)。
1. 队列(queue)——当某条消息被发送到队列中时,只有一个事件消费者拿到并处理该条消息,其他消费者不会看到它。队列可被持久化,以保证交付。最好的队列例子是邮递请求,某个web应用在用户注册时发布一条消息到队列 /myapp/mail/user-registration中,可能有多个邮件应用订阅了这一队列,如果没有的话,消息也不会丢失。
2. 主题(topic)——当某条消息发送到某个主题上时,每个订阅者都可以接收到它,主题通常是没有持久化的。一个例子是监控软件的一个主题/event/system/cpu/usage,生产者定期往其中发送CPU的使用情况;另一方面,这一主题可能会没有或是有多个订阅者,这取决于他们的兴趣所在。
发布/订阅(publish/subscribe)
事件驱动的解决方案实现了发布/订阅模式。事件生产者在处理软件中发布事件,事件消费者通过订阅来接收它们。事件消费者订阅的方式依赖于软件。在消息应用中,它们订阅信道(比如说,还可以有选择地把过滤规则应用在事件类型上)。使用诸如Esper一类的CEP,可通过类SQL的请求来定义你所感兴趣的事件,完成订阅操作。
为什么使用事件驱动的解决方案
在一个传统的通信方案中,如果系统A需要来自系统B的信息,就会发送一个请求给B。系统B会处理该请求,系统A则会停在那里等待响应。在处理完成时,响应会送回给系统A。在这一同步的通信模式中,资源的消耗是低效的,因为在等待响应时浪费掉了处理时间。
在异步模式中,系统A会从系统B中订阅它响应的信息。然后系统A可以选择性地给系统B发送通知,并立刻返回,系统A可以继续处理其他事情,这一步骤是可选的。通常情况下,在事件驱动的应用中,你不需要请求其他系统发送事件,因为你不知道它们是谁。当系统B发布响应时,系统A会立刻接收到。
事件驱动架构的一个优点是其允许更好的伸缩性。可伸缩性是系统在满足其目标的同时适应需求、容量或是强度变化的能力。通过消除暂停时间,事件驱动的架构通常有着更好的表现,以及有更高的处理效率。
另一个优点表现在应用的开发和维护方面。使用事件驱动的解决方案,应用的每个组件都可以是完全独立和解耦的。
事件驱动的解决方案允许有更好的反应时间,因为通信有着更低的延迟。
把事件驱动的解决方案应用在web上
web框架过去依赖于传统的请求-响应模式,这导致了页面的刷新。随着Ajax、反向Ajax以及诸如CometD和Atmosphere一类的功能强大的框架的出现,现在把事件驱动架构的概念应用到web上来获取解耦、可伸缩性和反应性的好处已经不是什么难事了。
客户端
事件驱动架构可应用在GUI开发的客户端。与创建一个传统的web页面不同,你可以把一个单独的web页面当作容器使用。每个组件(页面的每个组成部分)都可以是独立的,你可以在web上放一个Java Swing GUI,就像包含了小工具(gadget)的Google页面那样。
你需要一个事件总线(event bus),例如,你需要开发一个JavaScript事件总线,其允许每个页面组件从信道订阅或是在信道中发布。事件也可以是异步的,在两个或是多个事件到达后才触发行为。事件总线可以用于页面中的局部事件,但你也可以通过使用CometD或是Socket.IO来以插件的方式支持远端事件。
服务器端
在服务器端,你需要设置一个反向Ajax框架来支持事件驱动的框架。在本系列前几部分的考察中,发现只有CometD有着事件驱动的方法。对于其他框架来说,你需要增加自定义的支持,这不是什么大问题。你还可以加入第三方的消息系统,比如说JMS(例如Apache ActiveMQ)或是像Esper那样的CEP。一个更简单的解决方案是Redis,其支持基本的发布/订阅。
这一文章系列谈论的是事件驱动的web和反向Ajax,因此我们重点关注客户端,不会去设置一个复杂的消息系统。
事件驱动web的例子
本文将要创建的例子是一个聊天室web应用,该应用使用一个用户面板来列出连接的用户。你的用户名是加粗显示的,活动用户(20秒钟后还处活跃状态的那些)是绿色显示的,20秒钟后处于非活动状态的那些是橙色显示的。如果有用户连接或是断开连接,列表就会刷新。
出于安全目的,web.xml文件中配置了两分钟的会话超时,非活动状态两分钟后,就会弹出一个窗口,你会被重定向到登录页面。
只要你不再处于会话中或是还未连接,就会被重定向到登录页面。登录页面要求输入用户名并会查看是否可让你登录到聊天室中。
一旦登录成功,你就可以在聊天室中给所有用户发送消息。一个控制台也会显示出来,记录所有收到的事件。
该web应用是基于事件的,有了上述的信息,你可以很容易地定义几个事件:
1. 用户连接
2. 用户断开连接
3. 会话过期
4. 接收到聊天消息
5. 如果没有登录的话,安全过滤器阻拦请求
6. 用户变成非活动的
7. 用户变成活动的
8. 所有其他与UI协调相关的事件
某些事件只局部于web应用,由局部总线来识别,如清单1所示:
清单1. 总线设置
[javascript] view plaincopy
- bus = {
- local: new EventBus({
- name: ‘EventBus Local‘
- }),
- remote: EventBus.cometd({
- name: ‘EventBus Remote‘,
- logLevel: ‘warn‘,
- url: document.location.href.substring(0,
- document.location.href.length -
- document.location.pathname.length) + ‘/async‘,
- onConnect: function() {
- bus.local.topic(‘/event/bus/remote/connected‘).publish();
- },
- onDisconnect: function() {
- bus.local.topic(‘/event/bus/remote/disconnected‘).publish();
- }
- })
- };
其他事件是远端的,这意味着它们需要一个反向Ajax系统,比如说CometD来在所有客户端中发布它们。图1展示了该示例应用。
图1. 示例应用
你可以下载这一示例应用,许多类都是安全通道类,或是会话和用户管理通道类。本文给出了代码最重要的部分,不过建议你下载并运行该应用例子来更加深入地了解它的运作方式。
该web应用有不同的组件构成:聊天室、用户列表和控制台。每个都很独立,可以拿掉而不会影响到其他部分。
为了以局部的和远端的方式来设置这一事件驱动的系统,该例子使用了Ovea的EvenBus系统。其提供了一个局部的事件总线,一个活动远端事件的ComeD桥接,以及一种协调事件的方式(在几个事件完成之后触发行为)。当然,你可以选择使用另一个不同的系统。该例子使用了JavaScript来进行设置,如清单1所示。
一旦总线就位了之后,应用和组件就是基于事件的了。在本例子中,设置的是IDLE检测系统,如清单2所示。
清单2. IDLE检测系统
[javascript] view plaincopy
- bus.local.topic(‘/event/dom/loaded‘).subscribe(function() {
- $.idleTimer(20000);
- $(document).bind(‘idle.idleTimer‘, function() {
- bus.local.topic(‘/event/idle‘).publish(‘inactive‘);
- });
- $(document).bind(‘active.idleTimer‘, function() {
- bus.local.topic(‘/event/idle‘).publish(‘active‘);
- });
- })
有了清单2中的代码,IDLE系统就会在检测到活动时发送事件。这一代码可用在任何需要IDLE系统的应用中。在该例子中,你需要在用户活动的远端事件中转化一下该代码。其也可用JavaScript来实现,如清单3所示。
清单3. 用户活动管理
[javascript] view plaincopy
- bus.local.topic(‘/event/idle‘).subscribe(function(status) {
- bus.remote.topic(‘/event/user/status/changed‘).publish({
- status: status == ‘active‘ ? ‘online‘ : ‘away‘
- });
- });
- bus.remote.topic(‘/event/user/status/changed‘).subscribe(function(evt) {
- if(evt.user != me.name) {
- $(‘#users li‘).filter(function() {
- return evt.user == $(this).data(‘user‘).name;
- }).removeClass(‘online‘)
- .removeClass(‘away‘)
- .addClass(evt.status);
- }
- });
首个订阅接收来自IDLE系统的事件,然后把用户状况发送给服务器端。其他的订阅接收来自服务器端的用户状况事件。因此,只要用户的状况发生改变,用户列表中的用户的颜色就会变成绿色或是橙色。
当用户连接或是断开连接时,就会发送一个事件,如清单4所示:
清单4. 用户列表管理
[javascript] view plaincopy
- bus.remote.topic(‘/event/user/connected‘).subscribe(function(user) {
- $(‘#users ul‘).append(row(user));
- });
- bus.remote.topic(‘/event/user/disconnected‘).subscribe(function(evt) {
- $(‘#users li‘).filter(function() {
- return evt.user == $(this).data(‘user‘).name;
- }).remove();
- });
应用的代码简单、解耦且是独立的,通过重用Ovea的许多技术,你可以快速地创建事件驱动的web应用。不过,因为可以使用其他的系统来代替它,因此这并不是必需的。该例子只花了一天的开发时间,其中一半的代码都是管道代码,包括:
1. Maven:构建工程
2. 安全功能(登录、退出、会话超时)
3. 使用了Jersey的REST服务
结束语
该文章系列展示了如何构建响应式的以及是可伸缩的应用,这些应用能够提供很好的用户体验。事件驱动的web应用还是一个相当新的概念,某些WEB框架在内部用到了这些概念。不过本文展示的是不需要web框架来构建的这样的应用,对于要分离Java开发者和web设计者之间的职责来说,使用好的、专业的库就已是绰绰有余的了。关于如何把事件驱动的开发变成你的日常工作的一部分,我希望你已经有了一个较为深入的理解。它很容易让人沉迷于其中,一旦经过尝试,你就不想再回退到传统的框架上了。
代码下载