文章详情

  • 游戏榜单
  • 软件榜单
关闭导航
热搜榜
热门下载
热门标签
php爱好者> php文档>用 continuation 开发复杂的 Web 应用程序

用 continuation 开发复杂的 Web 应用程序

时间:2006-09-20  来源:pascal4123


developerWorks 中国  >  Java technology | Web architecture  >

用 continuation 开发复杂的 Web 应用程序

简化 MVC 在 Web 上应用的编程范式

文档选项
</noscript>

将此页作为电子邮件发送

样例代码

<noscript>

未显示需要 JavaScript 的文档选项


最新推荐

Java 应用开发源动力 - 下载免费软件,快速启动开发


级别: 初级

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(继续) 被定义为一个函数,它代表 "计算剩余的部分" 或者 "接下来要做的事"。换句话说,把中间结果(由前面的运算生成)发送给 continuation,会产生整体运算的最终结果。

例如,请看下面这个很基本的 Java 方法,它返回传给它的整数的平方:


清单 1. 计算整数输入平方值的方法

public static int computeSquare(int x)
{
return (x*x);
}

这个方法返回一个值,但是没有明确指定返回值的位置。而使用得当的 continuation 会明确指定返回的位置。

这样,假设我修改了以上方法(以及系统中的每个方法),在方法中包含了一个代表 continuation 的额外参数。通常,该参数应该是跟在方法中所有其他参数后面的最后一个参数。在调用函数时,它像以前那样执行内部逻辑,区别仅在于 返回的 输出值,函数会把该值传递给 continuation,要求继续进行计算,从而利用输出的值 继续。这样,上面的方法就会重写,如清单 2 所示:


清单 2. 用 continuation 对象重写后的方法

public static int computeSquare(int x, Continuation c)
{
c.evaluate(x*x);
}

高阶函数

请注意在清单 2 的示例中,函数 computeSquare 实际上是一个高阶函数,因为它用函子(functor)对象( Continuation 对象, c)作为参数,并用 x*x 计算生成的中间结果向函子的 evaluate 方法发送消息。

这种编程风格(不允许函数返回值)叫做 继续传递风格(Continuation Passing Style),或 CPS。函数 f1 通过把应当返回的值传递给必须显式地传递给它的 continuation 函数,模拟了返回操作。同样,如果 f1 在中间需要调用第二个函数 f2,那么它必须向 f2 传递一个代表 " f1 剩余部分" 的 continuation(与剩余的参数一起)。一旦 f2 完成,那么代表 " f1 剩余部分"的 continuation 会用 f2 的计算结果继续进行计算。

现在,为了添加一点花样,我要引入另一个函数 f3,它在 f2 的尾部被调用。如果 f2 要照着 f1 的方式做,传递它的 continuation,那么它最后只会把 f1 的 continuation 传递给 f3。一旦已经执行了 f3,所要做的所有其余操作将是继续执行 f1 的 continuation。

换种方式说,所谓 continuation 就是保存下来的程序在指定时间点上的执行状态快照。有可能恢复这个状态,并从这一点起重新开始程序的执行,就像堆栈追踪,所有的本地变量以及程序的计数器都能重新找回自己原来的值。请参阅 参考资料,学习更多有关 continuation 的内容。现在我要把重点放在向您演示 continuation 在减少复杂 Web 应用程序上投入的编程精力方面能做些什么。在我们进入这个话题之前,请让我先花点时间进一步解释我要解决的问题。





回页首


常规 Web 开发中的问题

模 型-视图-控制器(Model-View-Controller,MVC)是广泛采用的交互式应用程序(包括 Web 程序)的开发模式。这个众所周知的模型把交互式应用程序组织成三个单独的模块:一个针对应用程序模型,代表数据和业务逻辑;第二个针对视图,提供数据表示 和用户输入;第三个针对控制器,负责分派请求和控制流。

模型 1 架构 vs. 模型 2 架构

严 格地说,只有 MVC 的模型 2 架构有控制器 servlet。模型 1 架构的控制是分散的,在这种架构中浏览器直接与 servlet 或 JSP 技术交互,后二者则访问并使用表示应用程序模型的 JavaBean 组件。在这种架构中,要显示的下一页面由用户在当前页单击的链接或者随请求一起发送的参数决定。今天多数基于 MVC 的 Web 应用程序都实现了模型 2 架构。

那 么控制器管理的这个 "流(flow)" 是什么呢?从页面加载、等候填写的表单返回角度来讲,典型的 Web 应用程序由定义良好的与用户进行交互的序列组成。在这种情况下,Web 应用程序就像是一个事件驱动的状态机(state machine)。这个事件模型就是典型的 MVC 架构通过控制器实现的模型。

例如,假设用户向服务器请求某 个页面,页面中包含要填充的表单。用户花了些时间思考,填充答案,然后提交表单。当这个事件到达服务器(控制器模块)时,根据当前状态、用户提交的数据和 业务逻辑,把应用程序移动到下一个逻辑状态。这种状态转换的结果,正如用户所看到的那样,是按顺序排列的或早期页面(和错误信息)的下一页的显示。

