分类: 系统运维
2006-09-20 15:34:50
developerWorks 中国 > Java technology | Web architecture > 用 continuation 开发复杂的 Web 应用程序简化 MVC 在 Web 上应用的编程范式 |
级别: 初级 Abhijit Belapurkar, 高级技术架构师, Infosys Technologies Limited 2004 年 12 月 21 日 如 果您曾经开发过稍微复杂一些 Web 应用程序,那么您就会知道,Web 浏览器允许用户在应用程序中通过任意路径导航这一事实增加了开发的复杂性。不论用户导航到哪里,作为开发人员的您,都有责任跟踪可能发生的交互,并确保您 的应用程序工作正常。虽然传统的 MVC 技术确实允许您处理这些情况,但是还有其他一些选项有助于解决应用程序复杂性。作为开发人员和 developerWorks 的积极贡献者的 Abhijit Belapurkar,将向您介绍一个基于 continuation 的备选方案,来减少您在 Web 应用程序开发上投入的精力。 由于 HTTP 天生的无状态性质,Web 技术遇到了这样一个问题:在两个连续的用户交互之间会遗忘状态信息。一个交互性的 Web 应用程序由一组脚本构成,每个交互都包含两个脚本,一个脚本向浏览器递交页面(然后结束),用户及时完成并提交表单,然后另一个(可能是不同的)脚本处理 提交的表单。所以,应用程序逻辑分布在多个脚本之间。 由于浏览器还允许用户在交互中回溯,或者克隆一个正在处理 中的交互过程,之后并行执行这两个交互过程,所以问题变得更加复杂。因为有这些可能,用户可以在任意时间在应用程序中能找到多条导航路径,所以您必须编写 代码才能保证每个输出都成功。Web 开发框架(例如 Spring 和 Struts)允许您处理多条导航路径,但是它们做到这点的代价是:进一步提高了整体上已经很复杂的代码库(code base)的复杂性。 在 本文中,我将介绍一个基于 continuation 的备选方案,该方案可以简化复杂 Web 应用程序的开发。我将从介绍 continuation 开始,讨论基于 continuation 的技术如何能够成为传统的 MVC 编程风格的有力武器。然后,我将转到一个简单的示例:一个企业应用程序,用它演示使用 continuation 在简化开发和使应用程序代码更容易理解方面的优势。因为使用 continuation 主要的不足之一是 Java 平台上缺少对它的支持,所以我采用 Apache Cocoon 框架来演示用 JavaScript 实现的示例程序,以及一个纯 Java 语言的实现。最后,我用对 continuation 优势与不足的进行概述,以此结束本文。 请选择本文顶部或底部的 Code 图标下载示例应用程序的源代码。请参阅 参考资料,下载 Apache Cocoon 框架,运行示例需要这个框架。 传统上, continuation(继续) 被定义为一个函数,它代表 "计算剩余的部分" 或者 "接下来要做的事"。换句话说,把中间结果(由前面的运算生成)发送给 continuation,会产生整体运算的最终结果。 例如,请看下面这个很基本的 Java 方法,它返回传给它的整数的平方:
这个方法返回一个值,但是没有明确指定返回值的位置。而使用得当的 continuation 会明确指定返回的位置。 这样,假设我修改了以上方法(以及系统中的每个方法),在方法中包含了一个代表 continuation 的额外参数。通常,该参数应该是跟在方法中所有其他参数后面的最后一个参数。在调用函数时,它像以前那样执行内部逻辑,区别仅在于 返回的 输出值,函数会把该值传递给 continuation,要求继续进行计算,从而利用输出的值 继续。这样,上面的方法就会重写,如清单 2 所示:
这种编程风格(不允许函数返回值)叫做
继续传递风格(Continuation Passing Style),或 CPS。函数
现在,为了添加一点花样,我要引入另一个函数
换种方式说,所谓 continuation 就是保存下来的程序在指定时间点上的执行状态快照。有可能恢复这个状态,并从这一点起重新开始程序的执行,就像堆栈追踪,所有的本地变量以及程序的计数器都能重新找回自己原来的值。请参阅 参考资料,学习更多有关 continuation 的内容。现在我要把重点放在向您演示 continuation 在减少复杂 Web 应用程序上投入的编程精力方面能做些什么。在我们进入这个话题之前,请让我先花点时间进一步解释我要解决的问题。
模 型-视图-控制器(Model-View-Controller,MVC)是广泛采用的交互式应用程序(包括 Web 程序)的开发模式。这个众所周知的模型把交互式应用程序组织成三个单独的模块:一个针对应用程序模型,代表数据和业务逻辑;第二个针对视图,提供数据表示 和用户输入;第三个针对控制器,负责分派请求和控制流。
那 么控制器管理的这个 "流(flow)" 是什么呢?从页面加载、等候填写的表单返回角度来讲,典型的 Web 应用程序由定义良好的与用户进行交互的序列组成。在这种情况下,Web 应用程序就像是一个事件驱动的状态机(state machine)。这个事件模型就是典型的 MVC 架构通过控制器实现的模型。 例如,假设用户向服务器请求某 个页面,页面中包含要填充的表单。用户花了些时间思考,填充答案,然后提交表单。当这个事件到达服务器(控制器模块)时,根据当前状态、用户提交的数据和 业务逻辑,把应用程序移动到下一个逻辑状态。这种状态转换的结果,正如用户所看到的那样,是按顺序排列的或早期页面(和错误信息)的下一页的显示。 当 状态机在从开始状态到结束状态的途中推进时,就重复这个循环,在某一点上,Web 应用程序被认为是实现了特定用例要求的功能。状态图控制着从 "开始" 状态到 "结束" 状态的各种可能的数据流,它既可以由控制器模块(通常是 servlet)显式实现的,也可以像在某些 Web 开发框架中那样,被外部化为配置文件中的元数据。 不论框架是如何实现的,与状态机的基本思想总是一致。在开发基于这个模型的 Web 应用程序时,就会出现大量问题,如下所述:
一般来说,模型 2 Web 开发框架提供了定制技术,可以调节上述的一个或多个问题。但是,没有一个技术像基于 continuation 的方案那样直观、容易开发,基于 continuation 的方案提供了解决所有这些问题的一揽子方案。 编 程用户界面的事件驱动风格可以追溯到开始使用客户机-服务器架构的时候。它基于中央事件处理器以及在处理器上注册的大量事件句柄。每个句柄注册自己感兴趣 的事件,当特定事件发生的时候,中央事件处理器会根据通知注册了事件的相关句柄。用户的交互状态在中央模块中维护,根据内部持有的当前状态,中央模块把引 入的事件分派给注册的句柄。 大多数基于 Web 的互交是事件驱动编程的特例,在这里,界面显示被委托给 Web 浏览器,而不是由运行在用户工作站上的胖客户端可执行程序管理。虽然典型的胖客户端不允许用户驱动的功能(例如后向导航和克隆),但是 Web 浏览器支持甚至鼓励这类功能。当然,资源丰富的程序员已经找到了定制浏览器界面的方法(使用脚本代码),禁止这类操作,但是这样就形成了对不同浏览器的依 赖性。 虽然 MVC 的事件驱动风格的编程有许多优势,但是它还造成业务功能分布到多个模块中,从而使它变得非常复杂,难以开发、理解、维护复杂程度合理的 Web 应用程序。虽然开发了许多 Web 开发框架(例如 Struts、Spring、以及 JavaServer Faces)来隐藏大多数 MVC 风格的界面背后的复杂结构,但是有一些开发人员已经开始认识到这样的事实:其他编程模型值得深入研究。
基
于 continuation 的 Web 应用程序巧妙地避开了上面提到的与 Web 应用程序开发有关的问题。比起基于 MVC 的 Web
应用程序,基于 continuation 的应用程序是作为一个程序编写的。每当程序需要从用户得到输入的时候,就会把包含相应表单的 Web
页面发送回用户的浏览器,并生成代表应用程序逻辑 剩余部分 的 continuation,并把它放在一边。(我很快就向您解释把 continuation "放在一边" 的选项)。因为重要的事是在接收到用户的响应时能够重新开始应用程序逻辑的剩余部分,所以还要为服务器生成了一个惟一的
当必要的响应从用户到达,服务器(为部署的 Web 应用程序提供 continuation 的基础设施)在响应中检索 continuation 的
在继续执行 continuation 时,启动执行的代码可能要求用户输入更多数据,所以需要进一步交互。处理方法是创建第二个 continuation,把它保存在 continuation 存储库,并把嵌入了适当 continuation
从
外部来看,无法区分基于 continuation 的方法和 MVC 架构。客户仍然可以自由地返回前面已经提交的 Web
页面,修改必要的数据,然后通过浏览器再次提交表单。区别在于内部,在于为了让导航正确工作需要耗费的代码数量。基于 continuation
的方法不需要额外费力处理正确导航,因为每个提交的表单,都会有一个 continuation 假设用户正在查看页面。按照所谓的 continuation 树 的术语来说,这个页面用某个 "continuation 节点" 进行了 " 标记" 。如果用户单击 Back 按钮返回到前面已经提交的页面,那么 continuation 树中的标识器就会向上移动一级,并把指针设置到这个节点的父结点。每次用户单击 Back 时,continuation 树中的移动就会发生一次。现在假设用户停在某个页面上,重新输入数据,新输入的数据可能与以前在这个页面中输入的数据相同,也可能不同,然后用户重新提交 表单。这使 continuation 树中的标识器向下移动一级,指向子节点。但是,因为应用程序逻辑根据新提交的数据可能要显示不同的页面,所以现在的子节点实际可能是上一次移动中标识器指 向的子节点的兄弟节点。按照同样的逻辑继续执行,在树中,后退的路径可能会遇到不同的一套 continuation,也可能遇到相同的,具体情况取决于用户提交的数据。
虽
然有可能把 continuation 作为一个更简单的 MVC
框架替代品来实现,但是这种编程风格确实提供了一些独一无二的优势,特别是当它控制应用程序行为的时候。例如,支持 continuation
的框架通常允许使某个 continuation 无效。使某个 continuation 无效会导致无法返回与它对应的页面(例如,单击浏览器的
Back 按钮),并在重新提交表单之前修改关联的表单数据。(在系统内部,服务器删除了与指定 continuation 与 MVC 的实现不同,对于为了处理克隆而造成的代码混乱,基于 continuation 的技术提供了一个解决这个混乱的迂回解决方案。在基于 continuation 的技术中,用户可以在原来的窗口或克隆的窗口中输入不同的数据,把二者并行提交。然后在两个线程中(基本上,是分配给处理两个请求的服务器线程)用提交的 两套数据继续执行 continuation。对于不是基于 continuation 的应用程序来说,这是经常发生的,而这个输出更合适一些:要么禁用这类特性,要么使用一个事务覆盖另一个事务。禁用这个特性通常不是一个好的选择,因为用 户有时会用浏览器的克隆特性,对先后选择的两套数据进行对照分析。 还值得注意的是:基于 continuation 的方法消除了用户状态的概念。使用 continuation,用户在同一时间可以有多个状态,每个状态对应浏览器窗口中的一个克隆页面。
要管理 Web 应用程序的 continuation,重要的是要维护一个
continuation 存储库。一个方法是使用一个包含由服务器维护的全局惟一 continuation
把 continuation
到 现在为止,我们已经谈得够多了。示范 continuation 的最好方法就是让您实际看到它们的作用。在下面几节中,我会用示例应用程序演示使用 continuation 开发 Web 应用程序是多么简单。要运行示例应用程序,则需要从 Apache 下载 Cocoon 框架,因为 Java 平台自身并不支持 continuation。请参阅 参考资料,下载 Cocoon,并学习其他支持 continuation 的 Web 开发框架。
我 要用一个简单的应用程序,以便您能够很容易使用 continuation 进行 Web 应用程序开发。从导航的角度来看,这个购物应用程序的界面相当简单。在访问应用程序的第一个页面时,要求用户输入想要购买的产品的价格和数量。在输入这些 信息,并选择 Next 之后,用户被带到下一屏幕,要求输入用户的分类代码,通过分类代码可以决定用户在该用户的采购金额上能否得到折扣。(请注意,在这个过于简化的示例中,我 们假设用户提供的都是真实信息。)在这页上,要求用户选择送货方式是送货上门还是自取。如果选择了送货上门,那么界面返回第三个屏幕,这个屏幕中的用户必 须选择送货的类型:标准送货或快运,各自的成本不同。如果用户选择自取,或者完成了送货选择,这里显示最后一个屏幕。这个屏幕中有许多信息,其中有本次购 买的总额,这个总额等于购买总额减去分类折扣和运费。 这是一个简单的应用程序,但是对于学习 continuation 的内容,它是一个很好的基础。在我开始编码之前,我要花一分钟针对那些不了解、不喜欢 Apache Cocoon 框架的人对这个框架进行介绍。
Apache Cocoon 是一个 Web 开发框架,它允许您用 XSL 转换动态地发布 XML 内容。Cocoon 对不同转换的支持意味着您可以很容易地用多种格式表示内容。Cocoon 用处理管道描述处理请求以及生成对应的响应的时候执行的步骤顺序。每个管道均描述了一种获取输入的方式,接着一系列在数据上执行的处理步骤,以及最后生成 输出的机制。 为了形成管道而加入的每个组件都在叫做 站点地图 的结构中定义和组织。您可以为一个 Web 应用程序定义多个管道,并指定根据请求/环境参数,调用不同的管道处理不同的请求。 Cocoon 提供的组件可以分成许多类型:
管道要有用处,必须清楚地包含至少一个生成器或读取器,以及一个序列化器。处理步骤的数量取决于应用程序的业务逻辑。 上 面描述的 Cocoon 架构对应着 MVC 的模型 1 架构,在这个架构中缺少一个负责分派客户层请求并选择视图的中央控制器。但是,Cocoon 也有一个分支是用于模型 2 架构的。在这个例子里,除了正常的管道入口之外,站点地图还必须包含一个入口来指定控制器。像在其他模型 2 架构中一样,控制器负责导引与应用程序模型交互的业务逻辑的方向。在这个例子中,仍然用管道的概念来处理视图,但是管道由控制器驱动。 Cocoon 支持的第一个控制器引擎基于 Mozilla 的 Rhino JavaScript 版本,因为这个版本以第一级对象的形式提供了对 continuation 的支持。正如您将在以下示例中看到的,在控制器中使用 Cocoon 意味着您必须把整个应用程序编写成一个 JavaScript 程序,并把它注册成 Cocoon 应用程序指定的站点地图的流控制器(flow controller)。 要理解这些内容,读代码要比讲概念容易得多。我要做的第一件事是设置购物应用程序的站点地图。然后我们进一步查看用 JavaScript 如何实现应用程序逻辑。最后,我会查看应用程序的某些页面底层的 XML 文件,演示一些重要的概念。
示例购物应用程序的站点地图如清单 3 所示:
XML 文件中的第一个块(
下一个代码块则实际声明了应用程序要使用的管道。它定义了以下三个管道:
请注意,所有组件一般都要在站点地图中声明。我不需要声明清单 3 中的应用程序组件,因为我把示例应用程序作为 Cocoon 示例的一部分运行,其中有一个顶级站点地图,已经替我声明了这些组件(例如
我们的下一步是把应用程序逻辑写入脚本控制文件,在这个例子中,这个文件是 pos.js 文件。我前面提到过,这个文件包含一个叫做
在这个示例中,您会注意到,我使用了一个叫做
前一个函数有两个参数,一个是要发送回客户机的页面的站点地图 URI,另一个是
我在清单 4 的应用程序逻辑中使用的后一个函数接受的两个参数与前一个函数的一样,但是有一点区别。在页面生成并发回客户机之后,
也可以向
有了上面的解释,理解应用程序逻辑就应当很容易了。脚本首先要求把一个由
这个文件中要注意的要点是表单上的
继续执行 continuation 实际上把控制返回了脚本中紧跟在
为了更好地理解这些名称/值组合对的工作方式,我们来看
这个页面被装配好,把用户在前一页中输入的 rate 和 quantity 值显示给用户。您会注意到,这个 XML 文件中的占位符是
应用流的剩余部分,也可以用类似的风格理解。需要注意的一件有趣的事是,只有在用户选择 “S”为采购的产品选择送货上门时,
现 在我来到了用 JavaScript 开发的基于 continuation 的示例应用程序的末尾。还可以用另一个在纯 Java 语言上工作的 Cocoon 流解释器开发相同的应用程序,这样您就可以把整个应用程序逻辑编写成一个单独的 Java 程序。我将立即向您演示 Java 解释器的工作方式,但是首先我想讨论一下支持和反对用 Java 语言编写程序的争论。 关于用 JavaScript 代替 Java 语言最常见的争论是 Java 语言是更有名、使用更广的语言,有丰富的 IDE 支持,有丰富的设计模式等等。而支持 JavaScript 的一方认为,它是动态类型化的,使得进行快速原型设计(编写/更新-部署-测试周期更快)成为可能。作为语言,众所周知有大量的 Java 开发人员在客户机浏览器端使用它,而且非常容易掌握。JavaScript 是面向对象语言,而 Rhino 的实现则与 Java 平台有非常好的集成。可能访问、重用应用程序中已经存在的 Java 类或对象。所以,即使核心流是用 JavaScript 实现的,也有可能用 Java 语言实现实际的业务逻辑(因为可以在适当的位置在 JavaScript 流内访问 Java 类)。 简而言之,在两个选项中,没有任何一项比另外一项有非常明显的优势,具体采用哪种语言进行开发则基于 continuation 的应用程序,完全取决于您的个人偏好。而幸运的是,Cocoon 让您可以选择其中任何一个选项。
Cocoon 更新的发行版提供了对 Flowscript 的纯 Java 解释器的支持。清单 7 显示了用于纯 Java 解释器的源代码:
如清单 7 所示,Cocoon 提供了一个叫做
在清单 8 中,您可以看到基于 Java 的应用程序的站点地图。您可能注意到,它看起来与前一个站点地图非常相似。惟一的区别是流语言(flow language)是用
在 JavaScript 和 Java 实现之间的另一个小区别是:在与应用程序的 HTML 页面对应的 XML 模板中访问当前 continuation
正 如我在前面几节介绍的,continuation 实际上提供了把会话状态添加到 Web 应用程序的一种方法。使用 continuation 的优势是:可以很容易地处理异常的导航模式;可以很容易地用调试工具在应用程序中运行到某一点上,不必在分散的代码库的多个位置设置断点;理解和沟通程序 的结构变得非常容易,理解和沟通整个应用程序中可能的 Web 导航路径也变得非常容易。 用 continuation 进行 Web 开发最大的问题是,目前常用的开发 Web 应用程序的语言、框架和环境,支持 continuation 的不多。continuation 和 CPS 自身的概念看起来很神秘、不直观。第二个大问题是,应当在哪以及如何存储 continuation。我们可以把它们存储在客户端,但是由于前面提到过的问题(cookie 会在克隆的浏览器窗口的所有实例之间共享),所以可行的选择是把整个 continuation 以序列化的形式保存在隐藏的表单字段中。我们也可以把它们保存在服务器端,我在示例程序中就是这么做的,但是如果这么做,就不得不注意像垃圾搜集、集群结 点间复制这样的问题。最后,基于 continuation 的 Web 应用程序的效率(performance-wise)的情况目前还不十分明朗。
设 计和开发复杂的、交互的、基于 Web 的应用程序本身就相当复杂,而浏览器允许应用程序通过多条奇怪的导航路径使这些变得更加困难。Continuation 提供了一个很好的机制,可以把这类 Web 应用程序开发成单一的、容易理解、容易调试的线性程序。在文中,我对 continuation 背后的理论进行了基本介绍,并实际演示了如何利用 Apache Cocoon 中对 continuation 的支持开发复杂的 Web 应用程序。请参阅 参考资料,学习更多有关 continuation 的内容。
|