科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道延续、Web 开发和 Java 编程

延续、Web 开发和 Java 编程

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

本文讨论延续(continuation),这是 Smalltalk 的 Seaside 这类框架背后的技术。延续服务器(continuation server)提供了一个有状态的编程模型,同时又没有舍弃无状态所固有的可伸缩性,从而使构建 Web 应用程序更加容易。

作者:Bruce Tate 来源:IT专家网 2008年4月3日

关键字: 开发 web 延续 java

  • 评论
  • 分享微博
  • 分享邮件
本文讨论延续(continuation),这是 Smalltalk 的 Seaside 这类框架背后的技术。延续服务器(continuation server)提供了一个有状态的编程模型,同时又没有舍弃无状态所固有的可伸缩性,从而使构建 Web 应用程序更加容易。

  一般的 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: #{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 开发框架可以在处理每个请求之后捕获一个延续,并用一个标识符将它存储在会话中。然后,框架可以在处理每个新请求之前从会话中恢复延续,采用的方法与存储用户数据一样。

优点和缺点

  延续服务器方式在许多方面都很出色 —— 有状态的编程模型和具有无状态性能的用户体验。这种方式的优点如下:

  •   它确保了请求之间的无状态。框架可以识别一个 URL 中的各个延续并将延续存储在会话中。
  •   它提供了一个有状态编程模型。框架可以在任何时候恢复任何延续。如果用户第二次提交一个表单,延续可以恢复以前某个时间点上的状态。
  •   可以根据业务规则让延续失效,这样就很容易防止两次提交表单。
  •   解决了 Back 按钮的问题。因为可以直接获得任意时间点上的执行状态,如果用户点击了 Back 按钮,框架只需恢复适当的延续。
  •   线程化变得容易了,因为不共享任何资源。没有资源冲突就意味着大多数线程化问题消失了。

  延续大大简化了 Web 开发模型。有了延续,就可以将 Web 应用程序看成具有一系列请求的应用程序,能够提供更多的用户信息。图 2 给出了修改后的应用程序流。在用户启动应用程序之后,Web 服务器处于控制之中。应用程序不再是应付以任意次序到达的任意请求,而是变成了与一个用户进行统一且直接的交谈。

  图 2. 延续支持更自然的应用程序流

  与许多高阶抽象相似,延续也有缺点。这些缺点将影响依赖于延续的整体方式。延续服务器必须解决下面这些常见问题:

  延续是昂贵的。 将数据放进会话中更容易了,这意味着会有更多的人将更多的数据放进会话中。框架设计者可以利用一种称为部分延续(partial continuations)的方法来降低开销,按照这种方法,框架只保存延续中应用程序特有的部分。

  URL 变得难看了。 大多数延续服务器会在 URL 后面附加一个难看的延续标识符。

  用户范例改变了。 如果大量使用延续服务器,用户体验就会改变。Back 按钮实际上会工作,这种体验会让某些用户感到迷惑。例如,如果在购物车中放了某些东西之后点击 Back 按钮,您会期望应用程序将这些东西从购物车中去掉吗?

  总的来说,我相信延续代表着一种重大的技术进步,在不久的将来您可能会看到流行的延续服务器实现。现在我们来看看其他语言中的某些实现,然后我展示一些具体的细节。

  其他语言中的实现

  有好几种语言都具有基于延续的方法。最常见的实现是用 Lisp、Ruby 和 Smalltalk 编写的。

  •   Seaside 是最流行的延续服务器,由 Avi Bryant 用 Smalltalk 实现。如果您希望掌握一个完全不同的极具生产效率的 Web 开发框架,那么就看看 Seaside 吧。这种体验会改变您进行 Java 编程的方式。
  •   用 Ruby 编写的 Iowa 也是由 Avi Bryant 创建的,灵感来自 Web Objects。它现在在 Ruby 中使用和维护,具有一种不同的支持模式。
  •   Wee 框架是另一种 Ruby 框架,它具有延续服务器的许多特征,但是没有使用本机语言延续。
  •   ViaWeb 利用 Lisp 提供了延续功能,早在 1995 年就提供了出色的生产效率。
  •   Continuity 是一种基于 Perl 的延续服务器。Perl 6 本身支持延续。

  还有一些没有在这里列出的延续服务器。目前,大多数延续服务器实现是用支持延续的语言编写的,而这些语言不是主流语言。但是,以后的情况可能会不一样。

  Java 语言中的延续服务器

  Java 开发人员正在注意到基于延续的方式对构建 Web 框架的好处,但是 Java 语言不支持本机延续。如果您的语言不支持延续,那么就只有几种办法了:模拟延续、将延续添加到现有的语言中或者选用另一种语言。不同的 Java 框架采用了不同的办法:

  •   模拟延续。 一些 Java 框架使用替代方法来捕获执行状态。Lakeshore 使用线程,Spring Web Flow 使用状态机(在本文后面还将进一步讨论 Web Flow)。
  •   选用另一种语言。 Java 虚拟机可以基于其他语言。Cocoon 2 通过 Rhino 采用这种方式,Rhino 是一种虚拟机,它支持 JavaScript,也支持 Java。
  •   实现延续。 Jetty 6 servlet 容器、RIFE 和 WebWork 采用这种方式。