当 状态机在从开始状态到结束状态的途中推进时,就重复这个循环,在某一点上,Web 应用程序被认为是实现了特定用例要求的功能。状态图控制着从 "开始" 状态到 "结束" 状态的各种可能的数据流,它既可以由控制器模块(通常是 servlet)显式实现的,也可以像在某些 Web 开发框架中那样,被外部化为配置文件中的元数据。

不论框架是如何实现的,与状态机的基本思想总是一致。在开发基于这个模型的 Web 应用程序时,就会出现大量问题,如下所述:

  • 根据状态机的尺寸和维护客户当前状态所需要的数据量(因为一个 Web 应用程序在同一时间可能会有大量客户同时访问),应用程序逻辑可能会变得没有必要的杂乱或复杂。

  • 在 状态转换的序列中,客户单击浏览器的 Back 按钮的时间是不一定的,而且客户还可能克隆浏览器窗口,从而初始化并行的动作序列。任何一种情况都会导致已经传递的状态在原来的交互中发生多重(有时甚至 是并发)提交。结果,应用程序必须跟踪每一个事务,并对每个事务提供正确的响应。

  • 当 Web 应用程序要从跨多个页面的一系列表单中搜集用户信息时,也会出现类似的问题。如果后一个表单的生成取决于用户在前一个表单中提供的响应的组合,那么应用程序就必须跟踪在每个交互中输入的响应,并确保每一个交互都返回正确的页面。

一般来说,模型 2 Web 开发框架提供了定制技术,可以调节上述的一个或多个问题。但是,没有一个技术像基于 continuation 的方案那样直观、容易开发,基于 continuation 的方案提供了解决所有这些问题的一揽子方案。

关于事件驱动编程

编 程用户界面的事件驱动风格可以追溯到开始使用客户机-服务器架构的时候。它基于中央事件处理器以及在处理器上注册的大量事件句柄。每个句柄注册自己感兴趣 的事件,当特定事件发生的时候,中央事件处理器会根据通知注册了事件的相关句柄。用户的交互状态在中央模块中维护,根据内部持有的当前状态,中央模块把引 入的事件分派给注册的句柄。

大多数基于 Web 的互交是事件驱动编程的特例,在这里,界面显示被委托给 Web 浏览器,而不是由运行在用户工作站上的胖客户端可执行程序管理。虽然典型的胖客户端不允许用户驱动的功能(例如后向导航和克隆),但是 Web 浏览器支持甚至鼓励这类功能。当然,资源丰富的程序员已经找到了定制浏览器界面的方法(使用脚本代码),禁止这类操作,但是这样就形成了对不同浏览器的依 赖性。

虽然 MVC 的事件驱动风格的编程有许多优势,但是它还造成业务功能分布到多个模块中,从而使它变得非常复杂,难以开发、理解、维护复杂程度合理的 Web 应用程序。虽然开发了许多 Web 开发框架(例如 Struts、Spring、以及 JavaServer Faces)来隐藏大多数 MVC 风格的界面背后的复杂结构,但是有一些开发人员已经开始认识到这样的事实:其他编程模型值得深入研究。





回页首


continuation 的适用场合

基 于 continuation 的 Web 应用程序巧妙地避开了上面提到的与 Web 应用程序开发有关的问题。比起基于 MVC 的 Web 应用程序,基于 continuation 的应用程序是作为一个程序编写的。每当程序需要从用户得到输入的时候,就会把包含相应表单的 Web 页面发送回用户的浏览器,并生成代表应用程序逻辑 剩余部分 的 continuation,并把它放在一边。(我很快就向您解释把 continuation "放在一边" 的选项)。因为重要的事是在接收到用户的响应时能够重新开始应用程序逻辑的剩余部分,所以还要为服务器生成了一个惟一的 id,作为在 continuation 存储库 中搜索特定 continuation 的键。这个 id 用某种适当的方式和显示给用户的页面一起也向后发送,这样表单提交会使 id 在响应中发送回来。

还记得阻塞 I/O 吗?

本文中描述的方法在一个典型的读写程序中使用(或者与所用语言中的 read 和 write 等价)continuation。概括来说:对 read 的调用使应用程序阻塞,只有在用户提供了输入之后才继续。提供的输入被用来继续应用程序逻辑。任何中间输出则通过非阻塞的 write 调用显示给用户。下次程序要求用户输入信息的时候,它会进行另外一个 read 调用,并再次阻塞。这个序列持续进行,直到程序结束。

重要的是要注意:基于 continuation 的程序与使用阻塞读写的独立程序没什么不同。所有通常的编程结构都可以使用,包括基于 if 条件的分支、 for 和 while 循环,甚至更多。惟一的区别是:在 MVC 模型中,使用这些结构的应用程序代码有可能分散到多个不同模块和页面中,但是在基于 continuation 的程序中,整个逻辑包含在一个程序中。

