一般的 Web 开发有时候很有趣,但是通常却是令人烦恼的。Java Web 开发人员要花费很长时间来提供无状态模型,但是产生的性能和部署的简单性使得这种努力是值得的。在本文中,我将讨论一种完全不同的 Web 开发方式,称为延续服务器(continuation server)。延续服务器提供了一个有状态的编程模型,同时又没有舍弃无状态所固有的可伸缩性,从而使 Web 应用程序开发更加容易。
Web 的出现
当 20 世纪 90 年代中期整个行业转向 Web 开发时,软件开发人员们欣喜若狂。与 “终端加主机” 模式相比,我们现在构建的客户机-服务器应用程序对用户更友好,但是也有几个问题困扰着我们:
- 性能往往很差。基于终端的开发中好的方面之一是,通信开销受到编程模型的约束。一旦这些约束消失了,我们就会缺乏构建简单分布式应用程序所需的带宽、工具或技能。
- 应用程序无法移植。大多数客户机-服务器开发环境要求使用专门的硬件和软件环境。
- 应用程序难以部署。必须单独地管理数千个客户机。
- 最大的开销是隐藏的。部署成了最重要的约束,因为进入生产阶段之后的开销大大增加了。
客户机-服务器计算仍然向前发展。公司常常是根据比较低的软件和硬件开销来做出财务决策,但是在进入生产阶段之后管理开销会大大增加。到了 1995 年,客户机-服务器模型需要进行重大的改进,而且这种改进确实出现了。
进入 Web 开发
Web 开发在 20 世纪 90 年代中期迅速发展起来。由于 Java 语言的出现,开发人员可以用新功能来构建分布式 Web 应用程序,同时解决了最严重的客户机-服务器问题。这些新功能包括:
受约束的通信。 请求/响应 Web 模型具有基于终端的开发的所有特征。用户在表单中进行输入、发出请求并获得响应。客户机和服务器之间的频繁通信受到了控制,性能得到了提高。
不共享任何东西的体系结构。 基于 servlet 的编程可以是无状态的。这意味着一个 servlet 可以为任何客户机服务,固定的 servlet 池可以为许多用户服务。不需要为每个用户保留一个 servlet。性能也因此得到了改进。
客户机上的共同标准。 通过在所有客户机上部署一个共同的浏览器,就可以构建一个界面并在所有客户机上产生一致的可视效果。支持多种浏览器客户机虽还有一定问题,但是不像支持本地用户界面库那样困难。许多可移植性问题消失了。
更好的部署模型。通过将浏览器作为共同的客户机,软件分发大大简化了。公司可以将应用程序部署在少数几个互联网服务器上并在整个企业中共享它们。网络体系结构常常可以在多台服务器之间共享请求,所以要增加处理容量,只需增加服务器。客户端部署也很容易,只需确保客户机上有正确的浏览器。管理因此大大简化了。
性能、可伸缩性、可管理性和可移植性都大大提高了,互联网革命因此进入了快车道。但是,您必须面对一些重要的问题。
不是乌托邦
无状态(stateless)这个简单的单词将沉重的负担从系统转移到了开发人员身上。其后果是不容质疑的:尽管由于不必为每个用户维护一个服务(或 servlet),而获得了很好的可伸缩性;但是,对状态进行管理的责任从编程语言转移到了开发人员身上。目前,可以将 Web 开发看成一系列无状态的请求,见图 1:
图 1. Web 应用程序由请求/响应对组成
采用这种模型,浏览器得到了严格控制。应用程序只对浏览器发出的请求进行响应。如果请求是小型的独立请求,那么这个模型是有效的;但不幸的是,对于驱动有多个应用组成部分的 Web 应用程序,它是不合适的。最常见的例子是在线购物。点击 Submit 按钮两次,常常会意外地重复订购同一商品。以后当您发现购物车中有重复的商品时会大感意外。
向用户提供有状态体验的方法通常是:将与一次交谈相关的所有数据放进一个会话中,并用 cookie、隐藏字段或 URL 变量在客户机上标识用户会话。对于每个新的请求,必须依次执行以下步骤:
- 从客户机获得用户的标识符
- 从会话中恢复交谈状态
- 处理用户的请求
- 构建响应
- 将交谈状态存储在会话中
- 将响应发送给用户
我对这个问题说得太轻描淡写了,因为使用无状态模型来模拟有状态应用程序可能造成更严重的问题。最严重的问题从 Web 开发刚出现时就存在了,就是如何处理 Back 按钮。
老问题的新答案
在 Web 开发中,可以利用有状态模型为用户提供无状态体验。您听到这种说法可能会感到震惊。实际上,在 Hackers and Painters(参见 参考资料)中,Paul Graham 就讨论了早在 1995 年在 ViaWeb 中使用的底层方法。这种方法使用一种称为延续(continuation) 的编程控制结构。
基本思想是:可以让编程框架在请求之前装载应用程序的状态,并在每个请求之后保存应用程序的状态。我首先介绍一下 Ruby 编程语言中的延续。
一个 Ruby 示例
如果希望执行代码,请安装 Ruby 并输入 irb。通过在 > 字符后面输入命令,定义一个称为 loop 的方法,见清单 1:
清单 1. 创建 loop 方法
irb(main):001:0> def loop(interrupt) irb(main):002:1> for i in 1..10 irb(main):003:2> puts "Value of i: #" irb(main):004:2> callcc {|c| return c} if i == interrupt irb(main):005:2> end irb(main):006:1> end => nil |
loop 方法接受一个称为 interrupt 的参数。它启动一个从 1 到 i 的 for 循环,打印 i 的值,然后做一些奇怪的事儿。神秘的 callcc 语句意味着用延续进行调用。可以把延续看成在某一时间点上 “冻结的” 程序状态。Ruby 调用花括号中的代码块,同时传递一个延续对象。花括号中的代码是一个闭包,它仅仅是传递给 callcc 的代码块。最终结果是,callcc 捕获执行的状态并将结果存储在 c 中。现在,可以调用这个方法并在循环的任意位置中断执行,这会捕获程序的状态。在以后,可以恢复状态。
现在,执行这个方法两次,见清单 2:
清单 2. 执行 loop 方法
irb(main):007:0> cont = loop 5 Value of i: 1 Value of i: 2 Value of i: 3 Value of i: 4 Value of i: 5 => #<Continuation:0x2b5a358> irb(main):008:0> cont.call Value of i: 6 Value of i: 7 Value of i: 8 Value of i: 9 Value of i: 10 => 1 10 irb(main):009:0> cont = loop 8 Value of i: 1 Value of i: 2 Value of i: 3 Value of i: 4 Value of i: 5 Value of i: 6 Value of i: 7 Value of i: 8 => #<Continuation:0x2b562f0> irb(main):010:0> cont.call Value of i: 9 Value of i: 10 |
每次执行调用时,延续会获得执行的状态。所以,使用延续的 Web 开发框架可以在处理每个请求之后捕获一个延续,并用一个标识符将它存储在会话中。然后,框架可以在处理每个新请求之前从会话中恢复延续,采用的方法与存储用户数据一样。
出于测试的目的,你可能希望将触发器的频率调的高一点,以便观察到HelloWorld的动作。毕竟,你不想一直等到凌晨2点才能确定调度的任务确实运行了。相反,你可能需要一个每隔10秒运行的触发器:
Trigger trigger = new SimpleTrigger("trigger1", "group1"); trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY); trigger.setRepeatInterval(10000L); // milliseconds毫秒 |
注意,这个触发器没有使用类cron的语法。Quartz有大量各类的选项和配置方法,可适用于任何任务调度的需要。
其它计时方式的配置
Quartz提供了多种调度任务的方式。CronTrigger可能是最复杂的一种,不过还有其它的选择。大多数触发器可以由Quartz提供的TriggerUtils类创建。以下是一些常见的触发器的例子。如谚语所言,条条大路通罗马!
每天凌晨2:22触发的触发器
// 方法一:使用makeDailyTrigger
Trigger trigger = TriggerUtils.makeDailyTrigger(2, 22); trigger.setName("trigger1"); trigger.setGroup("group1"); |
// 方法二:使用CronTrigger
Trigger trigger = new CronTrigger("trigger1", "group1"); trigger.setCronExpression("0 22 2 * * ?"); |
每5秒执行一次的触发器 /* |
按间隔时间运行任务
Trigger trigger = new SimpleTrigger("trigger1", "group1"); // 24 hours * 60(minutes per hour) * // 60(seconds per minute) * 1000(milliseconds per second) // 24小时 * 60(分钟每小时) * 60(秒每分钟)* 1000(毫秒每秒钟) trigger.setRepeatInterval(24L * 60L * 60L * 1000L); |
结论
在这个演示中,我们只接触了Quartz框架的一些初级功能。记住,Java 5 和J2EE 5也有自己的任务调度机制,但是它们不像Quartz那样灵活易用。Quartz是目前唯一的开源Java任务调度框架,它的确为开发者的锦囊中增加了很有用的内容。你可从Open Symphony下载Quartz,并得到一份很好的教程和使用说明。