逃离 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 还可以以字符串形式返回输出。将一个输出方法的结果作为参数传递给另一个方法常常是很方便的,例如:
out.printA(URL_ELSEFORIF, out.IMG("/img/elseforif.gif", 88, 31));
然后是 MadnessWriter 类,它增加了这个 Web 站点需要的高级输出特性:页眉、页脚和菜单等常见元素,即这个站点特有的所有重复内容。这是一个轻量级、非线程安全的对象,抽象 servlet 基类 MadnessServlet 使用一个工厂方法为各请求实例化此对象。
这个基类负责处理核心 servlet 控制逻辑,使具体子类可以将注意力放在它们特有的任务上。在设置一些标准的 HTTP 头并执行一些页面级安全检查之后,它将 MadnessWriter 实例传递给受保护的 doBoth() 方法:
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 文件中可以找到更多信息。
表单和它的处理器
现在该看看代码了。在锦标赛的第一轮开始之前,用户进入 Picks 页面(见图 2),选择他们喜欢的球队。在此之后,他们可以通过只读输出的形式查看自己和其他玩家的选择情况。
图 2. Picks 页面
在生成这个页面时,Picks servlet 做的第一件事情是从业务层获取它的用户对象(在这个系统中,是 Player),并执行一项安全检查:
PlayerManager playerMan = PlayerManager.GetInstance();
Player player = playerMan.select(session.getAttribute(P_PLAYER_ID), true);
boolean readOnly = GetCutoffDateIsPassed()