当必要的响应从用户到达,服务器(为部署的 Web 应用程序提供 continuation 的基础设施)在响应中检索 continuation 的 id(与正常提交的数据一起),并用这个 id 在存储库中检索 continuation 。然后就继续执行 continuation(也就是说,应用程序逻辑在紧接着创建 continuation 的代码之后开始执行)。通常,前几行会提取用户提交的数据的剩余部分,这意味着在程序中服务器必须能够访问请求对象,而业务逻辑会用接收到的数据继续进 行。

在继续执行 continuation 时,启动执行的代码可能要求用户输入更多数据,所以需要进一步交互。处理方法是创建第二个 continuation,把它保存在 continuation 存储库,并把嵌入了适当 continuation id 的表单发送给用户。可以把生成的 外部(outer) 和 内部(inner) continuation 看作一个树结构,外部 continuation 作为父节点,内部 continuation 作为子节点。与此类似,如果业务逻辑有条件分支代码(根据 if 子句的结果有不同的动作),针对每个分支发送回浏览器的数据搜集页面可能是不同的,因此会针对每个分支形成对应的 continuation,每个 continuation 都是外部 continuation 的子节点,而彼此之间则是兄弟节点。按照这种方式,可以把完整的应用程序看成是与 continuation 树对应 —— continuation 的森林。





回页首


以用户为中心的导航

从 外部来看,无法区分基于 continuation 的方法和 MVC 架构。客户仍然可以自由地返回前面已经提交的 Web 页面,修改必要的数据,然后通过浏览器再次提交表单。区别在于内部,在于为了让导航正确工作需要耗费的代码数量。基于 continuation 的方法不需要额外费力处理正确导航,因为每个提交的表单,都会有一个 continuation id 与之关联。服务器只需要查看某个提交页面的正确 continuation,就可以要求它继续执行。

假设用户正在查看页面。按照所谓的 continuation 树 的术语来说,这个页面用某个 "continuation 节点" 进行了 " 标记" 。如果用户单击 Back 按钮返回到前面已经提交的页面,那么 continuation 树中的标识器就会向上移动一级,并把指针设置到这个节点的父结点。每次用户单击 Back 时,continuation 树中的移动就会发生一次。现在假设用户停在某个页面上,重新输入数据,新输入的数据可能与以前在这个页面中输入的数据相同,也可能不同,然后用户重新提交 表单。这使 continuation 树中的标识器向下移动一级,指向子节点。但是,因为应用程序逻辑根据新提交的数据可能要显示不同的页面,所以现在的子节点实际可能是上一次移动中标识器指 向的子节点的兄弟节点。按照同样的逻辑继续执行,在树中,后退的路径可能会遇到不同的一套 continuation,也可能遇到相同的,具体情况取决于用户提交的数据。





回页首


规则由您定!

虽 然有可能把 continuation 作为一个更简单的 MVC 框架替代品来实现,但是这种编程风格确实提供了一些独一无二的优势,特别是当它控制应用程序行为的时候。例如,支持 continuation 的框架通常允许使某个 continuation 无效。使某个 continuation 无效会导致无法返回与它对应的页面(例如,单击浏览器的 Back 按钮),并在重新提交表单之前修改关联的表单数据。(在系统内部,服务器删除了与指定 continuation id 对应的 continuation 对象)。结果,要继续执行操作却没有 continuation 可用,所以就会报告错误。这个错误可以用正在使用的开发框架的特定方式进行处理,例如,把浏览器重定向到出错页面。在某些情况下,排除这些动作,会对您的 应用程序的处理开支提供更高层次的控制。正如前面讨论过的,有可能在 MVC 框架中使用脚本代码来禁止某些导航模式。continuation 恰恰可以让您更容易地这样做。

与 MVC 的实现不同,对于为了处理克隆而造成的代码混乱,基于 continuation 的技术提供了一个解决这个混乱的迂回解决方案。在基于 continuation 的技术中,用户可以在原来的窗口或克隆的窗口中输入不同的数据,把二者并行提交。然后在两个线程中(基本上,是分配给处理两个请求的服务器线程)用提交的 两套数据继续执行 continuation。对于不是基于 continuation 的应用程序来说,这是经常发生的,而这个输出更合适一些:要么禁用这类特性,要么使用一个事务覆盖另一个事务。禁用这个特性通常不是一个好的选择,因为用 户有时会用浏览器的克隆特性,对先后选择的两套数据进行对照分析。

还值得注意的是:基于 continuation 的方法消除了用户状态的概念。使用 continuation,用户在同一时间可以有多个状态,每个状态对应浏览器窗口中的一个克隆页面。





回页首


continuation 存储库

