首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

纯 servlet:重新考虑视图(1)

纯 servlet:重新考虑视图(1)

设计 JSP 的目的是将 Web 开发人员的任务与设计动态页面 UI 的非开发人员的任务分离开来。遗憾的是,JSP 对于许多设计人员来说太复杂了,为解决各种动态内容问题添加的软件层让他们觉得非常棘手。(例如,国际化要求将文本存储在其他地方并通过键来引用。)所以对于大多数项目,Java 开发人员只好自己处理 JSP 代码,这常常会包含本属于设计人员的工作,使他们的精力消耗在标记库和其他东西上,无法集中于 Java 代码。
与正统的方式不同,可以使用简单的 helper 对象,根据常规 servlet 构建简洁优美的 Web 界面。本文讲解如何以标准的 Java 形式编写动态 Web 页面的视图输出。我将解释这种方法的好处,并用一个计分应用程序演示这种方法,这个程序管理一个 NCAA  奖金池。
HTML 是动态的这种纯 servlet 方法非常简单。它涉及一个 servlet 基类和一个定制的写出器对象,servlet 子类使用这个对象产生输出。代码很简洁,因为大多数 HTML 封装在 helper 对象的方法中,都可以按需重写。代码重用总是令人愉快,而且大多数 Web 站点的页面共享许多 HTML,所以重用应该是个重要的考虑因素。HTML 输出方法产生直观紧凑的 servlet 代码,因此可维护性很高,这使代码的维护成本差不多直接与代码规模成正比。通过将 JSP 界面重写成纯 servlet,可以将代码缩减三分之二。
例如,要根据用户权限输出一个链接,就需要下面这样冗长的构造代码:
1
2
3
4
5
<c:if test="${user.permission[ sessionScope.ConstantMap[ EDIT_WIDGET ] ] != 0}">
  <c:url var="editUrl" value="/EditWidget.jsp"/>
  <div class="navigation"><a href="<cut value="${editUrl}"/>">Edit
        this widget</a></div>
</c:if>




通过使用 Java 语法,代码就简洁多了:
1
2
if (user.getPermission(Constants.EDIT_WIDGET) != 0)
  out.printNavlinkDIV("/EditWidget.jsp", "Edit this widget");




另外,在同一个地方获取和输出业务对象,而不是通过请求对象传递它们,这也会节省大量代码。简洁是美。
使用 JSP 和其他视图技术可能是 Web 开发中最让人头疼的部分。JSP 页面不是 HTML 或 XML、Java 代码、JavaServer Pages Standard Tag Library(JSTL)代码或表达式语言(EL),而是这些东西的大杂烩。JSP 代码不但是奇怪的组合体,而且每个抽象层都给开发带来新的障碍。例如,对 JSP 页面进行调试简直就像探矿那样困难。您知道某个地方出了毛病,但是无法找到出问题的位置;神秘难懂的错误消息虽然指出了行号,但这个行号往往不是问题的真正所在。
JSP 技术不能扩展基类,所以代码重用只能通过 bean、include 文件和定制的标记库来进行。标记库太麻烦,不适合进行有效的重用。为您所做的每处 API 修改维护一个 XML 是非常麻烦的,而且 “标记设计就是语言设计”(参见  中 Noel Bergman 的文章)。结果是在本已分了很多层的接口上又加了一层。
HTML/XHTML 遵从性将 HTML 封装成 Java 方法可以在重用过程中保持一致性,帮助减少输入错误的数量。这进而帮助产生符合标准的 HTML 或 XHTML 输出。我按照 XHTML 1.0 Strict 实现了 March Madness 站点。完成全部功能之后,可能只需做五六处调整就能够通过 W3C Validation Service 的检验。

我们正面对着全新的 World Wide Web。无论 Ajax 能否引领 Web 开发的方向,Web 站点都会继续向着更加智能化的方向发展。另外,尽管 HTML 本身总是声明性的,但是产生它的代码却不一定如此。JSP 技术和其他模板化系统必然过分复杂,因为它们试图以声明式的方式表达本质上动态的输出。这正是开发人员无法容忍在 JSP 源代码中添加 scriptlet 的原因:我们试图表达的逻辑 具有各种各样的形式。
通过将 HTML 封装成 Java 代码,可以简洁地表达输出逻辑。if 语句和 for 循环可以采用大家熟悉的形式。页面元素可以重构成方法,这样就很容易理解和维护它们。(对较大的 JSP 页面进行维护是非常麻烦的,非常容易出现错误,尤其是在缺少良好的注释的情况下。)通过使用纯 servlet,可以尽可能增加代码重用,因为不需要为每个页面的构造编写新的类。
狂热的设计三月狂热每年三月,美国篮球迷都会陷入狂热之中,因为 National Collegiate Athletic Association(NCAA)中前 64 支男子篮球队会进行淘汰制的胜者全得的锦标赛。许多球迷喜欢随着比赛的进行记录他们的预测和比赛结果。还有许多球迷喜欢凑在一起建立非正式的奖金池,奖金将发给预测最准确的那些人。

