将视图组合在一起以便于构造 Web 应用程序
级别:中级
Wellie Chao(wchao@caraveltech.com)
总裁,Caravel Technologies
2002 年 8 月
“模型-视图-控制器(Model-View-Controller,MVC)”框架对于生成有组织的模块化应用程序(这些应用程序能够清晰地划分逻辑、样式和数据),是一种经证实的和方便的方法。在 Java 世界中,Struts 是最著名的也是最常被谈及的 MVC 的开放源码体现之一。致力于 Struts 的开发人员最近增强了该项目的核心功能,并改进了视图支持(合并了 Tiles 视图组件框架来加强对基于组件开发的支持),以便增加可重用性并增强一致性。本文中,Wellie Chao 说明了 Struts 和 Tiles 组合为什么是用来创建 Web 应用程序的最佳工具包,而且向您展示了如何开始使用它,这里主要讲述自 Struts 0.9 以来的更改。
我开始学习如何创建软件要追溯到 80 年代中期,最初两个月的时间都花在使用线性流及嵌入式显示、逻辑和数据编写单块代码。这在那时,好象是最常用的方法。随着经验的丰富,我学会了将代码封装到对象中,将数据与逻辑分离,重构公共代码并实践其它良好的编程公认原则。
Web 开发人员接受 MVC
眨眼到了 1994 年,当时主流的采用 Web 应用程序的开发才刚开始。由于 Web 的不成熟,只有较少的工具能帮助开发人员构建 Web 软件。结果,在特定解决方案中的应用程序混合了 HTML 代码与应用程序逻辑。很显然,UI 设计的更改和业务逻辑的更新在大型应用程序中既困难又昂贵,因为紧耦合的表示和逻辑将这两种元素搅和在一起,进而导致错误和缓慢的进展。而且,混合的代码要求部分开发人员具备 UI 设计知识,或者要求开发人员与图形设计人员之间有紧密的工作关系,这常常会造成时间上的浪费。
JSP 技术和标记的引入稍微改善了这种更改问题,因为能够将逻辑和显示分离。UI 设计人员能够对显示进行卓有成效的工作,同时开发人员能够专注于逻辑。然而,这种方法仍存在一些缺陷。尤其是某些操作(还有公共操作)的开发仍很困难。验证表单就是典型的例子。正如很多人所知,表单验证的过程类似于这样:
显示表单;等待用户填写然后提交数据。
检查各域值是否有效;如果有错误,则重新显示表单。
处理用户输入的数据,可能将其存储在一个数据库中。
在新页面上向用户显示处理的结果或下一步(可能是另一个表单)。
如果在这一过程中只使用 JSP 页面,那么在需要再次更改代码时,您会发现,按照可管理性这条思路,将控制从一个页面“路由”至另一个页面很难。您想把第 4 步和第 3 步置于同一个页面吗?如果使用多个单独的 JSP 页面,那么如何跟踪哪个页面链接至其它页面,以及在要更改一个页面的文件名或位置时该怎么做呢?而且,在第 2 步检测到某个域中的错误时,如何重新显示带有一条错误消息的原始表单,但还要保留用户已填入的值呢?Struts,一种开放源码“模型-视图-控制器”框架,通过帮助解决所有这些问题,从而使开发人员的工作更为轻松。
本文并不深入讨论 MVC 平台。有关这方面的信息,请参阅 Malcolm Davis 所写的标题为“Struts, an open-source MVC implementation”的 developerWorks 文章。您正在阅读的这篇文章讨论自 Malcolm 的文章发表以来对 Struts 所做的更改,包括 Tiles 库。至于代码的安装过程,本文仅涉及 Jakarta Tomcat 4.0(Catalina)最小安装所需的步骤。如果您没有使用 Tomcat,请查阅手册以了解您的应用程序服务器。
Struts 和 Tiles 的背景知识
Craig McClanahan 是 Apache Tomcat 项目的技术主管,他创立 Struts 项目以满足对这方面的渴望。它作为“模型-视图-控制器”框架首选的并经过正式认可的开放源码实现,已经越来越流行了。它以与交付产品一起分发的形式受到来自 Sun 和 IBM 的支持。因为 Craig 积极参与 Tomcat 和 Struts 的开发工作,所以 Struts 将继续与 JSP 和 Servlet 规范的参考实现高度兼容,进而与所有 J2EE 应用程序服务器高度兼容。
Malcolm Davis 的有关 Struts 的 developerWorks 文章涵盖了整个 Struts 0.9 的功能;为了简短起见,我将只讨论对 Struts 0.9 的更改以及他未涉及的 Struts 主题。目前的 Struts 发行版本是 1.0.2,但自 2002 年 3 月 19 日起有一个标记为 1.1-b1 的 beta 测试版可供使用。因为 beta 测试版表示编码工作已经完成,在这种情况下只进行错误修正,所以极有可能 1.1 的最终版本不久就将面市,而在 Struts 邮件列表上已经有这种呼声。因此,任何利用 Struts 的新项目很可能都将 1.1 代码作为基础,而这就是我将讨论的内容。
自版本 0.9 以来对 Struts 框架的有用添加包括经改进的表单验证功能、可以通过 XML 声明来指定表单元素和可以动态地定义 bean 特性。然而,最重要的添加可能是将 Tiles 模板库合并到 Struts 分发版中。
您是否曾希望用一种更简便的方法创建一组页面(或可能是整个应用程序),并且每个页面上的用户界面保持一致 ? 有相同的导航栏、页眉和页脚等等?在含有较多内容的页面内显示多个类 portlet 的矩形内容的方法又如何?在 Tiles 框架的帮助下,您可以完成这两项任务和其它更多任务。通过定义屏幕和一组可嵌入在 JSP 页面中的标记的核心 XML 文件来插入静态和动态内容,Tiles 框架允许您构建组件化的视图,并按您的希望来组装它们,从而有助于提高灵活性、可重用性、一致性和可维护性。
Struts 和 Tiles 之间交互良好,因为这两个项目的开发人员已经认识到这两者具有互补性,所以决定让这两者共同协作。开发人员可以指定 Tiles 页面定义作为 Struts 操作的目标视图(按照 Struts 的说法是一个 forward)。因为 Struts 和 Tiles 都遵循 JSP 标记库规范,所以可以在 JSP 页面中将 Struts 标记和 Tiles 标记相互混合在一起。
您可能渴望尝试 Tiles 框架,并确切地了解它可以做些什么。如果您希望在自己安装本文的示例前先了解这些示例的运行情况,可以看看它们在带有嵌入式 Tomcat 的 JBoss 服务器上是怎样运行的。
Struts 和 Tiles 是用于 Web 开发的辅助工具,所以您需要设置一个 Web 容器对它们进行实验;将 Tomcat 设置为您的容器,然后设置 Struts 和 Tiles 包,我会在下一节中循序渐进地讲述这一过程。这些指导信息还向您展示了如何安装本文的样本代码。一旦完成了这一切,您就准备好继续本文。示例 1 应用程序没有利用 Struts 和 Tiles;它演示了以页面为中心的方法。通过将它与示例 2 比较,您会看到 Struts 和 Tiles 将如何极大地提高您的 Web 开发的结构化程度和可管理性。最后,示例 3 演示了将功能添加到一个使用 Struts 和 Tiles 的、并且已经启动且正在运行的 Web 应用程序中是多么地简单。
安装 Struts 和 Tiles
在带有 J2SE 1.4 SDK、Ant 1.4.1、Tomcat 4.0.3 和 Struts 1.1-b1 的 Linux 机器上,下列指导信息已经经过了测试。如果因这些软件包的版本不同而遇到困难,您可能需要使用上面所指定的版本,以便开始了解 Struts 和 Tiles 的设置和开发。
如果您没有 J2SE 1.4 SDK(Java 2 Platfrom,标准版 1.4 软件开发工具箱),请从 http://java.sun.com/j2se/1.4/download.html 获取它,然后遵循该软件包中的指导信息来安装它。您需要 SDK 而不只是 JRE,但是您可以选择获取 Forte/SDK 组合。
确保您的环境变量 JAVA_HOME 设置为 J2SE 1.4 SDK 的安装目录。
如果没有 Ant 1.4,请从 http://jakarta.apache.org/builds/jakarta-ant/release/v1.4.1/bin/ 获取二进制分发版,然后将它解压缩。对于 Ant 1.4 以及安装指导信息中所要求的所有其它包,请确保获取的都是二进制分发版,而不是源代码;否则,您将不得不在使用它们之前编译这些包。同样,将 Ant 的 bin 目录添加到您的路径中。
请从 http://jakarta.apache.org/builds/jakarta-tomcat-4.0/release/v4.0.3/bin/ 获取 Tomcat 4.0.3 二进制分发版,然后将它解压缩。文件名应该与 jakarta-tomcat-4.0.3-LE-jdk14.tar.gz 相似。为了便于稍后在这些指导信息中引用此名称,让我们暂且任意地将安装目录(至并且包括 Tomcat 目录)的路径称为 TOMCAT_HOME。在 UNIX 系统上,该路径类似于 /home/wchao/jakarta-tomcat-4.0.3-LE-jdk14,在 Windows 系统上类似于 c:jakarta-tomcat-4.0.3-LE-jdk14。
请从 http://jakarta.apache.org/builds/jakarta-struts/release/v1.1-b1/ 获取 Struts 1.1-b1 beta 测试发行版,然后将它解压缩(不在 TOMCAT_HOME 中)。我们称该目录为 STRUTS_INSTALL。在 UNIX 系统上,该目录类似于 /home/wchao/jakarta-struts-1.1-b1,在 Windows 系统上类似于 c:jakarta-struts-1.1-b1。
请下载 struts-tiles-examples.tgz,然后将它解压缩。它将创建三个目录:ex1、ex2 和 ex3。我们分别称这些目录为 EX1_INSTALL、EX2_INSTALL 和 EX3_INSTALL。
转至 TOMCAT_HOME/bin 目录。
通过输入 ./startup.sh(如果在运行 UNIX)或 ./startup.bat(如果在运行 Windows)来启动 Tomcat 服务器。
将 Web 浏览器指向 http://localhost:8080/examples 来验证 Tomcat 是否已启动并正确运行。缺省情况下,Tomcat 附带了 Examples 应用程序。如果 Examples 不工作,则 Tomcat 发生故障;请参阅 Tomcat 文档来解决问题。
Hello, World:首次尝试
要研究我们第一个示例,请遵循下列步骤:
转至 EX1_INSTALL 目录。
编辑 build.xml 文件,为 tomcat.install.dir 填写适当值。尽管该值可以是绝对路径,也可以是相对路径;但如果您不了解 Ant 是如何工作的,或许最好使用绝对路径。
输入 ant deploy。这将把第一个示例应用程序构建到 WAR 文件中,以备部署,然后将它部署至 Tomcat。如果得到一个指出无法找到 Ant 的错误,请参阅“安装 Struts 和 Tiles”一节中的第 3 步,并确保您的路径环境变量包含 Ant。
将 Web 浏览器指向 http://localhost:8080/ex1。您应该会看到“Hello, World”页面。
示例 1 Web 应用程序非常简单,它演示了常见的 Web 应用程序功能。几乎所有应用程序(也包括这个最简单的应用程序)都要求所有页面具有一致的用户界面。通常,这意味着所有页面都有公共的徽标、顶部栏、上部或左侧导航栏、主体和页脚。在示例 1 中,我有意对每一页面中的公共项进行硬编码,以便说明这一点。Web 应用程序开发的新手一般会通过将现有代码复制粘贴到新文档中来添加新的功能页面。很容易预见这种方法难以应付将来的变化。随着每一次增加新内容,更改诸如菜单、徽标等公共页面元素的过程花的时间会更长,更容易出错。很明显,复制粘贴方法对于任何具有大量页面的应用程序是一个糟糕的模型。
敏锐的读者会认识到 JSP 技术提供了包括来自其它 servlet 和页面中的内容的功能。我们为什么不可以仅仅使用 <jsp:include/> 标记来合并公共元素呢?这肯定会使那些元素更易于更改。如果您需要更改菜单,只要更改包含菜单的文件。所有其它页面只需使用 <jsp:include/> 标记就可以得到菜单中的内容,这样这些页面可以自动获得对菜单的更改。但是,当需要更改实际布局或需要重新组织文件和目录时,这种方法有不足之处。当决定更改以页面为中心模型下的布局时,必须对每个单个页面进行更改,因为即使对公共元素的访问已做了集中化处理,但仍然是由每个页面中的 HTML 代码来描述布局本身(有哪些是元素及它们的位置)。同样地,当决定更改包含了某个公共元素内容的文件的文件名或位置时,必须逐个更改使用该元素的文件。什么原因呢?因为每个文件根据固定的物理文件名,而不是逻辑对象名来查找每个公共元素。因此,必须更新每个对物理文件名的引用。Tiles 视图组件可以解决这些问题。
如果更进一步地研究 index.jsp 和 form1.jsp(这两个 JSP 文件构成该应用程序),会发现另一个缺点:错误处理相当笨拙。错误处理代码是在 form1.jsp 中,其中我必须重复显示代码,并添加代码以插入用户在前一表单屏幕(index.jsp)中输入的值。如果用户概要信息域曾更改过,或者如果输入表单的显示曾更改过,就必须更新这两个地方中的代码。我可以将 form1.jsp 的错误处理部分与 index.jsp 中的初始表单显示结合在一起,但在初始表单装入时,我仍将需要做额外的工作以把域值设置成空字符串,并且我仍将需要有一个物理文件名来表示用户概要信息的最终静态显示,这意味着发生更改时,该应用程序结构仍是很脆弱的。Struts 表单自动化可以解决这种笨拙的表单处理缺陷。
下表 1 总结了由示例 1 应用程序演示的基于 JSP、以页面为中心的 Web 应用程序模型的优缺点。
表 1. 基于 JSP 方法的概述
优点 说明
入门容易。 只要设置 Tomcat,然后就可以开始编写代码。不需用核心文件来保持同步,也不需要冗长的库初始配置。由每个单独的 JSP 页面或 servlet 来指定连接。
缺点 说明
在应用程序的不同部分中重用表示很困难。 一定程度上,可以使用 <jsp:include/> 标记来解决一部分重用,但它们在管理更改方面不能很好地工作。
公共输入和数据处理任务枯燥且重复。 错误处理是普通 JSP 页面的常见问题。而且,必须手工填入表单值及手工检索请求对象中的那些值,这是一件既耗时又枯燥的工作。
业务逻辑和表示紧密耦合在一起,从而将两者的代码混合在一起。 如果研究一下 index.jsp 和 form1.jsp,就会发现 Java 代码是与 HTML 代码混在一起的。代码很难看,易于出错,而且要做到使 Java 编码或用户界面开发分离开非常困难。最终不得不同时了解 HTML 和 Java 编码对页面的作用。
没有对应用程序流或行为的集中描述。 除非逐个查看页面,否则根本无法了解应用程序的整体印象以及操作流是怎样运作的。随着项目越来越大,容易造成错误、失败和令人迷惑的地方。
Hello, World:经改进的新的应用程序
现在,让我们研究刚才看到的 Web 应用程序示例中的 Struts 和 Tiles 版本。请执行下列步骤:
转至 EX2_INSTALL 目录。
编辑 build.xml 文件,为 struts.install.dir 和 tomcat.install.dir 填写适当的值。
输入 ant deploy。这将把第二个示例应用程序构建到 WAR 文件中以备部署,然后将它部署至 Tomcat。如果看到关于无法复制文件的错误,请检查第 2 步以确保正确设置了 struts.install.dir 和 tomcat.install.dir。
将 Web 浏览器指向http://localhost:8080/ex2。您应该会看到“Hello, World”页面。
目录结构和文件的说明
在 EX2_INSTALL/src/web 目录下有不少文件,乍看起来可能会把人搞糊涂。以下是给大家的一些指导。
profileInput.jsp 和 profileOutput.jsp 页面是页面主体 panel3 的内容;它们驱动这一特殊应用程序。在 tiles-components 下有页面的各种组件,而在 tiles-layouts 下是有关布局的 HTML 代码。我喜欢这样的组织安排,因为这使得对不同的用户角色有不同的布局,并在一个中心位置中保存所有视图组件。Tiles 可以让您以您希望的任何方式安排文件,只要您在 tiles-defs.xml 文件中指定了如何组织事物,所以使用那些最适合于您工作的事物。
如果您已看过 EX2_INSTALL 目录,您很可能会说:“这里要做些什么呢?有好多文件。”与大多数强调更有序和结构化程度更高的技术一样,对于 Struts 和 Tiles,在一开始需要在管理文件上花些工夫。对于只有少许页面的小项目,这一额外开销可能微不足道。然而,随着项目变大,Struts 和 Tiles 方法逐渐会显示其优越性。让我们一点点地体会吧!这里我不想讨论 EX2_INSTALL/src/WEB-INF/web.xml;尽管这个文件实质上与其示例 1 中相对应的文件不同,而且大多数行都是样板,但理解这些设置对于着手开发并不太重要。
在 EX2_INSTALL/src/WEB-INF/struts-config.xml 中,自先前有关 Struts 的文章以来重要的更改有在 <form-beans/> 节中 DynaActionForm 的使用及在 <action-mappings/> 节中 tile 作为目标的使用。在 Struts 的以前版本中,您必须为每个所使用的表单 bean 定义一个 Java 类。仅当不同的 HTML 表单共享域时,才可以在这些表单之间共享表单 bean。总之,每个表单 bean 需要有一个 Java 类是一个非常麻烦的要求。现在,您可以在 struts-config.xml 文件中指定表单 bean 的特性,而且是迅速地!不必有保存 Java 类的单独文件,就能自动创建这种 bean。象处理 Hashtable 对象一样,用值的强类型对象来处理动态表单 bean。至于操作映射,一旦确定将 Tiles 库合并到 Struts 分发版之后,那么指定一个 tile 作为目标,就完全是增加一项逻辑而已。您会在概要信息表单的操作映射中看到 tile 目标(tile.profileInput 和 tile.profileOutput)。在 input 属性和 path 属性中指定 tile。注:可以指定 tile 目标弥补了我在分析以页面为中心的模型中提到的更改文件名和位置中的缺陷:tile 目标是虚拟名称或逻辑名称,而不是物理名称。
现在,进入激动人心的部分。让我们看一下 EX2_INSTALL/src/WEB-INF/tiles-defs.xml。
在 <definition/> 标记中指定 tile。您可以将定义命名为任何希望的名称,而且 name 属性不必是与 path 属性匹配的子字符串。我为第一个定义选择了名称 rootLayout,以表明它是应用程序中的页面要遵循的基本布局。注:路径是 /tiles-layout/rootLayout.jsp。如果查看 EX2_INSTALL/src/web 下的 /tiles-layout/rootLayout.jsp,您会看到这种布局是多么的简单整齐。用户界面的设计人员会“爱上它”。还请注意:它不包含任何代码,所以用户界面设计人员在进行更改时不必担心破坏什么。
rootLayout.jsp 中的 <tiles:insert/> 标记对应于 tiles-defs.xml 中 rootLayout 定义内的 <put/> 标记。注:每个 <tiles:insert attribute="x"/> 标记都有一个表示逻辑名称的属性。每个逻辑名称映射至通过在 tiles-defs.xml 内的 <definition/> 中使用 <put name="x" value="y"/> 标记指定的名称和值。通过在 rootLayout.jsp 页面中使用逻辑名称,而非物理名称,并通过在 tiles-defs.xml 中统一物理名称,我们就可以更改文件名,并使项目文件系统的组织易于管理。
真正节省时间和适应性方面最显著的增强方面体现在布局的继承,这是 Struts 的另一个特性。在 tiles-defs.xml 中,“Page definitions”栏下面的节有两个页面:tile.profileInput 和 tile.profileOutput。这些名称是任意的,如果您不喜欢 tile. 前缀,可以不使用它(但是您使用的名称必须与 struts-config.xml 文件中指定的目标相匹配)。这些名称应该与 struts-config.xml 中 <forward name="x" path="y"/> 标记中的 path 属性匹配。这些名称还应该与 struts-config.xml 中 <action ... input="" .../> 标记中的 input 属性匹配。在开发用户界面时,<definition name="x" extends="y"/> 标记中的 extends 属性是体现开发灵活性的地方。通过指定主布局并扩展它,您不仅能灵活更改象 topBanner、topMenu、panel1、panel2 这样的公共元素及其它组件, 还能随意地将不同元素放在页面上及更改它们的位置。例如,您可以添加 panel4(一个 tile)以在页面左侧的 panel2 下显示本地天气。只要天气代码不需要用户的任何输入或与页面上的其它组件交互,您就可以添加 panel