要管理 Web 应用程序的 continuation,重要的是要维护一个 continuation 存储库。一个方法是使用一个包含由服务器维护的全局惟一 continuation id 的散列表,由该表提供 continuation 的基础设施。这没有消除用户从浏览器中复制和重用属于其他用户的 continuation id。 为了防止这种情况发生,也可以在用户的 HTPP 会话中维护 continuation。不管是哪种情况,如果在集群环境中运行,都有必要复制 continuation 存储库。前面提到过,使某个 continuation 无效会导致支持框架把整个对象从存储库中清除。在其他情况下,这类框架也支持指定 continuation 的生存周期,过期的 continuation 会自动从存储库清除。

把 continuation id 发送到用户浏览器,有两个选项:它可以作为一个隐藏字段内嵌在发送回的表单中;它也可以内嵌在表单要提交的 URL 中。不论怎么说,把 continuation id 封装在 cookie 中不是个好主意,因为特定的 cookie,对于一台机器上的浏览器窗口的所有克隆实例来说是公共的,而 continuation 则是特定于浏览器窗口的每一个实例。

到 现在为止,我们已经谈得够多了。示范 continuation 的最好方法就是让您实际看到它们的作用。在下面几节中,我会用示例应用程序演示使用 continuation 开发 Web 应用程序是多么简单。要运行示例应用程序,则需要从 Apache 下载 Cocoon 框架,因为 Java 平台自身并不支持 continuation。请参阅 参考资料,下载 Cocoon,并学习其他支持 continuation 的 Web 开发框架。





回页首


示例应用程序

我 要用一个简单的应用程序,以便您能够很容易使用 continuation 进行 Web 应用程序开发。从导航的角度来看,这个购物应用程序的界面相当简单。在访问应用程序的第一个页面时,要求用户输入想要购买的产品的价格和数量。在输入这些 信息,并选择 Next 之后,用户被带到下一屏幕,要求输入用户的分类代码,通过分类代码可以决定用户在该用户的采购金额上能否得到折扣。(请注意,在这个过于简化的示例中,我 们假设用户提供的都是真实信息。)在这页上,要求用户选择送货方式是送货上门还是自取。如果选择了送货上门,那么界面返回第三个屏幕,这个屏幕中的用户必 须选择送货的类型:标准送货或快运,各自的成本不同。如果用户选择自取,或者完成了送货选择,这里显示最后一个屏幕。这个屏幕中有许多信息,其中有本次购 买的总额,这个总额等于购买总额减去分类折扣和运费。

这是一个简单的应用程序,但是对于学习 continuation 的内容,它是一个很好的基础。在我开始编码之前,我要花一分钟针对那些不了解、不喜欢 Apache Cocoon 框架的人对这个框架进行介绍。

关于开发环境

示例应用程序的开发环境是在 Windows XP 工作站上的 Apache Tomcat Server(V5.0.18)上运行的 Cocoon V2.1.5.1。我假设您已经下载了 Cocoon 源代码,构建它,并把生成的 Cocoon Web 应用程序部署到 Tomcat。这样,本文中的源代码就仅仅针对示例应用程序了。如果您需要 Apache Cocoon 的更多下载和设置信息,请参阅 参考资料 。





回页首


Apache Cocoon 中的 Web continuation

Apache Cocoon 是一个 Web 开发框架,它允许您用 XSL 转换动态地发布 XML 内容。Cocoon 对不同转换的支持意味着您可以很容易地用多种格式表示内容。Cocoon 用处理管道描述处理请求以及生成对应的响应的时候执行的步骤顺序。每个管道均描述了一种获取输入的方式,接着一系列在数据上执行的处理步骤,以及最后生成 输出的机制。

为了形成管道而加入的每个组件都在叫做 站点地图 的结构中定义和组织。您可以为一个 Web 应用程序定义多个管道,并指定根据请求/环境参数,调用不同的管道处理不同的请求。

Cocoon 提供的组件可以分成许多类型:

  • 生成器和读取器是管道的输入组件。
  • 转换器和动作是处理组件。
  • 序列化器是输出组件。
  • 匹配器和选择器负责处理条件处理(conditional processing)。

管道要有用处,必须清楚地包含至少一个生成器或读取器,以及一个序列化器。处理步骤的数量取决于应用程序的业务逻辑。

Cocoon 和 MVC

上 面描述的 Cocoon 架构对应着 MVC 的模型 1 架构,在这个架构中缺少一个负责分派客户层请求并选择视图的中央控制器。但是,Cocoon 也有一个分支是用于模型 2 架构的。在这个例子里,除了正常的管道入口之外,站点地图还必须包含一个入口来指定控制器。像在其他模型 2 架构中一样,控制器负责导引与应用程序模型交互的业务逻辑的方向。在这个例子中,仍然用管道的概念来处理视图,但是管道由控制器驱动。

Cocoon 支持的第一个控制器引擎基于 Mozilla 的 Rhino JavaScript 版本,因为这个版本以第一级对象的形式提供了对 continuation 的支持。正如您将在以下示例中看到的,在控制器中使用 Cocoon 意味着您必须把整个应用程序编写成一个 JavaScript 程序,并把它注册成 Cocoon 应用程序指定的站点地图的流控制器(flow controller)。