模拟本机延续

  不一定非使用延续来捕获执行状态。暂停的线程和状态机都可以捕获执行状态。线程化方式简单地冻结并存储现有的线程。这种方式有一点儿受限制,因为它不提供无状态性能 —— 实际上是为每个用户使用一个单独的线程。

  但是,状态机(state machine)是不错的延续方式。状态机是一种定义了状态之间的转换的程序。到目前为止,具有延续服务器特征的最流行的 Java 框架是 Spring 的 Web Flow。Web Flow 采用状态机形式对用户界面页面之间的导航进行建模。Web Flow 可以将程序流建模成 Java 对象,但是常常使用 XML 来描述程序流。请考虑清单 3 中来自 Web Flow 教程(参见 参考资料)的程序流表示:

  清单 3. Web Flow 程序流表示

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE webflow PUBLIC "-//SPRING//DTD WEBFLOW//EN"
        "http://www.springframework.org/dtd/spring-webflow.dtd">

<webflow id="myFlow" start-state="displayForm">

    <view-state id="displayForm" view="form">
        <entry>
            <action bean="myFlowAction" method="setupForm"/>
        </entry>
        <transition on="submit" to="processSubmit">
            <action bean="myFlowAction" method="bindAndValidate"/>
        </transition>
    </view-state>

    <action-state id="processSubmit">
        <action bean="myFlowAction"/>
        <transition on="success" to="finish"/>
    </action-state>
       
    <end-state id="finish" view="success"/>

</webflow>

  这个流程有三个核心状态:displayForm、processSubmit 和 finish。这个程序流定义了使机器从一个状态转换到下一个状态的动作。例如,submit 将状态从 displayForm 转换到 processSubmit。Web Flow 在典型的模型-视图-控制器级别上操作。可以使用许多不同的视图技术来显示表单。

  当从一个状态转换到下一个状态时,框架自动捕获当前状态,包括所有实例变量。这样就可以获得与其他延续服务器一样的 Back 按钮支持和有状态编程模型。这种方式不使用本机延续,但是具有延续服务器的许多优点,还有其他一些优点:

  •   持久性。 尽管不能捕获整个调用堆栈,但是可以捕获当前执行状态,甚至可以持久存储当前状态。
  •   长期存活的程序流,比如工作流。 因为状态不依赖于给定的调用堆栈,所以这种方式对于长期工作流这样的东西更稳定。
  •   工具。 很容易为 XML 构建工具。

  其他语言

  Cocoon 使用 Rhino,这是一种插入到 JVM 中的 JavaScript 虚拟机。Cocoon 控制器使用一个修改过的 Rhino 版本来表达延续,所以 Cocoon 允许用 Java 或 JavaScript 来表达控制器逻辑。请考虑清单 4 中来自 Cocoon 教程的应用程序(参见 参考资料):

  清单 4. 使用 Cocoon

  try {
    if (operator == "plus")
      cocoon.sendPage("result.html", {result: a + b});
    else if (operator == "minus")
      cocoon.sendPage("result.html", {result: a - b});
    else if (operator == "multiply")
      cocoon.sendPage("result.html", {result: a * b});
    else if (operator == "divide")
      cocoon.sendPage("result.html", {result: a / b});
    else
      cocoon.sendPage("invalidOperator.html", {operator: operator});
  }
  catch (e) {
    cocoon.sendPage("error.html",
      {message: "Operation failed: " + e.toString()});
  }

  注意清单 4 中的 sendPage 方法。这个方法将一个页面发送给用户。这里没有支持 Back 按钮或者将用户数据保存到会话中所需的冗长代码 —— 这些都封装在 Cocoon 的框架中了。

  实现延续

  RIFE 框架实现它自己的延续,而 WebWork 框架使用 RIFE 的延续实现。Jetty 6 也包含一个用 Java 编写的延续实现。清单 5 给出了一个来自 RIFE 教程的例子,这个例子是一个猜数字游戏:

  清单 5. RIFE 示例

while (mGuess != answer) {
  print(template);
  pause();
  guesses++;

  if (answer < mGuess) {
    template.setBlock("indication", "lower");
  } else if (answer > mGuess).{
    template.setBlock("indication", "higher");
  }
}

  在这个例子中,pause() 方法捕获延续并将模板发送回用户供操作。RIFE 使延续变得很简单,一般的 Web 开发人员也能够使用延续。

  不远的将来

  您可以看到,延续是 Web 开发框架中一项真正的技术进步。利用这种方式,可以获得更高的生产效率。另外,因为采用直观的 Java 代码(而不是数百个互不相连的 servlet)来表达 Web 应用程序,应用程序更容易理解和维护。

  Web 开发方面新的发展使延续方式变得越来越重要。Ajax 应用程序可以异步地获得 Web 页面的一小部分并将结果编织进现有的页面中,而不是用传统的请求/响应模型来获得整个 Web 页面。但是 Ajax 应用程序可能会迫使应用程序长期维持与用户的连接,这样应用程序才能进行响应并使状态跟踪代码比较容易编写。这种做法破坏了无状态编程的优势,因为需要为每个连接的用户占用一定的资源。有了延续,就可以在延续中保存状态并根据需要恢复状态。

  在不远的将来,硬件的改进会使延续所增加的资源消耗变得无足轻重。如果不经过彻底的革新,Web 开发框架仍然会太复杂。Ajax 会使 Web 开发更加复杂。这些因素都会促使人们接受延续服务器。在两年内,大多数新的 Web 开发将会使用某种延续服务器或延续的模拟。

    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章