为了演示纯 servlet 的概念,我为一个 NCAA March Madness 锦标赛奖金池构建了一个计分界面。(参见  和 )。用户可以从参加锦标赛的 64 支球队中选择他们认为最出色的 20 支球队,并给每个球队分配一个加权的分数。比赛开始之后,他们的选择就变成只读的;当比赛结束时,管理员输入获胜球队的名称。根据用户选择的球队,自动地计算用户的累积分数并显示分数的排名。
这个项目大约花费了我三周的业余时间,大部分时间花在样式和图像上(毕竟我不是画家)。除了一个 HTML 文件和其他静态资源之外,UI 层由 21 个 Java 类组成,根据 JavaNCSS 的度量标准,一共有 1,334 个 Java 语句(参见 )。
逃离 MVC这里演示的纯 servlet 设计在客户机和业务逻辑之间建立一个视图层。Model-View-Controller(MVC,或者说 Model 2)实际上不是万能的,而且支持它的 Web 框架往往比较难以处理。Spring MVC 和 JavaServer Faces(JSF)太过复杂,我可以断言,Struts 的麻烦程度不亚于此,每次调整控制逻辑时都必须调整臃肿复杂的配置文件。N. Alex Rupp(参见 )甚至将 MVC 称为反模式,一种 “看似聪明其实非常愚蠢的” Web 技术。
例如,开发人员常常误解 Struts 中 Action 模块的用途。业务逻辑常常被放在这里(如果不是都放在 JSP 中的话)。将视图和控制器实现为 servlet 可以促使业务逻辑放入恰当位置,因为 servlet 明确关注与浏览器的接口。
对于这个项目,我使用了几个来自我自己的 elseforif-servlet 库的类(参见 )。这 设计的关键,因为它为生成 HTML 提供了一个方便的接口。但是,本文的重点不是这个库,而是证明我的方法的优点。
图 1 是部分类图,其中的 elseforif-servlet 元素以绿色表示:
图 1. 部分类图树结构的顶部是一个包含 HTML 字符串常量的接口,它为 HTML 写出器对象和使用它们的 servlet 提供了方便。(在后面将看到它们的作用。)接下来是 HTMLWriter 和 HTMLFlexiWriter,它们实现一些基本的低级 HTML 方法,它们对于任何 Web 站点都是有用的。这两者之间的区别是,HTMLWriter 直接写到输出中,而 HTMLFlexiWriter 还可以以字符串形式返回输出。将一个输出方法的结果作为参数传递给另一个方法常常是很方便的,例如:
1
out.printA(URL_ELSEFORIF, out.IMG("/img/elseforif.gif", 88, 31));




然后是 MadnessWriter 类,它增加了这个 Web 站点需要的高级输出特性:页眉、页脚和菜单等常见元素,即这个站点特有的所有重复内容。这是一个轻量级、非线程安全的对象,抽象 servlet 基类 MadnessServlet 使用一个工厂方法为各请求实例化此对象。
这个基类负责处理核心 servlet 控制逻辑,使具体子类可以将注意力放在它们特有的任务上。在设置一些标准的 HTTP 头并执行一些页面级安全检查之后,它将 MadnessWriter 实例传递给受保护的 doBoth() 方法:
1
2
protected void doBoth(HttpServletRequest request, HttpServletResponse response,
      HttpSession session, MadnessWriter out) throws ServletException, IOException




MadnessServlet 还实现了 MadnessConstants,它使子类能够轻松地访问 HTMLConstants 中定义的静态值。所以,通过结合使用 MadnessWriter 对象和这些常量,servlet 实现了非常紧凑的 Java 风格的代码。
参数和检验不必借助于重量级系统,也可以有效地处理参数。elseforif-servlet 库包含一些 helper 类,servlet 可以直接使用它们按照 decorator 模式在 init() 方法中定义参数。init() 形成了某种签名,这使 servlet 具有良好的形态,使您可以一眼看出所需的参数。一个定制的 Map 封装了自变量和检验结果,可以根据需要在会话中传递并在 servlet 之间共享。

按照 MVC 的说法,servlet(这里的 UI 基本单元)构成了视图层和控制层。对于 HTTP 这样的无状态接口,这是有意义的。对视图的请求和对数据更新的请求采用同样的基本形式,这两者之间没有明确的区别。为了保持模块化,我在一个 servlet 类中实现表单页面,在另一个 servlet 类中实现它的处理器。但是,无论怎样对功能进行分隔,HTML 输出逻辑、servlet 参数的处理和页面流逻辑都自我封闭的同级别的对象。虽然 MVC 对它们进行抽象是出于好意,但是会导致功能混乱。
业务层的实现应该与视图层没有关联。关键是要有一个简单明了的业务接口,这样的话,UI 代码就可以只处理 UI 问题。(对于示例应用程序的业务层,我在 Apache Derby 上构建了一个相当粗糙的 CRUD 接口。)
运行应用程序这个 Web 应用程序是几乎完全自含的,但是可能需要修改 web.xml 描述符中的一些环境属性,然后才能将它部署到 webapps 目录中。至少需要指定创建嵌入式 Derby 实例和存储它的数据文件的位置。默认设置是 UNIX 路径 —— /var/derby/ —— 所以如果您运行 Linux,那么只需要创建这个目录(并允许 servlet 容器写这个目录)。用用户名 admin 和密码 password 登录这个站点。在下载包的 README 文件中可以找到更多信息。
返回列表