要理解这些内容,读代码要比讲概念容易得多。我要做的第一件事是设置购物应用程序的站点地图。然后我们进一步查看用 JavaScript 如何实现应用程序逻辑。最后,我会查看应用程序的某些页面底层的 XML 文件,演示一些重要的概念。





回页首


应用程序站点地图

示例购物应用程序的站点地图如清单 3 所示:


清单 3. 示例应用程序的 Cocoon 站点地图

<?xml version="1.0"?>
<map:sitemap xmlns:map="http://apache.org/cocoon/sitemap/1.0">

<map:flow language="javascript">
<map:script src="pos.js"/>
</map:flow>

<map:pipelines>

<map:pipeline>
<map:match pattern="page/*">
<map:generate type="jx" src="screens/{1}.xml"/>
<map:transform src="context://samples/common/style/xsl/html/simple-page2html.xsl">
<map:parameter name="servletPath" value="{request:servletPath}"/>
<map:parameter name="sitemapURI" value="{request:sitemapURI}"/>
<map:parameter name="contextPath" value="{request:contextPath}"/>
<map:parameter name="file" value="/samples/flow/jxrate/screens/{1}.xml"/>
</map:transform>
<map:serialize/>
</map:match>
</map:pipeline>

<map:pipeline>
<map:match pattern="continue.*">
<map:call continuation="{1}"/>
</map:match>
<map:match pattern="">
<map:call function="sellItem"/>
</map:match>
</map:pipeline>

</map:pipelines>

</map:sitemap>

XML 文件中的第一个块( flow)向 Cocoon 声明:Flowscript 解释器必须用 JavaScript 作为目标语言,实现应用程序的流逻辑(flow logic)的源代码在文件 pos.js 中。

下一个代码块则实际声明了应用程序要使用的管道。它定义了以下三个管道:

  • 任何对资源的请求,如果资源的 URI 与正则表达式 page/* 匹配,就会激活站点地图中声明的第一个管道。要在这个管道中使用的生成器叫做 jx,它基本上是一个标准的生成器组件,由叫做 XTemplateGenerator 的 Cocoon 组件提供。 JXTemplateGenerator 是一个页面模板处理器,它允许您把 Cocoon Flowscript(在示例应用程序中是 pos.js 文件)传递过来的来自 JavaScript 中的数据插入管道。正则表达式中的 * 部分根据输入的请求提取特定的值,然后生成器可以通过 {1} 使用这个值。这样,对 URI page/A 的请求会使 JXTemplateGenerator 把 screens/A.xml 作为源代码。

  • 任何对资源的请求,如果资源的 URI 与正则表达式 continue.* 匹配,就会激活第二个管道。您可以看到,示例应用程序把 continuation id 作为表单要提交的 URL 的一部分,从而把 continuation id 传递给浏览器。像以前一样,实际的 continuation id 作为 {1} 使用。 call 元素实际上要求继续执行由 {1} 当前持有的值标识的 continuation。 call 元素还有第二个变体,您接下来会看到。

  • 与在站点地图中声明的最后一个管道匹配的请求,实际上是与前两个正则表达式不匹配的任何请求(例如,对应用程序发出的第一个请求发向 URI“ /”,然后激活这个管道)。在这里, call 元素要求站点地图调用 Flowscript(pos.js)中定义的顶级函数 sellItem。实际的效果是:对应用程序(URI " /")的第一个请求会开始 pos.js 的执行。

请注意,所有组件一般都要在站点地图中声明。我不需要声明清单 3 中的应用程序组件,因为我把示例应用程序作为 Cocoon 示例的一部分运行,其中有一个顶级站点地图,已经替我声明了这些组件(例如 jx)。





回页首


应用程序逻辑

我们的下一步是把应用程序逻辑写入脚本控制文件,在这个例子中,这个文件是 pos.js 文件。我前面提到过,这个文件包含一个叫做 sellItem 的函数,它启动应用流(application flow),如清单 4 所示:


清单 4. 用 JavaScript 实现的应用流

function sellItem()
{
var rate, qty, zone, amount, discount, total, discrate, savings, delOpt, delCost, Webcon;

var url = "page/getRateAmt";
cocoon.sendPageAndWait(url);
rate = parseFloat(cocoon.request.getParameter("rate"));
qty = parseInt(cocoon.request.getParameter("qty"));
amount = rate*qty;

url="page/getZone";
Webcon = cocoon.sendPageAndWait(url, {"rate":rate, "qty":qty});
zone = cocoon.request.getParameter("zone");

discount=0.02;

if (zone=="A")
{
if (qty >= 100)
{
discount=0.1;
}
}
else if (zone=="B")
{
if (qty >= 200)
{
discount=0.2;
}
}

discrate = 100*discount;
savings = discount*amount;

delCost=0.0;
delOpt = cocoon.request.getParameter("delOpt");
if (delOpt=="S")
{
url="page/getShipOpt";
cocoon.sendPageAndWait(url);
delCost = parseInt(cocoon.request.getParameter("delCost"));
}

total = amount + delCost - savings;
url="page/displayResult";
cocoon.sendPageAndWait(url, {"discrate":discrate, "total":total, "savings":savings,
"delCost":delCost, "amount":amount, "discount":discount, "zone":zone});
}


cocoon 对象及其函数

在这个示例中,您会注意到,我使用了一个叫做 cocoon 的对象,但是没有在任何地方事先声明它。不需要声明 cocoon 是因为它是 Cocoon 提供的供 Flowscripts 使用的一组默认系统对象的一部分。这组对象叫 Flow Object Model-FOM。

cocoon 对象可能是 FOM 集中最重要、最常使用的对象,也是 FOM 的入口点。它是代表当前站点地图的全局变量。它提供了两个重要的函数: sendPage 和 sendPageAndWait。两个函数都把控制传递给 Cocoon 站点地图,以便生成输出页面。

前一个函数有两个参数,一个是要发送回客户机的页面的站点地图 URI,另一个是 context 对象,其中包含的数据可以提取出来,并替代生成的页面中的占位符。

我在清单 4 的应用程序逻辑中使用的后一个函数接受的两个参数与前一个函数的一样,但是有一点区别。在页面生成并发回客户机之后, sendPageAndWait 函数生成并返回一个新的 continuation 对象(也是 FOM 的一部分)。这时,Cocoon 内部也会生成一个惟一的 continuation id,并把两者之间的映射保存在一个全局结构中。

也可以向 sendPageAndWait 函数传递一个函数,在完成对管道的处理但还没有创建 continuation 之前,传递的函数会自动执行。这是一个重要的特性,它适用的场合是:管道处理要求昂贵的或者争用程度高的资源,资源不适合与执行上下文的剩余部分绑定(因 为用户可能在思考一段时间之后要求这个 continuation 继续执行,而在思考的整个时间中持有这些资源没有太大意义)。但是,我们的代码中没有使用函数的这个版本。





回页首


理解应用程序逻辑

有了上面的解释,理解应用程序逻辑就应当很容易了。脚本首先要求把一个由 page/getRateAmt 标识的页面发送到用户。这与站点地图中的第一个管道匹配,并会使 JXTemplateGenerator 组件拾取文件 screens/getRateAmt.xml,把它传递给管道中的下一个组件。清单 5 显示了这个组件的 XML 文件 getRateAmt.xml:


清单 5. getRateAmt 的 XML 文件

<?xml version="1.0"?>
<page>
<title>Get Rate and Quantity of item to be purchased</title>
<content>
<form method="post" action="continue.#{$cocoon/continuation/id}">
<para>Enter Rate: <input type="text" name="rate"/></para>
<para>Enter Quantity: <input type="text" name="qty"/></para>
<input type="submit" name="submit" value="Next"/>
</form>
</content>
</page>

这个文件中要注意的要点是表单上的 action 字段使用 JXPath 表达式 #{$cocoon/continuation/id}。当在控制脚本中执行对应于其页面的 sendPageAndWait 调用时,这个表达式会被 Cocoon 生成的 continuation id 自动替换。这样会使表单提交给与正则表达式 continue.* 匹配的 URL,然后会使站点地图中对应(实际上是在站点地图中声明的第二个)管道被激活。(正如前面所看到的,这个管道只包含一条 call 语句,它将继续执行指定的 id 标识的 continuation。)





回页首


继续执行 continuation

继续执行 continuation 实际上把控制返回了脚本中紧跟在 sendPageAndWait 调用之后的脚本行。接下来的两行代码将从请求中提取 rate 和 qty 参数。当对 page/getZone 页面(映射到物理文件 screens/getZone.xml)进行 sendPageAndWait 调用时,会生成下一个 continuation。这个调用也接受由名称/值组合对构成的 map 参数,这是用户在前一个页面中提交的两个基本参数。

为了更好地理解这些名称/值组合对的工作方式,我们来看 /getZone 页面背后的 XML 文件,如清单 6 所示:


清单 6. getZone 页面背后的 XML 文件

<?xml version="1.0"?>
<page>
<title>Get Buyer Category and Delivery Option</title>
<content>
<form method="post" action="continue.#{$cocoon/continuation/id}">
<para>You are buying #{qty} items, at #{rate} apiece</para>
<para>Please specify your category:<br/>
<input type="radio" name="zone" value="A">A</input><br/>
<input type="radio" name="zone" value="B">B</input><br/>
<input type="radio" name="zone" value="C">C</input><br/>
</para>
<para>Will you be picking up the item yourself from our warehouse,
or would you like it shipped?<br/>
<input type="radio" name="delOpt" value="P">Pickup</input><br/>
<input type="radio" name="delOpt" value="S">Shipping</input><br/>
</para>
<input type="submit" name="submit" value="Next"/>
</form>
</content>
</page>

这个页面被装配好,把用户在前一页中输入的 rate 和 quantity 值显示给用户。您会注意到,这个 XML 文件中的占位符是 #{qty} 和 #{rate} —— 当合成 HTML 页面的时候,会在 map 中查找这些名称,并用 map 中对应的值代替。像前面一样,这个表单的 action 字段指向指定页面的 continuation id,它代表为 getRateAmt 页面生成的 continuation 的子 continuation。

应用流的剩余部分,也可以用类似的风格理解。需要注意的一件有趣的事是,只有在用户选择 “S”为采购的产品选择送货上门时, getShipOpt 才显示给用户。如果用户选择了这个选项,就会生成一个新的 continuation。如果用户选择了 "P" 自取,那么不必显示 getShipOpt 页面,因此也就不会生成 continuation。





回页首


JavaScript vs. Java 语言

现 在我来到了用 JavaScript 开发的基于 continuation 的示例应用程序的末尾。还可以用另一个在纯 Java 语言上工作的 Cocoon 流解释器开发相同的应用程序,这样您就可以把整个应用程序逻辑编写成一个单独的 Java 程序。我将立即向您演示 Java 解释器的工作方式,但是首先我想讨论一下支持和反对用 Java 语言编写程序的争论。

关于用 JavaScript 代替 Java 语言最常见的争论是 Java 语言是更有名、使用更广的语言,有丰富的 IDE 支持,有丰富的设计模式等等。而支持 JavaScript 的一方认为,它是动态类型化的,使得进行快速原型设计(编写/更新-部署-测试周期更快)成为可能。作为语言,众所周知有大量的 Java 开发人员在客户机浏览器端使用它,而且非常容易掌握。JavaScript 是面向对象语言,而 Rhino 的实现则与 Java 平台有非常好的集成。可能访问、重用应用程序中已经存在的 Java 类或对象。所以,即使核心流是用 JavaScript 实现的,也有可能用 Java 语言实现实际的业务逻辑(因为可以在适当的位置在 JavaScript 流内访问 Java 类)。

简而言之,在两个选项中,没有任何一项比另外一项有非常明显的优势,具体采用哪种语言进行开发则基于 continuation 的应用程序,完全取决于您的个人偏好。而幸运的是,Cocoon 让您可以选择其中任何一个选项。





回页首


用 Java 代码实现的 Continuation

Cocoon 更新的发行版提供了对 Flowscript 的纯 Java 解释器的支持。清单 7 显示了用于纯 Java 解释器的源代码:


清单 7. 用 Java 代码实现的应用流

import org.apache.cocoon.components.flow.java.AbstractContinuable;
import org.apache.cocoon.components.flow.java.VarMap;

public class PosFlow extends AbstractContinuable
{
public void doSellItem()
{
double rate, amount, total, savings;
double discount, discrate;
int qty, delCost;
String zone, delOpt;

String url = "page/getRateAmt";
sendPageAndWait(url);
rate = Float.parseFloat(getRequest().getParameter("rate"));
qty = Integer.parseInt(getRequest().getParameter("qty"));
amount = rate*qty;

url="page/getZone";
sendPageAndWait(url, new VarMap().add("rate",rate).add("qty",qty));
zone = getRequest().getParameter("zone");

discount=0.02;

if (zone.equals("A"))
{
if (qty >= 100)
{
discount=0.1;
}
}
else if (zone.equals("B"))
{
if (qty >= 200)
{
discount=0.2;
}
}

discrate = 100*discount;
savings = discount*amount;

delCost=0;
delOpt = getRequest().getParameter("delOpt");
if (delOpt.equals("S"))
{
url="page/getShipOpt";
sendPageAndWait(url);
delCost = Integer.parseInt(getRequest().getParameter("delCost"));
}

total = amount + delCost - savings;
url="page/displayResult";
sendPageAndWait(url, new VarMap()
.add("discrate",discrate)
.add("total",total)
.add("savings",savings)
.add("delCost",delCost)
.add("amount", amount)
.add("discount", discount)
.add("zone", zone));
}
}

如清单 7 所示,Cocoon 提供了一个叫做 AbstractContinuable 的抽象类,以及 sendPage 和 sendPageAndWait 函数的实现。 PosFlow 类扩展了这个抽象类,并在叫做 doSellItem 的方法中包含业务逻辑。这个方法的实现与清单 4 中 sellItem 的 JavaScript 实现一样。

Java 实现的站点地图

在清单 8 中,您可以看到基于 Java 的应用程序的站点地图。您可能注意到,它看起来与前一个站点地图非常相似。惟一的区别是流语言(flow language)是用 Java 指定的,而脚本源代码则是用 PosFlow 类指定的。


清单 8. 基于 Java 的实现的 Cocoon 站点地图

<?xml version="1.0"?>
<map:sitemap xmlns:map="http://apache.org/cocoon/sitemap/1.0">

<map:flow language="java">
<map:script src="PosFlow"/>
</map:flow>

<map:pipelines>

<map:pipeline>
<map:match pattern="page/*">
<map:generate type="jx" src="screens/{1}.xml"/>
<map:transform src="context://samples/common/style/xsl/html/simple-page2html.xsl">
<map:parameter name="servletPath" value="{request:servletPath}"/>
<map:parameter name="sitemapURI" value="{request:sitemapURI}"/>
<map:parameter name="contextPath" value="{request:contextPath}"/>
<map:parameter name="file" value="/samples/flow/jxrate/screens/{1}.xml"/>
<map:parameter name="remove" value="{0}"/>
</map:transform>
<map:serialize/>
</map:match>
</map:pipeline>

<map:pipeline>

<map:match pattern="continue.*">
<map:call continuation="{1}"/>
</map:match>

<map:match pattern="">
<map:call function="sellItem"/>
</map:match>

</map:pipeline>

</map:pipelines>

</map:sitemap>

在 JavaScript 和 Java 实现之间的另一个小区别是:在与应用程序的 HTML 页面对应的 XML 模板中访问当前 continuation id 的方式。通过研究清单 9 所示的基于 Java 实现的 getRateAmt 页面的 XML 模板,您可以看到这个区别。可以用 JXPath 表达式 #{$continuation/id} 访问 continuation id。


清单 9.getRateAmt 页面的 XML 文件


<?xml version="1.0"?>
<page>
<title>Get Rate and Quantity of item to be purchased</title>
<content>
<form method="post" action="continue.#{$continuation/id}">
<para>Enter Rate: <input type="text" name="rate"/></para>
<para>Enter Quantity: <input type="text" name="qty"/></para>
<input type="submit" name="submit" value="Next"/>
</form>
</content>
</page>





回页首


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 的内容。






回页首


下载

名字 大小 下载方法
j-contin-source.zip
 FTP
关于下载方法的信息 Get Adobe® Reader®




回页首


参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文。

  • 请单击这个页面顶部或底部的 Code 按钮下载本文的源代码。

  • Cocoon 主页 下载 Apache Cocoon,这是与 Cocoon 有关的信息的原始资料。

  • Struts Flow 是 Apache Cocoon 的控制流中用于 Struts 的那一部分,它支持用 Apache Struts 开发基于 continuation 的应用程序。

  • Malcolm Davis 撰写的“ Struts,MVC 的一种开放源码实现”(developerWorks,2001 年 2 月)对 MVC 和 Struts 框架进行了很好的概括。

  • Matthias Felleisen 和 Amr Sabry 在他们的论文“ continuation in Programming Practice: Introduction and Survey”中对 continuation 进行了介绍。

  • Daniel Friedman 撰写的论文“ Applications of continuation”(PDF 格式)是关于 continuation 主题 题经常被引用的一篇论文。

  • 不要错过 extensive collection of papers on continuation and Continuation Passing Style 这篇文章。

  • Paul Graunke 和 Shriram Krishnamurthy 合著的论文“ Advanced Control Flows for Flexible Graphical User Interfaces”描述了一种编程模式,能够用这种模式为 GUI 程序提供克隆窗口、制定应用程序书签页这样类似浏览器的能力(请参阅 所有相关文件 列表)。

  • Paul Graunke、Robert Bruce Findler、Shriram Krishnamurthy 和 Matthias Felleisen 在他们的论文“ Automatically Restructuring Programs for the Web”中讲述了一种技术,该技术可以把交互式 Web 应用程序转换成基于模式的 CGI 程序,从而利用 continuation 进行应用程序流处理(请参阅 所有相关文件 列表)。

  • Christian Queinnec 在下面两篇论文中描述了如何在程序中使用 continuation 才能与浏览器的导航行为对应,论文格式为 PDF 格式:“ The Influence of Browsers on Evaluators or, continuation to Program Web Servers”和“ Inverting back the inversion of control or continuation versus page-centric programming”。

  • 请下载 Rife,这是一个支持 continuation 的 Web 开发框架。

  • Seaside 是一个用 Smalltalk 开发复杂 Web 应用程序的框架。

  • 请参阅 developerWorks 的 Web 架构专区 ,其中有更多关于 Web 应用程序设计与开发的文章。

  • 在 developerWorks Java 技术专区 可以找到数百篇有关 Java 各个方面的技术文章。

  • 请访问 Developer Bookstore,获得技术书籍的完整列表,其中包括数百本 Java 相关主题的书籍。




回页首


关于作者


Abhijit Belapurkar 拥有印度理工学院(IIT)(印度,Delhi)计算机科学的学士学位。他已经在分布式应用程序架构领域和信息安全方面工作了近 10 年的时间,在使用 Java 平台构建 n 层应用程序方面也已经超过 5 年的工作经验。他是印度班加罗尔 Infosys 技术有限公司 J2EE 部门的高级技术架构师。

相关阅读 更多 +
排行榜 更多 +
辰域智控app

辰域智控app

系统工具 下载
网医联盟app

网医联盟app

运动健身 下载
汇丰汇选App

汇丰汇选App

金融理财 下载