Struts在行动 使用领先的 Java框架构建 Web应用 Ted Husted Cedric Dumoulin George Franciscus David Winterfeldt (著 ) Eric Liu (译 ) 1 介绍 .........................................................................................................15 1.1 关于本书 ...............................................................................................................................16 1.1.1 谁创建了 Struts? ................................................................................................................16 1.2 什么是应用框架? .................................................................................................................17 1.2.1 其它类型的框架 ...............................................................................................................17 1.3 使用的技术 ...........................................................................................................................17 1.3.1 超文本传输协议 (HTTP) .................................................................................................18 1.3.2 公共网关接口 (CGI)..........................................................................................................19 1.3.3 Java servlet.............................................................................................................................19 1.3.4 JavaServer Pages ...................................................................................................................19 1.3.5 JSP标签 .................................................................................................................................20 1.3.6 JavaBeans ..............................................................................................................................21 1.3.7 Model 2..................................................................................................................................23 1.4 Struts开始于三万英尺 .........................................................................................................23 1.4.1 建立简单的Struts 应用 ......................................................................................................24 1.4.2 跳到开始开发了 ...............................................................................................................24 1.4.3.1 创建ActionForm ...........................................................................................................25 1.4.3.2 创建 RegisterAction .....................................................................................................25 1.4.3.3 创建Struts 配置文件 (struts-config.xml)....................................................................26 1.4.3.4 创建页面 .......................................................................................................................28 1.4.4 再看看 ...............................................................................................................................29 1.4.4.1 做了什么 .......................................................................................................................29 1.4.4.2 它如何工作 ...................................................................................................................29 1.4.4.3 什么还没做 ...................................................................................................................32 1.5 小结 .......................................................................................................................................32 译者的话 年初为了构建一个需要快速开发的大型应用的时候,我找到了 Struts。这个框架使我得 以从杂乱的思绪中解放出来,从而结合 EJB 构建了一个清晰的系统。从此以后,让我与 Struts 结下了不解之缘,从而也对开源项目有了新的认识。而不是仅仅限于现成的商业解决方案。 我接触的第一本书就是《Struts in Action 》。这之前也只是从从 Apache 的 Struts 主业 和相关链接和一些文章获取信息。当然这些信息显得杂乱。是《Struts in Action 》才使我对 这个强大的框架有了一个系统的理解和看法。从而用这些认识再来改进原来构建的系统。 年中,因为不开心,辞去了一家大型 IT 公司的技术总监职务,回家休养。一时闲下来, 还觉得浑身不舒坦,甚至原来没显现的程序员的职业病——“颈椎病”也发作了(真是奇 怪!!)。不过不能闲着,后来想反正没事,国内缺乏中文的 Struts 资料和书籍,何不将 此书翻译出来,供大家参考。国内还有好多不懂英语的程序员,也并不见得就不是很好的程 序员。 于是开干! 以前倒是翻译过许多资料,但第一次翻译书籍,其中艰辛难以言表。尤其是有颈椎的折 磨,每次总坐不到两三个小时。有时兴致所至忘记了休息,又只好去接受中医的“惨无人道” 的按摩治疗和难咽的中药。 最近终于将此书初稿译完,我将逐章进行校对和发布到一些论坛上去。个人精力和水平 毕竟有限,不妥之处,望各位读者能批评指正。 最后,感谢我的妻子女儿。我妻子在我赋闲的日子毫无怨言,默默支持我的工作。而我 的小女儿在我告诉她我忙不能给她讲故事的时候,也从没有纠缠过我,自己阅读去了。 Eric Liu shqlau@hotmail.com cn_fbi@yahoo.com.cn QQ 28730869 13 October 2003 前言 此刻你手中的是几个Struts最重要的开发者的艰辛成果。 Ted, Cedric, George, 和 David, 他们一起完成了一个卓越的工作,阐释了Struts 的工作原理和如何实际使用它。如果你是一 个新手,本书将使你很快入将Struts 应用到你的实际项目中。当然即使是最老道的Struts 开发 者也可以从中学到不少东西。 我从上世纪90 年代末开始对Web 框架发生兴趣。我准备写一种语言,它可以使我能解决 在进行高级开发时的某些问题--通过它可以使我从动态分配内存中的问题中解放出来。 一开始,我真正想实现的是如何使一些Web 应用开发者的工作变得容易一点。但后来 Struts那以置信的流行说明我并不是唯一在这些问题中挣扎的人—Struts 满足了通常的需要。 当早期的JSP规范草案发布时(0.91和0.92),此文档中最迷人概念的是JSP使用了两种 基本设计风格的理念。 Model 1 设计主要表现在表单提交可以回复到创建该表单的Servlet 或 JSP页面。这种设计鼓励混合 表现 逻辑(用来创建表单)和 业务 逻辑(用来验证表单输入和 处理请求事务)。这种设计在那些开发者仅具单一技能(比如页面设计者懂一些编程而JAVA 开发者也知道一些HTML )时经常使用。而且在时间第一的情况下也很有用(原型必须在下 周一运行而我们还没有获得风险投资)。而经验告诉我们, Model 1 设计在后来会很难进 行维护和升级。 相反, Model 2 设计是将表单提交给一个 控制器 组件。控制器组件调度相应的业务逻辑 组件来完成请求的事务。业务逻辑组件和数据库交互,并获取它在下次用户交互所需要的信 息。控制器组件将响应页面的创建委派给表现组件,表现组件只有一个目的,就是创建响应。 你可能想Model 2听起来好像更加复杂—也许甚至好想要完全扼杀简单应用。实际上,基于 Model 2 创建一个应用并不比用Model 1创建同一个应用花太多的时间。但主要的优点会很 快显现出来。如果你创建了一个正确的应用,对一层的主要修改,应该基本不会影响到其它 层(如果有的话),并且你可以马上在没受影响的层重用你的逻辑。 虽然,所有这些对Web 应用架构的聪明的研究正在进行,但我的专业工作让我也对方向 感兴趣。我正为一个专为长途运输行业提供信息服务的美国公司工作,并且我们正在准备将 业务扩展到欧洲。这样就产生了处理多语言和国际化问题的需要。我很快就受到了我用来实 现基本MVC 的单一的简单Servlet 的折磨,即它没有解决包含一个语言选择控件的问题。 对国际化问题的第一次努力是我尝试使用JSP1.1 种的新的标记来创建“用户接口组件” —它最终导致了现在Struts 一个标记 <bean:message>。 紧跟着,我加入到Sun Microsystems 并工作于Tomcat servlet 和 JSP container的创建( 我 是Tomcat 4 的基础 Catalina servlet 容器的主要架构者) 。这个开发的大部分工作是在Apache 的开源社区内进行,作为Jakarta项目的一部分—本项目由1999年Sun 贡献了servlet 和 JSP 参考实现源代码给 Apache起始。然而,我并不满意于那时候Model 2- 面向应用设计的状态,所 以我开始想怎样来解决这个问题。 虽然我已经有了一个漂亮的注意,来解决剩下的问题,但实际代码并没开始,直到在2000 年纪念日我和我的家人在Oregon海岸过周末的时候,我将我的笔记本电脑也带了去,并应起 了我妻子的很大抱怨。后来成为ActionForm 的最初最初版本就诞生在那个周末, 它组建开 始解决大量有趣的问题。另外,为表现组件和业务组件定义逻辑名称的理念—以及集中在一 个文件中定义这些名称的理念—非常清楚地有益于解决在开发两层之间的协调和重叠问题, 以及实现层间改变相互影响的隔离目标。 通过我在Tomcat 的工作,逐渐认识到开放源代码开发的好处,这也是使Struts 进入开源 世界的自然选择。这个选择—以及Struts 处理一些最基本的Web 应用设计问题的优雅方式— 导致了它真正的令人惊讶的接受程度。成千上万的开发人员下载 Struts ,克服学习曲线,通 过 STRUTSUSER 邮件列表提问和接收问题,并在世界各地成功地开发了许多基于Struts 的 应用。 当然,并不是我自己一个人能完成所有的事情。 Ted, Cedric, David,以及所有过去和现在 的Struts 项目的参与者,连同George 和 Struts 开发人员社区,共同使这个框架远远超过了 我自己能够独立完成的状况。对它们,我表示最衷心的感谢。而对你,本书的读者,我希望 你会发现Struts 是你另一件有用的武器,能够值得你去花时间修习其技术和API 。 Enjoy! Craig McClanahan Portland, Oregon 序 到2000年,我们进入了Java 时代。早期的宣传都已实现,并且出现了一些有趣的开发工 具和库。我已经写了好几年的Web 应用程序了。像许多开发者一样,我开始时使用JavaScript 和 Perl 写一些简单的应用。这是一个强大的组合,但难以维护。接下来是ColdFusion ,它更 强大,但那时它对我的客户的钱包来说还是太昂贵了。我甚至尝试了FileMaker Pro ,它很有 趣,但是非常非常的专有化。 我的这些连续Web 应用的主要客户是一个公共电视台。电视台的主要资金来源(现在仍然是) 年度拍卖。本地的支持者提供货物或者服务,人们通过拍卖购买它们来支持电视台。当然, 我们会很快在网站上递交这些高端东西的图像:艺术品,度假服务包,汽车,签名手稿,等 等。 1998年,我们使用一个JavaScript 和 Perl 写成的应用来接受这些高端物品的“预出价”。 实际的出价在现场直播的电视拍卖会进行。此应用要做的就是设定起拍价。 1999年,我们在 销售时接受在线和电话出价。每年我们都在在线拍卖上使用不同的平台,因为每一年我们都 发现已有的平台不能满足所有的需要。 因为我们已经很满意使用Apache HTTPD server,我花了一些时间来看看是否能使用初生 的Jakarta 站点,在这里我发现了Struts 。首先,我不能确保项目是不是还活动。但文档看起 来似乎是,所以我订阅了邮件列表,看是不是真的有人在。一个示例应用已经包含在文档中。 我从这个开始起步,试图决定这个框架是不是我想要的。于是我进入了“Struts 应用之旅”, 它描述了这个例子如何工作的,并逐屏幕进行了说明。我将这个经历递交到列表中,在这里 其他一些订阅者慢慢纠正了我对某些问题的误解。我继续跟踪回应这个列表,紧握所能帮助 其他人,也受到其他先行者的帮助。这个列表的访问稳定增长。到年底,Struts 的架构师和 领导开发者Craig McClanahan ,正在寻找帮助编写1.0 release 版本文档的人。2000年9 月,我 被推举为为一个Struts 提交者,最终我在2001年6 月提交了 1.0 。 接下来,我开始建立我的 “More About Struts” 网页。首先,首先,它仅是一个保持我所 写的Struts 材料的地方。后来我开始添加一些连接到其它人发布的一些Struts “ 扩展” 的链 接, 然后是逐渐开始出现的Struts 文章。我的 Struts资源页面增长很快,也更为许多人所知, 所以我将它移到了Struts 的主站。如今他已经是一个为Struts 世界的每个人所熟知的一个包 含许多资源链接的页面。 Struts列表是一个用用信息的宝藏,特别是因为Craig 自己也亲自来传授一些实现细节 和架构理念。但是在列表文档发现最好的信息的确是个挑战。所以我开始编写一个“精华区” 来只想那些最好的邮件链接,它最后成为一个大型的FAQ 。 2001年6 月, JGuru 决定开辟一 个 Struts 论坛和 FAQ,所以我们将主要的 Struts FAQ 移到了 JGuru,我还继续管理它。 同时,出版商开始注意起Struts ,索稿邮件也发到了我的邮箱。经过和其他提交者商量 的结果,我们终于决定和Manning Publication 合作。象 Apache一样,Manning 有一个长久 的质量承诺。虽然我们想尽快地完成一本Struts 书籍,但是我们也想把它写成是尽可能最好 的Struts 书。 成果就是Struts in Action 。 它完全是一本“团队书籍”。David Winterfeldt , Struts Validator的创建者,非常乐意地撰写了Validator 一章。同样, Cedric Dumoulin, Tiles 的创建者,也编写了Tiles 一章。 George Franciscus 提供了关键的第一章,这有助于帮助新 手上路。我们甚至让Craig 写了前言 ( 他说“宁肯进行编程”) 。 当然,其他 Struts 开发者和提交者在每个阶段都审阅书的手稿,我们也感谢他们提供了很 多有用的意见。 嗯,关于拍卖呢? 我们已经进入了使用Struts 的第三个年头了。 现在不是每年重写我们 的应用了,而是每年进行一些改进。 Ted Husted Fairport, New York 鸣谢 我们认识到许多人对使本书成为可能的支持和理解。我们希望那些我们最应该感谢的人 —那些在像这样一个项目进行时站在我们一边的家人和朋友—已经知道我们是多么的感激 他们的爱心和耐心。 当还有更多的人,他们并没有留下姓名,也为此书贡献了许多力量。 首先,是有许多志愿开发者进行了Struts 的开发。数百个人的帮助才使Struts 成为今天的 样子。许许多多的人是通过了邮件列表进行了宝贵的坦率的间接贡献。其他一些人则直接贡 献了代码或者文档。比如对于 1.02 release ,他们就包括Arun M. Thomas ,Chris Assenza , Chris Audley, Craig R. McClanahan, David Geary, dIon Gillard, Don Clasen, Ed Burns, Eric Wu,Florent Carpentier ,Jeff Hutchison ,Jimmy Larsson ,John Rousseau,John Ueltzhoeffer , Larry McCay, Luis Arias, Marius Barduta, Martin Cooper, Matthias Kerkhoff, Mike Schachter, Niall Pemberton, Oleg V Alexeev, Paul Runyan, Ralph Schaer, Rob Leland, Robert Hayden, Sean Kelly,Stanley Santiago ,Wong Kok Kai 。 好多 Struts 开发人员也慷慨的评审了本书的手稿,并提供了很多有用的意见。 Dan Malks 就提供了本书第2 章的很多注解。Richard Starr 对第3 章的彻底评论对我们定稿Hello World logon 例子帮助甚大。 我们也感谢那些对手稿提供反馈的其他开发者,如Martin Cooper , Vincent Masool, John Yu, Jon Skeet, Max Loukianov, James Holmes, Bill Wallace, Nathan Anderson, Cody Burleson, Darryl Thompson, James F. McGovern, Steve Wilkinson,和 Shawn Bayern。我们的技术责任 编辑, Steve Wilkinson 和Stephen LeClair ,更应该特别感谢,他们找出了书中尽可能的小 错误。我们也非常感谢我们的组版编辑, Liz Welch ,它修改了我们书中许多笔误。 本书也非常感谢我们的出版商, Marjan Bace和 Manning 的编辑小组。 Manning 并不是 仅要我们的工作,他们要我们最好的工作。这耗费了许多的努力以及大量的时间,但读者将 花费时间和金钱在此书上,他们应该值得我们向他们提供最好的东西。 关于此书 Struts 框架集合了几种相关的技术,使开发者可以创建易于构建、扩展和维护的基于标 准的应用。Struts 已经是全世界开发人员不管是新手还是老手的框架选择。 Struts in Action 一步步介绍了 Struts 框架。并且书中好包含展示此书介绍的最好实践技 术的几个示例应用程序。本书旨在帮助那些需要关于如何使他们的应用运行在Struts 下面的 实际和实战技术的专业技术人员。 开发者用Struts 构建Web 应用一般在其应用的各部分使用几种相关的技术。一本包含这 些全信息的书才可能会满足众多需求。为了能在一本书里面包含 Struts ,我们试图在本书中 包括 HTML 标记语言, JSP页面语法, JavaBean 开发的习惯,或者类似技术的细节。嘉定读 者已经熟知这些技术,以便能跟得上我们表述的例子。并且假定读者熟知 URL,文档层次, web 应用档案,其及其他创建并发布Web 应用的相关概念。 我们也不包括基本的Java 编程语言。关于 HTML,JSP , JavaBeans,和其他相关技术, 已经有大量的信息。我们假定阅读此书的读者熟知 Java 语法,应用开发生命周期,以及面 向对象设计概念。关于关系数据库的基础,加上 JDBC技术,我们建议掌握,但不是必需。 那么我们的注意力将着眼于Web 应用和 Struts框架。 技术之间的关系已经说明— HTML, Java,数据库,其及其他技术—是本书的焦点,也 是我们讨论最深入的范围。 然而,我了那些不太精通Struts 依赖的相关技术的读者,书中也包括了基本的 HTTP, Java servlet,JSP,核定指标签的介绍。 Roadmap 第1 张总体介绍了Web 应用的开发,特别介绍了Struts 。我们会看到Struts 是如何编写和 发布的,驱动应用的背后相关技术,以及Struts 的总体架构。在最后,我们开始创建我们的 第一个Struts 应用。 第2 章探索Struts 的架构。我们从Struts 架构的总揽开始,紧跟着会仔细看看控制六是如 何在整个框架中流动的。本章最后以一个坦率的关于Struts 的强项和弱点的讨论结论结尾。 本章旨在给开发人员关于Struts 的各个方面的真正情况的一个坚实基础。也帮助产品经理决 定是否Struts 会很好的适合他们的团队。 第3 章开始开发一个简单的应用。像第1 章的练习一样,这是也简单的登录程序,但是包 括了Web 应用的基本组成。其目的是给动手的开发人员在第2 部分进入详细的细节之前一个 总体印象。为了照顾实际,我们回过头来将该例子从Struts 1.02升级到Struts 1.1 release 。 第4 章探索Struts 框架的骨干—配置元素。我们也描述了配置web 开发描述符和Ant 的 build 文件,来帮助构建和部署你的应用。 第5 章涉及了 Struts ActionForm。这个关键的对象可以扮演应用中的许多角色:传输对象, 防火墙, API,数据校验器,以及类型转换器。我们介绍了几个技术,来有助于你得益于 调用form beans的 双刃剑的 Struts。 第6 章讨论 Struts ActionForward 。你可以从这章一探这个Web 应用中对狡猾的家伙的 究竟。ActionForward 我以帮助你清楚的定义你的应用的进入点,使你更容易看到你是否覆 盖了你所有的基础部分。 第7 张讲述的是Struts ActionMapping 。这个映射是 Struts 控制器的基础。 Action 类可以 在一个重根据不同的任务配置成不同的形式,来达到重用。这里我们会解释如何使用Action Mapping 来控制应用的流程,并得益于每个Action 类。 第8 章涉及 Struts Action 对象。这是Struts 应用主要承担工作的部件—在这里通常开发 者会花大量的时间。我们详细探讨了随Struts 绑定的Action 类,以及几个来自于 Scaffold 包 的东西,以及从输入的Struts ActionForm 组装业务组类的痛苦过程。 第9 章涉及到了 Struts ActionServlet 。这个控制器类是框架的“发言人”。它发号司令 去让其它的对象完成繁重的工作。我们也会涉及一些新的方式来定制ActionServlet 一边最好 的迎合与你的应用或者某个特殊模块的需要。 第10章我们会探讨通常的Struts JSP 标签和 JSP 页面。从用户的角度讲, web 页面就是应 用,它代表了应用项要做的一切。使用Struts 的关键优点就是它可以将表现内容和采集内容 分离开来。在这一章,我们会详细探索 Struts JSP 标签,并简要介绍和 Struts 一起使用其他 表现系统,比如 XLST 和 Velocity。大部分Struts 应用都依赖 JSP 来创建动态页面,但框架本 身可以和许多表现系统一起使用。 第11章我们涉及Tiles 页面装配框架。象Tiles 之类的动态模板系统,引入了一种新的编程 模式到Web 应用的表现层。一个tile 封装了一个标记块,和方法封装了Java 代码块非常相似。 用Tiles 构建Web 页面给不受约束和混乱的 HTML世界带来了一致性和灵活性。 第12章我们要讨论用户输入校验的重要内容。对Struts 核心的一个流行的扩展是 Struts Validator。这是一个非常强大的组件,它可以在相同的培植下提供基于客户端的校验和服务 器端的校验。我们会展示如何将校验集成到你的Struts 应用之中,使用预编写的校验器或者 专门为应用编写的。 第13章涉及Struts 的国际化特征。 Struts 从底层提供国际化特征。这一章国际化是如何 以及在何处构架到Struts 之中,你需要做的就是让它正确工作。 基本主题是,今天你为一种 语言开发的应用所必须做的,明天也可以加入另外的语言支持。 第14章探讨Struts 的数据服务。这一章讲述如何使用一个helper 类来将 Struts Action 连 接到不同的企业数据系统—包括数据库,搜索引擎,以及内容辛迪加服务。还提供了一个使 用 JDBC, Lucene,以及RSS 的例子。 第15章是我们的特征性应用, Artimus。这个企业级的应用将扫除所有的障碍,并在一 个精简的、精彩的可重用包内展示了Struts 的关键和附加特征。认证,客户化,本地化, Scaffold, Tiles,事务,校验器,还有许多,它是一个按顺序编排的 the best and brightest Struts 所提供的最好和最靓的特征集合。 第16章是Struts 1.1 升级指南。这一章我们仍然使用第15章的Artimus 应用,但用新的 1.1 特征进行了翻新,包括 DynaForms , plug-in,以及多重模块。如果你已经有了一个遗留 的Struts 1.0 应用需要升级,这一章就是为你写的! 第17章展示了如何在Struts 中使用Velocity 模板系统。我们用Velocity 模板修订了我们 的登录程序,并且从两方面比较了 Velocity 模板和 JSP页面。 Code 本书的源代码由Apache 软件基金赠与。源代码现今已经是Struts 份发包的一部分,并 且也可以从Manning 的站点, www.manning.com/husted上获得。 本章前面部分的源代码主要由一些说明文字的片段组成。如果是要给出完整的代码,他 们会被加上一个编号的程序清单;其间还有一些代码注释。 在展示源代码的时候,我们有时会使用 加黑 字体来强调特殊的部分。 在文本中,Courier 字体用来指示代码( JSP, Java, 和 HTML) 和 Java 方法, JSP 标签名, 以及其他源代码标识符: λ 文本中队方法的引用通常不包括方法体,因为可能存在对方法调用的不止一种形式 λ 对 JSP 标签的应用通常包括括号和前缀,但不包括标签接受的属性列表 (<bean:write>). λ 本本中对 XML元素的引用会包括括号,但不包括属性和关闭标签。 λ 当一个Java 类或者标签库在某一节中首次出现时,整个包的标识符会出现在括号 中,并设置为Courier 字体 (java.util.Map),其他对该类的引用会设置为常规的类 型。 λ 当 JSP 和HTML 一起散布在代码清单或者片断中时,我们将对 HTML 元素使用大写形式而 对JSP 元素使用小写字母。 参考 参考书目在一对方括号中给出。例如 [ASF, Artimus] 。完整的出版细节和/ 或在本书末 尾的“参考”一节给出。 作者在线 购买 Struts in Action 包含了对运行在Manning Publication 上的私有Web 论坛的访问权 限,在那里你可以发表对本书的意见,提技术问题,从作者和其他用户处得到帮助。为了访 问论坛并订阅它,可以在浏览其中访问 www.manning.com/husted。这个页面提供了在你注册 后如何访问论坛的信息,以及那些类型的信息是有效的,以及论坛的行为规则。 Manning义务是提供一个场所,在这里读者间,读者与作者间可以进行一个有意义的对 话。但作者不参与大量的其他特殊事务,他们会主动贡献(免费)一些东西给论坛。我们建 议读者提一些有挑战性的问题,免得他们失去兴趣! 作者在线论坛以及以前的一些讨论档案在本书付印的时候就可以从出版商的网站上访 问。 关于作者 Ted Husted 是一个知名的Struts 权威,Struts 开发团队的活动成员,以及JGuru Struts 论坛的管理员。作为一个咨询师,Ted 为全美的很多专业Struts 开发团队服务。Ted 也帮助 管理Apache 的Jakarta 项目,该项目包含Struts 框架。Ted 和他的妻子,两个孩子,四台计 算机,以及一只老猫生活在纽约的Fairport 。 Cedric Dumoulin 是 Struts 开发团队的活动成员,也是 Tiles 框架的作者。Cedric 目 前是Lille 大学的研究员。他也是一个领先的国际互联网银行公司的开发部工作。他生活在法 国的 Lille 。 George Franciscus 是Nexcel的负责人,该组织为许多行业提供技术和管理咨询,包括 电信,银行,人寿保险,财产和死亡保险。 George 擅长于 Java, J2EE, Domino,关系数据 库,主机系统技术等。他有多伦多大学的计算机科学学士学位。George 和他的妻子和三个 孩子生活在加拿大的多伦多。 David Winterfeldt 是一个Struts 贡献者,以及Commons Validator 包的作者。他是一个 实现J2EE技术的大公司的高级开发人员。David 目前住在纽约市。 Craig McClanahan, Struts 框架的创立者,并为此书写了前言。Craig 是Tomcat 4 的 主要架构师,以及Java Web Services Developer Pack 的实现架构师。他现在是Sun 的JavaServer Faces (JSR-127)的规范领导者和J2EE平台的 Web层架构师。 Craig,作 为 Struts的主要开发者, 也许是提供了本书最重要的部分—我们的写作框架。 关于标题 通过结合介绍、总揽,以及如何操作的例子, In Action 系列书旨在帮助进行学习 和 铭 记。根据认知科学的研究,人们记住的事情是他们自我动因进行探索的事情。 虽然Manning 中没有一个认是认知科学家,但我们确信要使学习成为持久的记忆必须 经过各个阶段的探索,动手,以及对正在所学的进行重复。人们理解和记住新的事物,即使 说他们掌握了它们,仅仅是要在主动地探索之后。人类的学习是主动的(in action ) 。 In Action 系列书的本质是它是例子驱动的。他鼓励读者去尝试,动手编一些新的代码,并探索新的想 法。 本书标题还有另一方面,也是最实际的,原因:我们的读者都是很忙碌的。他们用书来 完成工作或者解决问题。他们需要一本书,允许他们很容易跳进跳出,并在他们需要的时候 学习他们需要的部分。他们需要一本书来帮助他们行动(in action )。这个系列的书就是针 对这些读者。 关于封面 封面上的图是一个来自于波尔多荒野的牧羊人, “Berger des Landes de Bordeaux”。波尔 多地区位于法国的西南部,有很多非常适合葡萄种植的阳光充足的小山,以及散布着羊群的 开发和湿润的原野。踩在他的高跷上,牧羊人可以更好地在沼泽地中行进,并完成他的职责。 这个图来自于法国旅游手册 Encyclopedie des Voyages ,由 J. G. St. Saveur编写,出版于 1796年。现在放松旅行是一个新的现象,像这样的旅游书非常流行,将旅游者和自助旅行者 引向法国其他地区和国外的居住者。 Encyclopedie des Voyages中 多种多样的插图讲述了200年前的世界各地城镇的生动和独 特的景象。 可以通过穿着习惯来区分相隔数十英里的两个区域的人们区分各自属于哪一个 地方。旅游指南给人的生活以一种隔离和距离的感觉,关于每一个历史时期如何同我们的过 度兴奋的现在如何不同。 因为地区差异,服装风格会不同,并且非常丰富,但是随时间淡化。现在已经很难各个 大陆的居住者了。也许,试着乐观地看待它,我们会对各式的个人生活得文化和视觉的多样 性。或者更加多样和有趣的智慧和技术。 在 Manning 我们赞成书记涵盖的计算机业务的创新,主动。以及两个世纪前的丰富的 区域生活多样性由旅游书中的图片给我们的生活带回来的乐趣。 第一部分 Struts入门 第一部分是Struts 入门。我们介绍Java web 应用,检视了框架的结构,构建了两个简单的应 用,并浏览了Struts 的配置组件。 1 介绍 本章包括 λ 应用框架介绍 λ 理解HTTP, CGI, servlet, 和 JSP λ 使用 Model 2 架构 λ 构建简单的应用 The only stupid question is the one you never ask. —佚名 1.1 关于本书 欢迎你阅读《Struts 在行动 》。本书的目的是帮助Web 应用开发者可以最好的使用Struts web 应用框架。 Struts 是一个开源软件,有助于开发者更加快速和容易地建立Web 应用程序。Struts 依 靠绝大多数开发者已熟知的标准技术—比如JavaBeans, Java servlet, 以及 JavaServer Page (JSP)。通过基于标准的技术,“填空式”的软件开发方法, Struts 可以减轻在创建新项目时 的令人抱怨的极费时间的工作。 1.1.1 谁创建了 Struts? Struts是Apache软件基金下Jakarta项目的一部分。除Struts 之外, 还有其他成功的开源产 品,包括Tomcat, Ant, 和 Velocity 。 开始的代码基础从2000年5 月开始开发,直到2001年6 月,1.0版本发布。有 30 多个开 发者参与进来,并有数千人参与到讨论组中。 Struts 代码基础由一个志愿的Commnitter 团队 来管理。到 2002年,Struts 小组共有9 个志愿Commnitter 。 Struts框架的主要架构设计和开发者是Craig R.McClanahan 。Craig 也是Tomcat 4 的主要 架构师,以及Java Web Services Developer Pack 的主要架构师和实现者。他现在是Sun 的 JavaServer Faces (JSR-127) 以及J2EE平台的Web 层架构的规范领导。 Struts 在Apache 软件许可 [ ASF, License]下对公众是免费的。 使用此软件没有任何获 得和再现成本。不象其他一些开源许可,Apache 软件许可对商业用途是友好的。你可以在 你的商业项目中使用Struts ,并自由分发Struts 库。你也可以将Struts 组件集成到你的框架中, 就像他们是你自己编写的一样。详细情况,参见Apache Software License , www.apache.org/LICENSE。 1.1.2 为什么 Struts 要开源 ? 现在许多非常好的Java 程序和框架都是开源项目。许多开发人员为这些项目工作,同时 又在象 IBM, Sun Microsystems, 以及 Apple 这样的公司从事其常规工作。这类软件的开发 式协作有利于整个软件市场。今天,许多开源组件都集成到商业产品之中。公司可以向其客 户出售其专业的文档,保证支持水平,以及其他有价值的售后服务。 当软件是自由的的时候,对市场来说它更容易得到支持。 Struts 就是个典型例子。虽然 它还只是个很新的产品,也已经有很多文章和教程涉及到它,但却还没有什么象这样的书籍。 许多开发团队不喜欢使用不是自己内部开发的软件。开源组件提供了所有自行开发的软 件的优点,但绝不将你锁定在一个只有你们团队才懂的专有解决方案上。 开源软件对所有人都是双赢的。 1.1.3 为什么叫 Struts? 这个框架之所以叫“Struts” ,是为了提醒我们记住那些支撑我们房屋,建筑,桥梁,甚 至我们踩高跷时候的基础支撑。 这也是一个解释Struts 在开发Web 应用程序中所扮演的角色 的精彩描述。当建立一个物理建筑时,建筑工程师使用支柱为建筑的每一层提供支持。同样, 软件工程师使用Struts 为业务应用的每一层提供支持。 1.2 什么是应用框架 ? 框架(framework )是可重用的,半完成的应用程序,可以用来产生专门的定制程序 [Johnson]。象人一样,软件应用的相似性比不同点要多。它们运行在相同的机器上,期望从 相同的设备输入信息,输出到相同的显示设备,并且存储数据到相同的硬盘设备。工作在传 统桌面应用的开发人员习惯于那些可以覆盖应用开发同一性的工具包和开发环境。构架在这 些公共基础上的应用框架可以为开发人员提供可以为他们的产品提供可重用服务的基础架 构。 框架向开发人员提供一系列具有以下特征的骨架组件: λ 已经知道他们在其他程序上工作的很好; λ 它们随时可以在下一个项目中使用; λ 他们可以被组织的其他团队使用; 框架是典型的构建还是购买 命题。如果你自己构建它,在你完成时你就会理解它,但是 在你被融入之前将花费多长时间?如果要购买,你必须得克服学习曲线,同样,在你可以用 它工作之前得花多长时间?这里没有所谓正确答案,但许多观察者将会同意,象Struts 这样 的框架能提供比从头开始开发更显著的投资回报,特别是对于大型项目来说。 1.2.1 其它类型的框架 框架的概念不仅用于应用程序也可用于组件。通过此书,我们也介绍其他可以和Struts 一起使用的框架。这些包括 Lucene 搜索引擎, Scaffold 工具包, Struts 验证器,以及Tiles 标签库。象应用框架一样,这些工具也提供了一些半完成的版本,可以用在用户的定制组件 之中。 某些框架限制到专门的开发环境中。 Struts 以及本书中涉及的组件都不是这样。你可以 在很多环境中来开发Struts: Visual Age for Java, JBuilder, Eclipse, Emacs, 以及Textpad 。 如果你可以用来开发Java, 你就可以用它来开发Struts 。 译者注:目前很多大型公司也重视到它,它们的工具也提供相应的 Struts开发支持。比如 IBM WSAD, BEA WorkShop等。另外,一些公司专么提供可视化的 Struts集成开发环境。 1.3 使用的技术 使用Struts 的应用开发使用了大量的使能技术。这些技术并不是专门针对Struts ,而是 所有Java web 应用都可以使用的。开发者使用Struts 之类的框架是为了隐藏在诸如 HTTP, CGI, 以及JSP之类技术后面的繁琐的细节。作为一个Struts 开发者,你并不需要知晓所有的 相关知识,但是这些基本技术的工作原理可能有助于你针对棘手问题设计出创造性的方案。 如果你已经非常熟悉这些技术,你可以跳过这些章节到1.4节。 1.3.1 超文本传输协议 (HTTP) 当两个国家之间进行调解时,外交官们总是遵循一定的正式 协议 。外交协议设计来避免 误解,以及防止谈判破裂。同样,当计算机间需要对话,他们也遵循一个正式的协议。这个 协议定义数据是如何传输,以及他们到达后如何进行解码。Web 应用程序就是使用HTTP 协 议在运行浏览器的计算机和运行的服务器的程序间传输数据。 很多服务器应用程序使用HTTP 之外的其他协议。他们在计算机之间维护一个持久性的 的连接。应用服务器可以清楚的知道是谁连接上来,而且何时中断连接。因为他们知道每一 个连接的状态,以及每一个使用它的人。这称之为状态协议。 相反, HTTP 是一个无状态协议。 HTTP server 可以接受来自于各种客户的各种请求, 并提供各种响应,即使是这个响应仅仅是说No 。没有大量的协商和连接持久性,无状态协 议可以处理大量的请求。这也是Internet可以扩展到很多计算机的原因。 HTTP成为通用标准的原因是其简单性。HTTP 请求看起来就像一个平常的文本文档。 这使应用程序很容易创建HTTP 请求。你甚至可以通过标准的程序如Telnet 来手动传递一个 HTTP请求。当HTTP 响应返回时,他也是一个开发者可以直接阅读的平面文本。 HTTP请求的第一行包含方法,其后是请求的来源地址和HTTP 版本。HTTP 请求头跟在 首行后面,可以没有也可以有多个。 HTTP 头向服务器提供额外的信息。可以包括浏览器的 种类和版本,可接受的文档类型,浏览器的 cookies 等等。 7种请求方法中, GET 和 POST 是用得最多的。 一旦服务器接收到请求,他就要产生一个HTTP 响应。响应的第一行称为状态行,包含 了HTTP 协议的版本,数字型状态,以及状态的简短描述。状态行后,服务器将返回一个HTTP 响应头,类似于HTTP 请求头。 如上所述, HTTP并不在请求间保持状态信息。服务器接受请求,发出响应,并且继续 愉快地处理文本请求。 因为简单和效率,无状态协议不适合于需要跟踪用户状态的动态应用。 Cookies 和 URL 重写是两个在请求间跟踪用户状态的方式。 cookie 是一种特殊的信息 包,存储于用户的计算机中。 URL 重写是在页面地址中存储一个特殊的标记, Java 服务器 可以用它来跟踪用户。这两种方法都不是无缝的,是用哪一个都意味着在开发时都要进行额 外的工作。对其本身来说,标准的HTTP web 服务器并不传输 动态内容 。它主要是使用请求 来定位文件资源,并在响应中返回此资源。通常这里的文件使用Hypertext Markup Language (HTML) [W3C, HTML] 格式化,以使浏览器可以显示它们。HTML 页面通常包含一些到其 他页面的超文本连接,也可以显示其他一些内容比如图像和视频等等。用户点击连接将产生 另一个请求,就开始一个新的处理过程。 标准web 服务器处理静态内容处理得很好,但处理动态内容时则需要额外的帮助手段 了。 定义 静态内容 直接来自于文本或数据文件,比如HTML 或者 JPEG 文件。这些文件可以随时 改变,但通过浏览器请求时,却不能自动改变。 相反, 动态内容 是临时产生的,典型地,它是针对浏览器的个别请求的响应。 1.3.2 公共网关接口 (CGI) 第一个普遍使用来产生动态内容的标准是公共网关接口Common Gateway Interface (CGI)。 CGI使用标准的操作系统特征,比如环境变量和标准输入输出,在Web 服务期间以及 和主机系统间创建桥和网关。其他程序可以看到web server 传递过来的请求,并创建一个定 制的响应。 当web 服务器接收到一个对CGI 程序的请求,它便运行这个程序并提供它他请求里面包 含的信息。CGI 程序运行,并将输出返回给Web server ,web server 则将输出响应给浏览器。 CGI 定义了一套关于什么信息将作为环境变量传递,以及它希望怎样使用标准输入和输出 的惯例。象 HTTP 一样,CGI 是灵活和易于实现的,并且已经有大量现成的CGI 程序。 CGI 的主要缺点是它必须为每个请求运行一个程序。这是一个相对昂贵的处理方法, 对大容量站点来说,每分钟有数千个请求,有可能使站点瘫痪。CGI 程序的另一个缺点是 平台依赖性,一个平台上开发的程序不一定在另一个平台上能运行。 1.3.3 Java servlet Sun公司的Java Servlet 平台直接解决了CGI 程序的两个主要缺点。 首先, servlet 比常规CGI 程序提供更好的性能和资源利用。其次,一次编写,随处运行 的JAVA 特性意味着servlet 在有JVM 的操作系统间是轻便可移动的。 Servlet看起来好像是一个微小的web server 。它接受请求并产生响应。但,和常规web servers不同,servlet API 是专门设计来帮助Java 开发人员创建动态应用的。 Servlet 本身是遍译成字节码的Java 类,就像其他Java 对象一样。Servlet 访问HTTP 特 定服务的API ,但仍然有另外一个Java 对象运行于程序之中,并管理所有的Java 资产。 为了使常规web servers 能访问servlet ,servlet 被安插在一个容器之中。Servlet 容器连接 到Web 服务器。每个servlet 都可以宣称它可以处理何种样式的URL 。当符合所注册样式的 请求到达,web server 将请求传递给容器,容器则调用响应的servlet 。 但不像CGI 程序,并不是针对每个请求创建一个新的servlet 。一旦容器实例化一个 servlet,它就仅为每个新的请求创建一个新的线程。 Java 线程可比使用CGI 程序的服务器处 理开销小多了。 一旦servlet 被创建,使用它处理额外的请求仅带来很小的额外开销。Servlet 开发人员 可以使用init() 方法保持对昂贵资源的引用,比如到数据库或者EJB Home 接口的连接, 以便它们可以在不同的请求间进行共享。获得这些资源要耗费数秒时间,这比大多数冲浪者 愿意等的时间要长些。 Servlet的另一个好处是,它是多线程的, servlet 开发人员必须特别注意确保它们的 servlet是线程安全的。学习servlet 编程,我们推荐Java Servlets by Example , 作者Alan R. Williamson [Williamson]。 1.3.4 JavaServer Pages 虽然servlets 对CGI 程序来说前进了一大部,但它也不是万能药。为产生响应,开发人员不 得不使用大量的println 语句来生成HTML 。像这样的代码: out.println("<P>One line of HTML.</P>"); out.println("<P>Another line of HTML.</P>"); 在产生HTTP 响应的Servlet 中是很普遍的。也有一些库有助于你产生HTML ,随着应用越来 越复杂,Java 开发人员将不再扮演HTML 页面设计的角色。 同时,大多数项目经理更喜欢将团队分成不同的小组。 它们喜欢HTML 设计人员处理 表现层的工作,而Java 工程师则专注于业务逻辑。单独使用servlet 鼓励混合标记和业务逻辑, 很难区分团队人员的专业工作。 为解决这个问题,Sun 提出了一个结合脚本和模板技术到一个组件中的服务器页面技 术。为创建JSP 页面, 开发者按同样创建HTML 页面的方式创建页面,使用相同的HTML 语法。 为将动态内容引入页面,开发人员可以将脚本元素置入页面之中。脚本元素是一些标记,封 装了可以被JSP识别的逻辑。你可以在JSP页面中很容易的识别出脚本元素,他们被封装在一 对<% 和 %>标记中。例如,要显示页面的最后修改日期,开发人员可以将以下代码放入页 面中: <B>This page was accessed at <%= new Date() %></B> 有三种不同的脚本元素: 表达式,脚本小程序和宣称。如表1.1所示: 表格 1.1 JSP脚本元素 元素 目的 表达式 Java代码,封装在 <%= 和 %>之中,用来计算JAVA语句的值,并将结果插入Servlet的输出之中 脚本程序 Java代码,封装在 <% 和 %>之中,常用来创建动态内容 宣称 Java代码,封装在 <%!和 %>之中,常用来创建动态内容 为区分JSP 页面,程序将文件存为扩展名.jsp 。当一个客户请求JSP 页面时,容器将页面翻 译成Java servlet源代码文件,并将它编译成Java 类文件--就象你写的servlet 文件一样。在运 行时,容器也能检测JSP文件和相应类的最后更新时间。如果,JSP 文件自上次编译以来修 改了,容器将重新翻译和编译JSP文件。 项目经理现在可以将表现层分派给HTML 开发人员,将业务逻辑工作分派给JAVA 开发 人员。重要的是记住, JSP 页面事实上是一个servlet 。你可以在servlet 做的,也可以在JSP 中做。 1.3.5 JSP标签 脚本元素仅是两种产生动态内容的方式之一。 Scriptlet 是快捷、简单、强大的手段但要 求开发者在HTML 中混合Java 代码。经验告诉我们,混合业务逻辑到JSP页面中将导致难以 维护的应用和最小的可重用性。一个可选的方案是使用JSP 标签(tags )。JSP 标签可以和 HTML标记混合使用,就象它们是原生HTML 标记一样。一个 JSP 标签可以代表许多Java 语句,但是所有的开发者都需要了解如何在页面中插入标记。源代码隐藏在Java 类文件之中。 为在其他页面中使用同一代码,只需要在该页 面中重新插入相同的标签。如果标签代表的代码改 变了,所有的标签都将使用更新的版本。而使用标 签的 JSP页面并不需要进行修订。 JSP 标记比 scriptle提供了更好的可重用性,也 更易被页面设计者使用,因为它们看起来很象 HTML标记。 有大量的现成的 JSP 标签 库 ( tags libraries)可 用, 他们完成很多有用的功能。 其中就有新的 JSP 标 准标签库 (JSTL)。 这是一个新的标准, 提供丰富的可 重用的 JSP标签库。 关于 JSTL 的 详细情况, 我们高度推荐 《 JSTL in Action》,作 者 Shawn Bayern [Bayern]。 Struts 可 以很好的和 JSTL 以及其他公开标签库一起工作, 甚 至是你自己写的标签库。 关于 JSP 的详细内容, 我们强烈推 荐《 Web Development with JavaServer Pages》 , 作者 Duane K. Fields, Mark A. Kolb, 和 Shawn Bayern[Fields]。 JSP 是 Struts开发者工具箱的一部分。大多数 Struts开发者使用 JSP 和定制标记来创建应用的动 态内容。 1.3.6 JavaBeans JavaBeans 是一 种 Java 类 , 它遵从一定的设计 模式, 使他们易于和其他开发工具和组件一起使用。 定义 JavaBean 是一种 JAVA语言写成的可重用组件。为写成 JavaBean,类必须是具体的和公共的, 并且具有无参数的构造器。 JavaBeans 通过提供符合一致性设计模式的公共方法将内部域暴露 称为属性。 众 所周知, 属性 名称符合这种 模式, 其他 Java 类可以通 过自省机制发 现和操作这 些 JavaBean 属性。 JSPs 和 . ASPs Microsoft 和 Sun 都提供它们各自品牌 的服务器页面。 Sun提供 JavaServer Pages (JSP)而 Microsoft 提供 Active Server Pages (ASP)。 JSP 和 ASP 都设计来时 开发者能从后端系统产生动态页面。 虽然表面看起来相似, ASP 和 JSP仍有一 些不同之处: JSP 是平台独立性的,一次编写,随处运 行; 开发者通过 Java Community Process(JCP)指引方向; JSP 开发者可以可以通过定指标记扩展 JSP标记集; JavaBeans 和 Enterprise JavaBeans (EJB) 可以和 JSP一起使用,增强可重用 性和减小维护。 JSP 可以存取其他一些 Java 库,包括 Java 数据库 Connectivity (JDBC), Java Mail, Java Message Service(JMS),以及 JNDI。 JSP 遍译成二进制类文件,不需要在每次 请求时进行解释; JSP 有广泛的支持,包括工具,容器和服 务器; JavaBean 设 计模式提供两种类型的方式来访问 bean的内部状 态: 访问器 ( accessor) 用 来读 JavaBean的状态,修改器( mutator )用来改变 JavaBean的状态。 Mutator 通常以小写的 set 前缀开始,后跟属性名。 属性名的第一个字母必须大写。返 回值通常是 void,因为 mutators 仅仅改变属性的值,而不返回它们。简单属性的 mutator 在 其方法体重可能只有一个参数,可以是各种类型。 Mutator 也可根据其前缀称为设置器 setters 。例如, 对 Double类型的属性 weight的 mutator方法体可能是: 该你做的 整本书都在写要编写你自己的 JSP 标签,这里是这个过程的一个快速总揽: 1 编写一个累,通过实现实现 doStart()或者 doEnd()方法来实现 javax.servlet.jsp. -tagext.TagSupport 或者 javax.servlet.jsp.tagext.BodyTagSupport 接 口。这些方法获得一个 JspWriter对象,你可以用它来输出你需要的 HTML内容。 2 创建一个标签库描述文件 (TLD)来将你的新建的类,映射到一个标签名称。 3 在你的 Web应用描述符 (web.xml)中定义你的 <taglib> 元素。通过在 JSP页面的顶部 放置下面的语句: %@taglib uri="/tags/app.tld prefix="app" % 来告诉 JSP页面泥浆使用你自己的标签库。 4 这个语 句导入 将在本页中 使用的标签 库, 并分配它 一个前缀 。 关于更多细节 , 请参考 JSP 标签库技术页面。 public void setWeight(Double weight) 相似的设计模式也用于访问器方法的创建。 Accessor通常以小写的 get,为前缀,后跟属 性名。 属性名的第一个字母必须大写。 返回值必须匹配相应的修改器方法的参数。 简单属性 的 Accessor在其方法体中不能接受参数。同样,访问器 accessor 也经常称为获取器 getter。 属性 weight 的访问器方法体可能是: public Double getWeight() 如果访问器返回一个逻辑值, 这种情况下有个变体模式。 不使用小写的 get,逻 辑 属 性 的访问器可以使用小写的 is前缀, 后跟属性名。 属性名的首字母必须大写。 返回值肯定是逻 辑值,不管是 boolean 还是 Boolean。逻辑访问器在其方法体中不能接受参数。 On属性的逻辑访问器 的方法体可能是: public boolean isOn() 在使用 JavaBean时,规范的方法体扮演了极为重要的角色。其他组件可以使用 Java 的 反射 API 通过查找前缀为 set, is, 或者 get的方法 来发现 JavaBean的属性 。 如果一个组件在一 个 JavaBean中 发现一个这样的方法, 它就知道这个方法可以用来访问或者改变 JavaBean的属 性。 Sun 引入 JavaBeans是为了 工作于 GUI组 件 , 但它们已经用在 Java 开发的各 个方面, 包 括 Web应用。 Sun 的工程 师开发了 JSP 标签的扩展类时, 他被设计来可以和 JavaBeans一起 工作。 一个页 面的动态数据可以当作一个 JavaBean来传递, 并且 JSP 标记可以随后使用 bean 的属性来定制页面的输出。 1.3.7 Model 2 Servlet/JSP 规范的 0.92 版描述了在一个应用中使用 servlet 和 JSP 的架构。在其后的 规范中,Model 2 这个叫法消失了 , 但它已经在 Java web 开发人员中非常通用了。 根据 Model 2,servlet 处理数据存取和导航流, JSP 处理表现。Model 2 使 Java 工 程师和 HTML 设计者分别工作于它们所擅长和负责的部分。Model 2 应用的一部分发生改 变并不强求其他部分也跟着发生改变。HTML 开发人员可以改变程序的外观和感觉,并不 需要改变后端 servlet 的工作方式。 Struts 框架是基于 Model 2 的架构。它提供一个控制器 controller servlet 来处理导航 流和一些特殊类来帮助数据存取。随框架也提供一个充实的标签库,以使 Struts 易于和 JSP 一起使用。 1.4 Struts开始于三万英尺 抓紧你的帽子! 既然我们已经讲了一些基本知识,我们现在可以进行一个Struts 的飞速之旅了。在我们 打开框架吃到一些果子之前,我们先了解一个大概模样。 Struts 使用 Model 2 架构。Struts 的ActionServlet 控制导航流。其他Struts 类,比如 Action, 用来访问业务逻辑类。当 ActionServlet 从容器接收到一个请求,它使用URI (或者 路径“path”) 来决定那个Action 将用来处理请求。一个 Action 可以校验输入,并且访问业务 层以从数据库或其他数据服务中检索信息。 为校验输入或者使用输入来更新数据库, Action 需要知道什么指被提交上来。并不是 强制每个Action 从请求中抓取这些值,而是由 ActionServlet 将输入绑定到JavaBean中。 输入 bean是Struts ActionForm c类的子类。ActionServlet 通过查找请求的路径可以决定使用 哪个ActionForm ,Action 也是通过同样的方法选取的。ActionForm 扩展 org.apache.struts.action.ActionForm类。每个都必须以HTTP 响应进行应答。 通 常, Struts Action 并不自行加工响应信息,而是将请求转发到其他资源,比如JSP 页面。 Struts 提 供一个ActionForward 类,用来将一个页面的路径存储为逻辑名称。当完成业务逻辑后, Action 选择并向Servlet 返回一个ActionForward 。Servlet 然后使用存储在ActionForward 对 象中的路径来调用页面完成响应。 Struts 将这些细节都绑定在一个ActionMapping 对象中。每个ActionMapping 相对于一 个特定的路径。当某个路径被请求时, Servlet 就查询ActionMapping 对象。ActionMapping 对象告诉servlet ,哪个Actions, ActionForms, 和 ActionForwards 将被使用。 所有这些细节,关于Action , ActionForm, ActionForward , ActionMapping ,以及其 他一些东西,都在struts-config.xml 文件中定义 。 ActionServlet 在启动时读取这个配置文件, 并创建一个配置对象数据库。在运行时, Struts 应用根据文件创建的配置对象,而不是文件 本身。图 1.1 显示了这些组件是如何一起工作的。 图 1-1 Struts组件 不管你相信与否,你已经知道了足够的东西,可以组装一个简单的 Struts应用了。它并 不需要做很多工作,但它显示了 Struts实际是如何工作的。 1.4.1 建立简单的 Struts应用 开发者进行开发, 我们大多数则从例子中学习最好的方法。 虽然我们花了几乎一页不到的篇 幅来讲 Struts是如何工作的, 我们也需要建立一些什么, 以便你可以看到如何完成这个工作 。 完成了这章,我们将建立一个非常简单但全功能的 web 应用。这个程序将用来注册用户和 用户密码。一旦你完成它,你将接触到部署你的 Web应用所需要的所有 Struts组件。 1.4.2 跳到开始开发了 当我们渴望要创建点什么时, 接下来再设置环境并遇到障碍的时候就可能让所有人都觉得受 挫。根据这章所述,你所需要的是一个 Java Development Kit (JDK),一个 web 容器(比如 Tomcat), 以及一个简单的文本编辑器。 如果你还没有一个 Java 开发环境 和 web 容器 , 下 面 是你该做的: 下载并安装 JDK 1.4. 下在并安装 Tomcat 4 . 校验 Tomcat 是否工作正常。 1.4.3 落到实处 我们的第一个 Struts程序 将是一个用户注册程序。 用户将看到一个注册屏幕, 包含 3个字 段: 用户名, 密码和密码确认。 成功的注册要求两次密码相符。 如果注册成功, 控制将转向 一个页面, 显示注册成功 successful!.。 如果两次输入密码不同, 控制流将转向一个显示失败 的页面。 这个简单的练习将展示以下内容: λ 创建 HTML 表单; λ 从 HTML 表单获取输入; λ 处理输入(业务逻辑); λ 根据动态输入改变控制流; 为完成这个程序,你需要建立: λ 一个ActionForm λ 一个Action λ struts-config.xml 文件 λ 三个页面 就这些! 1.4.3.1 创建 ActionForm ActionForm 是一个JavaBean ,扩展了 org.apache.struts.ActionForm类。这个 对象捕获通过请求传送的输入。当浏览器提交一个表单,它在请求中为每个表单中的字段创 建一个参数。 ActionForm 针对每个HTML 表单中的字段具有一个对应的属性。 ActionServlet 匹配请求中的参数和ActionForm 中的属性。当匹配好后,ActionServlet 为属性调用setter 方 法,并将请求中的值传入。在我们的练习中,表单中的username 字段需要一个 setUsername(String)方法。password 字段需要setPassword1(String) 和 setPassword2(String)方法。这些方法负责组装隐藏在RegisterForm JavaBean中的实例 变量。RegisterForm 的源代码显示在清单1 中。 package app; import org.apache.struts.action.*; public class RegisterForm extends ActionForm { protected String username; protected String password1; protected String password2; public String getUsername () {return this.username;}; public String getPassword1() {return this.password1;}; public String getPassword2() {return this.password2;}; public void setUsername (String username) {this.username = username;}; public void setPassword1(String password) {this.password1 = password;}; public void setPassword2(String password) {this.password2 = password;}; } 代码清单 1.1 RegistrationForm 创建一个文件,取名为RegisterForm.java ,内容示于代码清单1.1。存储在 < Base Directory>/webapps/register/WEB-INF/classes/app下。默认情况下, <Base Directory > 可能是 C:/PROGRAM FILES/APACHE TOMCAT 4.0。对于其他容器, 使用其classes 目录的路径来部 署我们的Register 程序。 1.4.3.2 创建 RegisterAction Action 一个 Java 类,扩展了 org.apache.struts.Action。ActionServlet 组装 ActionForm ,然后将其传递给Action 。Action 通常负责输入校验,存取业务信息,以及决 定向Servlet 返回哪个ActionForward 。 现在,创建一个文件,命名为RegisterAction.java ,其内容为代码清单1.2的内容: package app; import org.apache.struts.action.*; import javax.servlet.http.*; import java.io.*; public class RegisterAction extends Action { public ActionForward perform (ActionMapping mapping, ActionForm form, HttpServletRequest req, HttpServletResponse res) { // ①Cast the form to the RegisterForm RegisterForm rf = (RegisterForm) form; String username = rf.getUsername(); String password1 = rf.getPassword1(); String password2 = rf.getPassword2(); // ②Apply business logic if (password1.equals(password2)) { try { // ③Return ActionForward for success UserDirectory.getInstance().setUser(username,password1); return mapping.findForward("success"); } catch (UserDirectoryException e) { return mapping.findForward("failure"); } } // ④Return ActionForward for failure return mapping.findForward("failure"); } } 代码清单 1.2 RegisterAction.Java 将文件存放在< Base Directory>/webapps/register/WEB-INF/classes/app 目录下。 虽然很简单,但是我们的RegisterAction 却做了Action 的典型事情。在①,输入 ActionForm 被转换为RegisterForm 。我们就可以获取username, password1, 和 password2的内容。如果两次密码匹配②,我们将用户添加到 UserDirectory 中③,并返回 与success 对应的ActionForward 。UserDirectory 是一个 helper 类,它记录usernames 和 passwords 到一个标准的属性文件之中。否则,返回与failure 对应的 ActionForward 。 当我们在下一步创建struts-config 文件时,我们将标识代表success 和 failure 的 ActionForward 对象。 注: Struts 1.1提供另外一个进入方法, 名为 execute。这个方法提供更好的意外处理,但不和 Struts 1.0 的 perform方法一样。 在这里我们将使用 perform 方法,以使我们的例子可以运行在两个版本之下。 1.4.3.3 创建 Struts 配置文件 (struts-config.xml) struts-config.xml 文件包含了ActionServlet 需要用来处理对应用请求的详细信息。为了 练习,我们创建一个空壳的struts-config.xml 文件。你需要做的是填入一些细节。 文件存储在< BaseDirectory>/webapps/register/WEB-INF/目录下,需要改变的是: 首先,添加/register 到<action>元素的 path 属性。 ActionServlet 使用 Web容器转发 给它的URI 来选择正确的Action 类。 URI 和ActionMapping 的path 属性匹配。这里,请求 给出的路径必须在去除前缀和后缀后和/register 匹配。前缀或后缀通常是/do/ 或 者 .do。 我们的练习中,将后缀设置为.do。当URI 具有一个.do 扩展名,容器就知道将请 求转发给ActionServlet 。Struts 会自动去除 扩展名,所以我们在配置时不必加上它们。 下一步添加 registerForm 到<action> 元素的 name 属性。 <action> 元素使用name 属性来识别哪个ActionForm 将被创建,并将提交的表单组装给他。 然后,添加 app.RegisterAction 到<action> 元素的 type 属性。ActionServlet 使用这个属性来识别将用来处理请求的 Action 类。 接下来,在<forward> 元素下,添加 success 到 name 属性,并且 /success.html 到 path 属性。最后,再在另一个<forward>下添加 failure 到 name 属性, /failure.html 到 path 属性。 这些元素将创建ActionForward 对象,我们将用它来选择程序的控制流。 <forward> 元 素定义了在RegisterAction 中使用的逻辑名称之间的关联。 Struts-config.xml 源代码见代码1.3。 <?xml version="1.0" encoding="ISO-8859-1" ?> <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.0//EN" "http://jakarta.apache.org/struts/dtds/struts-config_1_0.dtd"> <struts-config> <form-beans> <form-bean name="registerForm" type="app.RegisterForm"/> </form-beans> <action-mappings> <action path="/register" type="app.RegisterAction" name="registerForm"> <forward name="success" path="/success.html"/> <forward name="failure" path="/failure.html"/> </action> </action-mappings> </struts-config> 代码清单 1.3 Struts-Config.XML Struts框架将struts-config.xml 文件视为部署描述符使用。它使我们可以创建和改变 ActionMapping 和路径的关联而不用重新编译java 类。我们也可以改变页面之间的连接,而 不改变JSP模板。 1.4.3.4 创建页面 最后的步骤是创建success.html, failure.html, 以及register.jsp 页面。 3个文件的源代码如下, 见代码清单1. 4,1. 5,1.6。存 放 在 <Base Directory>/webapps/register 目录下。 <HTML> <HEAD> <TITLE>SUCCESS</TITLE> </HEAD> <BODY> Registration succeeded! <P><A href="register.jsp">try another?</A></P> </BODY> </HTML> 代码清单 1.4 Success HTML <HTML> <HEAD> <TITLE>FAILURE</TITLE> </HEAD> <BODY> Registration failed! <P><A href="register.jsp">try again?</A></P> </BODY> </HTML> 代码清单 1.5 Failure.html <%@ taglib uri="/WEB-INF/struts-form.tld" prefix="form" %> <form:form action="register.do"> UserName:<form:text property="username"/><br> enter password:<form:password property="password1"/><br> re-enter password:<form:password property="password2"/><br> <form:submit value="Register"/> </form:form> 代码清单 1.6 Register.jsp 这时,所有构建一个简单 Struts应用的工作都做完了。 现在,试一下运行如何。 如果, Tomcat 没有运行,启动它。 在浏览器中输入以下地址: http://localhost:8080/register/register.jsp 图 1-2 Registration 程序主界面 1.4.4 再看看 让我们再回头看看,我们做了什么,它如何工作的,以及我们还需要做些什么。 1.4.4.1 做了什么 建立 Register应用我们实际上完成了以下内容: λ RegisterForm ActionForm λ RegisterAction Action λ 3个页面 1.4.4.2 它如何工作 当你知识浏览器到地址 http://localhost:8080/register/register.jsp, Tomcat按通常情况加工这个 页面。 输入 username 和 password,点 击 Register 提交 页面。 浏览器在请求中 post表单的内容。 容器检查请求将送到哪一个注册的路径去。然后请求被转发到 ActionServlet ,并由 RegisterAction来处理。在返回成功或失败之前, RegisterAction 校验输入的有效性。最后 servlet将控制根据返回的 ActionForward转发到响应页面。图 1.3 是程序结构。 图 1-3 Registration程序结构 再看看代码 1.6中的 Register.jsp, 可以看 到表单是提交给 URI /register。 但是如果你观察正 提交的页面, 会发现是提交给 register.do。 Struts 表单标记自动加上 .do 前 缀 。 当我们设置程 序骨架时,我们要求所有匹配 *.do的请求都传递给 ActionServlet。 当接收到一个请求, ActionServlet 做 的第一件事情就是查找 ActionMapping来匹配请求 的路径。 ActionMapping是 Struts根据 struts-config.xml 文件创建的 JavaBean.。我们给出了 XML 的具体文件,但运行时, Struts 引用的是对象,而不是 XML 文档。 从代码 1.3中可以看到,我们使用这个元素创建了一个到 path /register的映射: <action path="/register" type="app.RegisterAction" name="registerForm" input="/register.jsp"> 然后, ActionServlet 检查是否有 name 属性和这个映射相关: <action path="/register" type=" app.RegisterAction" name="registerForm" input="/register.jsp"> 这里 /register 映射通过 registerForm的名称标识了一个 form bean。 ActionServlet 使用这 个名字属性来查找相应的ActionFormBean 对象。由From Bean标识的类型(type )用来创建 ActionForm 对象: <form-beans> <form-bean name="registerForm" type="RegisterForm"/> </form-beans> <action-mappings> <action path="/register" type=" app.RegisterAction" name="registerForm" input="/register.jsp"> <forward name="success" path="/success.html"/> <forward name="failure" path="/failure.html"/> </action> </action-mappings> 这里,servlet 将使用RegisterForm 类: <form-beans> <form-bean name="registerForm" type="app.RegisterForm"/> </form-beans> 一旦RegisterForm 被实例化, ActionServlet 就试图为请求中的输入域调用 RegisterForm 的setter 方法。在例子中,它们是setUsername, setPassword1, 和 setPassword2。如果某个setter 方法不存在,该参数就会被忽略。 ActionMapping 对象的type 属性是ActionServlet 用来实例化ActionForm 的类名。这 里,将使用你创建的RegisterAction 对象。RegisterAction 对象的perform 方法被调用,并 传递一个到在前面一步中创建和组装的RegisterForm 的引用: <action path="/register" type="app.RegisterAction" name="registerForm" input="/register.jsp"> <forward name="success" path="/success.html"/> <forward name="failure" path="/failure.html"/> </action> 依赖于 perform 方法的 执行结果, 将返回两个 ActionForward之一。 findForward() 方法使用一个 String 参数来查找 与 name属性相匹配的 forward 对象 。 而 path 属性则 由 ActionServlet 用来决定用哪个页面来完成响应: <forward name="success" path="/success.html"/> <forward name="failure" path="/failure.html"/> 1.4.4.3 什么还没做 为了尽快入门, 我们省略了一些内容。 我们没有编译源代码, 而依赖于这个程序绑定的 早就编译了的类文件。 我们想给你一个机会来开始开发 Struts程序, 而没有心烦的日常事务, 比如 Ant 构建文件。 在第 3章, 我 们开发另外一个程序来展示框架的其他特征。 在那里, 我们也介绍 Ant和一 个编辑器 jEdit的入门。我们业开始深入介绍 Struts组件。 本书的第 2部 分非常详细的讨论了框架组件。 在第 4部分, 我们 将它们和起来开发一个实 际的应用 Artimus。 1.5 小结 在本章,我们介绍了 Struts应用框架。并介绍了基本知识,关于 HTTP, CGI,, Java servlet, JSP,以及 JavaBeans。我们也说明了 Model 2 应用架构,以及他如何用来结合运用 servlets 和 JSPs 在同一个应用之中。 本章末尾, 我们快速建立第一个简单的 Struts应 用 。 现在你已经有关于 Strtus Web应用 程 序象什么的初步印象,下一章我们将更深入的讨论 Strtuts框架的理论和具体实践。 2 深入 Struts架构 本章包括 λ 介绍应用框架 MVC和 Model 2 λ 理解 Struts 原理 λ 使用 Struts 控制流 λ 讨论 Struts 的优缺点 A common mistake that people make when trying to design something completely foolproof is to underestimate the ingenuity of complete fools. —Douglas Adams, Mostly Harmless 2.1 Talking the talk 本章深入探讨Struts 框架,以及它能给你的应用开发所带来的好处。我们相信一旦你也 能“随便谈谈 ”web 架构和设计,你就可以很好的在你的应用中使用Struts 。 为了能对Struts 架构有个充分的全面印象,我们将总体介绍Struts 的控制流和它处理请 求- 响应事件循环的方式。只有彻底理解这个处理原理才能最好的在应用中使用这个框架。 选择一个web 应用框架不应该是个漫不经心的决定。很多人将使用这本书,特别是用 这章的内容来评价Struts 是否适合它们的项目。因此,我们在这章的最后将有一个关于Struts 优缺点的公正的看法,并阐明其总体性能。 Struts 设计针对专业开发人员。为做出正确的决策,专业人员应该知晓工具的能力和限 制性。 2.2 为什么我们需要 Struts 今天的web 应用基本上都是代表共同理念的关键组件。通常,开发团队需要在有限的 时间里创建应用,然它们不得不正确的构建,并能持续构建它。 Java web 开发人员已经有一些工具可用来建立表现层,比如JavaServer Pages 和 Velocity 模板。也有一些机制来处理数据库—如 JDBC 和 Enterprise JavaBeans (EJBs)。但我们 用什么来将它们集合在一起? 我们已经有了型材和砖墙…还缺什么? 2.2.1 退一进三 在上世纪80年代,当GUI 被发明时,软件架构认为,应用具有3 个主要部件: 管理数据的部件,创建屏幕和报表的部件,以及处理用户交互和子系统交互的部件 [Ooram]。 在90年代早期,ObjectWorks/Smalltalk 编程环境将这个3 角结构引入为一个开发框架。 按Smalltalk 80 的说法,数据系统称为模型 Model,表现系统称为视图 View, 而交互系统称为 控制器Controller .。许多现代开发环境,包括Java 的Swing, 都使用Model/View/Controller (MVC) 架构 作为它们的基础架构 。 图 2-1 MVC架构 Java web 开发者已经有很多有用的工具,比如 JDBC 和 JSP, 作为 Model 和 View的 手段,但作为控制器的工具和组件在哪? 2.2.2 进入 Struts Struts 的核心是一个 MVC风格的控制器。 Struts 控制器搭起了 Model 和 View之间的桥 梁。框架也包括开发人员想用来开发可伸缩的、先进的应用的其他组件。 Struts是一个“隐 藏支柱” 的集合, 帮助开发人员将分散的材料如数据库和页面, 结合成一个整体的应用程序。 2.2.3 Struts控制器组件 Struts 控制器 组件是一个可编程的组件集, 允许开发人员定义它们的应用如何准确的和 用户进行交互。 这些组件在逻辑名称后隐藏了令人讨厌的、 繁琐的实现细节。 开发人员可以 一次性编写这些实现细节, 然后转头考虑它们的应用应该做什么, 而不是考虑应用应该如何 做。 用户通过超链接和 HTML form与 Web应用程序进行交互。 超链接引导页面显示数据和其 他内容,如文本和图像。表单通常通过一些定制动作向应用提交数据。 图 2-2 主要Struts组件 如图 5中所示, Struts提供了开发人员可用来定义超链接,表单,和定制动作这些交互 的组件。 我们已经使用这些组件在第 1章创建了 一个入门程序。 第 3章, 我们还要用他们来创 建另一个程序。然后,在第 4章,我们将讨论这些组件的详细配置。随后的章节,将详细讨 论如何将每个组件用在你的程序之中。在第 4章,我们将展示如何在运行的程序上下文中使 用这些组件。但是,因为这章是架构性的总体介绍,所以我们继续介绍 Struts的主要部件。 注 Struts 组 件是通过 一个 XML文件进行配置的。 实践中, 配置项是 Struts框架 的有 机组成部 分。 为了 将他们糅合在一起,我们在讲述它们的时候,会展示每个组件的 XML配置项。 2.2.3.1 超链接 对应用开发人员来说, 超链接是指向应用中某些资源的路径。它们可能是web 页面, 或者是定制动作。 它可以包含特殊的参数。在Struts 中,开发人员可以定义超链接为一个 ActionForward.。 这些对象都有个逻辑名称和一个path 属性。这使得开发人员可以设置path 然后通过 名称来引用ActionForward 。ActionForward 通常在一个XML 文件中定义,这个配置文件在 Struts启动时读入。 Struts 使用 XML 定义来创建Struts 配置,包括一个ActionForward 列表。 可用来创建到欢迎页面链接的ActionForward 对象的XML 元素看起来可能像: <forward name="welcome" path="/pages/index.jsp"/> 这个元素创建一个 ActionForward JavaBean , 其name 属性设置为welcome, path 属 性设置为/pages/index.jsp. JSP 页面和其它组件就可以引用welcome 转发。Struts 框架将查找welcome ActionForward bean 并检索path 属性来完成这个超链接。这时开发人员可以改变链接的目 标而不用改变所有引用该链接的组件。在大多数Web应用中,像这样的细节被硬编码到JSP 或 Java code 中,使维护变得困难并且容易发生错误。在Struts 应用中,这些细节可以通过 应用配置来改变,而不用触及到具体的页面和Java 类。 2.2.3.2 HTML表单 web 协议,HTTP 和 HTML ,提供了一个从表单提交数据的机制,但却把数据接收作 为一个难题留给了开发人员。Struts framework 提供了ActionForm 类, 它设计来处理从 HTML 表单来的输入,校验输入,重新显示表单以供用户进行修订(如果需要),以及伴 随相应的提示和信息。ActionForm 其实是具有一些标准方法来管理校验和修订循环的 JavaBean。 Struts 自动匹配 JavaBean 属性和 HTML 表单控件的属性。开发者定义ActionForm 类,余下的就交给Struts 。 例如,这个类将自动用HTML 表单中同名的属性来组装username域: public final class LogonForm extends ActionForm { private String username = null; public String getUsername() { return (this.username); } public void setUsername(String username) { this.username = username; } } 其它属性也会根据表单中的每个域被自动加入。这使其它组件可以从标准的JavaBean 取得它 们想要的属性。所以,完全不需要详细分析HTTP 请求。 ActionForm 是按通常的Java 类创建的。Struts 配置通过一系列描述符引用ActionForm 类: <form-beans> 和 <form-bean> 元素。 <form-bean> 元素是框架用来识别和实 例化ActionForm 对象的描述符: <form-bean name="articleForm" type="org.apache.artimus.struts.Form"/> Struts 配置列出它使用的ActionForm bean 的清单,并给每个bean 一个在应用中被引用时 的逻辑名。 1.0 和 1.1 Struts 1.1中, ActionForm 可以使用 Map (java.util.Map)来存储属性名,而不是单独定义它们。 一种新的 JavaBean, DynaBean, 可以在 Struts 1.1和后来的版本中使用。 你可以使用 XML元素来标识 DynaActionForm的属性。这使你可以使用 Struts配置文件来定义 ActionForms。 2.2.3.3 定制动作 HTML 表单使用 action 参数来告诉浏览器将数据送到哪里? Struts 框架提供相应 的Action 类来接收数据。框架会自动创建、组装、校验和最后处理Action 对象对应的 ActionForm。这样,Action 就可以直接从ActionForm bean取得它需要的数据。比如下例: public final class LogonAction extends Action { public ActionForward perform(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { MyForm myForm = (MyForm) form; // ... return mapping.findForward("continue"); } } Action 根据返回到控制器的ActionForward 对象作出控制流的决定。这允许Action 选 择一个逻辑名称,象continue 或者 cancel, 而不是系统路径。 为保证扩展性,控制器也传递当前的请求和响应对象。实际上, Action 可以做所有Java Servlet 可以做的事情。 1.0 vs 1.1 在 Struts 1.1中,新的 execute 方法应该是首选,而不是我们例子中的 perform方法。 perform 已经不被推荐,但还保持向后兼容,予以支持。 execute 方法体允许更好的例外处理。新的 ExceptionHandler 在第 9章讲述。 关于 Action 对象的详细信息,见第 8章。 除了ActionForward , ActionForm,和 Action 对象, Struts控制器层还提供几个特殊的组件, 包括ActionMappings 和 ActionServlet。 Struts 也提供在控制器层的应用本地化。 2.2.3.4 ActionMappings 在一个web 应用中,每个资源必须通过URI 来进行引用。 资源包括HTML 页面, JSP 页 面,和定制动作。为了给定制动作一个URI , 或者说路径,Struts 框架提供了一个 ActionMapping 对象。 象ActionForward 和 ActionForm 一样, ActionaMapping 通常也在 XML配置文件中定义: <action-mappings> <action path="/logonSubmit" type="app.LogonAction" name="logonForm" scope="request" validate="true" input="/pages/logon.jsp"/> </action-mappings> 这也允许同一个Action 对象定义为不同的ActionMappings 。例如, 一个映射要求校验 而另一个映射不要求校验。 2.2.3.5 ActionServlet Struts ActionServlet 完全在幕后工作,它将其他组件绑定在一起。虽然它也可以子类化, 但大多数Struts 1.0 的开发人员将ActionServlet 处理为一个黑盒:他们只是配置它,然后让 它自己工作。 在Struts 1.1 中,ActionServlet 是比较易于扩展的。第9 章将讨论Struts 1.1 ActionServlet 新的扩展点和配置选项。 2.2.3.6 本地化 Web 应用也通过各种提示和信息与用户进行交互。 Struts 组件均有内建的本地化特征, 以便Struts 应用可以为国际化用户使用。我们在此书中贯穿使用本地化特征。本地化的详细 讨论见第13章。 2.2.4 用 Struts开发 Web应用 要使用Struts 开发web 应用,开发人员将需要的超链接定义为ActionForward , HTML 表 单定义为ActionForm ,定制的服务器端动作定义为Action 类。 需要访问JDBC和EJB 的开发人员也可通过Action对象进行。这样,表现层不需要和Model 层打交道。 Struts Action 对象将收集 View 需要的 数据, 然后将它们转发到表现页面。 Struts 提供 JSP 标记库,它们和 JSP 页面一起使用,简化 HTML 表单和存取 Action 要转发 的其 它数据。 其它表现机制, 比如 Velocity templates, 也可用来访问 Struts 框 架 , 来创建动态的 web 页面。这种处理流程入下图: 图 2-3 数据回传给视图 关于和 Struts中如何使用各种数据系统,见第 14章。 第 10章和第 11章学习如何创建 Struts的表现页面。 在深入 Struts 架构前,让我们看看一个 Web应用框架必须说明的问题。 2.3 为什么需要框架 第 1章,我们介绍了应用框架,简短讨论了为什么框架很重要。但为了真正理解一个解 决方案, 我们需要了解问题所在。 为 web开发应用虽然是值得的, 但也要迎接各种挑战。 让 我们快速看看是什么使 web 开发富有挑战。 2.3.1 Web—永无休止的修补 Web 开发者 受到两种 web缺陷的影响。首先,我们希望使用浏览器作为客户端。其次, 我们必须使用 web协议进行通讯。 Web 浏览器通过 HTTP协议通信,并用 HTML显示页面。 Web浏览器发送 HTTP请求, 并加工和显示它收到的响应。 在处理很少改变的预先编好的页面时, 这是个很好的平台。 但 我们大多都是编写动态程序, 页面针对不同的用户是不同的。 虽然有一些现成的动态特征的 手段, web 仍然受到 HTTP/HTML 的制约。 如下表所示: web 协议和客户端的限制,确定了如何编写 Web程序。 表格 2-1 HTTP/HTML的限制 限制 导致的困难 协议 缺省情况下 ,H TTP接收 来自于网络 上各种客户 端的连接。 但各种服 务器间的行为是不一样的; 首先,H TTP用简单的文 本域来传输 数据。传输 二进制文件 需要复杂 的协议扩展 HTTP协议是无状态的,需要额外的努力来跟踪程序的用户 HTTP信赖并期望客户能提供正确的信息 客户端 浏览器是个独立的程序,处于应用的控制之外 所有的浏览器都是不一样的,都只支持官方标准的一个子集 从浏览器的 输入可能是 不正确或者 不完整的。 甚至可能是 敌意的 , 和对程序有害的信息 HTML不能够建立一些在桌面环境中有的接口元素 用缺省数据建立HTML控件对应用来说是个考验 很不幸,这种状况现在并没有些许改变。 Web开发人员在想战胜挑战时必须看到这些缺 陷。因为对编写强壮的Web 应用有太多障碍,使用框架便显得至关重要,免得你的应用陷入 无休止的工作和改进之中。 在开发Web 应用时我们面临的挑战是很巨大的。但同时也是值得的。HTTP 协议和 HTML 客户端使所有的人都可以访问你的应用。没有其他哪个平台能声称这样。 2.3.2 Servlet 解决方案 如第一章所述,Java Servlet 平台[Sun, JST] 扮演了一个基本框架,为Java web 应用提 供了大量的能力。Servlet 提供了一个处理HTTP 请求和确保响应的基本接口。它在HTTP 之 上构建了一个“ 会话” 上下文,帮助跟踪应用程序的用户。当然它也提供其他的上下文,帮 助应用传输数据到浏览器或者应用中的其他servlet 。 Java web 应用也具有对基本安全特性的 统一访问,而这些安全特性在不同的服务器上的管理是不一样的。 为了将这些内容集成在一起, Servlet规范引入了一个容器来管理它们。容器也可以提供 其他服务,比如作为一个JSP的处理器。Servlet 容器可以包含它自己的web server ,也可以 简单的作为一个现存服务器的附属组件。 对数据库访问, Java 应用在其建议中有另外一个通用的框架: JDBC。开发者可以写 标准的SQL 接口,而将烦人的细节留给适配器来处理。这使得可以很容易的改变数据库厂 商,而不用重写源代码。 为了取得远程服务器的高性能数据库访问, web 开发人员可以使用EJB 平台。大多数 Java 应用框架,包括Struts, 都可以和EJB 一起使用。 总之,这使得基于Servlet 的web 应用非常轻便,并相对易于编写和维护。 Servlet 和 JSP 在编写应用中扮演了完全不同的角色。 象Strtus 这样的Java web 应用框架构架于Servlet 之 上,给开发者提供一个无缝的集成环境。 2.3.3 Servlet 框架 大多数,不是全部, Java web 框架使用 Sun Servlet 平台为基础。这些框架都绑定一 些预制的servlet ,你可以插入到你的应用中去。框架也包括一个类结构树,这些类你可以在 你的应用中实现或者扩展。通常, 应用框架的目标是帮助你将你需要的数据从浏览器发出, 进入到编程结构之中,这样你的应用就可以使用它—或者从编程结构中发出,进入到浏览器 之中,这样你就可以看到。 一些框架, 如 Turbine [ASF, Turbine] ,也提供 helper 类来运行于 JDBC 数据库。其他框 架, 如Struts, 则是模型中立的。它们既不阻碍数据库访问,也没有提供帮助。而某些框架, 如 dbForms [dbForms],则专注于数据库访问,而将其它任务留给开发人员或者其他框架。 2.3.3.1 通用框架策略 如下图所示, Java web应用框架使用一些通用技术来帮助产品易于设计、 编写和维护, 包括: 外部配置文件 提供开发人员不想嵌入源代码中的实现细节。 中心控制器 提供一种方式, 将 HTTP 请求排入一个易于管理的队列。 这种设计有时叫前端控制器模 式( Front Controller [Go3]) 外部表现系统 让不同的人同时工作在同一应用的不同部分。如, Java工程师可以工作在和中心控制 器相关的类, 而页面设计者则专注于 JSP。除 了 JSP, 其他表现系统, 如 Velocity Templates 或 者 XLST, 都可以和 Struts一起使用。 框架通常有各种组件, 但基本上都共享这个特性。 这些公共策略早已在一些书象 Design Patterns [Go4] 和 Core J2EE Patterns [Go3]给出的范例中根深蒂固了。 许多开发者在讨论并 使用这些模式,但也许还没有第一次在 Web环境中实现它们。使用设计模式,如 MVC,使 你可以容易的通过做正确的事情来构建你的应用。 在桌面环境中使用设计模式的优点已经众 所周知了,但在 Web环境部署这些模式却对大多数开发者来说还是不确定的。 图 2-4 通常使用配置文件,控制器和表现系统的框架 2.3.4 黑盒 -白盒统一体 框架有时分为两极标有白盒和黑盒的统一体 [Fayad]。白盒框架严重依赖于面向对象的 语言的特征, 如继承和动态绑定。 黑盒框架则注重定义可插入组件的接口, 然后基于这些接 口提供基本的起始组件。 接口和基本组件通常提供热点 ( hotspot) 方法 , 这个方法可以直接 使用或者重写后提供特别的行为。 定义 热点有时也称为是灵活点或者扩展点,它其实是框架中的一些位置,在这里可以加入一些代码来定制框架。热 点 (热点 系统) 每个应用 中可以 被框架所 支持的 不同特征 。 本 质 上 , 它 们 代表框 架所解决 的问题 。 许多面 向对 象 框架有一个核心系统和一些热点子系统组成 [Braga, et al] 像许多正在使用的框架, Struts 使 用混合的黑盒和白盒技术。 但总体上, 框架将偏向 统一体的黑盒一端。 黑盒框架通常依赖于设计模式。 Struts 也不例外 。 事实上, 设 计模式通常被用来作为框 架的总体描述 [Johnson]。为保持这个趋势,我们先介绍一下设计模式,以及它们是如何用 在 Struts 框架之中。 2.4 Struts, Model 2,以及 MVC Struts 关于自己要说的第一句话就是:框架 …鼓励应用架构基于 Model 2方法,即经典的 MVC设计模式的变体 这句话打消了一些 web 开发者的疑虑, 对那些还没深入 Model 2 或 MVC的人来说却更加疑 惑了。 事实上,没有深入了解 MVC 和 Sun Model 2,要理解很多 Struts的文章都很困难。 2.4.1 MVC的演化 如 2.1节所说 , Model/View/Controller原本是建立 Smalltalk 应用的框架。框架支持代表 应用状态、屏幕表现和控制流的 3个类,分别叫做 Model, View, 和 Controller。 图 2-5 MVC三角 Smalltalk MVC 框架 在流行的书 Design Patterns: Elements of Reusable Object-Oriented Software[Go4]是作为案例来研究的。 Design Patterns 这本书有四个作者,被称为“*** GoF”。 Design Patterns中的 MVC 例子称赞 通知 /订阅者( notify/subscribe) 协议和观察者 Observer模式 的使用。 例子的基础是, 对同一数据, 系统可能需要不同的显示视图, 比如条 形图、饼图、数据表格等等。这是一个划分应用的精彩理由,经常被重复引用。 图 2-6 The Model data can be used in several d如果ferent views. 在图 2-6所示的例子中,每种视图可能在同一时间显示给不同的用户。应用必须保证在 其下面的数据或者模型改变时视图的更新。 为改变模型, 用户提交一个请求给控制器, 由控 制起来配合改变模型。数据视图必须跟着改变,以反映最近的模型改变状态。 Smalltalk MVC 方案使用 观察者通知模式。 在这种模式下, 每个视图注册为一个模型数 据的观察者。然后模型可以通过发送消息给所有这册观察者,通知它们相关的改变。其为 Smalltalk MVC 框架已经通用化了,他也可以应用它其他平台上面。 2.4.2 Model 2的出现 JSP的意图是使创建动态页面更容易。 JSP 首先是作为 servlet的替代引入的,还有就是 MS的 ASP。 Servlet的强大功能当作易于创建服务器页面的工具提供给开发者。但强大的功 能伴随着巨大的责任。 很多团队发现, 如果他们一不小心, 他们的项目就会因为纠缠如麻的 页面变的容易崩溃。 进一步的特性需要使用复杂的脚本程序。 但脚本程序是非常难于重用的 —除非你在页面间把代码“拷贝粘贴”。 工具页面也可以包括进来, 但他们很难被组织在一起, 并且造成非常丑陋的 “资源” 树 。 有些东西会出错。 很多开发人员很快意识到, JSPs 和 servlets 可以一起使用来部署 web 应用。 Servlet 可以应付控制流, 而 JSP则可专注于讨厌的编写 HTML的任务。 在例行课程中, 结合使用 JSP 和 servlet 开始被称为 Model 2 (单独使用 JSP称为 Model 1)。 当然, 从 Sun哪里仍然没什么新东西 ... 而且很多人 很快指出 JSP Model 2 类 似于经典的 Model-View-Controller 架构。 在很多场合, 现在交互使用 Model 2 和 MVC这 两个词已经很平常了, 虽然还有一些争 论,即一个应用是否是 MVC,以及是否支持经典的观察者通知模式。没有观察者通知的 Model-View-Controller 有时被称为 MVC2 或 Web MVC。 . 2.4.3 应用层 —和视图去耦合 Model 2 被认为区别于 MVC 的一个原因是,观察者 /通知模式不能在 web 环境内工作 的很好。 HTTP 是一个 “拉 ” 的协议 : 客户请求然后服务器响应。 没有请求就没有相应。 观 察者模式需要一种 “推” 协议来进行通知, 以便服务器能在模型改变时将信息推送到客户端。 虽然也有一些方法能模拟将数据推送到客户端, 但这和基本情况相悖, 并且会视为是权宜的 修补。 图 2-7 MVC 通常表示为3个互相连接的组件 图 2-7是典型 的 Model-View-Controller范式, 经常 被表示为: 一个互相连接的三角形。 在 web 应用中维护范式中的“通知改变”部分是非常困难的。 这些东西在所有资源都在一台服务器上, 而且客户端保持一个开放连接的情况下工作得 非常好。 如果资源分布在不同的服务器上, 并且客户端不能维护一个开放的连接情况下, 工 作的并不理想。 许多分布式系统架构, 包括 web应用, 在视图进行状态查询的概念时退缩了。 绝大多数 情况下, 远程应用是按 层模 式 [POSA]设计的。 基本上, 层模式下, 层内的对象可以和同一层 或者相邻层的对象进行通信。 在一个复杂应用中, 这可以在添加组件时, 防止依赖关系呈指 数增长。在设计远程应用时,分层是一个核心模式。 从 MVC 上下 文中, 引入层模式将状态改变和状态查询的职责加于控制器之上, 并伴随 着改变通知。 图 2-8 Web 应用的层模式 ` 如图 2-8 ,分层的 web 应用使用一种比传统 MVC模式更加“扁平”的模式。控制器被 夹在表现层 (View) 和 应用逻辑 (Model)之间。 每个组件的主要职责并没有改变。 流程有轻微改变, 即查询状态和改变通知都必须通过 控制器。 另一个改变是, 当视图, 或 者表现层需要加工动态页面时, 它使用从控制器传递的 数据而不是直接来自于模型层。 这种改变去除了 View 和 Model的耦合 , 允许控制器选择数 据和显示这些数据的视图。 2.4.4 Struts如何实现 Model 2, MVC, 和层 Struts 通过提 供一个控制器 Servlet实现 了 Sun的 Model 2 架构 , 这个控制器可以用来管 理 JSP页面和 其他表现设备之间的流程。 Struts 通 过使用 ActionForward和 ActionMapping来保 证表现层之外的控制流决策来实现 MVC/层 模式。 JSP可以引用一个逻辑目标。控制器组 件在运行是提供准确的 URI。 表列出了 Struts的核心类,即对应的经典的 MVC组件职责。 表格 2-2 核心 Struts 类和MVC的对应 类 描述 ActionForward 用户指向或者视图选择 ActionForm 状态改变的数据 ActionMapping 状态改变事件 ActionServlet 控制器,接受用户请求和状态改变,以及发出视图选择 Action 控制器的一部 分,于模型交 互,执行状态 改变或状态查 询,以及告诉 ActionServlet 下一个选择的视图 除了这些核心类, Struts 使用一些配置文件和视图助手( view helpers)来沟通控制器 和模型。下表列出了 Struts 配置文件和描述了他们在架构中的角色。 表格 2-3 Strtuts配置文件 文件 目的 ApplicationResources.properties 存储本地化信息和标签,以使应用可以国际化 struts-config.xml 存储控制器对象的缺省配置,包括模型支持的用户指向,状态改变, 状态查询 为将 Struts配置数据暴露给视图,框架以 JSP标签的形式提供了大量的助手类,如表: 表格 2-4 Strtus视图助手 标记库描述符 目的 struts-html.tld 扩展 HTML Form的 JSP标记 struts-bean.tld 扩展处理 JavaBean的 JSP标记 struts-logic.tld 扩展测试属性值的JSP 标记 将以上内容放在一起,下表按层列出了Struts 组件: 表格 2-5 Struts 组件,按层索引 视图层 控制器层 模型层 JSP 标签扩展 ActionForwards ActionForm classes ActionMappings ActionServlet Action classes ActionErrors MessageResources GenericDataSource JSP, Velocity 模板, 以及其他表 现系统 各种工具类, 比如 CommonsDigester和 CommonsBeanUtil 开发者提供的其他数据服务和 API 注意,根据层模式 ( 2.4.3), 组件应该只能和相同层和相邻层的组件交互。因此, Model 组 件不能直接和 View组件进行交互。 实践中,控制器与视图的交互通过请求,会话以及Servlet 平台提供的应用上下文进行。 (2.3.2)。 控制器和模型的交互通过文件和存储系统完成 ( 比如装入XML 文档或者属性文件) ,或 者通过其他服务,如TCP, 创建一个到JDBC数据库的连接。 2.5 Struts 控制流 因为web 应用是动态的,所以很难表现“One True Control Flow” 。依赖于环境,不同的 方式下有很多不同的事情发生—特别是在web 应用中。但是事情仍然有一个通用的秩序。 如果你是个Struts ,应用框架,甚至web 应用的新手,这些流程刚开始可能难以跟得上(理 解)。亟待解决的各种问题不一定那么明显。我们将在本书中详细涉及。首先,在介绍树木 之前我们先认识这片森林。你读完此书后,我们建议你再次回来,看看每一部分是如何切合 进这个总图的。 2.5.1 总图 图 2-9 Struts 请求-相应流程 图 2-9以 UML以次序图的方式展示了 Struts 请求 -响应流程。我们来按这个请求 -响应流 程走一遍。 括号内的数字请参照图 11中的相关地方: λ 客户请求匹配 Action URI 样式的路径 (1). λ 容器将请求传递给 ActionServlet. λ 如果这个是模块化应用, ActionServlet 选择响应的模块。 λ ActionServlet 查询路径的映射。 λ 如果映射标明了一个 form bean, ActionServlet 看 是否已经有一个实例, 或者创建一 个新的实例 (1.1)。 如果已 经有一个 form bean, ActionServlet 重 设它, 并根据 HTTP 请求重新组装它。 λ 如果 mapping 的 validate 属性设置为 true, 它将调用 form bean 的 validate 方法 (1.2)。 λ 如果失败, Servlet 将控制转发到 input 属性标明的路径,控制流终止。 λ 如果 mapping 标明一个 Action 类型,如果它已经存在或已经实例化,它将被重用 (1.3)。 λ Action的 perform 或 execute 方法 被调用, 并传递一个实例化的 form bean (或者 null)。 λ Action 可以 组装 form bean, 调用业务 对象, 以及其他需要做的事情。 (1.3.1-1.3.4)。 λ Action 返回一个 ActionForward 给 ActionServlet (1.3.5). λ 如果 ActionForward 指向另一个 Action URI, 重新开始; 否则, 显示页面或者其他 资源, 流程结束。 通常, 结 果是一个 JSP页面, 或者 Jasper, 或其它 类似技术 (非 Struts) 加工的页面。 (2, 3). λ 如果 JSP 使用 Struts HTML 标记 , 并且在请求中看到正确的 ActionForm (1.1),他们 会从 ActionForm中组装 HTML控件。否则, <html:form> 标记将创建一个。从 Struts 1.1开始 , 如果 form 标记自行创建一个 ActionForm ,它将调用 ActionForm的 Reset方法。如果你只是想创建一个空白的表单 (1.1) ,你可以使用标准的 ForwardAction(见第 8 章) 来通过Action 传递控制,然后离开页面。 2.5.2 出色的细节 他们说,恶魔藏在最隐秘的地方。 前面章节的大纲和图示很好的显示了Struts 的概揽,但忽略了重要的细节。让我们更深 入到更好的地方。因为这里是 HTTP, 所有东西都是从请求开始。 2.5.2.1 请求由容器接收 Struts 框架的核心组件是 ActionServlet 。象所有的servlets, 它生存在容器中, 比如 Tomcat, Resin, 或者 WebLogic 等。当容器启动时,读入部署描述符 (web.xml) ,告诉容器要 装入哪些个servlet 。 一个标准的servlet 设定是 servlet mapping。 容器使用这个设定来决定哪个请求将被送 到哪个servlet: <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>/do/*</url-pattern> </servlet-mapping> 这里,我们让容器将ActionServlet 映射到那些符合/do/*样式的请求。这些请求可以是: /do/This /do/That /do/something/Whatever. 许多应用喜欢使用前缀: <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> URL 样式也可以使用象this.do 或 that.do 或 /something/whatever.do 的样式。可以用任 何有效的扩展名,但.do 是比较简单和常用的选择。 当一个请求随着符合Servlet 上下文的路径组件提交时,容器将其转发给ActionServlet 。 不匹配的请求则不转发到ActionServlet 。比如,匹配*.jsp的请求将直接转发给容器的 JSP 服 务,比如Jasper (如果你使用Tomcat 或者WebSphere 的话)。在应用中可以有其他的servlet 来处理其他格式的请求路径。不匹配所有servlet mapping 的请求将直接送给容器默认的web server。 2.5.2.2 请求被 ActionServlet接收 当ActionServlet 收到一个请求, 它通过一系列的流程处理locale, mapping, form bean, 最 后是Action 来运行这个请求。这里某些步骤只在Struts 1.1 应用才有: λ 处理多部分请求 . 如果请求是个多部分请求 ( 比如,一个表单加上一个MIME 附件) , Servlet 用一个特殊的句柄包装这个请求,避免处理错误。 λ 处理路径 ActionServlet 首先检查这个请求路径是否是一个应用模块。如果是,相 应模块的配置被选中。[Struts 1.1] λ 处理场所 . 默认下,ActionServlet 会检查是否有一个标准的locale 对象在用户会话 的上下文当中。如果没有, ActionServlet将放入一个。这个对象将为每个用户提供 本地化表现。 λ 处理内容和不缓存 默认的MIME 类型和可选的请求头将加在响应之上。 λ 处理映射( Mapping) ActionServlet检查ActionMapping ,是否有注册的路径符合 正处理的请求。如果没找到, ActionServlet 转发到默认( 或 “unknown”) Action (如 果设定有默认或未知Action ), 否则,产生一个 “bad request” 错。如果找到相关映 射,将被放入请求之中,供后面引用。 λ 处理角色 . ActionServlet 检查是否用户被授权可以访问action. [Struts 1.1] λ 处理 ActionForm. ActionServlet 检查是否mapping 标明一个ActionForm. 。如果是, servlet 检查是否已经有一个存在于特定的范围之内( 默认是会话) 。如果不存在, ActionServlet 创建一个。 λ 处理组装 . ActionForm的 reset 方法被调用,然后通过反射机制被自动组装。匹 配ActionForm 属性的参数将被使用。其他参数和属性被忽略。 λ 处理验证 ActionForm的 validate 方法被调用。如果方法返回false, 控制传递给 ActionMapping的input 属性标明的路径, Action 未被处理。 λ 处理转发和包含 如果ActionMapping 标明forward 或include 属性,控制被传 递给其他资源。否则ActionServlet 将请求委托给一个Action 对象。 λ 处理 Action. 如果mapping 标明一个Action 类型, ActionServlet 检查是否已经有一 个被实例化了。如果没发现, Action 对象将被实例化。每个类只能有一个Action 对 象 (Singleton 模式) ,它通过多线程处理所有对它的请求。Servlet 调用 Action 的 perform 或 execute 方法,传递请求, 响应, mapping, 以及form bean 。 Action 执行要求的行为,可以包括: λ 访问数据系统,比如 JDBC 数据库 λ 在请求中创建被视图使用的对象 λ 如果需要,在会话中创建对象 λ 根据需要更新会话对象,比如用户的场所 λ 执行需要的业务功能 λ 处理以外和其他错误条件 λ 发送一个直接发送一个响应,或者返回一个ActionForward 给servlet 这里某些行为,象访问数据库,通常由Action 调用的业务对象来处理( Business Delegate 模式) 。 Action 处理一些web 特定的任务,可以放在业务对象中的代码都应该放入业务对象。 Action 是一个控制器类,不应该用来处理业务的核心逻辑。 2.5.2.3 Action返回 ActionForward Action完成后,它返回一个ActionForward 。如果 ActionForward 为null, ActionServlet 假 定响应产生了,但不做任何事情。否则, ActionServlet 读入 ActionForward ,重定向或者转 发请求到相应的资源。 如果请求是另一个Action URI, 容器将请求返回给ActionServlet 。否则容器发送请求到其 它servlet 或service。 如果ActionForward 设为重定向(redirect ), 请求被发送回客户端,并提示重新提交一 个新请求到特定的地址。 2.5.2.4 由 Jasper (或类似的东西 ) 加工 JSP页面 ActionServlet 发送一个请求 到 JSP, 请求是被另外的服务处理, 如 Jasper 。典型地, Struts和其他标签扩展用来编写页面的动态部分。有时,也使用JSP模板,以便页面可以从其 它组件进行构建。 通常, 动态数据在JavaBean中传递到请求上下文中的页面。这就是熟知的视图助手 (View Helper )模式 [Go3]。 标签扩展简单的调用JavaBeans 的方法,并返回格式化的数 据。而数据如何被放入页面中那是表现逻辑的事情。数据本身的格式通常是业务逻辑的一部 分,所以委托给了bean。 Struts 标记也可以访问框架提供的视图助手。这些包括本地化标签和提示,错误信息, 以及超链接路径。另外, Struts 标记可以计算表达式,通过列表反复,以及在HTML 表单中 组装控件。 2.5.2.5 其他 servlet 加工响应 处理完Action 后,请求可以被送到应用中的其他 servlet或服务。其他表现系统,如 Velocity templates, 可以通过servlet 上下文访问框架资源。 2.5.3 Struts 是富有效率的吗 ? 详细描述完Struts 处理流程后,你可能会想知道这些的花多长时间。通常, Struts应该能 提升大部分正确设计的Web 应用的性能。在本节中,我们检查一些关系到框架效率的特殊设 计点。 定义 Performant 是一个法语词,意思是有效率( efficient)。软件工程常用这词来表示一个流程或者 设备在实际中执行得很好。 Struts不仅是线程安全( thread-safe)而且是线程依赖( thread-dependent)的。 Struts使用轻量的Action 对象,而不是各个单独的servlet ,来对请求处理响应。 Struts 实 例化每个Action 类一次,并允许其他请求通过原有的对象线程化。这种核心策略节省了资源, 并提供最大的吞吐性。一个正确设计的应用将通过使用一个单独的Action 来路由各种相关操 作来发挥这种特征。 ActionForm bean最小化子类代码并缩短子类层次 . Struts 框架的一个关键点是可以从请求中自动组装ActionForm bean 。没有这个组件, 用户不得不自行编写代码并实例化来组装每个bean 类。小心使用反射机制会节省不少资源, 资源是有限的,并允许它们更好的使用。 Struts 标签库提供通用功能 Struts一起提供的bean 和 logic 标记库符合大部分JSP 标记的需要。它们减少甚至消除 了编写额外标签的需要。 JSP 规范在JSP重用标签 。使用相同的通用标签3 次比使用3 次不同 的标签来的有效率。 Struts 组件对应用来说都是可重用的 . 框架绑定的工具可以在大部分应用中使用。 BeanUtil.populate 方法就是个例子。 这个方法用来从HTTP 请求组装一个ActionForm Bean,但也可以用来从其它类型的映射中组 装一个FormBean 。重用组件可以减少开销和节省资源。 Struts本地化策略减少了大量冗余 JSP. 通过允许本地化页面在运行时才获取,国际化应用可以为每种可能需要的语言只提供一 个单独的页面。同时,相同的消息系统也可以用于处理错误信息。同一对象提供了双重用途。 Struts设计为一个开放架构 . Struts 组件设计来是可以被应用子类化的,以便可以提供其它的服务功能。这使得开发 人员可以扩展存在的类而不是重新编写新类。而且, Struts 也和应用共享资源。这时开发人 员可以使用存在的组件,而不用编写和实例化它们自己的类。 Struts是轻量型架构 . 类似的框架也许提供数百个类和几十个包。整个Struts 框架 由5 个标记库和5 个核心包 组成。 Struts是标准兼容的 t. Strtus在许多运行标准组件的容器上都工作的非常之好。 Struts是开源的,具良好的文档 . 这意味着开发人员可以检查源代码,找出一些潜在的瓶颈。而且Struts 是模型中立的。 因为Struts 并没有对后端模型做任何假定,一个应用可以按其最有效率的方式实现模型层。 Struts Actions 可以调用一系列助手类来访问需要的数据。一旦数据被检索到, 对JavaBean 的依赖,使Struts 更容易保持值对象,这样来减少了大量的模型层调用。 2.6 Struts的长处和弱点 象一些复杂系统一样, Struts 是个解决方案包。它也有其长处和弱点。这里的某些观 点具有主观性,但也希望能对你有些帮助。 2.6.1 弱点 不管有多么喜爱Struts ,重要的是看到框架是干什么的,它的瑕疵和所有。有些弱点已 经在Struts 1.1 release 中解决。下表列出了Struts 1.0 的弱点,并在Struts 1.1 种解决了。如果你 以前层使用过Struts ,这里某些问题是很精彩的,现在Struts 1.1 可以更加符合你的要求。 表格 2-6 Struts 1.0 的弱点,在Struts 1.1中解决了 弱点 说明 Logging(记录) Struts 使用容器的缺省记录系统,没有提供一个自己的记录包来 为应用提供记录机制。 (Struts 1.1 实现了 Commons-Logging 接口包) 每个应用装入一个单独的配置文件 大型项目可能需要使用多个不被整个团队共享的配置文件 (Struts 1.1 支持多配置文件) 每个场所装入一个单独的资源文件 大型项目可能需要使用多个不在整个团队共享的资源文件。 (Struts 1.1 支持多资源文件) 没有服务管理器 ActionServlet 必须被子类化来提供附加服务,比如用户记录器或 者身份认证系统 (Struts 1.1 提供一些新的扩展点和组件) ActionForm red tape Struts 1.0 希望开发人员创建定制JavaBeans 与HTML 输入表单 一起使用 (Struts 1.1 支持 Map 和 DynaBean 来代替定制JavaBean.) 表2-6 列出了框架当前的弱点。在下一节,我们将详细的讨论它们,并象资产一样描述。 “Goofus” 或者 “Gallant,” 选择在你。 表格 2-7 Struts1.1的弱点 弱点 说明 没有事件模型 Struts 紧密和HTTP 的请求-响应模型结合,这限制了开发人员更 好地处理事件 调试 不支持自动调试(除错),开发人员不得不手工创建“断点”, 并向容器的记录系统写标准输出 没有缺省的数据模型或者具体的推荐 访问持久数据模型留给了开发人员 单一ActionServlet 一个应用终止可以使用一个单一的ActionServlet ,这个限制可能 导致配置冲突 需要理解Struts 组件 开发人员需要理解一些特殊的类以及他们如何交互。 不能提供优先技术支持 ASF是个志愿者组织,没有全职人员提供可担保的响应 Mailing list已经成为知识的障碍 Struts 有一个日益增长的邮件列表,但要在其中找到最好的间以 非常困难。 正式发布版本并不快速 Struts正式发布版相对于其他项目来说先得慢了。开发人员必须经 常检查“每日构件”或的最新改进。 i18n 限制 Struts 的消息资源对建立国际化的资源和错误信息非擦好,但不 适合于处理大文本块 JSP mindset 因为使用MVC 架构, 使得资源对所有表现层都是有效的。这是个 长期的弊病对JSP 来说。 JSP意外本地化 很多系统级消息,象JSP 意外,都没有本地化,通常显示为英语 标记属性冗长 许多标记扩展要求很多参数,对编程来说很笨 perform 和 execute 方法体 Struts 架构的关键是将请求委托给一个 Action 类或者叫分发者 dispatcher。Action 类是Struts 支持的唯一分发者,并仅通过其 perform 方法来调用。这将应用限制在只能和perform 方法传递 的数据一起工作。即使有办法超出这个限制, perform 方法也 是个瓶颈。 一个通常的请求要求ActionServlet 组装几个 ActionForm。但是因 为 perform 接受单个ActionForm 参数,如果不经过较大的框架 革新是不可行的。 Struts 1.1 添加了一个execute 方法,它有助于改善perform 的其他 主要缺陷:因为他返回意外。然而,主要的问题已就存在。 模糊的术语 Struts 框架在明显的增长。而给一些应用选项和 类的名称却容易让人混淆。例如, web.xml “ 中的validate” 选项 却和Action 对象的 validate 方法无关,而和如何解析配置文件相 关。同样,神秘的 “ null” 选项则表示当消息关键字未找到时,是否返回一个错误信息。 有个趋势是在类层次树种使用复合名称。在Action 包中的每个类 都有个前缀为 “ Action,” 这却是多余和容易混淆的。同时,在 Struts配置文件中, ActionMappings 定义的元素名是 “ Action” 而不是“ ActionMapping” 。如果开发人员引用一个 “ action,” 很难区别它们是指Action 类或是配置类的 ActionMapping。 在 Struts 配置中, “ name” 域标识ActionForward 和 ActionForms“ path” 域标识 ActionMapping 。 “action-mapping 元素的 name” 属性则指出使用哪个ActionForm 。 ActionForward 的URI 域也称为 “ path”,但可以包括伴随path 的查询组件。到 ActionMapping 的 “ path” 不包括servlet 样式, 象 *.do, 但是ActionForward 的path 却包括 *.do 扩展名。 应用资源其实是真正的消息资源。 等等。 凡此种种,这些小矛盾可以把一些新手搞糊涂,并且使框架难以 学习 2.6.2 Struts的强项 强项 说明 以HTTP 为中心 Struts设计围绕标准 HTTP 请求 -响应模式,为许多Web开发人员所熟 悉 标准记录 Struts 可以使用容器的缺省记录系统,而不需要配置和理解其他包 可选的调试记录 Struts 可选记录大量状态处理时的信息,它们可以有助于进行调试 模型中立 Struts 并不倾向于哪个特定的持久层 在一个中心配置中汇集实现细节 Struts 配置封装了应用,或者应用模块[Struts 1.1] 的实现细节。所以它 们可以作为一个整体评价和管理 允许为每个场所配置不同的消息 资源 不同的语言翻译可以工作在他们自己的消息资源文件版本上。添加一个 新场所的支持仅需简单地添加一个资源文件 轻量 Struts 仅有几个核心类要学习 开源 全部源代码在自由的 Apache 软件许可下,所有的选择都在你 强大的开发人员团体 有一个强大的开发人员团体使用 Struts。 邮件列表时非常活跃的。 许多 开发人员的扩展是很成功的 强大的供应商团体 Struts 已经和其他一些产品合在一起, 包括 Jcorporate的 s Expresso 和 IBM的 WebSphere。一些厂商也提供 Struts专用工具。 强大的产品支持 Struts 有其自身的专业管理的 JGuru 论坛 。 Struts邮件列表至少可以通 过两种支持门户进行访问。 Struts 已经被许多文章和书籍涉及,并有一些组织提供专业的教程 强大的开发团队 超过 30个开发人员为 Struts 1.1做贡献。 Struts团队现在有九个活跃的志 愿者,他们全为源代码负责 稳定发布版本 Struts正式发布版本要经过长期的测试, 并没有最后期限, 所以团队可 以提供高质量的产品 i18n 支持 支持内建的国际化 高度兼容 Struts专注于提供公共标准 100%兼容的产品 全方位的标记扩展 Struts包括一系列通用标记扩展。他们一起可以符合你所有的 JSP 需 要,而不用编写脚本程序 良好文档的源代码 Struts JavaDoc非常详细,以使你几乎不需要参考源代码。这其实是个 高级别的用户指南。 建立在设计模式之上 Struts 框架在其架构中实现了一些经典的设计模式,这些模式为许多 开发人员所熟知。 可扩展性 所有默认的设置都可以配置。 核心 Struts可以被重写, 和子类化。 开发 人员可以定制关键类如 ActionForm 和 Action. 2.7 小结 今天的开发人员需要建立全特征的应用, 并且可以随时维护。 Web 应用框 架, 如 Struts, 解决了这个普遍问题,所以开发人员可以专注于它们应用的特定功能。在开发 Web应用时, 框架特别重要,因为 HTTP 和 HTML 要创建动态应用非常困难。 Struts 使用了大多数标准的 Java servlet API 并成为一些 servlet 容器的兼容性测试。 Struts 也构建于通用的设计模式,特别是 MVC 架构。框架鼓励应用采取分层设计。这种 设计使应用具有强壮性和伸缩性。 架构的一个关键之处是它扩展了 HTTP 请求 -响应循环的流程。 Struts 控制器管理着应 用使用的路径,帮助安全的收集用户输入,并可以本地化应用消息,特别是错误消息。 Struts 是一个 富有效率的解决方案。 它绝不会抑制你的应用, 并有随处有一些免费资源 可以使用。 当然, Struts 还有一些缺 陷。 许多类名的选择在开发时显得比较轻率, 容易引起混淆。 其他一些地方也值得改进。 尽管有一些阻碍, Struts 也很容易地成为现今最流行的 Web应用框架。 在下一章,我们回过头来运行 Struts,并创建另一个应用。 3 构建一个应用 本章包括 创建一个简单应用 扩展一个应用 修改一个应用 在应用中使用JSP 页面 A human being should be able to change a diaper, plan an invasion, butcher a hog, conn a ship, design a building, write a sonnet, balance accounts, build a wall, set a bone, comfort the dying, take orders, give orders, cooperate, act alone, solve equations, analyze a new problem, pitch manure, program a computer, cook a tasty meal, fight efficiently, die gallantly. Specialization is for insects. —Excerpt from the notebooks of Lazarus Long, from Robert Heinlein’s Time Enough for Love 3.1 被支柱支撑的支柱 现在,各种团队编写很多web 应用。在应用中使用分层架构 [ POSA] ,如第1 章和第2 章所述,可以很容易的让团队的成员分别专注于应用的不同部分。但是,仍然很有必要让团 队的每个人都从头到尾理解整个处理流程。在我们深入探究Struts 的各部分是如何优雅的相 互结合之前,让我们先从头开始构建一个简单但有用的程序。在这一章,我们将通过教程、 解剖来和Struts 来一个初次接触、然后构建一个供用户登入和登出的程序。 虽然没有第一章的例子显得琐碎,我们仍然暂时保持例子的简单性。在第4 章才会涉及 到一个实际的应用。 在本章,我们将从用户的角度来讨论一个经典的登录logon 应用。第一章的练习只是对 从注册页面输入的密码信息进行比较,根据是否匹配来控制页面的分支流转。该应用让您使 用第1 章的练习所创建的账户来进行实际登录。控制流和页面内容的改变依赖于你的状态。 引入这个应用后,我们将对它进行分解来仔细探讨其各个部分。如果你的机器上安装了 Struts开发环境,你可以跟着来做。当然,你也可以舒服地靠在你的椅子上,喝着卡普契 洛咖啡,来看它是如何工作的。 然后,打好基础后,我们就开始一步步构建这个应用。 每部分的表述都力图使你可以根据它自行完成,目的是你可以编写之(而不是单调的重 写工作)。如果你在你的计算机前面,你可以跟着我们来输入源代码。如果不,每个部分的 足够的细节也使你可以只根据书就跟上我们。 3.1.1 为什么选择 logon应用 ? 我们的例子程序允许用户登录到应用中去。一旦用户登入,页面的改变将反映出用户已 经得到授权。一般情况下,这都是大型应用的第一步,授权用户进入,进行它们感兴趣的东 西。但我们现在的目的,只是登录进一个用户,已经足够用来演示Struts 应用是如何实际工 作的。 如表3.1所示,我们选择 logon 应用仅仅因为它易于理解,简单,自包含,而且在许多 应用中都需要。 表格 3-1 为什么选择登录应用 原因 解释 好理解 我们几乎都需要登录到应用的共享部分,所以这个流程很 好理解 简单,且自包含 一个接受用户登录的应用可以写得很简单并且是自包含 的,不需要复杂的模型 被许多应用需要 我们几乎都需要编写使用某种登录流程的应用,所以这就 是我们需要的代码 3.2 漫游 logon应用 在开始我们的漫游之前,我们先讨论这个应用的范围,以及你如何能跟上我们。然后我 们来看看应用中使用的屏幕,请注意在登录以后是如何变化的。我们完成这个漫游之后,会 再回头过来看一看。 3.2.1 从这里开始 我们创建logon 应用的目的是给你一个关于Struts 个部件间是如何配合工作的印象。为 有助于不至于跑题,这个应用仅包含我们需要用来展示整个框架的那些组件。它没有包含实 际的业务逻辑,单元测试,或者漂亮的对话框。虽然这些东西对一个要交付的成型产品来说 是很重要的,但在会跑之前我们得先学会走。 Logon 应用也有意搞得很刻板。它没有修饰HTML 来愉悦眼睛—仅仅是一些我们需要来 接受登录的初步功能。 当然, 你的 Struts 应用可以编写得很漂亮,只要你高兴。 如果你愿意在你的机器上运行这个应用,请访问本书网站的下载页面 [Husted] 。 这是 一个可以自动部署的WAR 包。 我们并不要求你保持打开这个应用,但在某些时候是很有趣的。你需要跟着做的东西都 在本章列出来了。 首先,我们从用户的角度来漫游一下各个屏幕。然后,再回过头来看看如何编写实际的 代码。 3.2.2 我们看到的屏幕 如表5 所示,logon 应用有两个屏幕: 欢迎屏幕和登录屏幕。 如果你跟着做,并将应用部署在你的本地计算机上,你可以在浏览器中用以下地址访问 欢迎页面: http://localhost:8080/logon/ 表格 3-2 Logon应用的屏幕 屏幕 目的 欢迎屏幕 欢迎用户并提供到应用的连接 登录屏幕 允许输入用户名和密码 3.2.3 欢迎屏幕 第一次访问欢迎屏幕时,它只有一个链接, “Sign in” (见图 3-1)。如果点击这个链接, 就会出现登陆屏幕。 图 3-1欢迎屏幕 3.2.4 登录屏幕 登录屏幕输入和提交用户名和密码,如图 3-2所示。 图 3-2 登录屏幕 为了看看实际动作的登录表单, 什么都不输入, 点击提交。 登录屏幕将返回, 但伴随了 一个消息,如图 3-3。 图 3-3 登陆屏幕告诉缺少用户名和密码 如果你输入用户名但是忘记输入密码提交后,消息变为图 3-4所示: 图 3-4 缺少密码 这里有关工作流过程中重要的是,从用户的角度来看: null 它马上告诉用户缺失所有信息 null 如果用户只是提交一个信息,它会提醒用户还缺其它一个信息; null 它重新显示用户进入的屏幕,而不是要求用户按下 Back键 null 用户设法输入用户名和密码,表单被接受,并显示下一个屏幕。 logon 应用根据一个属性文件来校验用户的输入,就象第 1章所用的那个文件。如果你 从本书的网站上下载了这个 logon应用,你可以用本书的作者名字登陆,如表所示: 表格 3-3 缺省的登录用户 用户名 密码 Ted Husted Cedric Dumoulin George Franciscus David Winterfeldt 3.2.5 再显示欢迎屏幕 成功登录后,将重新显示欢迎屏幕 -不过有两点不同。 图 3-5 用户登录后的欢迎屏幕 首先, 它根据登录的用户做了调整。 不再显示 “Welcome World!” 而是显示欢 迎具体的 用户名字。 另外,你会注意到,加入了另一个链接,除了 sign in外你会还看到 Sign out。 3.2.6 欢迎屏幕,再见 为结束这个循环,点击 sign-out 链接 , 我们将返回原先的欢迎屏幕,如图 3-1所示。 3.2.7 所用的特征 虽然简单,我们的这个应用却展示了以下的技术: λ 编写链接 λ 编写表单 λ 校验输入 λ 显示错误消息 λ 重装表单 λ 显示代替文本 虽然不明显,也展示了: λ 从动态页面引用图象 λ 重写超链接 在下一节,我们将深入应用的源代码去看看其核心特征是如何实现的。 3.3 解剖 logon 应用 既然我们已经和 Struts logon 应用打过招呼了,我们现在回过头去仔细打量一下它。 现在,我们将展示每个页面的代码以及它们的相关组件,并解释每一部分是干什么的。 待介绍完所有的部分后,再来演示如何将它们组装在一起。 3.3.1 欢迎屏幕的浏览器代码 你可以回头看看, 我们的应用开始于一个欢迎屏幕。 让我们来看一下这个页面在浏览器 中的代码:黑体部分是显示在屏幕上的内容: <HTML> <HEAD> <TITLE>Welcome World!!</TITLE> <base href="http://localhost:8080/logon/pages/Welcome.jsp"> </HEAD> <BODY> <H3>Welcome World!</H3> <UL> <LI><a href="/logon/logon.do">Sign in</a></LI> </UL> <IMG src='struts-power.gif' alt='Powered by Struts'> </BODY> </HTML> 清单 3-1 欢迎页面的浏览器代码 如果你是个 web应用的新手,一个重要的事情是,请注意这里除了标准的 HTML外没有 其它东西。 事实上, 除 了浏览器能理解的通用标记外, 页面中也绝不可能有其它任何东西。 所有的 web 应用都基于于 HTML 的限制,并不能做一些 HTML不能做的事情。 Struts可以 很容易的使用 Velocity 模板, JSP,以及其它表现系统来编写你想要的 HTML,但是你所 必须做的所有事情都应是浏览器能理解的标记。 jsessionid 关键字 在这个浏览器的源代码中可能有一个你不认识的非标准 HTML。当首次访问这个页面时, sign-in链接实际上可能是这样: <LI><a href="/logon.do;jsessionid=aa6XkGuY8qc">Sign in</a</LI> 大多数 web应用都需要保持对使用该应用的用户的跟踪。 HTTP有一些基本的对维护用户登 录的支持,但其方法非常有限并且不安全。 Java servlet 框架对维护用户会话提供了强大的 支持,但需要有一个机制来通过 HTTP维护绘画。 Jsessionid是一个容器维护的关键字,用来通过 HTTP跟踪用户。在一个超链接中包含一个会 话关键字称为 URL重写。 Servlet规范 [Sun, JST]鼓 励使用 cookie来维护会话。 当这种方法行 不通时, 可使用 URL 重写来代替。 浏览器在第一次请求容器时, 容器并不知道浏览器是否 接受 cookie。所以容器可以先向浏览器发送一个 cookie,但并不告诉它下次请求时是否还被 接受 (HTTP不进行 “握手 .” )同时,对当前请求的响应必须输出。所以,面对浏览器的第一 个页面通常需要使用 URL 重写。如果后来的请求中,容器发现了浏览器可以接受 cookie, 它可以跳过 URL重写。 3.3.2 欢迎页面的 JSP源代码 现在让我们看一下产生图 3-1页面的 JSP 源代码。 JSP 标记以黑体形式显示。 <%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-html" prefix="html" %> <%@ taglib uri="/tags/struts-logic" prefix="logic" %> <HTML> <HEAD> <TITLE>Welcome World!!</TITLE> <html:base/> </HEAD> <BODY> <logic:present name="user"> <H3>Welcome <bean:write name="user" property="username"/>!</H3> </logic:present> <logic:notPresent scope="session" name="user"> <H3>Welcome World!</H3> </logic:notPresent> <html:errors/> <UL> <LI><html:link forward="logon">Sign in</html:link></LI> <logic:present name="user"> <LI><html:link forward="logoff">Sign out</html:link></LI> </logic:present> </UL> <IMG src='struts-power.gif' alt='Powered by Struts'> </BODY> </HTML> 清单 3-2 欢迎页面的JSP代码 现在我们来看看,黑体部分做些什么 <%@ taglib uri="/tags/struts-html" prefix="html" %> <%@ taglib uri="/tags/struts-logic" prefix="logic" %> 这是JSP中相当于import 语句的东西,它使标明的标签扩展在接下来的页面中有效。代 码 <html:base/> 产生一个标准的HTML base 标记, 以便对如图片这样的引用要相对于这个JSP页面的地址。 你可能会注意到有时logon 应用引用到.do 页面。这不是实际的服务器上的页面文件, 而是对开发人员编写的Java 类, 或者Action 的引用。这些 Action 然后转发到创建响应的JSP 页面。 JSP通常包括到HTML 资源如图片和样式表的引用。最方便的方法是通过相对于JSP模版 的路径来引用。但当Action 转发控制时,它并不事先通知浏览器。如果浏览器被给定一些相 对路径,它就会根据Action URI 来解析它们, 而不是JSP template 的位置。 依赖于你何时访问欢迎页面, 其地址可能被浏览器显示为: http://localhost:8080/logon/ http://localhost:8080/logon/LogonSubmit.do http://localhost:8080/logon/Logoff.do 这对动态应用来说是个很常见的问题。 HTML 规范[W3C, HTML] 提供了一个标记作为 解决方案。 Struts 也提供了一个类似的html-base 标记,可以插入JSP之中。如果你查看logon 页面的HTML 源代码,查询其外观地址,你就可以看到该标记被被加工成: <base href="http://localhost:8080/logon/pages/Welcome.jsp"> 这可以让浏览器找到 “Powered by Struts”图片,该图片也存放在pages 文件夹下面。 现在,让我们来看这段代码: <logic:present name="user"> <H3>Welcome <bean:write name="user" property="username"/>!</H3> </logic:present> 你可能记得welcome 页面根据用户是否登录进去来定制页面显示。这一段就是检查是 否在客户会话中存储了一个“user” bean。如果这个bean存在,则显示欢迎这个用户。 下面的代码显示了为什么维护用户会话是这么重要。( 第3.3.1节) 。庆幸的是,Struts 标 签和servlet 容器一起来自动维护会话( 不管浏览器是否设置为使用cookie) 。对开发人员来说, 感到就象会话是内建在HTTP 里面一样—这就是框架的作用。框架扩展了基础环境,所以开 发人员可以专注于更高级的任务。 <logic:notPresent scope="session" name="user"> <H3>Welcome!</H3> </logic:notPresent> 相反,如果user bean 不存在,我们使用一个通用的欢迎信息。所有的Struts 逻辑标签 都使用 “this” 和 “notThis” 格式。其它标签记并未提供。 虽然这意味着要进行一些重复测试,但它简化了总体语法和标签的实现。当然, 你也 可以使用其它标签扩展。 你并未被局限于Struts 软件包中所提供的东西。其它志愿者贡献的标签扩展列于Struts 资源面 [ ASF, Struts],甚至有提供if/then/else 语法的,你可以用它们作为替代。 如3.3.1 所述, Struts自动重写超链接来维护用户会话。它也使你可以给链接取个逻辑名 字并将实际的链接存放在配置文件之中。这就象通过关键字引用数据库一样,记录中的名字 和地址可以根据需要进行更改,其它表格可以通过关键字来查找更新的版本。 这里: <LI><html:link forward="logon">Sign in</html:link></LI> 使用 logon作为关键字来查询存放到用户登录页面的记录。如果我们想改变这个链接,只 需要在配置文件里面改写就行了。页面再其下次被加工的时候将以新的链接开始。 这段代码结合了 <logic:present> 和 <html:link> 标记,仅当用户已经登录进去时 显示登出链接。 <logic:present name="user"> <LI><html:link forward="logoff">Sign out</html:link></LI> </logic:present> 3.3.3 Welcome屏幕的配置源代码 Struts 用配置文件来定义应用的一些东西,包括链接的逻辑名称。这是一个XML 文档, Struts在启动时读入,用来创建一个所需的对象数据库。各种Struts 组件都引用这个 数据库 来提供框架的各种服务。配置文件的缺省名称是struts-config.xml. 。 因为配置文件要被各个不同的组件使用,全部展示这个文件可能会让我们昏头。现在我 们仅提供与我们这个阶段相关的部分。后面,当我们全部构建应用时,我们会展示整个配置 文件。 在最初的 welcome屏幕中,我们引用了一个 logon forward。它在配置文件中是这样 定义的: <forward name="logon" path="/Logon.do"/> 这儿, logon 是个关键字,用来查找链接的实际路径。在这里引用了Struts action ,但 path 也可以引用到JSP 页面, Velocity 模板,HTML 页面, 或其他具有URI 的资源 [W3C, URI]。 定义 统一资源标识符 (URI) 是一个简短的字符串,用来标识在 Internet 或者其他网络中的资源。资源可以是一个 文档,图像,下载文件,电子邮箱,或者其它东西。一个 URI可以对应一个服务器文件系统的路径,但通 常也有一个别名。 Strtuts应用中的许多 URI都是 Java类或者 Action的别名。 因为路径是在配置文件中定义的,你可以随时改变你的主意文不用触及到 JSP源代码。如果 你更改并重载 了配置文件,改变会在页面下次加工时生效。 3.3.4 logon 屏幕的浏览器代码 如果我们进入 welcome 页面的Signin 链接, 它将我们带到logon 屏幕(图3-2 )。下面是 logon屏幕的浏览器代码。同样,黑体部分是显示在屏幕上的内容: <HTML> <HEAD> <TITLE>Sign in, Please!</TITLE> </HEAD> <BODY> <form name="logonForm" method="POST" action="/logon/LogonSubmit.do"> <table border="0" width="100%"> <TR> <TH align="right">Username:</TH> <TD align="left"><input type="text" name="username" value=""></TD> </TR> <TR> <TH align="right">Password:</TH> <TD align="left"><input type="password" name="password" value=""></TD> </TR> <TR> <TD align="right"><input type="submit" name="submit" value="Submit"></TD> <TD align="left"><input type="reset" name="reset" value="Reset"></TD> </TR> </table> </form> <script language="JavaScript" type="text/javascript"> <!-- document.forms["logonForm"].elements["username"].focus() // --> </script> </BODY> </HTML> 清单 3-3 登录屏幕的浏览器代码 而下面是相应的JSP 代码: <%@ taglib uri="/tags/struts-html" prefix="html" %> <HTML> <HEAD> <TITLE>Sign in, Please!</TITLE> </HEAD> <BODY> <html:errors/> <html:form action="/LogonSubmit" focus="username"> <table border="0" width="100%"> <TR> <TH align="right">Username:</TH> <TD align="left"><html:text property="username"/></TD> </TR> <TR> <TH align="right">Password:</TH> <TD align="left"><html:password property="password"/></TD> </TR> <TR> <TD align="right"><html:submit/></TD> <TD align="left"><html:reset/></TD> </TR> </table> </html:form> </BODY> </HTML> 清单 3-4 登录屏幕的JSP代码 让我们象前面一样一步步来看这个页面的各个部分。 首先,象前面一样,使 Struts html 标签扩展在后面的页面中有效: <%@ taglib uri="/tags/struts-html" prefix="html" %> 象Struts action 一样, 标记库 URI 是一个逻辑引用。标签库描述符(TLD) 的位置在 web.xml文件中给出。 你还记得,我们如果没有输入信息就提交的话,将会显示一个错误信息。下面的标签就 是加工此错误消息的。如果没有错误消息,标记则什么都不输出,就象从页面消失了一样: <html:errors/> <html:form> 标签产生一个HTML 表单供数据输入。它也产生一个简单的JavaScript 来 将光标移到第一个输入域上。 Action 属性是对配置文件中的ActionMapping的引用。它告诉表单哪个JavaBean helper 类将用来组装 HTML 控件。Java-Bean helper 基于Struts 框架类ActionForm: <html:form action="/LogonSubmit" focus="username"> <html:text> 标签创建一个HTML 输入控件供文本域输入。它也用JavaBean helper 的 username 属性来组装这个输入域: <TR><TH align="right">Username: </TH><TD align="left"> <html:text property="username"/></TD> 所以,如果这个表单被返回来做校验,并且最后提交的username是 Ted, 这个标记就会输出: <input type="text" name="username" value="Ted"> 否则, 标签将使用JavaBean helper 类中标明的username 属性的缺省值初始化。通常, 那是个null, 当然也可以是任何值。 同样 <html:password> 标签也创建一个 HTML 输入控件: <TR><TH align="right">Password: </TH> <TD align="left"><html:password property="password"/></TD> Password 控件象一个文本域,但是显示文本为* 而不是输入的字符。如果表单被返回校 验,缺省下,password 标记会重写先前的值,而不用重新输入。如果你想每次重新输入 password ,可以将redisplay 属性关闭。 如果初次登录失败, 这段代码将清除浏览器的缓存,并以重新输入password ,而不管是 否通过验证。 <html:password property="password" redisplay="false"/> 下面的标签创建标准的 HTML Submit 和 Reset按钮: <TD align="right"><html:submit/></TD> <TD align="left"><html:reset/></TD> 当表单提交时,将涉及两个框架对象: ActionForm 和 Action 。这两个对象都必须由开 发人员创建并包含应用细节。如图3-6 ,ActionServlet 使用 Struts 配置来决定使用哪个 ActionForm 和Action 。 图 3-6 配置决定使用哪个Actionform和Action 下面我们看看 Struts配置中的 logon 屏幕的 ActionForm 和 Action配置。然后再看看这 些类的源代码。 3.3.5 logon 的配置源代码 logon 屏幕自 身在配置文件中仅仅引用一个元素: /LogonSubmit ActionMapping。这 个元素再依次引用其他两个对象 , app.LogonForm 和 app.LogonAction。 全部 3个对象 如下表所以: 元素 描述 /LogonSubmit ActionMapping,封装了构建并提交一个 HTML表单给 Struts 框架所需要 的各种细节 app.LogonForm 描述了 HTML form所需的属性 app.LogonAction 处理提交的表单 3.3.6 LogonSubmit配置 上一节 , 我们说过 <html:form> 标签紧密配合 Struts配置,使 HTML表单更加有用: <html:form action="/LogonSubmit" focus="username"> action 参数告诉 <html:form> 标签究竟使用哪一个 ActionMapping。 这里, Struts 配置中的 mapping 可能会像这样: <action path="/LogonSubmit" type="app.LogonAction" name="logonForm" scope="request" validate="true" input="/pages/Logon.jsp"/> 表 3-4提供了提供一个关于 mapping中每个属性的意义的索引。 就象在第2 章所述, Struts 框架使用的许多对象和属性名字都是很模糊的。比如, name 属 性并不是 mapping 的名字;而是 JavaBean helper 的名字 , 或者是和这个 mapping 一起使用的 ActionForm bean的名字。 相同的form bean 在配置中是这样设置的: <form-bean name="logonForm" type="app.LogonForm"/> 这个元素使logonForm 的逻辑名字和特定的 Java 类app.Logonform相关。这是一 个Struts ActionForm 类的子类。ActionForm 类提供标准的方法给框架使用, 包括 validate 校验方法。 让我们先看看LogonForm 的源代码再回过头看LogonAction 。 表格 3-4 Actionmapping设置属性 属性 目的 path 这个属性是mapping的唯一标识。它可以包括在Web地址中,就象 http://localhost:8080/logon/LogonSubmit.do. type 路径被请求时调用的Action 对象 name 和HTMl 表单一起使用的JavaBean helper (ActionForm) 类 scope 属性,标明是否将助手类存储在请求或者会话中的属性 validate 属性,标明form bean helper ( 由name 属性标明的) 在调用Action 对象 ( 由type 属性标明)前是否调用标准的校验方法 input 属性,标明在校验方法返回False 时,将控制发送到哪里 3.3.7 LogonForm 配置源代码 虽然HTML 给用户一个地方来输入数据,但却没有给应用一个地方来放置数据。 当用户点击提交时,浏览器收集表单中的数据,并按一个名- 值对列表的方式发送给服 务器。所以,如果用户在logon 页面输入username和passwaord 并点击提交,应用所看见的是: username=Ted password=LetMeIn 浏览器将所有的东西都按字符串提交。你可以使用JavaScript 校验来强迫用户在某个域 里面只能输入数字,或者使用固定的数据格式,但是这也仅是镜花水月。所有的东西仍然以 字符串的方式提交给服务器—而不象准备传递给Java 方法的二进制对象。 重要的是要记住,这是浏览器和HTML 工作的方式。 Web 应用无法控制这些。Struts 之类的框架的存在是使我们必须做的事情做的最好。 Struts对HTTP 数据输入难题的解决方法是使用ActionForm 。 在象Swing 之类的环境中,数据输入控件有一个内建的文本缓冲区,可以校验所输入的 字符。当用户离开控件,缓冲区可以转换为二进制类型,可以传递给业务层。 不幸的是,HTTP/HTML平台不提供可以缓冲、校验和输入转换的组件。所以Struts 框 架提供了一个ActionForm ( org.apache.struts.action.ActionForm)类来沟通web 浏览器和业务对象。 ActionForm 提供了想要的缓冲/ 校验/ 转换机制,我们可以用来保证用户 输入它们想要输入的东西。 当 HTML 表单提交时,名- 值对被Struts 控制器获取,并应用到ActionForm 。 ActionForm 是一个 JavaBean,有属性和HTML 表单控件中的域相对应。 Struts 比较ActionForm 属性 的名称和输入名- 值对的名称。当匹配时,控制器设置属性值为相关的输入域的值。其它的 属性会被忽略。错过的属性会保持它们的缺省值(通常是null 或者false) 。 这里是LogonForm 的公共属性: private String password = null; public String getPassword() { return (this.password); } public void setPassword(String password) { this.password = password; } private String username = null; public String getUsername() { return (this.username); } public void setUsername(String username) { this.username = username; } 大部分 Struts ActionForm 的属性看起来就象如此。节约的开发人员可以当作一个宏来 创建它们的属性并简单地替换属性名称。其他人则可以使用这个代码骨架,并使用查找替换 的方法进行改写。Struts 代码生成器可以通过解析HTML 和 JSP来自动生成ActionForm 。 注意 在 Struts 1.1中,如果你使用 DynaActionForm或者向后映射 ActionForm,创建 ActionForms更加 简单。第 5章有详细信息。 基本ActionForm 也包括两个标准方法—reset 和 validate。 当使用ActionForm 作为向导工作流的一部分时,reset 方法非常有用。 如果mapping 设置为请求范围,这个方法就没必要实现。 当 mapping 设置为 validate=true, validate 方法就会在 formbean 从HTTP 请求中组装完成后调用。 validate 方法经常用于主要的外观校验。它仅仅检查数据 “ 看 起来” 正确,并且所有要求的域都提交上来。再说一下,这些都是Swing 控制在数据传输到 应用之前内部做的事情。 你也可以手工作这些检查,或者使用象 ValidatorForm的机制( 第12章) ,这种Form 可以 从配置文件中创建。 下面是LogonForm 的validate 方法。它检查是否两个域都输入了一些数据。应用有一些关 于用户名和密码的长度之类的要求,都可以在这里进行检验。 public ActionErrors validate(ActionMapping mapping, HttpServletRequest Request) { ActionErrors errors = new ActionErrors(); if ((username == null) || (username.length() < 1)) errors.add ("username", new ActionError("error.username.required")); if ((password == null) || (password.length() < 1)) errors.add("password", new ActionError("error.password.required")); return errors; } validate 返回的ActionErrors对象是另一个框架类。如果validate 没有返回 null, 控制器对象将在请求上下文中的一个未知关键字下存储ActionErrors对象。 <html:errors> 标记直到关键字,将会在他们存在时加工错误消息。或者如果错误信息 不存在,就不做什么。 记号 error.username.required 和 error.password.required 也都是关 键字。它们用于从Struts 消息资源文件中查找实际的消息。每个场所可以有其自己的资源文 件, 这使得消息容易被本地化。 Struts消息资源文件也是用名- 值对格式。我们所用的消息条目是: error.username.required=<li>Username is required</li> error.password.required=<li>Password is required</li> NOTE 在 Struts 1.1 中有一个方法将标记保持在消息之外。一个新的 errors.prefix/error.suffix 特征可以用于表示包围每个消息的 <li>和 </li>标 记。一个新的消息标记集也可用于代替原有的 <html:error>标记。 message 标记使其保 持在其属于的表现页面中变得相对容易。 参见第 10章查看更详细的 Struts JSP 标记说明。 . 使没有使用本地化, Struts 应用资源文件也将集所有的消息收集到一个单独的地方, 在此你可以对它们进行修改和修订,而不用触及任何Java 源代码。 3.3.8 LogonAction 源代码 收集数据项到ActionForm 并且执行了一些初始化校验后,控制器将FormBean 传递给 Mapping标明的Action 类。 Struts 架构期望你能用自己的Java 类来完成大部分的请求处理。JSP 页面可以加工结 果,但Action 则是获得结果的方式。 就象第2 章中看到,这就是熟知的MVC 或 Model 2 方式, Action 充当了一个请求分派 器 dispatcher 。 当对一个Action 的请求被送到Struts servlet, 它通过调用perform (或 execute)方法来 调用( 分派) Action 。 注 在 Struts 1.1中有另外一个替换方法,叫做 execute。这个方法提供了更好的例外处理,其它都和 Struts 1.0 的 perform 方法一样。 我们在这章仍然调用 perform 方法,使代码可以运行在两种版本之下。本书中的 其他应用都是基于 Struts 1.1,可使用更好的特性。 下面是logonAction 的全部源代码: package app; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpServletResponse; import org.apache.struts.action.Action; import org.apache.struts.action.ActionError; import org.apache.struts.action.ActionErrors; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.apache.struts.action.ActionServlet; public final class LogonAction extends Action { // Validate credentials with business tier public boolean isUserLogon (String username, String password) throws UserDirectoryException { return (UserDirectory.getInstance(). isValidPassword(username,password)); } // end isUserLogon public ActionForward perform(ActionMapping mapping, ActionForm form, HttpServletRequest Request, HttpServletResponse Response) throws IOException, ServletException { // Obtain username and password from web tier String username = ((LogonForm) form).getUsername(); String password = ((LogonForm) form).getPassword(); // Validate credentials boolean validated = false; try { validated = isUserLogon(username,password); } catch (UserDirectoryException ude) { // couldn't connect to user directory ActionErrors errors = new ActionErrors(); errors.add (ActionErrors.GLOBAL_ERROR, new ActionError("error.logon.connect")); saveErrors(Request,errors); // Return to input page return (new ActionForward (mapping.getInput())); } // Save our logged-in user in the session, // because we use it again later. HttpSession session = Request.getSession(); session.setAttribute(Constants.USER_KEY, form); // Log this event, if appropriate if (servlet.getDebug() >= Constants.DEBUG) { StringBuffer message = new StringBuffer("LogonAction: User '"); message.append(username); message.append("' logged on in session "); message.append(session.getId()); servlet.log(message.toString); } // return success return (mapping.findForward (Constants.WELCOME)); } // end perform } // end LogonAction 清单 3-5 LogonAction类的Java代码(/WEB-INF/src/java/app/LogonAction.java) Action 位于Struts 食物链的最顶层,所以会导入好些类。 我们将每个类在此说明,以 便你可以知道他们来自于何处。 package app; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpServletResponse; import org.apache.struts.action.Action; import org.apache.struts.action.ActionError; import org.apache.struts.action.ActionErrors; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.apache.struts.action.ActionServlet; 如果我们比较懒,这个语句块可以写成: package app; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; import org.apache.struts.action.*; 但是这恐怕是没什么好处的。像许多Apache 产品一样, Struts 源代码遵循最好的实践经 验并且是很全面的。我们在源代码中也要遵循这一套。虽然我们在这儿忽略了JavaDoc ,但 我们的源代码和 Struts 源代码都是文档齐全的。 接下来,我们使用了一个 helper 方法来调用业务层方法。我们也可以将同一段代码放 在Action的 perform 方法内,但是,我们强烈建议将通用业务方法和Struts 控制器代码分 开。 如果你加入了一行(业务代码),很快就会是三行,五行, …然后你的Action就象一个 大球陷入泥浆 [Foote] 。避免 “ 代码蔓延” 的方式就是在从Action 调用之前,将业务层代码 封装到helper 方法中: // Validate credentials with business tier public boolean isUserLogon (String username, String password) throws UserDirectoryException { return (UserDirectory.getInstance(). isValidPassword(username,password)); } // end isUserLogon 如我们在别处所说, Struts 1.1 使用新的execute 方法来替代原来的 perform 方 法,但是它们两个都可以工作。在这里我们使用perform 方法,以使代码可以运行在两个 版本之中: public ActionForward perform(ActionMapping mapping, ActionForm form, HttpServletRequest Request, HttpServletResponse Response) throws IOException, ServletException { Action 的目的是将输出从 web 层带到业务层,即应用其它部分生长之地。在这里,我 们从ActionForm (JavaBean helper) 中获取username和 password 并经它们存为平面的 Strings: // Obtain username and password from web tier String username = ((LogonForm) form).getUsername(); String password = ((LogonForm) form).getPassword(); 然后我们就可以将username 和 password Strings 传递给业务层功能,看是否通 过校验。这儿,我们小心地将调用封装进一个单独的方法,并没有将代码直接放入Action的 perform 方法之中。这个调用是对类中另一个方法的调用,但也可以容易的对其它Java 类 的方法进行调用: // Validate credentials boolean validated = false; try { validated = isUserLogon(username,password); } catch (UserDirectoryException ude) { // couldn't connect to user directory ActionErrors errors = new ActionErrors(); errors.add (ActionErrors.GLOBAL_ERROR, new ActionError("error.logon.connect")); saveErrors(Request,errors); // 返回到输入页面 return (new ActionForward (mapping.getInput())); } isUserLogon 方法的API说明,如果校验符合,它将返回 true ,如果不符合则返回 false,以及如果它不知道结果 ( 比如,连接不到目录) ,则抛出一个异常。如果异常发生, Action 就会捕获它,将这个事件转换成ActionError, 并将控制转发回input 页面。 如果业务层返回说logon 没有成功, Action 将送出错误消息,并将控制路由到输入页 面。当ActionForm 的validate 方法失败时,也会做同样的事情。( 第3.3.7节): if (!validated) { // post the error ActionErrors errors = new ActionErrors(); errors.add (ActionErrors.GLOBAL_ERROR, new ActionError("error.logon.invalid")); saveErrors(Request,errors); // return to input page return (new ActionForward (mapping.getInput())); } 因为错误并不适合于一个具体的属性, 我们在ActionErrors.GLOBAL_ERROR 关键 字下记录此错误,而不是一个属性名。为了指出logon本身无效,我们也在 validate外定 义了一个错误信息。在Struts 应用资源中,它是: error.logon.invalid=<li>Username/password combination is invalid</li> 显示时,表现层用适当的信息代替error.login.invalid 标记。如果有一个单独的 信息针对用户的当前场所,则用户会收到一个消息的本地化版本。 如果业务层说登录成功,我们就会告诉浏览器保留用户的认证信用。Java Servlet 框架 为此目的提供了一个用户会话。这儿我们存储用户的 logonForm 到会话上下文之中。 每个用户都有一个上下文, 由servlet 容器负责维护。那些在上下文中存储有 logonForm的用户都是登录用户。否则,便是未登录用户: // Save our logged-in user in the session, // because we use it again later. HttpSession session = Request.getSession(); session.setAttribute(Constants.USER_KEY, form); 这种策略称之为基于应用的安全。许多Java web 应用都使用这个方法,因为它轻便且 易于实现。其它身份认证方法也可以用在我们的应用中。 Struts 框架依靠容器的默认记录系统。这儿,我们仅在当Web.xml 文件中Servlet 的debug level设置为足够高时记录这个事件。 // Log this event, if appropriate if (servlet.getDebug() >= Constants.DEBUG) { StringBuffer message = new StringBuffer("LogonAction: User '"); message.append(username); message.append("' logged on in session "); message.append(session.getId()); servlet.log(message.toString); } 在web.xml 中设置为: <init-param> <param-name>debug</param-name> <param-value>2</param-value> </init-param> 在生产应用中, 你可以将 debug 设置为 0, 这些项目将不会出现。如果要插入其它记录 系统,开发者可以继承 Struts ActionServlet 类,并重写 log 方法。 NOTE 在 Struts 1.1中 ,通过支持 Jakarta Commons Logging 组件 [ASF Commons]使用另一个注册包。 当所有的信息发出,所有的工作干完, perform 方法返回一个ActionForward 给控制 器 (ActionServlet) 。这里, 我们发送控制到success forward: // 返回 success return (mapping.findForward (Constants.SUCCESS)); } 这个转发是在Struts 配置中定义: <forward name="success" path="/pagess/Welcome.jsp"/> 既然我们已经登录,表现页面就会应该有点变化。所以将显示一个登出连接。 3.3.9 LogoffAction 源代码 请看图3-5 ,当用户登录进去后,欢迎页面是如何改变的。在Welcome.jsp 中, <logic:present> 标签首先查看是否在会话上下文中被 LogonAction 放入一个user bean: <logic:present name="user"> 然后置入一个 <html:link> 标记,引用forward: <html:link forward="logoff">Sign out</html:link> 在 Struts 配置中, logoff 是这样定义的: <forward name="logoff" path="/logoff.do"/> 这里的路径引用到.do 动作。这应该是个相应的 /logoff ActionMapping,也在 Struts 配 置中定义: <action path="/logoff" type="app.LogoffAction"/> 如你所见, /logoff 是个极其简单的mapping; 它仅仅是将控制传递给 LogoffAction 类, 没有其他特殊参数和设置。LogoffAction 类的工作也非常简单。只是将用户的 logonForm 对象从会话上下文移除。 如果没有 logonForm 在会话上下文中, 用户就被 认为是已经登出了。 我们来看看LogoffAction 的源代码: public ActionForward perform(ActionMapping mapping, ActionForm form, HttpServletRequest Request, HttpServletResponse Response) throws IOException, ServletException { // Extract attributes we will need HttpSession session = Request.getSession(); LogonForm user = (LogonForm) session.getAttribute(Constants.USER_KEY); // Log this user off if (user != null) { if (servlet.getDebug() >= Constants.DEBUG) { StringBuffer message = new StringBuffer("LogoffAction: User '"); message.append(user.getUsername()); message.append("' logged off in session "); message.append(session.getId()); servlet.log(message.toString()); } } else { if (servlet.getDebug() >= Constants.DEBUG) { StringBuffer message = new StringBuffer("LogoffAction: User '"); message.append(session.getId()); servlet.log(message.toString()); } } // Remove user login session.removeAttribute(Constants.USER_KEY); // return success return (mapping.findForward (Constants.SUCCESS)); } } // end LogoffAction 清单 3-6 LogoffAction的源代码 首先,我们获得logon 对象。本应用的做法是存放用户的logon 对象到会话的上下文, 并 以 Constants.USER_KEY作为关键字, 所以我们会看到: // Extract attributes we will need HttpSession session = Request.getSession(); LogonForm user = (LogonForm) // Log this user off if (user != null) { if (servlet.getDebug() >= Constants.DEBUG) { StringBuffer message = new StringBuffer("LogoffAction: User '"); message.append(user.getUsername()); message.append("' logged off in session "); message.append(session.getId()); servlet.log(message.toString()); } } 之前,如果debug level 设为为高,我们记录了一些细节。 这是该类的核心行为。我们移除了存放在USER_KEY下面的对象,就意味着, 用户登出了。 // Remove user login session.removeAttribute(Constants.USER_KEY); 如果想要移除为用户存储在会话中的所有东西,可以简单的使用invalidate 方法: session.invalidate(); 但这也消灭了所有用户对象,比如用户场所locale, 它要用来进行本地化。 当登录操作完成后,返回到 welcome 页面: // return success return (mapping.findForward (Constants.SUCCESS)); 在下一节,我们要看看应用是如何从头构造的。到此,我们已经讨论了页面和类的内部 细节。现在我们将眼光放在Struts 配置文件和源代码结构树。 3.4 构造应用 我们已经详细探讨了应用的驱动,踢了踢轮胎,并检查了引擎盖下面的东西,准备上路 了。我们已经知道了要做什么,并且怎样做,但是从那里开始构建的你应用呢? 在这一节,我们回到开头并向你展示可以如何从头至尾建立你的应用。因为我们已经有 一个很好的关于我们想要应用干什么的印象,我们从一个实际的需求集开始。这样我们可以 创建一个白板计划,包括那些明显的对象以及我们准备叫它们什么名称。然后我们就可以开 始为对象编码,精化和扩展所作的计划。这种 计划/ 编码, 再精化- 计划/ 再精化- 编码的方法称 之为 “ 螺旋式” 方法。 当然,有其它方式来进行软件开发。其它方法也可以很好的用于Struts 。但本节的目的 并不是节是软件开发方法论。我们想要的是展示,构架一个简单的Struts 应用需要做些什么。 所以,让我们开始上路喽... 3.4.1 定义需求 Requirements are the effects that the computer is to exert in the problem domain, by virtue of the computer’s programming —Practical Software Requirements, by Benjamin L. Kovitz 既然我们已经有一个关于应用需要做些什么的充分理解,从一个需求集开始是一个很好 的实践方法。下面的章节,我们将汲取最简单有用的需求集。 我们的简单需求文档包括3 个主要部分:目标、需求、规约。 表格 3-5 需求文档的主要标题 标题 目的 目标 我们在问题领域中需要达到的结果 域需求 为达到目标,我们需要实现的东西 编程规约 我们要实现需求需要做的事情 3.4.1.1 目标 l λ 允许有权限的用户向应用标识自己 3.4.1.2 域需求 λ 允许用户递交他们的身份信用信息(username 和password) λ 检验递交的信息是否有效 λ 允许纠正无效的信用信息 λ 身分信用信息有效时,通知用户 λ 允许校验有效的用户访问需要权限的特征 λ 允许用户在需要时,使访问无效 3.4.1.3 程序规约 λ 可以从标准的web 浏览器访问, 可以使用或不使用JavaScript λ 为新访问者从welcome 页面提供登陆链接 λ 允许在logon 页面输入用户信用(身份)信息 λ 要求每个身份信息包含1-20个字符 λ 要求输入username 和password λ 提交信息给业务层方法来进行校验 λ 返回无效的身份信息给用户做纠正 λ 如果身份有效,使用户登入 λ 登陆后,用用户名定制welcome 页面 λ 允许登陆的用户从页面登出 当然, 这是一个非常简单的规约。许多规约要消耗大量的纸张,并用很多图, 数据表 , 屏 幕定义和问题域的详细描述来进行修饰。 3.4.2 规划应用 根据手头的这些需求,我们可以开始勾画应用并规划要用哪些对象来实现这些程序规 范。一个方法是列出规约的列表,以及有助于实现他们的组件清单。通常,团队做这种事情 是在一个大白板上进行,作为一个初始项目会议的一部分。 注意 这个过程中非常重要的一点是要注意到,规约项目和实现它们的组件之间并不是严格的 1: 1对应关系。虽 然规约和程序都是为了一个相同的目标,它们却是从不同的方面的接近目标。 所以,象这样的列表并不是 一个“规范化”的东西。某些规约可能已不止一个组件的方式出现。 3.4.2.1 视图 实际上,大多数应用都是以一个故事板(情节串联板)开始的。 JSP定义 应用的可见部 分。 下面是表现层的大概要求。 表格 3-6 白板视图规划 规约 JSP页面 向新访问者提供从欢迎页面开始的登陆 Welcome.jsp 允许在登陆页面输入用户身份信息(username 和 password) Welcome.jsp 返回无效的身份信息给用户纠正 Logon.jsp 当用户登陆进去时,根据用户名定制欢迎页面 Welcome.jsp 允许将校验的用户从welcome 页面登出 Welcome.jsp 可以从标准浏览器中访问,有或者没用JavaScript Logon.jsp; Welcome.jsp 导引用户到欢迎页面 index.jsp 请注意我们在规约中加了一个自定的规约。这是个小技巧,在 Struts 应用中使应用的 welcome 页面重定向到一个Struts action 。这尽可能将控制流纳入框架,以便有助于在应用 增长时最小化改变。 3.4.2.2 控制器 在一个强壮的分层应用中( 见第2 章), 所有对页面和数据的请求请求都传递给控制层。表 8是我们的控制器( “ 前端控制器” [Go3]) 的需求。 表格 3-7 控制器规划 规约 ActionForms 允许在登录页面中输入身份信息(username and password) LogonForm Action 用业务层方法校验身份信息 LogonAction 向返回返回无效身份信息供纠正 LogonForm;LogonAction 如果身份信息有效登入用户 LogonAction 允许经过校验的用户从欢迎页面登出 LogoffAction ActionForwards 向新访问用户提供从欢迎页面的登录 Welcome, logon 允许经过校验的用户从欢迎页面登出 Logoff ActionMappings 提交身份信息给业务方法校验 LogonSubmit Utility 记录所有内部常数 Constants 请注意我们自己添加了一个规约 “ 记录所有内部常数” 。这在多层应用中也非常重要, 因为这些常数可以“ 松散绑定” 。Java 编译器不能验证我们在 XML 配置 文件中使用的标 志,所以必须仔细跟踪我们所使用的标志。 3.4.2.3 模型 我们对数据访问层仅有一个需要。 表格 3-8 model规划 规约 方法接口 提交身份信息给业务方法校验 boolean isUserLogon(String username,String password); 3.4.3 规划源代码树 现在有了一个基线应用计划,我们可以规划一下应用的代码结构。因为应用很简单,我 们可以为页面使用一个单独的子目录,为Java 类使用一个单独的包。 我们的结构如图3-6 : 注意 Struts希望在 classpath中有一个应用资源包。 logon 应用将它的属性文件方在其自己的资源包中。 国际化应用可能具有多个属性文件。将他们放入包中可以方便进行组织。我们的构建文件( build file )将这些文件拷贝到类文件夹,以便在运行时它们可以在 classpath中找到。在进行某些资源 改变的时候,请记住要重新构建应用。 图 3-7 代码结构 如果你想跟随我们并建立了你自己的 logon应用 , 跳到代码结构和 Struts classes得好办法 是部署一个空白的应用: 下载一个空白应用。 将 blank.war拷贝为 logon.war。 将 WAR 放入容器的自动部署文件夹 (通常是 webapps). 这既是为什么要提供一个 Blank 应用。它们其实是一个通用的应用模板。我们将在第 3.4.4到 3.4.8节讨论基本结构。然后再来关注 logon 应用。 为了修改和重新部署应用 , 需要安装一些开发工具。 3.4.4 设置开发工具 略 3.4.4.1 安装 Ant 略 3.4.4.2 安装 jEdit 略 3.4.5 设置 build.xml文件 象现今的其他一些Java 产品, Struts 也希望使用Jakarta Ant 工具 [ASF, Ant] 作为构建过 程的一部分。 Ant 也使用一个 XML 配置文件, 名字叫 build.xml。 通常,你可以为你的应用 设置一个固定的build 文件,自始至终不变。在第4 章,我们再表述 logon 应用的构建文件。 3.4.6 设置 web.xml文件 Java 2 Servlet 框架使用一个配置文件来帮助设置应用。 web 部署描述符, web.xml, 标明需要的servlets ,以及其他应用设定参数。其格式在 servlet 规范[Sun, JST] 有描述。大多数 Struts 应用仅需要部署一个单一的servlet 和几个标记 库,以便保持相对简单。我们在第4 章表述logon 应用的web.xml 文件。 3.4.7 设置 struts-config.xml 文件 非常象web 部署描述符, Struts 也有一个 XML 配置 文件。你的应用在此注册其 ActionForm, ActionForward, 和ActionMapping 。每个类在文件中都有其自己的一个配置段, 在这里你可以定义在启动时需要创建的缺省对象。下面是我们的起始Struts 配置文件: 表格 3-9 配置文件 <?xml version="1.0" encoding="ISO-8859-1" ?> <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts configuration 1.0//EN" "http://jakarta.apache.org/struts/dtds/struts-config_1_0.dtd"> <struts-config> <form-beans> <!-- ... --> </form-beans> <global-forwards> <forward name="welcome" path=" /Welcome.do"/> <!-- ... --> </global-forwards> <action-mappings> <action path="/Welcome" type="org.apache.struts.actions.ForwardAction" parameter="/pages/Welcome.jsp"/> <!-- ... --> </action-mappings> </struts-config> 在你配置你的应用时, 你可以从一个空白配置文件开始,就像这个一样,并一步步加入 你需要的对象。我们将在本章余下的内容做这个事情,以便你可以看到是如何实际配置Struts 配置文件。第4 章将更深入讨论Struts 配置文件。你可能会注意到我们的起始并不是完全空 白的。为方便已经提供了一个缺省 welcome forward 。 3.4.7.1 welcome action 通常, 路由页面流要尽可能的通过Struts 控制器。这样将应用总体设计都保持在Struts 配置之中。这样你可以从一个单一的地方来调整应用的控制流。不幸的是, 容器要求一个物 理的welcome 页面。在web 部署 描述符 (web.xml) 中列出一个Struts action URI作为 welcome 页面是不允许的。 最好的方案是在一个根index.jsp文件中将控制重定向到你的welcome action 。 struts-blank 就提供了这样一个根。这是一个极其简单页面仅有两行的页面: <%@ taglib uri="/tags/struts-logic" prefix="logic" %> <logic:redirect forward="welcome"/> 空白应用提供 index.jsp 转发页面和一个缺省的welcome 页面。我们将照原样使用 index.jsp 弹要对welcome 页面做些改变。然而在开始之前,我们测试一下部署情况。 3.4.8 测试部署情况 在测试一个新的应用之前确保一切OK, 应该打开一个运行的应用作为基线。Struts Blank 应用是一个基线应用的好选择。其缺省welcome 页面包括了一些基本的系统检测,以 看看配置文件是否被正确装入,标签扩展是否能找到,以及消息资源文件是否有效。 Struts Blank 应用的 WAR 文件可以在本书的站点或者Struts 发布包中找到。将blank.war 文件放入容器的自动部署文件夹并重新启动容器。然后可以使用这个url 访问应用的welcome 页面 : http://localhost:8080/blank 如果一切OK, 就会看到页面图3-8 一样的页面: 图 3-8 空白应用的欢迎页面 下面是它的源代码: <%@ page language="java" %> <%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %> <%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %> <%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %> <html:html locale="true"> <head> <title><bean:message key="index.title"/></title> <html:base/> </head> <body bgcolor="white"> <logic:notPresent name="org.apache.struts.action.MESSAGE" scope="application"> <font color="red"> ERROR: Application resources not loaded -- check servlet container logs for error messages. </font> </logic:notPresent> <h3><bean:message key="index.heading"/></h3> <p><bean:message key="index.message"/></p> </body> </html:html> 清单 3-7 空白应用的缺省欢迎页面源代码 3.4.9 构造欢迎页面 大多数软件开发方法的一个基本原则是尽快的得到一个可以工作的原型。如果你同意这 点。那么我们应该做的第一件事就是根据我们的规约构造我们的welcome 页面。这个 welcome 页面早期版本, 没有逻辑条件,可能看起来像: <%@ taglib uri="/tags/struts-html" prefix="html" %> <html> <head> <title>Welcome World!!</title> <html:base/> </head> <body> </ul> <li><html:link forward="logon">Sign in</html:link</li> </ul> <img src='struts-power.gif' alt='Powered by Struts'> </body> </html> 清单 3-8 欢迎页面的早期版本 因为这里引用到 logon ActionForward,我们需要将它添加到我们的Struts 配置中去。我 们也可以改变缺省 welcome 页面从Index.jsp 到Welcome.jsp: <global-forwards> <forward name="logon" path /Logon.do"/> <forward name="welcome" path /Welcome.do"/> <!-- ... --> </global-forwards> 这时,我们可以重新启动容器装入新的Struts 配置。 某些容器, 象 Tomcat ,让你重新装入一个单独的应用。 1.0 vs 1.1 在 Struts 1.0中 ,有大量的管理 Action,包括重新装入 Struts 配置。在 Struts 1.1被取消了,因为他们 和支持多应用模块相冲突 . 一旦新的配置装入, 我们可以打开新的welcome 页面: http://localhost:8080/logon/ 你将看到图3-9 的屏幕。 图 3-9 登录前的欢迎屏幕 然而,如果你点击连接,你将无法前进,会得到图 3-10的信息。 为修正这个错误,我们需要看看下一个目标,构造 logon 页面。 图 3-10 文件未找到错误 3.4.10 构造 logon 页面 回去看看 3.4.2的白板 , 我们看到 logon 页面需要收集 username 和 password, 并提交到 一个名为 /LogonSubmit的 Mapping。这意味着我们需要创建一个 Struts form 来标明 /LogonSubmit action, 作为一个文本域和密码域的输入控件。下面是 logon.jsp的源 代码: <%@ taglib uri="/tags/struts-html" prefix="html" %> <html><head><title>Sign in, Please!</title></head> <body> <html:errors/> <html:form action="/LogonSubmit" focus="username"> <table border="0" width="100%"> <tr> <th align="right>"Username: </th> <td align="left"><html:text property="username"/></td> </tr> <tr><th align="right">Password: </th> <td align="left"><html:password property="password"/></td> </tr> <tr> <td align="right"><html:submit property="submit" value="Submit"/></td <td align="left"><html:reset/></td> </tr> </table> </html:form> </body> </html> 清单 3-9 Logon页面的源代码 <html:form> 标签记引用一个 ActionMapping 对象, 它再引用其他对象 (org.apache.struts.action.ActionMapping)。我们先配置ActionMapping ,然后是它使 用的对象: <action-mappings <action path="/LogonSubmit" name="logonForm" scope="Request" validate="true" input="/pages/Logon.jsp"/> <!-- ... --> </action-mappings> 两个相关的对象是 logonForm form bean 和 LogonAction 。我们也需要在Struts 配置 中注册ActionForm Bean 。我们使用的名称成为对象在请求上下文中创建时的缺省属性名称。 <form-beans <form-bean name="logonForm" type="app.LogonForm"/> <!-- ... --> </form-beans 这时我们需要加入两个特别的Java 类, LogonForm 和 LogonAction 。 3.4.11 构造 Constants类 虽然不是严格要求,但是将ActionForward 名称和其他标记记录在文档中则是强烈推荐 的。这其实很简单,而且可以使你的代码易于管理。我们展示代码时,通常忽略了 JavaDoc 说明。但是,在这里我们将留下他们。为什么?因为整个类都是记录常数的类。所以,在这 里,文档就是代码。下面是整个类的代码: package app; public final class Constants { /** * The session scope attribute under which the Username * for the currently logged in user is stored. */ public static final String USER_KEY = "user"; /** * The value to indicate debug logging. */ public static final int DEBUG = 1; /** * The value to indicate normal logging. */ public static final int NORMAL = 0; /** * The token that represents a nominal outcome * in an ActionForward. */ public static final String SUCCESS= "success"; /** * The token that represents the logon activity * in an ActionForward. */ public static final String LOGON = "logon"; /** * The token that represents the welcome activity * in an ActionForward. */ public static final String WELCOME = "welcome"; } 清单 3-10 Constants类的源代码 3.4.12 构造其他类 我们在3.3.8 和 3.3.9展示了logonForm 和logonAction 的源代码。我们也需要在第1 章中讲述过 的UserDirectory 和UserDirectoryException 类。我们可以将他们不经改变的加入到我们的新 应用中。在我们的源代码结构中将他们放在/ WEB-INF/src/java/app/。 图 3-11 LogonAction,LogonForrm以及其它Java类的位置 LogonAction 也引用 Constants 类。在便以前我们要先加入它。 3.4.13 创建 user directory 在第 1章,我们引入了一个简单的 registration 应用来存储 user ID和 password。这些登陆 账号纪录在一个标准的属性文件中, 名为 user.properties。 它 可以从那个应用中引入, 或者在 WEB-INF/src/java/resources下重新创建,如图 3-12: 图 3-12 用户属性文件的位置 属性文件是一个简单的文本文件。 这里是一个简单的例子, 使用本书作者的名为用户名, 姓为密码。 TED=Husted CEDRIC=Dumoulin GEORGE=Franciscus DAVID=Winterfeldt CRAIG=McClanahan 如果你喜欢,你可以只输入这些,或者你愿意输入的内容,并将他们存放在 /WEB-INF/src/java/resources/user.properties。只是保证user ID 全部大写,因为这是业务逻辑中 要求的。 当然,你的应用也可以容易的根据一个JNDI 服务,或者数据库来进行校验, 或者使用 容器的安全领域。我们在第14章说明在Struts 中使用数据服务。 3.4.14 配置 ActionErrors 可能记得, LogonForm 和 LogonAction 都要产生错误信息。ActionError 系统是集成 在应用消息之中的。 在测试LogonForm 和LogonAction 之前,我们需要添加这些消息到 Application.properties 文档中: errors.header=<H3><font color="red">Validation Error</font></H3>You must correct the following error(s) before proceeding:<UL> errors.footer=</UL><HR> error.username.required=<LI>Username is required</LI> error.password.required=<LI>Password is required</LI> error.logon.invalid=<LI>Username and password provided not found in user directory. Password must match exactly, including any lower or upper case characters.</LI> 1.0 vs 1.1 Struts 1.1中的新标记允许从消息中忽略标记。见第 10章的详细内容。 作为构建过程的一部分,我们将应用资源文档从/ WEB-INF/src/java/resource拷贝到 classes 文件夹下的资源包,这里ActionServlet 才能找到它们。 3.4.15 编译并测试 logon 页面 在3.4.8 中,我们为logonForm 创建了 JSP 页面。但为了使他们运行,我们得添加相应的 配置元素和JAVA 类,如表10。 表格 3-10 Logon 页面配置元素 配置元素 Java 类 LogonSubmit action-mapping logonForm form-bean LogonAction, subclass of Action LogonForm, subclass of ActionForm 现在我们可以编译应用并测试logon 页面了。有一个存好的build.xml 文件放在 WEB-INF 目录,你可以在Ant 使用它。 按缺省方式构建目标,编译它,将从java 源代码构建 Java 类,并将应用资源消息文件拷 贝到classes 目录。 当构建成功,你可以使用下面的链接进到应用中的 logon 页面。 ( 根据你的容器如何 重新装入Java 类,你可能需要重新启动容器。如果不确定,就重启它。) logon 应用应该可以工作的象我们原来漫游的一样 ( 从3.3.3 到 3.3.6) 。所不同的是 welcome 页面在用户登录后不会发生变化,或者不提供登出的机会。下面我们将修正它, 然后我们的应用就完成了。 3.4.16 修改 welcome页面 我们先前的welcome 页面忽略关于判断用户是否登陆的条件逻辑。既然,用户已经可 以登陆,我们就可以将这些语句加入,以符合3.3.2中的版本 。黑体部分是我们加入的内容: <%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-html" prefix="html" %> <%@ taglib uri="/tags/struts-logic" prefix="logic" %> <html> <head> <title>Welcome!</title> <html:base/> </head> <body> <logic:present name="user"> <h3>Welcome <bean:write name="user" property="username"/>!</h3> </logic:present> <logic:notPresent scope="session" name="user"/> <h3>Welcome World!</h3> </logic:notPresent> <html:errors/> <ul> <li><html:link forward="logon">Sign in</html:link></li> <logic:present name="user"> <li><html:link forward="logoff">Sign out</html:link></li> </logic:present> </ul> <img src='struts-power.gif' alt='Powered by Struts'> </body> </html> 清单 3-11 welcome 页面 (/页面s/Welcome.jsp)的修订版本 如图3-13, 这使我们回到了原来的情形。当用户到来时,他被邀请登录进去。一旦登录 进去,则按名字欢迎它并提供登出的机会。这是你的进步! 3.4.17 Struts ActionForward Action 如果你注意到你浏览器的地址栏,你可能会注意到我们一直没有暴露 JSP 页面的地址。 许多应用可能不注意这个方式,但如果你想要严格的MVC 架构( 见第2 章), 你可能不希望暴 露一些关于你的视图的实现,包括你是否使用 JSP 页面或者你如和存储它们。理想情况下, 所有的导航都应该通过.do Action 进行,因为他们都通过控制器管理。 当然,并不是所有的事情控制器都能干的很好。这也是为何使用logon 和 welcome 页 面。他们并不需要任何来自于model 的信息,直接提供显示JSP 页面的连接。 但这也使用户可以将页面的位置存为书签。接下来,你可能在显示logon 页面之前需要 执行一些背景动作,或者你可能需要移动或者将JSP 页面改名。如果用户将这些页面存为书 签了, 他们就会回到旧的地址,绕开你的逻辑,并返回一个 File not found 错误。在实践中, 通常导致将遗留检查放入服务器页面并重定向到web server— 事情发生错误的方式更多了, 并有更多代码需要维护。 道德?我们必须尽可能虚拟化详细的导航。否则,我们就会不断的为浏览器是否进行缓 存或者存储进行补偿。不要直接连接到物理的JSP, 我们应该总是连接到一个虚拟的 Struts Action, 然后由它提供相应的页面。 当然,不管是否需要,为每个页面编写一个Action, 会是一个非常繁琐的工作。一个更 有效的办法是,部署一个有用的 Action ,它可以在Struts 配置中定制,并在需要的地方可 以重用。因为Struts 为每个Action创建一个多线程实例,这是一个非常有效的办法,确保控 制保留在控制器之中。我们需要做的就是将Action 和 path传递给页面。 高兴的是,你可以使用struts.jar 中的标准的ForwardAction 对象。你可以简单的目标路 径作为ActionMappin 的parameter 属性进行传递: <action path=" /Welcome" type="org.apache.struts.action.ForwardAction" parameter="/pages/Welcome.jsp"/> ForwardAction 将会从mapping 中查找目标路径,并使用它来返回一个ActionForward 给servlet 。 实际结果是 http://localhost:8080/logon/pages/Welcome.jsp不会出现在浏 览器的地址栏中,那样的话就可以被标为书签直接访问,浏览其中的Action URI 会出现: http://localhost:8080/logon/Welcome.do 用户仍然可以把这个地址记为书签,但是现在你有更多的控制了。你可以logon 活动的 实现而不用担心有什么被浏览器记为书签了。之前,你可能不得不考虑如果他们直接访问原 来的服务器地址,会发生什么事情。 在MVC 架构中, action就是你的API 。服务器页面是一个实现细节。如果JSP作为导航系 统的一部分暴露给了浏览器,控制器和试图层就混合了,MVC 的优势被削弱。 在需要直接导航至其他页面时,我们也可以加入其他ForwardAction 实例。因为应用仅 仅实例化一个ForwardAction ,我们所添加的实际上是一个ActionMapping 对象。如果要求使 用ForwardAction 通常的 MVC架构原因还不充分, Struts 1.1引入的模块化特征要求所有的 JSP 请求都要通过Action 。这是因为每个模块有其自己的配置上下文并且控制必须通过 ActionServlet 控制器传递,以便为JSP 页面选择配置。当然,如果你使用单个的缺省应用, 这并不是必须的。但是如果从一开始便遵循这个实践,那么你便可以在不改变任何东西的情 况下将你的应用变成一个模块。 3.5 小结 不管你在开发团队中担任何种角色 —工程师,设计员,架构师, QA—对整个应用是如 何运行的有个总体印象总是有帮助的。 在这一章, 我们综合的解剖了一个小而有用的应用 。 通过漫游,解剖,然后构造一个 logon 应用,我们向你展示了 Struts 框架实际上是如 何工作的。 作为构造阶段的一部分, 我们还创建了一个设计文档, 来规划我们的目标、 客 户 需求和程序规约。要使一个应用能运行,我们还构建了应用的 web.xml, Ant的 build.xml脚 本,以及 Struts配置文件。 为了使正确的构建部件到位,我们按需要的顺序构建了每个组件。 在此过程中,我们 也指出了最好的实践方法,并强调分离模型、视图和控制器的重要性。 在第 4章,我们会详细探讨 Struts配置文件。如我们在此所见,配置扮演了一个强有力 的角色,使应用易于设计和维护。 Struts In Action Page 3- 1 - 4 配置Struts组件 本章包括 Web应用部署描述符 Struts配置文件 应用资源文件 Ant的构建文件 Struts In Action Page 3- 2 - Change alone is unchanging. —Heraclitus (c 535–c 475 B.C.) 4.1 三个 XML文件和一个属性文件 除了Java类和JavaServer页面之外,开发人员必须创建,或者修改,几个配置文件以使 Struts应用能运转起来: web.xml. 这是Java Servlet 要求的web应用部署描述符。Servlet/JSP 容器使用这个文件来载入和配 置你的应用。 struts-config.xml. Struts框架的部署描述符。它用来载入和配置Struts框架使用的各种组件。 Build.xml. Jakarta Ant构建工具使用它来编译和部署你的应用。使用Ant不是必需的,但它在Struts 开发人员中很流行。 Application.properties. 该文件为你的Struts应用提供资源。像build.xml文件一样, 它不是严格要求的,但是大 多数Struts应用都要用到。 尽管处理这些文件看起来也许不象是在进行“Java 开发”,但是正确的使用它们却是使 你的web应用能拿得出手的基本要求。在这一章,我们会仔细讨论这些文件是如何工作的, 以及它们能对你的应用的开发和部署做些什么。 4.1.1 家族的其他人员 除了每个Struts应用都需要的配置文件外,还有其它一些Struts 应用也可能要用的东西。 如果使用可选的组件,可能还需要另外的XML配置文件,比如Tiles框架和Struts Validator。 如果你想要把你的应用分成多个模块,每个模块也要有其自己的Struts 配置和资源文件。 在本章,我们将首先关注核心配置文件,然后来配置Struts 1.1中的标准配置项。 1.0 vs 1.1 When this book was written, Struts 1.1 beta release 2 was in circulation. Some details may have changed between beta 2 and the final release. Be sure to check the book’s site [Husted] for any errata. 4.2 Web应用部署描述符 框架的核心是ActionServlet,Struts 把它当作是一个控制器。 虽然它也可以被子类化,但大多数开发人员都将它看成是一个黑盒。他们总是在web应 用部署描述符 (web.xml) 中配置它,然后让它自己工作。 Struts In Action Page 3- 3 - ActionServlet可以接受许多初始化参数。大多数都有合理的缺省值,不需要重新设定。 但有一些却必须设置,以使你的应用能正常工作。 在这一节,我们将考察一个典型的Struts web 部署描述符,并且详细讨论ActionServlet 的初始化参数。 4.2.1 Web.xml 文件 Web应用部署描述符的目的和格式在Sun Servlet 规范[Sun, JST]中定义。基本上,它哟 该告诉servlet容器如何配置servlet和其它你的应用需要的高层次对象。 Struts框架有两个组件需要从应用部署描述符中配置:ActionServlet 和标签库(可选)。 虽然大多数Struts应用的确需要使用标签库,但它也不是严格要求的。使用XLST 或者Velocity 的应用根本不需要配置标签库。 清单4.1 是我们的logon应用的web.xml 文件(见第3章)。 <!-- A --> <?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN" "http://java.sun.com/j2ee/dtds/web-app_2_2.dtd"> <web-app> <!-- B --> <servlet> <servlet-name>action</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>application</param-name> <param-value>application</param-value> </init-param> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/conf/struts-config.xml</param-value> </init-param> <init-param> <param-name>debug</param-name> <param-value>2</param-value> </init-param> <init-param> <param-name>detail</param-name> <param-value>2</param-value> </init-param> <load-on-startup>2</load-on-startup> </servlet> <!-- C --> <servlet-mapping> Struts In Action Page 3- 4 - <servlet-name>action</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> <!-- D --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <!-- E --> <taglib> <taglib-uri>/tags/struts-bean</taglib-uri> <taglib-location>/WEB-INF/lib/struts-bean.tld</taglib-location> </taglib> <taglib> <taglib-uri>/tags/struts-html</taglib-uri> <taglib-location>/WEB-INF/lib/struts-html.tld</taglib-location> </taglib> <taglib> <taglib-uri>/tags/struts-logic</taglib-uri> <taglib-location>/WEB-INF/lib/struts-logic.tld</taglib-location> </taglib> </web-app> 清单 4-1 Logon应用的Web.XML 代码中编号的地方对应下面的注释。 A 标识为web应用部署描述符—前两行将文件标识为一个web应用部署描述符。 B 配置 ActionServlet—这一段告诉容器装入action名称下的ActionServlet。有四个参数传递 给ActionServlet: application,config, debug, 和detail。(ActionServlet也可以接受其它 参数;我们将再下一节涉及)。这一段的最后一个设定,<load-on-startup>,给容器一个action servlet 的权重。如果设置为2,则允许其它 servlet 在需要时首先装入。这将在你子类化了 ActionServlet时显得很重要,以便你可以使用其它servlet先期装入的资源。对一个应用来说, 仅可以装入一个ActionServlet 或者 ActionServlet 的子类。ActionServlet 设计为可以和应用 中的其它组件共享资源。装入多个ActionServlet会造成冲突;因为一个 ActionServlet可能会 改写另一个ActionServlet提交的资源。Struts 1.1 支持模块化应用,但仍然仅允许装入一个 ActionServlet。 C 标识Struts请求—这一段告诉容器将匹配*.do 格式的文件请求转发到action servlet。这就 是我们在A段中配置的ActionServlet。不匹配这种格式的文件请求将不被Struts处理。比如对 *.html 或 *.jsp 文件的请求通常由容器内建的服务来处理。 D 创建welcome文件—不幸的是,在这里设置一个index.do 文件将不会工作。容器希望 welcome 文件也是一个物理文件。在第3章,我们展示了如何使用welcome 文件来转发到一 Struts In Action Page 3- 5 - 个Struts action。 E 配置标签库—这里我们配置应用中使用的标签库。 3个核心的Struts标签库—bean, html, 和logic—将在大多数应用中使用。如果你的应用中使用了其它的标签库,他们也在此进行配 置t。第一个元素,<taglib-uri>,给出标签库的逻辑名称。这通常看起来像是一个文件路径, 但其实不是。JSP在导入标签库时将引用这个URI。第2个元素,<taglib-location>,提供提供 了一个上下文-相关的标签库描述符 (*.tld)路径。TLD 标识了库的实际类型(Java 类)。当需要 这个库时,容器会搜索标签库类文件的 classpath 。对Struts 标签库来说,容器将在struts.jar 文件中找到这些包。 关于web应用部署描述符得更多细节,请参见Java Servlet规范 [Sun, JST] 以及书籍Web Development with JavaServer 页面 [Fields]。 4.2.2 ActionServlet的参数 Struts ActionServlet 可接受多个参数,这些参数都总结在表 4.1中。大多数在Struts 1.1 中都不赞成了,这样有利于提供模块化支持的新的配置包中的组件。 表格 4-1 ActionServlet 参数 参数 缺省值 说明 备注 config /WEB-INF/strutsconfig. xml 包含配置信息的XML文件的上 下文-相关路径 config/${prefix} 使用标明的前缀的应用模块的 XML配置文件的上下文-相关路 径。 在多模块应用中可以根据需要 重复多次 1.1以后 convertNull false 一个参数,在组装表单时强制模 拟Struts 1.0 行为。如果设置为 true,数字的Java 包装类类型 (如 java.lang.Integer) 将缺省 为null(而不是0). 1.1以后 debug 0 调试的详细级别,控制针对这个 servlet将记录多少信息。接受的 值为0 (off) 和1 (最不严格)直到 6 (最严格)。大多数Struts 组件 设置为级别 0 或者2 detail 0 用来处理应用配置文件的 Digester 的调试详细级别。. 接受值为0 (off) 和 1(l最不严 格)到6 (最严格). validating true 标识是否使用一个检验XML 的 解析器来处理配置 文件(强烈 推荐)。 application 无 应用资源束的名称,风格像是一不推荐;使用 Struts In Action Page 3- 6 - 个类名称。引用到位于名为 resources的包中的一个名为 application.properties 的文件, 这里使用 resources.application。这种情 况下,资源可以是classes 下的 子目录(或者JAR 文件中的一个 包)。 <messageresources> 元素的 parameter属性进行配置 bufferSize 4096 处理文件上传时输入文件缓冲 区的大小 不推荐;使用<controller>元素的 buffer-Size 属性配置 content text/html 每个响应的缺省内容类型和字 符编码; 可以被转发到的 servlet 或者JSP重写。 不推荐;使用<controller>元素的 contentType属性配置 factory org.apache.struts.util.pr opertyMessageResource sFactory MessageResourcesFactory用来 创建应用消息资源对象的类名 不推荐;使用 <messageresources> 元素的factory 属性配置 formBean org.apache.struts.action. ActionFormBean ActionFormBean 实现使用的 Java类名称 不推荐;使用每个<form-bean> 元素的class-Name 属性配置 forward org.apache.struts.action. ActionForward ActionForward 实现使用的 Java类名。 不推荐;使用每个<forward> 元 素的className 属性配置 locale true 如果设置为 true,并且存在一 个用户会话,在用户会话中存储 一个合适的java.util.Locale对象 (在Action.LOCALE_KEY标识 的标准关键字下) (如果还没有 Locale 对象 存在的情况下). 不推荐;使用 <controller>元素 的locale属性配置 mapping org.apache.struts.action. ActionMapping ActionMapping 实现使用的 Java类名 不推荐;使用每个<action>元素 的className 属性配置,或者使 用模块应用的 <action-mappings>元素的 type属性配置 max文件Size 250M 文件上传时可以接收的最大文 件尺寸(Byte)。可以表示为"K", "M", "G"。分别解释为 kilobytes,megabytes, 或者 gigabytes, 不推荐,使用<controller>元素的 maxFileSize属性配置 multipartClass org.apache.struts.upload DiskMultipartRequestH andler MultiPartRequestHandler 实 现l类的全限定名称,用来处理文 件上传。如果没有设置,禁止 Struts 多部分请求处理 nocache false 如果设置为true,将在每个响应 前加上HTTP 头。这样可以使浏 览器对我们产生和转发的响应 不推荐;使用<controller>元素的 nocache属性设置 Struts In Action Page 3- 7 - 的缓存失效 null True 如果设置为true,那么如果使用 了未知的消息关键字,应用资源 将返回null。否则,将返回一个 包含不愉快信息的错误信息 不推荐;使用 <message-resources>元素的 null属性配置 tempDir 提供给web 应用作为 servlet上下文 属性的工 作目录 处理文件上传时的工作目录 不推荐;使用<controller>元素的 tempDir属性配置 4.3 Struts配置 Struts配置文件 (struts-config.xml)用来装入多个关键的框架组件。这些对象一起构成了 Struts 配置。 Struts 配置和 Struts ActionServlet 一起工作,来创建应用的控制层。在这一节,我们将 探索为什么需要Struts 配置。下一节,我们讲看看Struts 开发人员要如何来创建和维护配置。 注 从Struts 1.1开始,一个应用可以分成多个模块模块。每个模块都有其自己的Struts 配置。每个Struts 应 用至少有一个缺省,或成为 “根” 模块。如果你没有使用多模块,或者正使用Struts 1.0,那么当我们 说模块时,你可以将它想象成应用。 我们在本章的末尾讨论Struts 1.1 应用于多模块的配置。 4.3.1 细节, 更多细节 Struts 配置是你的应用的真实蓝图。它知道表单中有什么字段。它知道哪里可以找到JSP 文件。它也知道应用执行的每一个Action,以及每个action需要的实际资源。 这看起来好像是把许多信息集中在了一个地方。实际上就是。但是通过将这些实现细节 放在一起,许多开发人员会发现他们的应用更加易于创建和维护。 Struts 配置中的每个组件都是Java对象。ActionForm对象知道字段和表单。 ActionForward对象知道何处可以找到JSP。ActionMapping 对象知道那个表单和转发用于每 个应用能理解的命令。 一个非常简单的应用可以在一个实例化方法内创建所有这些信息对象,然后设置需要的 缺省值。例如: ActionForwards globals = new ActionForwards(); ActionForward logoff = new ActionForward(); logoff.setName("logoff"); logoff.setPath("/Logoff.do"); globals.addForward (logoff); ActionForward logon = new ActionForward(); logoff.setName("logon"); logoff.setPath("/Logon.do"); Struts In Action Page 3- 8 - globals.addForward (logon); 等等。 但是,实践中,初始化方法很快就会成为维护负担,并造成许多问题。 具有讽刺意味的是,像这样的类并不涉及到多少编程问题。它只是从存在的类中实例化 对象。它几乎不需要位于Java代码中。 事实上,它也不。Java 语言可以通过名称创建一些给定的类。Java 也支持一些如可以 决定一个类在运行时支持那些方法的反射特征。 定义 反射 告诉我们Java类提供什么方法。自省(Introspection)帮助我们推论出这些方法哪些是可以在运行时 用来配置JavaBean 的属性。Java 工具和框架 (如Struts)使用反射和自省来自动化装入和配置JavaBean 对象。这样就消除了哪些因为粗心易导致错误的编写和装入仅仅为了装入其他对象的简单对象时的任务。 将这些特征结合在一起,其实并不需要一个Java类。你需要的是一个文档来描述如何实 例化一个Java 类使之成为一个全功能的对象。 当然,象Struts这样的框架并不是唯一具有这个问题的东西。Servlet 容器基于同一原因 也需要同样的东西。开发人员不得不告诉容器应用中需要什么servlet以及其他一些对象。 不是编写一个Java 类并插入到容器之中,Sun 的工程师却是选择了使用一个XML文档。 容器读入这个文档并使用它来实例化和配置应用需要的servlet。 Struts 配置文件对Struts来说就像部署描述符对容器一样。Struts 控制器读入配置文件并 使用它来创建和配置框架需要的那些对象。 每个编写过部署描述符 (web.xml) 文件的web开发人员都应该使用过XML 元素来创建 一个Java对象。例如,我们可以在web.xml 中部署一个servlet: <servlet> <servlet-name>action</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <load-on-startup>2</load-on-startup> </servlet> 而下面则是我们在前面的Struts 配置 文件中部署一个forward 对象的代码片段: <global-forwards> <forward name="logoff" path="/Logoff.do"/> <forward name="logon" path="/Logon.do"/> </global-forwards> Struts In Action Page 3- 9 - 事实上, struts配置并不是配置 应用而是部署 它。但大多数开发人员,还是很自然的将它 这些对象视为是“配置,” 所以我们还是使用这个词汇。 4.3.2 变更管理 按这种方式部署预配置的Java 对象是个强大的特征。当然,强大的功能也带来强大的 职责。因为Struts配置文件装入框架对象,所以它也对框架对象负责。 通过描述框架组件间如何交互,Struts 配置文件成为了一个管理应用变更的非常有效率 的工具。实践中,文件要胜过一个简单的对象载入器,并被用作为动态的设计文档。 不难想象,可以用工具来读入Struts 配置文件并用它来产生和创建一个UML图。有好 多Struts GUI 现在都可以帮助你维护这个XML (参见4.4)。不久就会出现可视化的工具可以帮 助你维护由Struts配置文件表达的架构设计。 4.3.3 受保护的变更原则 Struts 配置文件帮助你可以以最小的努力对应用变更作快速的反应。如果一个对象需要 初始化为另一个值, 你并不需要编辑,编译和部署一个Java类。许多配置细节都涉及到表 现层。团队中工作于该层的人员可能不都是Java 工程师。使用XML文档可以使配置被页面设 计员和项目管理者都能访问到。需要Java工程师来创建和修改应用的基对象,但配置这些对 象却可以委派给其它人。实践中,我们常常将不经常的变更—基础Java 类—从经常变更的 事物—Java 对象在运行时如何部署中分离出来。这就是受保护的变更原则(principle of Protected Variation [Larman]). 定义 Protected Variation 受保护的变更是一个设计原则,它鼓励使用一个稳定的接口来封装变更的可以预知 点。数据-驱动设计,服务查询,解释器驱动设计,反射设计都是这种机制的不同实现。 受保护的变更可以让我们记住一个单一的变更点可以产生一个单一的维护点。通常从基对象 (不常改变)中分离出实现细节(经常改变),我们就可以减少维护应用要做的努力。 4.4 Struts配置元素 我们在第4.3节中所讨论过,Struts 配置文件是一个用来部署Java对象的XML文档。 配 置中的每个元素对应一个Java对象。当你在Struts 配置文件中插入一个元素,你就是告诉 Struts 控制器在应用初始化时要创建一个Java 对象。如果从一个有一些示例性注释的空白 的配置文件开始,可以很容易得将它修饰为你的应用所用。但是如果你只是遵循一些通用示 例的话,也可能容易丢掉一些重要的特征。 大多数Java 开发人员都知道,如果他们需要关于Java类的更详细的信息时,他们会查 找JavaDoc。但是,你如何查找关于一个XML 文档的更多信息呢? 每一个良构的XML 文档,包括Struts 配置文件,都包括一个描述该文档可用元素的指针。 这就是文档类型定义 (DTD)。如果你看看struts-config.xml文件的顶部,你就会发现这个元素: <!DOCTYPE struts-config PUBLIC Struts In Action Page 3- 10 - "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN" "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd"> 这是告诉我们这个文档的DTD的官方参考版本可以从所标示的URL找到。在内部, Struts 使用来 自于Jakarta Commons[ASF, Commons]项目的Digester来解析Struts配置文件。Digester 使用 struts-config DTD 来校验文档的格式,并且创建文档所描述的Java对象。如果 XML文件包含了 非正式文档化的元素,或者以非正式文档化的方式使用了元素,Digester 将不会处理这个文 件。 如果基于某些原因, XML校验产生了问题,你可以使用4.3节所描述的validating servlet 参数将校验特性关闭。但我们不推荐这样做。 在内部,Struts 使用其自己的DTD拷贝来处理配置。并不是每次在你的应用程序载入的 时候都要从Internet取回DTD的参考版本。 (基于某些神秘的原因,一小部分开发人员说, 将 validating设置为false好像可以在没有Internet 连接的时候有助于应用的载入。通常,不管 有没有Internet连接你都应该设置validating=true。) 表 4.2总结了可以在Struts 1.1中使用的配置元素。对Struts 1.1新加的元素我们会简要说 明。 表格 4-2 Struts配置元素 元素 说明 data-sources 包含一个DataSource 对象 (JDBC 2.0 Standard Extension)的集 合. data-source 标识一个DataSource 对象,可以被实例化,和进行配置,并在 servlet 上下文中作为一个属性 (或者在 application-scope的 bean中). set-property 标识一个附加的JavaBean 配置属性的方法名称和初始化值。 从Struts 1.1 global-exceptions 描述一个可以被Action 对象抛出的例外的集合 从Struts 1.1 exceptions 为一个例外类型注册ExceptionHandler form-beans 描述这个应用模块中的form bean 描述符集合 form-bean 描述一个可以被<action>元素引用的ActionForm 子类 从Struts 1.1 form-properties 描述一个 JavaBean 属性,可用来配置一个DynaActionForm 实 例或者其子类 global-forwards 描述对所有Action对象都可以作为返回值的ActionForward 对象 集合 forward 描述一个可以被Action作为返回值的ActionForward对象 action-mappings 描述一个可以用来处理匹配ActionServlet 注册到容器的 url-pattern格式的请求的ActionMappings 对象集合 action 描述一个ActionMapping 对象,可以用来处理一个对特定的模块 相关的URI的请求 从Struts 1.1 controller 描述一个封装了应用模块运行时配置的控制器配置bean 从Struts 1.1 message-resources 描述该模块的消息模板一起的消息资源MessageResources 对象 从Struts 1.1 plug-in 标识一个通用应用的plug-in模块的全限定类名,它接受应用的启 动和退出事件的通知 为了更方便,附录B 还以标准的API格式列出了struts-config DTD。在这一节,我们会讨论这些 Struts In Action Page 3- 11 - 元素并提供一些使用示例。关于每个元素及其接收的属性的具体细节,请参考附录B。 NOTE 如果你不喜欢手工编写XML,那么你可以在一个IDE环境中使用Struts,可视化工具会帮助你维护Struts 配 置。Scioworks Camino [Camino] 和 Struts Console [Console] 可以直接管理Struts配置文件。 其他产品,比如Synthis [Adalon]的Adalon以及ObjectVenture [ObjectAssembler]的ObjectAssembler, 都可以帮 助你以可视化的方式设计应用,并为你编写初始的Struts 配置,Java 类和JSP。关于当前最新的Struts相关 产品,请参见Struts Resources 页面 [ASF, Struts]. 如上所述,现今许多编成组件都以XML文件的方式进行配置,包括Java servlet容器。关 于Java 和 XML的更多知识,我们推荐 J2EE and XML Development [Gabrick]这本书。 4.4.1 <global-exceptions> 在一个Struts应用中,ActionServlet位于调用树的最顶层,但工作会委托给Action对象。 这种分而治之的策略在许多环境下都工作的很好,例外会被进行例外处理。许多应用喜欢用 一致的方式来处理例外,但这会在多个Action复制例外处理代码。 为了在全部Action对象中进行一致的例外处理,你可以在一个Struts配置文件中注册一 个ExceptionHandler。框架提供了一个缺省的 xceptionHandler (org.apache.struts.action.ExceptionHandler) ,它可以在一个请求范围的属性 下存储例外,为里外信息创建 ActionError 对象,并转发控制到JSP 或 其他你选择的URI。 Struts <html:errors> 标签会自动输出你的例外信息的本地化版本。所以,你可以使用同 一个页面来显示你想用来显示的校验错误的例外错误信息。 如果你还需要做些其他的事情,ExceptionHandler可以被子类化而加入新的行为。如果 需要的话,每个里外都可以标识其自己的句柄类。 你可以为一个例外注册一个全局句柄以及针对某个ActionMapping的局部句柄。要注册 一个例外,你需要提供Exception 类型,消息关键字,以及响应路径,如下所示: <exception type="org.apache.struts.webapp.example.ExpiredPasswordException" key="expired.password" path="/changePassword.jsp"/> 请参见第9章,关于如何编写你自己的例外处理句柄。 4.4.2 <form-beans> Struts ActionForm (org.apache.struts.action.ActionForm) 提供了一个方便的存 储通过HTTP请求提交的输入属性的方法。但是为了存储输入属性,控制器必须首先创建一 个ActionForm并将其存储在请求或者会话的上下文中,在这里,可以被其他框架组件—比如 JSP 标签—所找到。. 如果在输入页面上有多个表单,那么每个表单都需要其ActionForm 对象有不同的属性 名。因此,你不能仅使用标准的命名。因为bean特性名称是其公共API的一部分,我们应该能 提供开发者友好的ActionForm命名,比如logonForm之类。 某些特别的ActionForm,比如 DynaActionForms (org.apache.struts.action.DynaActionForm),需要在创建时传递额外的属性。所以, Struts In Action Page 3- 12 - 我们也需要有地方来放置这些元素。ActionFormBean (org.apache.struts.action.ActionFormBean)通过将之存储为一个ActionForm对象描 述符来解决了所有的这些问题。每个ActionFormBean 都有描述一个ActionForm 特性名称和 类型的属性。ActionFormBean 也包含 property特性,可以在DynaActionForm中使用。 Struts 配置文件提供了一个 <form-bean> 元素来归类一个模块所使用的 ActionFormBean。每个ActionFormBean 都由一个相应的 <form-bean> 元素创建。在运行时, 控制器调用适当的ActionFormBean 来找出哪一个ActionForm 对象被创建,在哪里存储它, 以及什么特性需要使用。s 下面是一个针对常规ActionForm和DynaActionForm的 <form-bean> 元素配置: <form-bean name="menuForm" type="org.apache.struts.scaffold.MenuForm"/> <form-bean name="logonForm" type="org.apache.struts.action.DynaActionForm"> <form-property name="username" type="java.lang.String"/> <form-property name="password" type="java.lang.String"/> </form-bean> menuForm <form-bean>表达了一个常规的ActionForm 子类;它要求一个对应的Java 类支持。logonForm <formbean>并不要求使用一个特别的子类,但可以使用DynaActionForm。 (DynamicActionForm从Struts 1.1中引入) 请参见第5章获取关于ActionForm得更多信息,包括DynaActionForm。 4.4.3 <global-forwards> 通过集中细节,Struts 配置将改变最小化。当环境改变时,多数实现细节可以通过培植 进行改变,而不用改动Java 或者 JSP 源代码。 Web应用中一个令人痛苦的细节处理就是URI [W3C, URI]。多数URI都直接映射到应用 目录书中的物理文件。这对常规的站点来说还比较容易。要将 “一个页面放到web上,”你 只需要讲页面存储到站点的某个目录。而目录已经映射到站点的公共URI,没有其他需要配 置的了。 发布web 页面其实就是传输文件。这其实很简单,直到你想删除么某些页面或者使用 不同的页面。当这些事情发生了,(实际上它们经常发生),你不得不更新应用中所有对该页 面的引用之处。如果漏掉了一些,某些地方仍然引用到旧的页面,你就会遇到我们的数据库 人员所称的“异常更新”(update anomaly)。两个假定为相同的情况现在都不同了。数据 库 对这个问题的解决方案是规格化(normalization.)我们将情况存放进一个表,而每个人 都从这个表中进行查找。如果情况改变,我们仅仅需要更新情况表,每个人都会被带到相同 的页面。 Struts In Action Page 3- 13 - Struts 处理 URI 表的方法是 ActionForward 。一个ActionForward 对应一个URI的逻 辑名称。其他组件可以引用这个名称,而不需要知道任何有关 URI的情况。如果URI 改变, 我们只需要改变它的ActionForward。 其他组件通过请求ActionForward的路径来得到更新过的 URI。这样,通过封装实现细 节到逻辑名称之后, 我们最小化了变更并减少了潜在错误。 ActionForward的主要使用者是Action 对象。当一个Action 完成时,它返回一个 ActionForward 或者null。如果 Action 没有返回null,ActionServlet 就将控制转发到返回的 ActionForward的路径。典型地,Action将通过名称查找ActionForward ,而不需要知道关于 URI的任何事情。你可以部署一个全局转发Global ActionForward在<global-forwards> 元素中,像这样: <global-forwards> <forward name="logoff" path="/logoff.do"/> <forward name="logon" path="/logon.do"/> <forward name="welcome" path="/welcome.do"/> </global-forwards> 这些 forward对应用中的每个Action都有效。你也可以部署一个局部 ActionForward到 <action> 元素中。局部转发仅针对该ActionMapping有效。 关于ActionForward参见第6章。 4.4.4 <action-mappings> ActionForm将应用需要的数据存储到collect中。ActionForward归类那些应用要用的URI。 ActionMapping 描述应用要采取的操作、命令。 Action 对象处理操作的实际工作。但一个操作有大量的管理细节。ActionMapping 就 是用来包装这些细节。 一个重要的细节就是用来调用Action对象的URI [W3C, URI]。Action的 URI 被用作一个 ActionMapping的逻辑标识符,或者路径。当web 浏览器请求一个Action的URI,ActionServlet 首先查找相应的ActionMapping。ActionMapping 则告诉 ActionServlet 哪个 Action 对象要 用于这个URI。 除了URI 路径和Action类型以外,ActionMapping 还包含了几个可以用来在Action被调用 时发生的行为的属性。改变这些属性会改变Action 对象的行为。这可以帮助开发人员是同 一个Action对象有更多的用途。如果没有ActionMapping 对象,开发人员可能需要创建比现 在多得多的Action类。 你也可以使用ActionMapping来简化到另一个路径的转发或者重定向。但绝大多数情况 下,它只是用来连接Action 对象。 <action-mappings> 元素描述了我们的应用要用来处理请求的ActionMapping 对象 Struts In Action Page 3- 14 - (org.apache.struts.action.ActionMapping) 的集合。请求要到达应用然后到达 ActionServlet,它必须匹配上下文和我们在容器中注册的 url-pattern 格式。 因为所有的请求都匹配匹配这个格式,我们就不需要使用上下文或者 url-pattern 来 标识一个ActionMapping。所以如果如果URL是针对 http://localhost/myApp/myAction.do 我们只需要引用 /myAction 作为 ActionMapping 的路径。每个 ActionMapping 都由对应的嵌入<action-mappings>元 素中的<action> 元素创建,如下所示: <action-mappings> <action path="/logoff" type="app.LogoffAction"/> <action path="/logonSubmit" type="app.LogonAction" name="logonForm" scope="request" validate="true" input="/pagess/Logon.jsp"/> <action path="/logon" type="app.ContinueAction"> <forward name="continue" path="/pagess/Logon.jsp"/> </action> <action path="/welcome" type="app.ContinueAction"> <forward name="continue" path="/pagess/Welcome.jsp"/> </action> 一个ActionMapping 可以引用很多属性。紧随Action对象之后,ActionMapping可能是 Struts 应用另一个最重要的对象。 关于ActionMapping的详细信息,参见第7章。 4.4.5 <Controller> Struts允许多个应用模块共享一个单一的控制器 servlet。每个模块有其自己的Struts 配 置并且可以相对于其他模块独立开发。<controler> 元素允许每个模块为ActionServlet标识 一套不同的配置参数。它们大多数是部署描述符中的原始 <init-params>设置。 Struts In Action Page 3- 15 - <controller> 元素设置的属性值存储在一个控制器配置bean (org.apache.struts.config.ControllerConfig)之中。每个应用模块要创建一个控制器 配置,包括缺省的根模块。如果一个模块的 struts-config 提供了一个 <contrller>元素, 就是用来设置模块的控制器配置bean的属性。 因为各个模块共享ActionServlet,你也可以为每个模块插入不同的请求处理器。这可以 使每个模块按自己的方式处理请求,而不用子类化共享的servlet。 下面是一个 <controller> 元素的例子,它设置nocache 和 null 配置属性为true 并 且装入一个定制的请求处理器: <contrller nocache="true" null="true" processorClass="com.myCompany.struts.RequestProcessor"/> 请求处理器是ActionServlet处理循环的核心。大多数情况下,你可以编写和装入一个请 求处理器,来代替创建你自己的ActionServlet子类。关于ActionServlet 和请求处理器的更多 信息,参见第9章。 4.4.6 <message-resources> 每个模块都应该有其自己的缺省消息资源束。这是一个Struts 组件,如JSP 标签,在没 有其他特别标明的情况下要使用的资源束。你也可以装入其它额外的资源束,连同特定的消 息模板。例如,许多开发人员喜欢将图像相关的消息放在一个单独的资源中。 <message-resources> 元素用来部署应用需要使用的资源束。下面是一个 <message-resources>元素的例子,它为模块部署了缺省的资源束,另一个则部署了一个图 像消息的资源: <message-resources parameter="resources.application"/> <message-resources parameter="resources.image"/> 在需要时,框架会在名为resources的包中的名称为application.properties的文件中寻 找缺省的消息资源束。包,或者文件文件夹,可以在容器的classpath路径的任何地方。典型 的,资源束也以JAR文件的方式,或者放在WEB-INF/classes 文件夹中。 如果 JSP标签,或者其他组件,标识了resources.image 资源束,框架会在resources 包中查找名为image.properties 的文件。 参见本章4.5节,获取关于消息资源束的更多信息,以及第13章如何本地化应用。 4.4.7 <plug-in> 对Action 来说,需要特别的资源来完成其工作的情况并不常见。不过,它可能需要使 用一个非数据源兼容的连接池。或者也许需要创建一个应用bean来为表单使用。也许需要读 入自己的配置文件来创建一系列的对象,就像struts-config所做的一样。在一个常规web 应 用中,这些任务通常委托给一个特殊的servlet。在Struts 应用中,我们倾向于将这些任务委 Struts In Action Page 3- 16 - 托给Action。当一个Action 需要初始化和销毁它自己的资源时,它可以实现 PlugIn 接口 (org.apache.struts.action.PlugIn)。这个接口宣称了init 和 destroy 方法, 控制器可以在适当的时候进行调用。 PlugIn Action 可以在Struts 配置中通过<plug-in>元素进行注册。下面是一个标准的 plug-in ,用来初始化Struts Validator: <plug-in className="org.apache.struts.validator.ValidatorPlugIn"> <set-property property="pathname" value="/WEB-INF/validator-rules.xml"/> <set-property property="pathname" value="/WEB-INF/validation.xml"/> </plug-in> 请参见第9 张获取关于ActionServlet和PlugIn Action得更多信息。 4.4.8 <data-sources> 虽然Struts 框架是模型中立的,它仍然需要和业务层甚至数据-访问层进行交互。一个 组件期望调用这给它传递一个活动的SQL连接(java.sql.Connection)的情况并不是不 常见。这也使调用者(比如Struts) 有责任管理连接的生命周期。为了提供给应用在连接到数 据访问组件时更多的灵活性, JDBC 2.0 标准扩展包提供了一个基于工厂的方法来获取数 据。对一个应用连接到数据库,或者其他数据服务的首选方法是使用一个实现数据源接口 (javax.sql.DataSource)的对象。 在一个web应用中,一个数据源对象通常代表一个应用中所有用户都可以共享使用的连 接池。获取一个数据库连接可能在实践和资源上都有很昂贵的代价。典型地,web 应用以 一个单独的账户登陆到数据库中,然后自己管理这个单独账户的安全性。 为了帮助开发人员使用连接, Struts 提供了一个数据源管理组件。你可以使用这个组 件来实例化和配置一些实现数据源的对象,并且可以从JavaBean的属性进行整体配置。 如果你的数据库管理系统没有提供满足这两个要求的组件,你也可以使用Jakarta Commons 数据库连接池基本数据源类 (org.apache.commons.dbcp.BasicDataSource)。 在Struts 1.1中,Struts 通用数据 源(org.apache.struts.util.GenericDataSource) 是一个BasicDataSource类的包 装类。(这个Struts 类已经不推荐了,仅提供向后兼容之用)。 如果你的数据库管理系统提供了它自己的数据源可供Struts使用,你就应该考虑使用这 个实现。在Struts中使用BasicDataSource 或者 GenericDataSource并不比使用其它类更有效 率。 要根据你的环境选择最好的实现。 你也可以配置不止一个数据源,然后根据名称进行选择。这个特征可以提供更好的安全 型和扩展型,或者用一个数据源实现和另一个进行比较。 下面是一个数据源配置,使用MySQL数据库的Struts 缺省配置: <data-sources> <data-source> <set-property property="maxCount" Struts In Action Page 3- 17 - value="4"/> <set-property property="minCount" value="2"/> <set-property property="description" value="Artimus:MySQL Data Source Config"/> <set-property property="driverClass" value="org.gjt.mm.mysql.Driver"/> <set-property property="url" value="jdbc:mysql://localhost:3306/artimus"/> <set-property property ="autoCommit" value="true"/> <set-property property ="user" value="root"/> <set-property property ="password" value=""/> </data-source> </data-sources> 不像其他struts配置元素, <data-source> 元素非常依赖于 <set-property> 元 素。 因为开发人员经常需要配置他们自己的DataSource子类,所以只有较少的属性内建进 了<datasource>元素之中。 数据源对象是Struts 和数据访问层之间的桥梁。而配置中的其他组件组成了Struts的控 制 层。 4.4.9 该你了 如果你子类化了一些Struts配置对象,你可以使用<set-property>元素来自己的属性到子 类之中。这就使你可以扩展框架类,而不用改变配置文件如何解析。下面是一个例子,它传递 一个XLS 样式表引用到一个 (假定的) ActionForward对象的定制实现: <global-forwards type="app.struts.XlsForward"> <forward name="logon"> <set-property="styleName" value="default"/> <set-property property ="stylePath" value=" /logon.xsl"/> </forward> </global-forwards> 当logon元素的XlsForward 对象被实例化时, Digester相当于调用 logon.setStyleName("default"); logon.setStylePath("/logon.xls"); Struts In Action Page 3- 18 - 你也可以在很多Struts 配置元素中使用这种方法,使所有的对象都成为完全可插入的。 4.4.10 Struts config 骨架 清单 4.2 是一个Struts 配置文件的骨架,展示了最常用的元素和属性。这个文件也和 Struts 空白应用中的配置文件非常相似: <?xml version="1.0" encoding="ISO-8859-1" ?> <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Config 1.1//EN" "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd"> <struts-config> <data-sources> <data-source> <set-property name="${}" value="${}"/> </data-source> </data-sources> <form-beans> <form-bean name="${}" type="${}"> <form-property name="${}" type="${}"/> </form-bean> </form-beans> <global-exceptions> <exception type="${}" key="${}" path="${}"/> </global-exceptions> <global-forwards> <forward name="${}" path="${}"/> </global-forwards> <action-mappings> <action path="${}" type="${}" name="${}" Struts In Action Page 3- 19 - scope="${}" validate="${}" input="${}"> <forward name="${}" path="${}"/> <exception type="${}" key="${}" path="${}"/> </action> </action-mappings> <controller processorClass="${}" /> <message-resources parameter="${}"/> <plug-in className="${}"> <set-property property="${}" value="${}"/> </plug-in> </struts-config> 清单 4-2 Struts配置骨架 完整的Struts配置元素和属性清单,参见附录B,struts-config API 。 4.5 应用资源文件 Struts框架提供了好用和灵活的消息系统。我们在第3章接触过这个系统,在第13章还要 详细讨论它。在这一节,我们需要用来使系统运转的配置文件。 在4.4.6节,我们看到了如何告诉Struts 在何处 找到资源束。在这一节,我们要讨论如 何创建资源束。消息的文本被存储在一个标准的Java 属性文件 (java.util.Properties) 之中。 在Java 和JSP 代码中,要给定一个消息的关键字;消息文本在运行时丛属性文件中检索。框架 文档将消息属性文件引用为 application resources 或者 message resource bundle。 如果你想要本地化你的应用,你可以为你想要支持的场所创建一个额外的应用资源文 件。这实际上是创建一个资源束 (java.util.ResourceBundle)。框架会为每个用户维护 一个标准的Locale对象(java.util.Locale)。针对用户场所的合适的消息会自动从资源束中进 行选取。关于本地化的更多信息,参见第13章。 定义 Struts In Action Page 3- 20 - Locale(场所) 对象是一个特定的语言和地区的识别符。 ResourceBundle 对象包含场所特定的对象。当需要一个场所特定的对象时,可以从资源束中取得,它返 回匹配用户场所的对象。 Struts 框架为消息文本使用基于字符串的资源束。 属性文件自身是一个平面的文本文件,每一行是一个关键字-值对。你可以使用任何文 本编辑器进行编辑,包括Windows Notepad。 应用资源文件的缺省名称是通过在web.xml向Struts ActionServlet传递一个初始化参数 决定的。下面这个片断,参数的名称是application: <init-param> <param-name>application</param-name> <param-value>application</param-value> </init-param> 这个参数没有缺省值。在应用中使用Struts应用资源束之前必须首先进行配置。应用资 源文件位于应用的CLASSPATH之中,这样Struts可以找到它。最好是放在你的应用的class 文 件夹中。这可能是在WEB-INF/classes 文件夹中,或者,如果你以二进制部署你的应用时在 WEB-INF/lib下的一个 JAR 文件中。 param-value 应该使你的文件按包命名格式的全限定名称。这意味着如果如果你将资源文 件直接放在classes下,你可以直接使用文件名,如前面的代码片断所示。 如果你将文件放在一个子目录中,那么该子目录就相当于一个Java 包。如果应用资源 束在一个名为resources的子目录下,你就应该这样来标识: <init-param> <param-name>application/param-name> <param-value>resources.application</param-value> </init-param> 物理文件的系统路径应该是: WEB-INF/classes/resources/application.properties 如果你将类移到了JAR中,不需要进行什么改变。Struts可以在 JAR 文件中找到资源束, 就想找到其他类一样。 为了本地化你的应用,为每个支持的场所添加资源文件,并修改基本名称: WEB-INF/classes/resources/ application.properties application_es.properties application_fr_CA.properties Application名称只是一个习惯。对框架来说没有缺省设定。你可以将名称改为你认为适 Struts In Action Page 3- 21 - 合的任何名字。另一个通用的缺省是使用应appplicationResources 作为名称,因为它一些 早期的Struts 例子中经常使用。 关于更多属性文件和资源束的信息,参见Sun Java 教程,以及本书地13章的国际化[Sun, i18n]部分。 本章稍后表述的Ant构建文件通过自动将资源束从源代码树中拷贝到二进制class 文件 夹中来在构建应用时帮助你管理应用资源束。这可以将原始文件和其他源代码放在一起。 4.6 Ant构建文件 虽然不是使用和配置Struts需要的严格要求的部分,许多开发人员还是使用Ant及其构建 文件来装配,部署,甚至测试他们的应用。我们的logon应用 (第2章)中的build.xml 文件就 是基于空白Struts 应用中提供的一个实际框架。 构建文件设置为使用源代码存储在WEB-INF子目录下的项目树。这使得整个应用,包括源 代码和编译文件,都集中在一个目录系统之中。这就能使你得应用的工作目录可以位于你的 开发服务器中。如果容器可以很好的重装类文件,你就可以重新构建应用来测试最新的改变, 而不用重启容器。 这个build.xml希望的源代码书结构请参见第4.3节。 清单4.3展示的是我们的一个简单的完整build.xml 文件。 <!-- a--> <project name="logon basedir="." deafult="dist"> <property name="project.title" value="Logon"/> <property name="project.version" value="1.2"/> <property name="dist.name" value="logon"/> <!-- b --> <path id="project.class.path"> <pathelement path="lib/struts.jar"/> <pathelement path="./classes/"/> <pathelement path="${classpath}"/> </path> <!-- c --> <target name="prepare"> <tstamp/> </target> <!-- d --> <target name="resources"> <copy todir="classes" includeEmptyDirs="no"> <fileset dir="src/resources"> <patternset> <include name="**/*.properties"/> </patternset> </fileset> Struts In Action Page 3- 22 - </copy> </target> <!-- e --> <target name="compile" depends="prepare,resources"> <!-- property name="build.compiler" value="jikes"/ --> <javac srcdir="src" destdir="classes"> <classpath refid="project.class.path"/> </javac> </target> <!--f--> <target name="clean" description="Prepare for clean build"> <delete dir="classes"/> <mkdir dir="classes"/> </target> <!-- g --> <target name="javadoc" description="Generate JavaDoc API docs"> <delete dir="./doc/api"/> <mkdir dir="./doc/api"/> <javadoc sourcepath="./src/java" destdir="./doc/api" classpath="lib/struts.jar:" packagenames="app.*" author="true" private="true" version="true" windowtitle=" API Documentation" doctitle="&lt;h1&gt;${project.title} Documentation (Version ${project.version})&lt;/h1&gt;" bottom="Copyright &#169; 2002"/> </target> <!-- h --> <target name="dist" description="createe binary distribution"> <delete dir="./dist"/> <mkdir dir="./dist"/> <war warfile="./dist/${dist.name}.war" webxml="../WEB-INF/web.xml" manifest="../META-INF/MANIFEST.MF" basedir="../" excludes="WEB-INF/dist,WEB-INF/web.xml,META-INF/MANIFEST.MF"/> </target> <!-- i--> <target name="project" depends="clean,prepare,compile,javadoc,dist"/> </project> Struts In Action Page 3- 23 - 清单 4-3 Ant的 Build.XML a project 给出一个构建文件总体名称,并且标识一个基础目录和缺省目标。当Ant装入文 件时,目标会首先锁定它的调用。要使用不同的目标, 改变这个缺省设置并存储文件,或 者在命令行中覆盖它。缺省基准目录设置为build.xml的当前目录。脚本的其他部分这是 WEB-INF文件夹,并且要在这个基础目录的子目录下查找源代码。这个块中还有一些属性要 设置,以备后用。要让这个文件用于另一个应用,你可以只修改这些属性,而让剩下的其他 属性保持原样。 b path 块构建了Ant 构建应用是要使用的classpath。它每次都会执行而不管是选择哪一个 目标。通常,这是一个WEB-INF/lib 文件夹中的JAR的清单。 d prepare 帮助Ant通过比较类文件和源文件的时间戳来最小化编译工作。 e resources 目标从源代码树中拷贝一些属性文件 (java.util.Properties) 到classes 树。这样你可以保持原始的属性文件和文件源代码中的保持一致。 f compile 目标首先调用prepare 和 resources 目标,然后开始构建源文件。Jikes [Jikes]或 者标准的javac编译器都可以使用。 g clean 目标通过删除和恢复类文件夹来确保所有的东西都重新构建。 h javadoc 目标为应用构建JavaDoc。通常,你需要象标明项目的classpath 一样为JavaDoc classpath标明相同的JAR路径。注意,这是一个冒号分隔的列表。JavaDoc编译器会发出警告,但 会继续为它能找到的类产生文档。 i dist 目标为应用创建一个Web归档(WAR) 文件。这个文件可以用来在你的生产服务器上 部署你的应用。 j project 目标将全部构建所有东西,并准备一个二进制的分发包。 关于Ant得更多信息,我们强烈推荐你阅读Java Development with Ant[Hatcher]。 4.7 配置Struts核心 当此为止,我们已经全部涉及了要使你的Struts应用运行需要构建和定制的四个文件。 部署描述符 (web.xml) Struts配置文件 (struts-config.xml) 应用资源束 (application.properties) Ant 构建文件 (构建.xml) 现在,我们把他们结合起来,作为一个Struts 配置的检查表。 4.7.1 安装Java和Java servlet 容器 Struts In Action Page 3- 24 - 第一步是安装一个servlet 容器,比如Tomcat。我们在第1章讲述过这个内容,但现在是 一个让你能从头开始的快速检查表: 从JavaSoft 站点[Sun,Java]下载并安装Java 1.4 (或更高版本)。 从Jakarta [ASF, Tomcat]下载并安装Tomcat 4 LE (或更高版本)。 当然,Struts也可以工作于其它web容器和其他 Java虚拟机。这仅仅是我们的一个建议。 Struts 分发包中包括配置各种容器的技术规范和注意事项。 4.7.2 安装开发环境 我们在第3章也涉及了安装开发环境。Struts 在大多数 Java 环境下都能工作的很好。 如果你还没有一个好的环境,作为入门比较好的选择是jEdit 和Ant: 下载并安装Ant 1.4 (或更高版本) [ASF, Ant]. 下载并安装jEdit [jEdit]. 安装Ant add-in for jEdit 但是再次提醒,这仅仅是建议。如果你已经有一个开发环境了,它也会工作的很好。 4.7.3 安装Struts 核心文件 运行Struts需要的常备文件都在Struts 库文件分发包(jakarta-struts-1.1-lib.zip)中提供了。 这些包括几个JAR,标签库描述符,DTDs,和标准的 XML 配置文件。这些常备文件和你要 提供的4个配置文件一起,创建了一个核心的Struts配置。 下面是一个检查表: ? 下载并解压Struts library distribution [ASF, Struts]. ? 拷贝所有的*.jar 文件到应用的 /WEB-INF/lib 文件夹. ? ?拷贝所有的*.tld文件到 /WEB-INF 文件夹. ? 拷贝所有的*.xml和 *.dtd 文件到 /WEB-INF 文件夹. ? 创建部署描述符 ( 4.2). ? 创建Struts 配置文件 (4.4). ? 创建资源束 ( 4.5). ? 创建Ant构建文件 (4.6). 4.7.4 配置Tiles框架 Tiles是一个Struts 框架的可选组件,是一个强大的页面组装工具,在其意义上是一个真 正的框架。我们在第11章讨论Tiles的使用。使用Struts 框架的其他部分时并不需要使用和配 置Tiles 。但是如果你喜欢Tiles,这就是一个练习。 注 这个Tiles 设置例子是基于Struts 1.1 beta release 2。这个过程在beta 1 和 beta 2 件都经过了改变,并可能 在Struts 1.1 final也发生改变。请检查本书的站点获取[Husted] 勘误表。 你需要用来使用Tiles的所有文件都在Struts library distribution 中提供了( 4.7)。如果你让 你的应用基于Struts空白应用(4.10)或者你已经安装了所有必需的Struts 库文件夹到应用的 /WEB-INF 或者r /WEB-INF/lib文件夹中,那么基本的所需文件已经有了。 Struts In Action Page 3- 25 - 下面是检查表: 从Struts lib 文件夹拷贝struts-tiles.tld 和 tiles-config.dtd 文件 (如果没有)到 /WEB-INF 文件夹. 插入下面的语句快到(如果没有)到 /WEB-INF/web.xml 文件中,并且紧跟其他 <taglib> 元素: <taglib> <taglib-uri>/tags/tiles</taglib-uri> <taglib-location>/WEB-INF/tiles.tld</taglib-location> </taglib> 创建一个空白的tiles-defs.xml (如果没有)在 /WEB-INF 文件夹中,像这样: <!DOCTYPE tiles-definitions PUBLIC "-//Apache Software Foundation//DTD Tiles Conig//EN" "http://jakarta.apache.org/struts/dtds/tiles-config.dtd"> <tiles-definitions> <!-- skeleton definition <definition name="${name}" path="${path}"> <put name="${name}" value="${value}"/> </definition> end blank definition --> </tiles-definitions> 插入这个 <plug-in> 元素到struts-config.xml,位置在关闭的 </struts-config> 元素之前: <plug-in className="org.apache.struts.tiles.TilesPlugin" > <set-property property="definitions-config" value="/WEB-INF/tiles-defs.xml" /> </plug-in> 至于能运行的具体Tiles 应用的例子,请参见Struts分发包中的Tiles示例应用,以及第16 章我们要讨论的Artimus 1.1示例。Struts 空白应用 ( 4.10) 其实也激活了Tiles。 至于Tiles 应用的Struts 1.0版本,参见第15章的Artimus 1.0 示例。 4.8 配置Struts Validator Struts In Action Page 3- 26 - 像Tiles一样,Struts Validator 也是框架的一个可选组件。 我们会在第12章涉及Struts Validator。使用Struts 框架的其他部分时并不需要使用和配 置Validator。不过如果你喜欢,这也是一个练习机会。 注 这个Validator 设置例子是基于Struts 1.1 beta release 2。这个过程在beta 1 和 beta 2 件都经过了改变, 并可能在Struts 1.1 final也发生改变。请检查本书的站点获取[Husted] 勘误表。 你需要用来使用Validator 的所有文件都在Struts library distribution 中提供了( 4.7)。如 果你让你的应用基于Struts空白应用(4.10)或者你已经安装了所有必需的Struts 库文件夹到 应用的/WEB-INF 或者r /WEB-INF/lib文件夹中,那么基本的所需文件已经有了。 下面是检查表: 从Struts lib 文件夹拷贝struts-validator.tld 和 validator-rules.xml 文件 (如果没 有)到/WEB-INF 文件夹. 插入下面的语句快到(如果没有)到 /WEB-INF/web.xml 文件中,并且紧跟其他 <taglib> 元素: <taglib> <taglib-uri>/tags/validator</taglib-uri> <taglib-location>/WEB-INF/struts-validator.tld</taglib-location> </taglib> 创建一个空白的validations.xml (如果没有)在 /WEB-INF 文件夹中,像这样: <form-validation> <formset> <!-- skeleton form <form name="${}"> <field property="${}" depends="${}"> <arg0 key="${}"/> </field> </form> end skeleton form --> </formset> </form-validation> 插入这个 <plug-in> 元素到struts-config.xml,位置在关闭的 </struts-config> 元素之前: Struts In Action Page 3- 27 - <plug-in className="org.apache.struts.validator.ValidatorPlugIn"> <set-property property="pathnames" value="/WEB-INF/validator-rules.xml,/WEB-INF/validation.xml"/> </plug-in> 至于能运行的具体Validator应用的例子,请参见Struts分发包中的Tiles示例应用,以及 第16章我们要讨论的Artimus 1.1示例。Struts 空白应用 ( 4.10) 其实也激活了Tiles。 至于Validator 应用的Struts 1.0版本,参见第15章的Artimus 1.0 示例。 4.9 从空白Struts应用开始 Struts分发包中的Struts Blank 应用提供了一个新的应用可以采用和修改的骨架配置。它 包从分发包中含了所有的基础文件以及开发人员应提供的四个配置文件( 4.7),以及Tiles和 Validato的骨架配置。 Struts Blank 是以一个WAR 文件的形式分发的。开始时你需要做的是: 从二进制分发包中的webapps文件夹拷贝struts-blank.war 文件到容器的WAR 部署目录(通常是webapps). 将struts-blank.war更名为你想要的应用名称 启动或重启servlet 容器 Struts Blank 应用就会以你的新应用名称进行部署。 如果你访问缺省页面: http://localhost/myApp 你的新应用就会显示一个简单的欢迎页面。 Blank 应用并没有包括一些Java 源代码文件—这是你的工作—但它的确包含了一些有 用的文件和页面,我们总结在表 4.3中。 Struts Blank 应用设计目的是让你可以很容易的开始一个Struts项目。如果将它部署在一 个servlet 容器的本地拷贝,比如Tomcat 或者Resin,你就可以马上在上面工作了。构建文件 设计来为了你再过程中可以在不时的就地工作,只是需要重启容器或者重新装入应用。 如果你正在一个团队内工作,你可能需要使用一个不同的方案,因为你需要随时将文件 Check in 或者Check out。但是简单的事情总归简单,Struts Blank 应用时开始构建一个Struts 真正变得非常简单。 表格 4-3 Struts Blank 起始文件 文件名 目的 /index.jsp 一个常备的欢迎页面,它转发到一个Struts Welcome action. /pages/Welcome.jsp 一个欢迎JSP,通过一个Welcome action访问 /classes/resources/application.properties 部署的应用资源文件。这是一个运行拷贝,原始文件和其 Struts In Action Page 3- 28 - 他源代码在一起 /WEB-INF/struts-config.xml 一个起始 Struts 配置文件,常用元素有示范性说明 /WEB-INF/tiles-def.xml 一个起始 Tiles 配置 文件,常用元素有示范性说明 /WEB-INF/validator-rules.xml 一个起始标准 Validator 配置文件,设置基本的 validators. /WEB-INF/validations.xml 一个起始Validations 配置文件可以在此描述你的form /WEB-INF/*.tld Struts标签的标签库描述符文件。 /WEB-INF/lib/*.jar 框架类依赖的JAR文件 /WEB-INF/src/build.xml Ant 构建 文件. /WEB-INF/src/java/ Java源代码文件的起始目录 /WEB-INF/src/resources/application.proper ties 原始的application.properties文件。编辑后要重新构建 需要注意的一件事情是build.xml 文件引用的本地系统路径可能存在或者不存在。 <property name=" jdbc20ext.jar" value="/javasoft/lib/jdbc2_0-stdext.jar"/> <property name="servlet.jar" value="/javasoft/lib/servlet.jar"/> <property name="distpath.project" value="/projects/lib"/> jdbc2_0-stdext.jar 和 servlet.jar 需要用来产生JavaDocs。Blank build.xml 会在你的默认 驱动器的/javasoft/lib 文件夹下查找这些文件。如果必要,你可以修改这些设置以指向你保 存这些文件的地方。 你的容器应该包含其所支持的servlet API (如, 2.2或2.3)的servlet.jar。 Tomcat 4 kee在其$TOMCAT/common/lib 文件夹中保存servlet.jar文件。 jdbc2_0-stdext.jar包含在Struts 库文件分发包中。它没有和Struts web 应用绑定在一起。 许多容器,如Tomcat,会共享这个文件的一个拷贝。在WEB-INF/lib 文件夹中放置另一个 拷贝可能会造成冲突。 其他目录是保存你的一个WAR文件的地方。这可以用来在开饭完成后在生产系统上部 署你的应用,或者在准备测试的时候也行。在一次提醒,你可以修改路径或者创建目录。 Blank build.xml 工厂设定是默认驱动器上的/projects/lib文件夹。 4.10 配置模块化应用 Struts 架构的一个关键优点是所有的请求都通过一个单一的控制点。开发人员可以集中 那些应用于每一个请求的功能,并且避免整个应用中的代码重复。因为Java是一个多线程平 台,Struts使用一个单独的控制器 servlet 也提供了最好的性能可能性。开发人员编写更少的 代码,机器在更少的资源下更快的运行。一切都很完美。 在Struts 1.0中,引用以单独模式运行。Struts配置文件对用户来说是一个。如果多个开 发人员工作于一个应用,他们需要有一个方法来管理Struts配置的更新。 Struts In Action Page 3- 29 - 在实践中,开发人员倾向于将他们的工作分割为逻辑单元,非常想他们将Java 代码分 派到包中。团队成员在应用中有他们自己的名字空间并不是不常见。 在一个在线拍卖应用中,一个团队可能工作于registration模块;另一个团对可能工作于 bidding模块。第1个团队可能具有一个/reg 文件夹来存放他们的JSP 页面以及一个app.reg包 来存放他们的Java代码。同时,团队2可能有一个/bid 文件夹存放JSP 页面以及一个app.bid 包来存放Java代码。在资源文件中,每个团对可能都有他们以reg. 或 bid.为前缀的关键字。 当然,你不需要按这种方式以大型团队来组织大型项目。许多单独的开发者也做同样的 事情。限制一个文件夹中的文件数量和包中的类数量,被认为是很好的项目管理。 许多开发人员也按逻辑模块来组织项目,而不管是否需要共享文件。 4.10.1 分而治之 在Struts 1.1中,将应用分为模块的理念成为了一种习惯—现在已经集成到框架中了。 让 我们回头看看,Struts 是如何将应用组织为模块的。 web应用容器是我们可以为每个应用创建上下文来共享服务。这个上下文对应于服务器 的webapp 目录的一个子目录。按同样的道理,Struts 1.1 通过为每个应用创建一个前缀来共 享应用的使用。多个模块可以运行于相同的应用空间,每一个都在起自己的前缀之下,与多 个应用可以运行于同一个服务器空间非常相似—每一个都有其自己的上下文。 我们在编写web应用时,我们经常引用到上下文-相关的 URI。这是一个不包含应用名 字或上下文的路径。同时,我们在编写Struts 应用时,我们也经常应用一个模块-相关的URI。 不用惊讶,这也是一个路径,不包含模块名称,或者模块前缀。表 4.4所示的是绝对的,上 下文-相关的,和模块相关的同一个URI。 就像你可以编写一个web 应用,并将它配置在某个上下文中一样,你可以编写一个Struts 应用模块并配置它在某个前缀下运行。 编写一个模块和编写一个独立运行的应用并没有多大的不同。 所有世纪的配置都在部署描述符中。如果你将某个模块从某个前缀移动到根,或者从根 移动到某个前缀下,或者从一个前缀移动到另一个前缀,模块中没有JSP,Java代码,或者 XML代码需要改变。 要设置一个应用来使用分离的reg和bid 模块,我们可以这样配置servlet 描述符: <servlet> <servlet-name>action</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <!-- The default (or "root") module --> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <!-- The register module --> <init-param> <param-name>config/reg</param-name> <-- Includes prefix! --> <param-value>/WEB-INF/struts-config-reg.xml</param-value> </init-param> <!-- The bidding module --> Struts In Action Page 3- 30 - <init-param> <param-name>config/bid</param-name> <-- Includes prefix! --> <param-value>/WEB-INF/struts-config-bid.xml</param-value> </init-param> <!-- ... other servlet elements ... -- > </servlet> 表格 4-4 表 4.4 Absolute, 上下文-relative, and 模块-relative portions of the same URI URI类型 URI 绝对URL http://localhost/myApp/mymodule/myAction.do 域-相关 /myApp/mymodule/myAction.do 上下文-相关 /mymodule/myAction.do 模块-相关 /myAction.do 在这个例子中,reg 团队可以工作在sturts-config-reg.xml而bid 团队可以工作在 struts-config-bid.xml配置。每个团队都工作于他们的模块,就像她们工作于一个单模块的应 用。框架作了关于模块前缀的所有调整,就像容器为应用上下文所作的调整和配置。 对了,几乎所有的调整… 4.10.2 给页面加前缀 当框架针对模块前缀调整了URI,它会调整所有URI,不管它是指向一个ActionMapping或者 是模块中的其他资源,如JSP。在struts-config-reg.xml文件中,我们可能有一个模块-相关的 URI,比如 /input.do。则该URI的上下文-相关版本就成为/bid/index.do。同时,我们可能有一 个到/pages/Index.jsp的引用,那么该引用的上下文-相关就成为/bid/pages/Index.jsp。 移动ActionMapping是非常容易的,它们实际上是虚拟的。但是许多URI引用到JSP,他 们实际上和物理文件相关。其他 URI也可能引用到HTML页面,图像文件等等。 这意味着虽然bid 团队可以在Struts 配置中忽略模块前缀,他们仍然需要知道,他们是 在使用哪一个前缀,这样才知道应该在哪里存放他们的文件。如果前缀改变,需要重新命名 那个子目录。不需要修改JSP, Java, 或者XML编码,尽管如此,他们必须使他们的页面目录 名称和模块名称保持同步。 这其实和我们要在应用层次要干的事没什么不同。如果我们上载文件到应用,我们需要 知道其应该存放的上下文-目录。如果我们上载文件到模块,我们就需要知道他在哪一个模 块目录下面。 4.10.3 修改配置 如果你已经你已经有了一个Struts 1.0 应用设置为象一个模块应用,诀窍就是使用模块 前缀从模块的配置文件中分离出来。 如果页面已经在以模块名称命名的子目录种,他们可以继续留在哪里就可以了。如果不, 你也能够将它们移动到模块目录下面,而不用作任何改动 (当然,除非一些硬编码的连接, Struts In Action Page 3- 31 - 比如<html:link> 标签,必须修改)。 4.11 共享Struts JAR 如果你的容器支持这个功能,Struts JAR 可以被web容器中的所有应用所共享。 (现在来 说多数都行或者即将行)。 请参考你的容器的文档,看如何配置共享的JAR。大多数情况下, 你可以简单的将Struts JAR 放在共享的lib 文件夹下面,然后在/WEB-INF/lib 文件夹下删除它 们。 对Tomcat 4来说,共享库文件夹位于 $TOMCAT/common/lib 对缺省的Windows安装,这可能是对应于\Program filess\Apache Tomcat 4\common\lib。 当然,这意味着所有共享这些JAR 的应用必须是同一版本的Struts应用。 4.12 小结 没有一个应用应该是孤岛。 要将应用的所有组件集合在一起工作,你必须创建并维护 一些配置文件。要使一个Struts 应用建立并运行,需要开发者提供一个Struts 配置文件,一 个资源文件, web应用部署描述符文件,以及一个 Ant构建文件。本章一步步讲述了创建 这些文件的细节。 Struts 配置的一个关键优点是它集中了变更。因为实现细节被集中了,Struts 应用就可 以从Struts 配置进行全面重新配置,不用进行一些修改Action类或者表现页面的工作。 所有的组件都由Struts 配置进行初始化—ActionForm,ActionForward,ActionMapping— 形成了Struts 控制器的核心。组合在一起,这些对象表达了应用的外部和内部API —盛夏的 就是实现细节了。这些核心组件映射出了应用能接受的输入,要转发的地方,以及要做的事 情。 那么,接下来是什么呢? 本书的第2部分,我们会深入到每一个核心组件来帮助你全面了解它们。在第3部分,我 们集中于你的应用的可见部分:页面。第4部分,会集中起来用几个实例研究来使用1到3章 所表述的技术。 但是如果你已经非常渴望想要开始了, 你指下应该已经有足够的知识可以开始构建你 的Struts应用。然后,如果你喜欢,就象本书剩余部分所展示的,你可以使用我们介绍的新 技术重构,改进,或者扩展你的现有应用。 5 用 ActionForm进行复制 与 Ted Husted 和 George Franciscus合著 本章包含 λ 理解 ActionForm生命周期 λ 检查 ActionForm的职责 λ 讨论 ActionForm设计特征 λ 使用 ActionForm的最佳实践 λ 组装和报告 ActionForm λ 介绍 Scaffold BaseForm 5- 5- 1 - The sweat of hard work is not to be displayed. It is much more graceful to appear favored by the gods. —Maxine Hong Kingston, The Woman Warrior: Memoirs of a Girlhood among Ghosts 5.1 吃的是草,挤出的是奶 使用 Web应用的人通常会花费大量的时间通过 HTML form提交数据。有时是一些新的 数据,通过填写空白表单来提交。而其它时候,则可能是将修改过的数据重新提交。 HTML form 给 Web开发 人员提出了两个挑战: 一是在数据被提交时获取数据, 以及使 用用户可以修改的数据预装一个表单。 如果用户选择一个必须被修改的地址记录, 我们就需 要将数据从记录拷贝到 HTML form。这就意味着,我们必须能够通过传送动态值来针对每 次请求改变页面。 HTML 并没有提供一个内建的手段来使用动态值预组装一个控件。需要定制的页面由 混合了静态和动态的组件以一种特殊的方式写成,作为运行时的响应。 这里有许多方式为 Java Web应用编写动态页面, 最通常的办法是 JavaServer页面。 Struts 分发包包括了一套 JSP标签,你可以用来编写动态 HTML 控件。像其它许多标签库一样, Struts 标签设计来同 JavaBeans一起工作。如我们在第一章所见, JavaBean 是一个遵循一 定设计规则的简单但是非常强大的对象。 不像 HTML 元素, Struts 标签提供了标准的方式来组装控件。 每个 HTML 标签对 应一个标准的 HTML标记元素。每个 JSP 标签都有一个属性项提供 bean中的属性名称。 JavaBean属性的返回值用于控件的 value 属性。 所以,如果有一个元素象这样: <input name="address"/> 它可以由这样的 Struts JSP 标签来代替: <html:input property="address"/> 标签会从 Javabean中检索 address 属性,并将它作为 HTML 元素的 value来插入。当 浏 览器获取这个标签,它可能看起来像这样: <input name="address" value="6 Lost Feather Drive"/> 这里,他实际上是调用 ActionForm的 getAddress()方法,返回字符串 "6 Lost Feather Drive"。 注 在某些编程上下文中 ,词语 “属性( property) ”等同于 attribute, field,或者 variable。在这些场合 , 属性 ( property) 表达了 一个 存 储地址 。 JavaBean 属性 ( property) 通常使 用字段 /域( field) 来 存储值 , 但 是 JavaBean “properties”确实说来是可以用来检索值的方法。当我们说一个公共属性 public properties, 我们实 际上 是 在 说 JavaBean 对 象的公 共域 。 我 们 说 的 方 法则是 用来 检 索和设 置值 的 。 有时 候, 这 些 值 存 储于域( field)中。其它时候,它可能是从几个域中计算出来,或者从其它对象中检索出来。 JavaBeans 5- 5- 2 - 的强大 之处 在 于对象 可以 控 制值的 存储 方 式 , 以及 使他 们可 以 通 过 mutator 和 accessor方法进 行公 共 存取。 为完成这个流程,当表单被提交到 Struts 控制器 , 它将 HTTP 参数传递给 JavaBean。 大部分来自于 HTML 表单的输入都将在传递给业务层之前进行校验。如果一个字段假定是 包含数字,我们就得确保它就是数字。如果校验检查失败,我们可以将 JavaBean 回传给 页面。然后 JSP 标签根据 JavaBean 属性重新组装 HTML元素,用户就可以纠正输入,并重 新尝试。 JavaBean 都可以和 Struts JSP 标签一起来组装控件。但要提供输入的自动校验, Struts使用它自己的 JavaBean 子类 , 称作 ActionForm。 一旦从 HTML表单的输入传递给 ActionForm bean, 并且属性经过了校验, 属性就要作 为一个漂亮整齐的 JavaBean传递给 Action。 Struts Action 对象 使用 form bean来进行其 业务 操作,处理错误,并选择相应响应页面。第 8章将详细讨论 Action 对象。 5.1.1 ActionForm 的要求 创建一个 ActionForm 并不困难,但是你的类必须符合一些要求: λ ActionForm 必须扩展自 org.apache.struts.ActionForm。基 类 ActionForm 是不能实 例化的。 λ ActionForm 必须为每个应该从请求中收集的 HTML控件定义一个公共属性。 (Struts 1.0 要求每个属性都要 mutator 和 accessor。 Struts 1.1 则没有如此严格 ) ActionForm还可能要符合一些可选的要求 : λ 如果你要求 ActionForm 在传递属性到 Action之 前校验它们, 你就必须实现 validate 方法; λ 如果想在组装前初始化属性,必须实现 reset , 它在 ActionForm 组装前被调用; 下面是一个简单的 ActionForm 类: import org.apache.struts.action.*; public class MyForm extends ActionForm { protected String name; protected String address; public String getName() {return this.name;}; public String getAddress() {return this.address;}; public void setName(String name) {this.name = name;}; public void setAddress(String address) {this.address = address;}; }; 在 Struts 1.1, 你也可以使 用 DynaActionForm 类来 在 Struts 配置文件中宣称你的属性。 下 面是一个简单的使用 DynaActionForm的 ActionForm: <form-bean name="myForm" 5- 5- 3 - type="org.apache.struts.action.DynaActionForm"> <form-property name="name" type="java.lang.String"/> <form-property name="address" type="java.lang.String"/> </form-bean> 关于 Struts 配置 文件的详细情况,请参见第 4章。 虽然 ActionForm的要求非常简单,但它在许多应用的开发中都扮演了令人惊奇的强大 角色。 ActionForm也许是 Struts 框架最令人费解的部分。这个“勤劳”的 JavaBean扮演了 一个域收集器、防火墙、类型转换器、数据缓冲器以及传输对象。 有时它们似乎显得是多余的, 有时它们的价值又是无法估量的, 但是每次, 它们都是框 架的焦点部分 —也是 Struts之所以是 Struts的关键部分。 5.2 千面女郎 ActionForm ActionForm是一个多能的对象。就像我们先前解释的,它可以扮演字段收集器,防火 墙, 数据缓冲 , 数据校验 器, 类型转换器, 以及传输对象 ——在一个单一请求的范围内的 所 有东西。让我们一一来看看它可以在应用充当的角色。 5.2.1 ActionForm 作为字段收集器 绝大部分应用都需要用户数据。许多应用甚至需要大量的数据。在 web 环境中 , 有效 收集数据成了其一大挑战之一。 HTML 定义 了一个几乎不能使用的数据输入控件。 HTTP 也定义了一个几乎不能使用 的数据传输协议。 Struts 使用 ActionForm来帮助补偿 HTML和 HTTP的不足。 HTTP 是个 如此的简单协议, 它使用可能是最基本的方式来传输数据。 但作为协议本身, 它非常易于实 现,并且在它自己的方式上似乎运行的非常有效率。但作为应用使用 , HTTP将许多实现细 节作为考验留给了开发人员。 5.2.1.1 通过 HTTP提交字段 当一个 HTML 表单通过 HTTP被提交 , 所有的内容都被视为文本。 web 服务器接收到 的表单元素是以名 -值对的方式来到的。它们是文本字符串,不是二进制数据。 下图是一个简单的表单,两个字段, Name 和 Amount。 5- 5- 4 - 图表 5-1 有两个字段的简单表单 而下图则是浏览器要提交这个表单的 URI. 图表 5-2 来自表单的Get请求 而如下图, POST 方法则 可以用来隐藏提交的内容, 但结果是一样的: 表单中输入的内 容被作为名 -值对编码进 URI之中。 图表 5-3 来自表单的POST请求 定义 HTTP 规范 允许 表 单 数 据作为 URI的一部 分进行 提交 ,但 不是每 个字 符 都可以 用在 URI之中。 如果 其 它字符 需要被 表单 数 据使用 , 他 们 必须先 进行 编 码 。 URL 使用 % 符号 , 紧 跟 两 位 十 六进制 ISO-Latin代码 数 据 (非大小写敏感 )。详细内容见 RFC 1738 和 2396 [W3C URL; W3C URI]以及 HTML 4.01规范 17.13.4 [W3C HTML4]. 5.2.1.2 通过 HTTP提交文件 一些表单允许将文件作为一个附件进行上载。 这时, 附件要进行特殊编码, 以便仅使用 文本内容被提交。这样就允许我们通过 HTTP传递二进制文件 , 文件被转换成文本字符流来 传输,然后又被转换回来。 5.2.1.3 通过 HTTP提交空字段 如果一个文本字段是空白的 , 大部分 浏览器都会提交一个空参数。 但是如果一个选择框 是空白的,浏览器则根本不提交任何东西。应用必须根据浏览器没有提交选择框时判断出, 意味着它一定是 false 或者 null。 实 践中, 这仅适用于选择框, 但规范允许浏览器忽略任何 5- 5- 5 - 空白字段。 5.2.1.4 通过 HTTP接收 解析 HTTP 文本流来查找名值对可不是一件愉快的事。 Servlet API 提供一些有用的方法帮助查找提交的内容,并找出你需要的内容。但许多 烦人的代码仍然需要调用。 5.2.1.5 ActionForm方案 Struts 对 HTTP 参数处理的方案是将输入参数传递到 JavaBean 属性来进行处理。当 ActionForm的属性与某个请求参数匹配,框架自动以参数的值设置属性。 Struts 开发人员 可以工作在一个可以信赖的 JavaBean上,并将处理 HTTP的麻烦留给框架处理。 为收集请求参数 , Struts 开发人员需要做所有事情就是提供一个具有 JavaBean属性的 ActionForm,这些属性名称与 HTTP请求参数匹配。其余的工作将会自动进行。 5.2.2 ActionForm 作为数据缓冲 在一个常规 (非 web)用户接口中,捕获用户输入的控件具有一个内部缓冲,以便数据可 以进行校验。 如果校验失败, 用户不能离开控件。 如果校验成功, 数据被传送到适当类型 的 其它字段。开发人员通常看不到内部缓冲,但它确实在。 HTML 控件并没有一个内建的缓冲器,或者其它有效的方法,来使数据在提交之前进 行校验。当然,可以用 JavaScript来做这些,但 JavaScript 是可以被浏览器禁止的。 ActionForm 扮演了我们想要的 HTML控件的输入缓冲。 它保持输入, 直到数据被校验 然后送到相应类型的字段为止。 如果用户在一个 Integer 字段中输入了一个字母, 原始输入应该被返回, 包括无效的字 符。用户可以看看到底是什么地方出错了,以便纠正数据 , 然后再试。这也意味着 ActionForm 属性应该是 Strings 类型, 以便各种类型输入都可以捕获, 不管有效还是无效。 ActionForm 字段不是输入的目的地,但它要缓冲数据,以便在提交之前进行校验。 5.2.3 ActionForm 作为数据校验器 虽然现存的项目也许已经有一些 JavaBean来执行校验,但它们很少有能在输入无效时 提供缓冲, 以便能进行纠正。 ActionForm的 validate 方法是 一个扩展点, 你可以在此插入 对业务方法(它们知道如何校验数据)的调用。当校验失败, ActionForm 可以将整个内 容都回传给 web 页面 ,这样用户就可以重新尝试。 但是 ActionForm的校验职责并不比它的纠正职责多一些。许多字段在传递给业务逻辑 处理之前必须具有正确的类型。 在一个 Integer 字段中的数据不能包含字母。 如果这样, 你 应该在让用户继续之前使用户能纠正此数据。 通常 , 这只是一个表面形式上的校验。即一个字段是一个 Integer 并不能告诉我们它是 正确的 Integer。 许多应用将校验分为两段执行。 首先, 他们使用 ActionForm的 validate 方 法来决定输入是否是正确的类型以及它们是否可以被业务过程使用。一旦这个阶段完成, Action 可以执行额外的校验,来决定输入是否满足业务层的其它要求。如果业务层校验失 败, 你可以将控制返回到输入页面, 就象这是在 ActionForm 的 validate 方法 校验失败一样。 Struts 框架给了你处理数据校验的灵活性,用 ActionForm, 用 Action, 或这两者,按 5- 5- 6 - 你所需而定。 在第 12章,我们将讨论 Struts Validator, 它扩展了 ActionForm validate 方法的使用。 5.2.4 ActionForm 作为类型转换器 ActionForm的一个强制点是应该使用 String 和 boolean属 性 。 实际上, 这意味着属性 必须得从一种转换到另一种类型。 大部分应用也需要一些属性, 比如电话号码或者数量, 以 一种格式化的方式出现。核心 Java 包提供一些工具来做这种事情,但是要清楚地将他们集 成到应用中仍然是一个挑战。 Struts 开发 人员经常在 ActionForm中包含 helper方法, 来进行类型转换。 helper 方法 可以有很多种实现方式,这我们在 5.6种叙述。 5.2.5 ActionForm 作为传输对象 ActionForm 可以被其它 bean或者过程 作为数据载体。 就是说 ActionForm 是一个传输 ( transfer) 对象。象其它 transfer 对象以一样 , 它承载的数据通常对应着持久层中的不止 一个实体 (比如不止一个数据库表 )。但不像常规的 transfer 对象 , ActionForm 的各个属性 都必须是可变的。 HTTP 将每个属性表达为名 -值对。如果每个属性都使可以独立设置的, 这就足够简单了。 另一方面,其它 transfer 对象通过仅在实例化时被设置,而后不可改变。 定义 Transfer 对象 (也称 为 值对象 ( value object) [Go3])用 来通过 发送 粗 糙 —规整 的数 据视图 来交 换 精细规 整的数据。通常用在远程应用环境之中, transfer 对象可以将多个相关的属性成组,以便它可以被序列 化,以便它可以在一个单一操作中被送到远程服务器。 Murable意味 着 “可以被 改 变或者 变更 ” 计算机 程序 可 以包含 可变 ( mutable) 和不可 变 ( immutable ) 元素。某些对象设计来是不可改变,并且作为不可变对 象进行引用。 大部分对 象则是设计来是可变的,作 为可变对象应用。 STRUTS TIP 使用粗糙 —规整 ActionForm来减小类维护。实践中,应用中的表单一般共享属性。这通常更 易于创建一个基本的 ActionForm,具有表单需要的所有属性。如果必要,你可以子类化这个 粗糙规整的属性 bean,并提供特殊的 validate 和 reset 方法。 5.2.6 ActionForm 作为防火墙 当请求提交时, ActionServlet 使用一个自动组装机制来从请求参数中设置请求的 ActionForm属性。这让你可以通过控制哪个 ActionForm 属性被暴露来控制哪个请求参数 是可接受的。 这也意味着如果你的 ActionForm 的 粗心设计你可能失去控制哪个参数可以接 受的能力。 ActionForm 一定不能包含看起来像是一个 JavaBean 属性,但不能从 HTTP 请 求设置的属性方法。 5- 5- 7 - 设计 ActionForm时你应该当心的是自动化组装机制。自动化机制是可以乐于根据请求 来设置 ActionForm的公共属性,而不管它们是否是来自于 HTML表单。所以,如果你要重 用一个 bean为 ActionForm, bean上的一些公共属性 —以及它的超类 —可从 HTTP 请求中设 置。因为引用是可以被嵌套和链接的,某些作为成员属性的 bean也被暴露,连同其超类和 一些成员属性。如果这些 bean中的某些可以在系统状态中立即改变,那么一个欺骗就可以 影响到状态改变 —即便那不是一个有意的用法。 如果你从头创建一个 ActionForm,并按其原本的意图,你就完全没有必要担心自动组 装。但某些开发人员喜欢将业务对象置于 ActionForm中以便它们可以从请求中通过值来传 递。如果这样,某些看起来像 JavaBean 属性的方法 , 接受一个 String 值,就可能被从 HTTP 请求来调用。 一个例子是 ActionServlet的上载缓冲区大小。在 Struts 1.0中, ActionServlet 被暴露 为 ActionForm的一个成员属性。这意味着在 Struts 1.0, 你可以从 HTTP请求中来调用 ActionServlet.setBufferSize。 幸运的是, 这并没什么影响, 因为 ActionServlet 仅在 启动初 始化时使用这个值。然而,如果 servlet 是在运行时引用它,一个欺骗可以将它设置为 0, 来创建一个拒绝服务的攻击。 使用 ActionForm这样的可以影响系统状态的 bean, 或者一个 ActionForm的一部分 , 就 像直接传递输入字段到一个外壳脚本。 这就像不能区分在误导的人群中哪些是狂欢纵乐的 聪明人一样。 ActionForm 就像一个防火墙系统中的 DMZ:它使你在数据被允许传递到应用的其它 部分之前必须检查数据。 5.3 ActionForm设计推论 ActionForm的设计产生一些推论。一个 ActionForm 可以 : λ 与业务逻辑 bean共享属性名称 λ 最小化用户代码 λ 封装助手方法 λ 包含其它 JavaBeans 5.3.1 ActionForm可以共享名称 因为它可以和业务逻辑 bean进行交互 , ActionForm通常可以使用业务逻辑 bean中的 一套相同的属性名称。 通常, 这些属性间并没有相关性, 所以这不失为一个很好的实践方法。 虽然 form bean和逻辑 bean最终都是表达相同的数据, 但是它们在各自的生命周期内表达数 据的不同视图。 逻辑 bean表达的是模型的状态。 form bean表达 的是状态的改变。 ActionForm bean收集和校验输入。业务逻辑 bean处理捕获的数据并将新数据合并到模型中。 所以, 虽然 bean可以共享 属性名称并创建一个公共协议, 它们实际上并没有共享数据。 已经被接受到模型中的数据是一回事,可以被接受到模型中的数据又是另一回事。 定义 消 息协议 ( message protocol) (或 称消息 接口( message interface))是 一个基于 反射的 技术, 它允许在一个共享的公共层次场合下发现公共的命名约定来使对象共享信息。如果两个 JavaBean具有 能返回 可比值 的相同 的属性 名称, 它们可 以共享 相同的 消息协 议。 不 管 JavaBean 5- 5- 8 - 是 同一个 类或者 超类都 无所谓 。 消 息 协议仅 通过反 射进行 操作, 也仅仅 关心的 是属性 ( “ 消息” ) 的 名称。 不 同 的 JavaBean可 以使用 相同的 Struts JSP, 只要这 些 bean遵 循 相同的协 议。 如果 bean 具 有相同 名称的 属性, Struts 标 签就可以 找到正 确的方 来来调 用。 许 多 Struts 应 用都会 直接从 业务层返回一个 Bean,并将它传递给 HTML form。当该页面被提交时,一个共享相同协议的 ActionForm bean被用于捕获和校验参数。如果教研失败,控制会返回到同一个页面,标签将 使 用来自 ActionForm bean的 属性。 每个都 是不同 的类, 但是因 为 JSP 标 签是基于反 射, 标 签 才 不在乎是来自于哪个 ,只要 bean是使用相同的消息协议 (或者说相同的方法名称集 ) [Johnson] 5.3.2 ActionForm可以最小化用户代码 在一个典型的部署中 , 在 ActionForm 中使用的代码直接相关于表现层特别是一个 HTML 表现层。定制的助手方法,以及特定的业务输出,通常在业务逻辑 bean之中 , 这样 他才可以被其它环境所重用。 ActionForm和 Action 类都是设 计来作为一个适配器, 鼓励将 业务代码保持在业务层,表现代码保持在表现层。 5.3.3 ActionForm可以封装助手 实际应用中 , 开发人员发现根据其应用的业务规则来让 ActionForm 格式化和校验数 据是非常有用的。 因为 ActionForm 是一个 JavaBean, 它可以 传递输入到业务助手方法, 来 执行准确的格式化和校验,然后将结果返回表现层。 静态方法是有用的,因为操作通常是简单的过滤处理: public String getTelephoneText() { return ContactBean.formatTelephone(this.telephoneText,getLocale()); } 这儿 , ActionForm从 业务层引入了一个 ContactBean 。大部分 开发人员都同意控制层可 以从业务层引入方法 (但其它方式则不行 !)。 5.3.4 ActionForm可以嵌套其它 bean 因为 Struts 标签扩展和自动组装机制都支持点号语法来从 ActionForm访问其它 bean。 这 是 一个方便的方式, 可以通过 ActionForm来组装存在的 bean。在 JSP 页面 , 你可 以象这样引用一个嵌套的 bean: <html:text propety="values.telephoneText" size="14" maxlength="14"/> 5- 5- 9 - 这相当于是调用: aForm.getValues().getTelephoneText() 浏览器将通过 HTTP提交参数: values.telephoneText=555-1234 自动组装机制然后会调用相当的方法: aForm.getValues().setTelephoneText( (String) Request.getAttribute(valueBean.telephoneText); 5.3.4.1 避免嵌套冲突 在进行嵌套 bean时 , 请记住 ActionForm 必须准备好能处理各种 HTTP 请求 , 并不是 仅仅是来自于你控制的表单的提交。 HTTP 请求 是非常易于欺骗的。 如果被嵌套的 bean具有 一个接受 String 参数的方 法, 并且看起来像是一个 JavaBean 属性 , 该方法就可 能被从 HTTP 请求传递一个值。 在 Struts 1.0, 基本 ActionForm 暴露了一个 servlet 属性链接到应用的 ActionServlet。对 servlet 的存取需要处理一个 MIME 请求。一个后果是,这个查询字符串 可能被在运行时被传递到 Struts 1.0 Action 来改变 TempDir属性: ?servlet.setTempDir=/whatever 这相当于是调用: ActionForm.getServlet().setTempDir("/whatever") 幸好, 实际上 这并没有什么影响, 因为 这个属性仅在实例化时被使用。 在 Struts 1.0.1 以 及后来版本中,将使用一个包装类( wrapper class)来保护 ActionServlet对象 : public class ActionServletWrapper { protected transient ActionServlet servlet = null; public void log (String message, int level) { servlet.log(message,level); } 5- 5- 10 - public void log(String message) { servlet.log(message); } public String getMultipartClass() { Return servlet.multipartClass; } public void setServletFor(MultipartRequestHandler Object) { Object.setServlet(this.servlet); } public ActionServletWrapper (ActionServlet servlet) { super(); this.servlet = servlet; } } 包装类仅仅暴露了框架需要的属性。 其它属性则保护起来, 避免篡改。 嵌套冲突强调了 一个基本设计原理: 对象应该仅仅暴露其希望被改变的东西。这是一个很好的设计原理, 不仅仅是针对 ActionForm也包括其它 JavaBean。 嵌套 bean是一个很强大的技术但必须小心使用。 5.4 ActionForm 的风情 在 Struts 1.1中 , 引入了两个基本的 ActionForm的替代类 : 后端映射( mapbacked) ActionForm 和动态 DynaActionForm。 5.4.1 后端映射( Map-backed) ActionForm 许多大型 web 应用需要 使用大量的属性。 这些应 用的开发人员开玩笑说, 创建并维护 ActionForm bean仅仅比 声明一个字段, getter, 和 setter多一点点工作。 这些应用通常联 合使用一些原本在其它平台一起使用的成熟的业务组件,现在被改编到 web 应用中。因为 bean 协议已 经是良好定义的, 所有这些应用真正需要的是, 以最小的错乱存储这些提交的 字段。 当然,同时,并不是每个 Form的属性都是简单类型的属性 —也许十之八九是。 在 Struts 1.1中 , ActionForm可以支持 map属性。和其它东西一起,这使在一个 ActionForm中混合使用 map和常规的 JavaBean属性成为可能。 Map可以用来在没有事先定 义相关的属性的 q情况下捕获提交的参数。每个字段只是映射中的一个项目。 从 ActionForm的定义中 , 你可以定义这些方法来访问 Map中的项目: public void setValue(String key, Object value) public Object getValue(String key) 5- 5- 11 - 然后你就可以在 JSP中使用这个标签 : <html:text property="value(key)"/> 以及 <bean:write name ="formBean" property="value(key)"/> 这使你可以在没有其它方式的帮助下将 Map使用为一个简单属性。 Map也有 accessor, 所以如果一个属性需要特殊的处理, 你可以观察它, 并插入一个助手方法。 在下面的代码中: public object getValue(String key) throws Exception { // telephone number needs to be reformatted. if ("telephone".equals(key)) { return (object) getTelephone(); } return getMap().get(key); } 你需要为 telephone 属性 定义 accessor, 但其它所有 东西都可以直接传送, 并存储在映 射( Map)之中。很显然,调用 telephone 属性要多做一点工作。但大量的工作节省了, 因为并不需要定义在 ActionForm 使用的其它属性。因为 ActionForm可能具有很多简单属 性,这种节省可能是很可观的。 通常的约定是, 页面引用映射的值和引用标准的属性是不同的。 如果页面已经使用了标 准的 JavaBean 属性标签, JSP 代码需要更改,以使用影射值标签来代替。 如果 HTML 标签引用的是: <html:text property="telephone"/> 应该改为 <html:text property="value(telephone)"/> 存储在 Map中的字段也没有象通常 JavaBean属性一样来暴露。某些 Java 开发工具可以 使你直接在 JavaBeans工作。这些工具并不将这些字段看成独特的属性,所以你不能完全使 用这些工具的能力。 如果觉得将每个字段看成是 JavaBean 属性是很重要的,而且你也需要最小化维护开 销,还有一个可以考虑的就是 DynaActionForm 类。 5.4.2 DynaActionForm 对一个大型应用来说, 以常规方式宣称简单属性可能会带来大量工作。 要创建一个常规 JavaBean 属性 , 你需要对 field, getter, setter进行编码 —大量的基础工作仅仅是 get这个或者 5- 5- 12 - set那个。 如果你仅仅是宣称某个属性,而不用对每个属性单独进行设置可能会快些。 DynaActionForm (org.apache.struts.action.DynaActionForm) 就是设计来是你可以 通过 Struts配置 文件来说明简单属性的对象 , 就象我们在程序清单 5.2种所见一样。 DynaActionForm 是基于 Jakarta Commons [ASF, Commons]的 DynaBean 组件。这 是一个聪明的对象, 它可以将字段存储在一个内部 map之中, 但把它们作为标准的 JavaBean 属性来暴露。你可以在使用 ActionForm 的任何地方使用 DynaActionForm。你也可以用 DynaActionForm 替代常规的 ActionForm 而不用改变任何现存的 Java 或者 JSP 代码。 5.4.2.1 还有什么 这里有个告诫:缺省下 , DynaActionForm的 reset 方法将设置 所有的字段为其初始值 。 第一个常规 ActionForm来说 , 开发人员可以决定哪些字段将受到 reset方法的影响。许多开 发人员将他们全部初始化,所以在这种情况下,将 ActionForm 换成 DynaActionForm其 行为没有什么改变。 5.5 关于 ActionForm的疑问 在开源社区,很少有设计是不受争议的。下面是一些开发人员关于 Struts ActionForm 设计的常见问题。 5.5.1 为什么 ActionForm不仅仅是一个 Map? 这里有一些用于设计 JavaBean 和 Map的准则 ,如下表: 理由 解释 封装性 绝大多数情况下,属性会返回简单值,但有时又有些值的确需要生成 扩展性 Map不能被扩展以提供相应的方法,比如 validate和 reset方法 自省 一个清晰定义属性的 bean可以被 GUI借口和其它工具所使用 表 5-1 JavaBean和Map的设计理由 Struts 1.1 对 两者都提供了最好的设计。 你可以定义自己的 Map来存储 字段或者直接使 用 DynaActionForm 来自动宣称属性。 (5.4.2). 5.5.2 为什么 ActionForm不是一个普通 JavaBean? 虽然一些 JavaBean 也可以用来和 Struts JSP 标签一起组装 HTML表单 , 但也需要一个 具有一些已知方法的类来对提交后的输入进行管理。 当通过 HTTP来接受输入时 , 校验是必须的。某些从 web层进来的数据在使用前必须经 过检验,如不适合,坚决拒绝。 ActionForm 具有一个 validate 方法,可以被 ActionServlet 调用,以确保数据输入 是正确的。 其它校验可以随后进行, 但是具有 ActionForm 的初始校验将设计一个健壮的系 统。 另一个问题是选择框 ( checkboxes) 。 如果一个选择框没有被选择, 浏览器将不会送出 什么东西。 有就是真, 无就是假。 所要做的是在组装它之前, 将其设置为 false。 这样, 如果 有选择则返回属性 true。否则 , 保持 false。为解决类似问题, ActionForm 有一个内建的 5- 5- 13 - reset 方法。 因为这些方法, ActionForm可以正确处理输入的数据, 而旧式的普通 JavaBean 则不行。 Form bean 不得不提供这两个方法给框架使用。 5.5.3 为什么 ActionForm不是一个接口 ? 这问题问得好。 总的来说, 使用一个基类而不是接口可以使我们能扩展这个类而不是仅仅使用一个预定 义的类。 ActionForm 最常用的是暴露来自于业务对象的属性。如果他是一个接口 ,我们也 许可以直接使用业务对象并且避免通过一个分离的 ActionForm 对象来传送东西。 这看起来似乎很好,但实际上,这样做有一些障碍。下表列出了这些问题的总结。 原因 解释 扩展性 添加一个方法到 ActionForm的接口(在未来的 Struts版本中)可能会破坏应用的 Forma Bean,如果他严重依赖于前一版本的接口的话。作为一个基类,我们可以 添加一个具有默认行为的新方法。先存在子类可以简单的继承这个默认行为 正确使用 如果一个 ActionForm有一个接口,开发人员可能试图使用现存的数据 bean作为 ActionForm使用。 ActionForm应该被视为是控制层的一部分,提供的 API因该鼓 励这样使用它。 ActionForm相关的核心 API必须是重视反映用户提交的输入内容, 而不管怎样, 用户提交的东西必须是否是确实有效的。 这就允许用户能就诊过输入 的数据并重新提交。 大多数之对象都不是设计来缓冲不正确的数据的, 并且许多输 入都是不可更改的。 性能 一些数据 Bean, 特别是 EJB, 其设计可能会开始一个事务, 并且锁定一些资源, 比 如基本的数据库资源。 应用应该具有很好伸缩性, 因为直至输入起码在形式上有效 前,数据库事务会处于延迟状态。 安全性 Struts的自动组装首先设置属性, 然后对它进行校验。 在一个新的输入值传递到一 个对象的属性时, 它可以设计来可以改变系统的状态。 这时, 一个敌意的用户就可 能从浏览器输入一个之给 ActionFrom的属性,从而进行非授权的状态改变,即便 你从来不曾希望从浏览器改变属性。 ActionServlet并不知道那一个属性是要在 HTML Form中使用,它只知道他在请求中发现了它。 这还有一个问题 : “为什么业务 bean 不是一个接口 ?” ActionForm 可以实现业务接口 , 而且,一旦通过校验,将传递到业务层。 传输数据从 ActionForm到业务层是一个非常重要的步骤,我们在下一节说明。 5.6 小结 ActionForm Struts 标签 扩展有助于根据 ActionForm组装 HTML 控件。 ActionServlet根据 HTTP 请求 组 装 ActionForm 。 但是, 如图 5.4, 我们仍然 需要越过最后一英里。 为了数据能完成其生命周期, 我们不得不在 ActionForm和业务对象间传递值。 Struts 开发人员使用一些策略来在层间传递值。在这一节中,我们将讨论最通用的策略。 业务层的需求通常决定项目应该采用的最好方法。许多 web 开发人员被要求为业务方法使 5- 5- 14 - 用一些预先存在的层, 这可能会限制它们的选择。 有时, 整 个应用都需要重建, 这样开发人 员就有一些富裕的选择, 可以选择它们最喜欢的方案。 在我们的讨论中, 我们使用词语传输 对象 ( transfer object) 来描述用来从一个地方到另一个地方传递数据的 JavaBean 。这 种 方 法通常用在一个使用 Enterprise JavaBean 的环境 中, 以减少应用需要的对远程数据库的调 用次数。这种办法是,将多个属性绑定在一个单一的 JavaBean值中。 Bean中的属性可以和 多个表现关,但那是在远程数据库中处理。 你的应用或许根本没有使用传输对象 , 但可能会使用某种 “数据 bean”来访问本地数据系 统。 这种方法其实是一样的工作原理。我们讨论的策略总结在下面的表中: 策略 说明 实现业务层接口 ActionForm 实现了业务层接口,所以可以直接使用 嵌套可改变的值对象 具有独立属性的值对象是 ActionForm的一个成员属性 设置不可改变的值对象 由 Action调用构造器或设置器 设置可改变的值对象 Action调用一个或者多个设置器 使用工厂方法 一个 helper方法封装,实例化和组装一个业务 bean. 传递一个 Map 一个数据 Map由 Action 传递给业务层实体 通过反射数据传递值 is transferred from one bean to the 其它 by 匹配 ing the 属性 名称 s. 使用适配器类 一个特殊的类从 ActionFrom复制属性到几个业务层实体 为帮助你选择流程, 我们在每一个策略中包含了 “结论” 和 “转换数据类型” 两个内容。 5.6.1 实现业务层接口 业务层通常为其中的 bean定义接口。 当一个业务层接口可用时, 你可以使你的 ActionForm 实现该接口,以便它可以直接传递到业务方法 : public class ArticleForm extends ActionForm implements ArticleBean { // ... } 然后, 在你的 Action中 , 你就可以直 接传递经过校验的 ActionForm到那些需要业务层类 型 的方法 : articleModel.save((ArticleBean) form); 5.6.1.1 转换数据类型 在某些时候, Web客户端提交的许多基于 String的输入必须转换为 业务层需要的原生类 型。不幸的是 , JavaBean 规范不允许属性的 getters 和 setters方法被重载。如果你试图在 ActionForm中这样做 , ActionServlet自动组装机制就会抛出一个意外。 一个有用的技术是使用一个具有可预知名称的助手方法来按需要转换属性类型 : 5- 5- 15 - private String keyDisplay = null; public String getKeyDisplay { return keyDisplay; } public Integer getKey() { return new Integer( getKeyDisplay() ); } 标签扩展可以这样引用 keyDisplay: <html:text property="keyDisplay"/> 但是 Action 或者业务层方法都可以简单的引用原方法: ArticleBean articleBean = (ArticleBean) form; articleModel.update(articleBean.getKey(),articleBean); 5.6.1.2 结论 null ActionForm 成为业务 API无缝的一部分; null 必须注意输入在业务方法中使用前必须经过校验 ; null 非字符串属性必须事先经过转换。 5.6.2 嵌套可变值对象 如果你足够幸运,仅仅对 String 和 boolean类型的属性使用值对象,那么你就可以使 你的值对象成为 ActionForm的一个属性。而值对象中的属性则可以通过点号语法来进行引 用,像这样: values.telephone 这相当于是调用 getValues().getTelephone(); 和 5- 5- 16 - getValues().setTelephone(values.telephone); 5.6.2.1 转换数据类型 不适用。这个策略要求的 String 和 boolean 值不需要进行转换。 5.6.2.2 结论 这个策略快捷、易于实现; 这个策略不适用于非文本类型,比如 Integer 或者 Date; 如果值对象拒绝未良好格式化的数据时,这个策略可能不能很好工作; 这个策略将表现层绑定到特定的值对象。 如果值在对象中发生改变, 表现代码也需要反映这 种改变; NOTE 头两种策略 , 实现业务层接口和嵌套可变值对象, 将自动传输数据。其它策略则需要 Action协助 出发传输。接下来的代码片断将可以在Action 类中看到。. 5.6.3 设置不可变值对象 如果你要用一个使用批量构造器的不可变值对象 ,则随后的组装可能是这样: ArticleForm aForm = (ArticleForm) form; ArticleBean aBean = new ArticleBean{ aForm.getArticleKey(), aForm.getContributor(), aForm.getconstructor(), aForm.getTitle(), aForm.getContent() }; 批量 setter方法也看起来是一样,除非 bean 已经被创建,并且随后调用 setter方法。如果涉 及到大量的参数被调用,这有助于将它们分组,然后为每个组定义它们的 setter方法 (如果 这是个选择的化 )。这就是经典的分而治之: ArticleForm aForm = (ArticleForm) form; ArticleBean aBean = new ArticleBean(); aBean.setRecordHeader( aForm.getArticleKey(), aForm.getEditor()); aBean.setPeople( aForm.getContributor(), 5- 5- 17 - aForm.getconstructor()); aBean.setText( aForm.getTitle(), aForm.getContent()); 当然 , 这样做是值得的,每个块都带了一些参数,但你获得了你想要的方法。有时,多个 setters 可能和部分修改相关,即是说,文本域可以被修改而人员域却没有触及。如果数据 是通过一个远程链接来传送,仅传送你想要更改的数据是非常重要的。 5.6.3.1 5.6.3.2 转换数据类型 如果属性类型需要转换,你可以在数据整被传输或者放入你的 ActionForm的助手方法时进 行。 因为数 据可能是已经经过校验并且可以进行转换的, ActionForm 助手就比较乐观 , 认为转换会成功。例如: Integer getArticleKeyInteger() { return new Integer(this.getArticle()) } ArticleBean aBean = new ArticleBean( aForm.getArticleKeyInteger(), aForm.getContributor(), aForm.getconstructor(), aForm.getTitle(), aForm.getContent() ) 当然 , 你也可以在这个语句中使用 try … catch,以便在不希望的事情发生时,抛出意外: try { ArticleBean aBean = new ArticleBean( aForm.getArticleKeyInteger(), aForm.getContributor(), aForm.getconstructor(), aForm.getTitle(), aForm.getContent()) } catch (Throwable t) { errors.add(ActionErrors.GLOBAL_ERROR, new ActionError("error.conversion")); } 5- 5- 18 - 5.6.3.3 结论 null 这个策略提供类型检测; null 这个策略与值对象一起工作; null 如果属性易变这个策略设计到高维护性; null 这个策略强烈和一个具有特定 ActionForm的 Action类耦合; null 有很多属性的值对象那一管理这种方式。 5.6.4 设置可变值对象 如果值对象没有批量 setters,并且某些原因不能添加新的方法,这就会显得混乱: ArticleBean aBean = new ArticleBean(); aBean.setArticleKey(aForm.getArticleKeyInteger()); aBean.setContributor(aForm.getContributor()); aBean.setconstructor(aForm.getconstructor()); aBean.setTitle(aForm.getTitle()); aBean.setContent(aForm.getContent()); 结论和数据类型转换同上一节。 5.6.5 使用工厂方法 如果 ActionForm 值需要传输到另一个值对象,你可以将传输封装在 ActionForm 重而不 是将处理流程暴露给 Action类。 如果值对象必须被创建 ,一个好办法是使用工厂方法,他实例化,组装并返回一个值对象。 下面是一个示例方法,可能会在 ActionForm中找到 : public ArticleBean getArticleBean() { ArticleBean aBean = new ArticleBean( this.getArticleKey(), this.getContributor(), this.getconstructor(), this.getTitle(), this.getContent() ); return aBean; } 5- 5- 19 - 另外,你可以传递一个存在的值对象 : public void setArticleBean(ArticleBean aBean) { aBean.set ( this.getArticleKey(), this.getContributor(), this.getconstructor(), this.getTitle(), this.getContent() ); } 5.6.5.1 转换数据类型 对这个策略关于数据转换要考虑的雨前两节相同。 这个策略只是简单的把代码从 Action 移到 ActionForm中。 5.6.5.2 结论 null 这个策略将 ActionForm绑定到业务层类型; null 这个策略简化了 ActionForm; null 这个策略允许多个 Action传输数据而不要代码复制。 5.6.6 传递 Map 值对象通过 Map(java.util.Map)来传递属性并不常见。如果 ActionForm 属性与值对象 属性匹配 , 传输数据就会非常容易。 如果你有一个值对象来接收 Map, 有两种办法来使 ActionForm使用 Map,这取决于你 使用的 Struts版本,是 Struts 1.0 还是 Struts 1.1 及以后版本。 5.6.6.1 Struts 1.0 BeanUtils类包含一个 describe 方法,它可以返回一个 JavaBean公共属性的 Map。他也 提供一个相应的 populate 方法, 来从 一个 Map组装 JavaBean 的公共属性。 在下面的例子中, 我们假定我们假设的值对象具有表达其值的 setMap 和 getMap 属性。 我们先从表单 bean到值对象传递: Map map = BeanUtils.describe(form); bean.setMap(map); 而从值对象到表单 bean则可以: Map map = bean.getMap(); 5- 5- 20 - BeanUtils.populate(form,map); (为了清楚起见,我们将 Map是为一个中间变量。在你的代码中你可以结合这两个语句 ) 如果因某些原因属性名称不匹配, ActionForm 可以给定别名( alias) 属性: public String getKey() { return getArticle(); } public String getAuthor() { return getconstructor(); } 或是 ActionForm可以给定一个定制方法来返回相应的 Map: public class ArticleForm extends ActionForm { // ... public Map describe() { map = new HashMap(); map.add("key",this.getArticle()); map.add("author",this.getconstructor()); // ... return map; } } 在下面的情况下,你可以调用 ActionForm的 describe 方法而不是 BeanUtil的 : bean.setMap(form.describe()); 5.6.6.2 Struts 1.1 如果你的模型接收 Map (java.util.Map)的数据输入 , 极有可能它也以 Map形式返回数 据。从 Struts 1.1开始 , 你可以结合使用 Commons BeanUtils [ASF, Commons] 类的强大 功能和 Struts 的点号语法。这使得易于使用 Map来存放 ActionForm 属性。 这个技术十分简单。首先,添加一个属性到 ActionForm 中来访问 Map: 5- 5- 21 - private Map map = null; public void setMap(Map map) { this.map = map; } public Map getMap() { 返回 this.map; } 然后,添加一个属性来访问 Map中的元素 : public void setValue(String key, 对象 value) throws Exception { getMap().put(key,value); } public 对象 getValue(String key) throws Exception { 返回 getMap().get(key); } 在 Struts 1.1 JSP 标签 (第 10章 )中 , 你可以象这样访问 Map中的元素: <html:text property="value(key)"/> 以及 <bean:write name ="formBean" property="value (key)"/> 这里 key 是属性的名称。 如果业务层值对象已经使用了 Map, 你可以通过使用各自的 getMap 和 setMap 方法 (或者等同的方法 )来劲传递数据 : form.setMap(bean.getMap()); bean.setMap(form.getMap()); 5.6.6.3 转换数据类型 如果业务层期望一个原生类型的 Map而不是 String类型 , ActionForm 的助手方法被要 求将 String转换为原生类型,并返回一个修改过的 Map: public Map getTypedMap() { 5- 5- 22 - Map map = this.getMap(); String keyString = (String) map.get("key"); Integer keyInteger = new Integer(keyString); map.put("key",keyInteger); return map; } 5.6.6.4 结论 这个策略在 Map已经用来传输数据时,导致一个很好的和业务层的集成; 因为如果没有相应项目, Map将返 回 null, 所以 需要一些额外的校验代码来监视哪些缺 失的参数。 5.6.7 通过反射传递值 如果你使用的是一个现代的 JVM, 将 ActionForm 数据传输到其它 JavaBean 的一个很 好的方法是使用反射。 起初, 反射机制会造成性能损失, 但每个 JVM都减 少了 这个损失。 在 Sun 的 Java 1.3以及以后的版本,性能不同已经可以忽略不计。 你可以添加一些简单的方法 ActionForm 基类中,使其易于使用反射机制来和其它 bean之 间传出和传入数据,就象在程序清单 5.3使用的一样。 这些方法其实是在框架其它地方使用的 BeanUtils类方法的包装。 ActionServlet 使用 BeanUtils 来从 HTTP 请求组装 ActionForm。 公式 1 Listing 5.3 数据-transfer 方法s public Map describe() throws Exception { try { return BeanUtils.describe(this); } catch (Throwable t) { throw new PopulateException(t); } public void set(object o) throws Exception { try { BeanUtils.populate(this,BeanUtils.describe(o)); } catch (Throwable t) { throw new PopulateException(t); } } public void populate(object o) throws Exception { try { BeanUtils.populate(o,this.describe()); } catch (Throwable t) { throw new PopulateException(t); 5- 5- 23 - } } 1.0 vs 1.1 Struts 1.1所使用的 Commons BeanUtil 包比原来 Struts 1.0提供了更好的类型转换能力。如果你 正使用反射传递你的数据,我们推荐你导入 Commons BeanUtil 包,而不管你的 Struts时使用哪 一个版本。最全面的数据传递方法是 BeanUtils.copy属性。下面演示如何使用 copyproperty来从其 它 Bean中组装你的 bean: BeanUtils.copyproperty(this,source); copyproperty方法将自动在两个方向上对原生类型转换应用字符串 String 类型。新的 copyproperty使在你的 ActionForm和 业务 beans之间的 “旅行 ”更加容易 : BeanUtils.copyproperty(mybusinessBean,myActionForm); myBusinessOperation(myBusinessbean); BeanUtils.copyproperty(myActionForm,myBusinessBean);</code> 这个代码片断传递 myActionForm中的 String 属性到 myBusinessBean中的原生类型 , 允许业务操作来 更新值,然后将元生类型的值统统传递回 myActionForm中的字符串。真漂亮! 你 也可以 用 BeanUtils注册你 自己的 转换器 来处理 特殊的 类型转 换, 请参 考 Commons 的 站点获 取更多详细内容 [JSF, Commons]. 5.6.7.1 转换数据类型 BeanUtils 方 法可以在字符串类型和原生类型间转换数据,所以当你实现其它接口时, 并不需要写很多“桥”方法 (虽然,非原生类型将是很大的问题 )。 当使用 BeanUtils 方法来传递数据时 , 所有的东西都哦必须通过原生类型来进行传递。 这对 ActionForm 来说并不是问题,因为它仅仅使用 Strings 或者 boolean类型。 而对业务 bean那一方来说则是个问题,因为试图在 String setCurrent(); 和 Date getCurrent() ; 间传递数据,或者还有其它 Struts 1.0 BeanUtils 类不支持的非原生数据。 一个解决方案是在业务 Bean那一方提 供转换方法, 将 Date 转换 为 String 然后传回来。 下面 是一些方法,可以将 String 转换为 Timestamp 或者转换回来。这些方法都应该是业务 bean的成员 —而不是 ActionForm的成员。通过将它们放入业务层,他们对其它也需要处理 字符串的组件有效: public String getTicklerDisplay() { Timestamp tickler = getTickler(); 5- 5- 24 - if (ConvertUtils.isNull(tickler)) return null; return tickler.toString(); } public void setTicklerDisplay(String ticklerDisplay) { if (ticklerDisplay==null) setTickler(null); else try { setTickler(Timestamp.valueOf(ticklerDisplay)); } catch (Throwable t) { setTickler(null); } } 而在业务 bean的其它地方则是实际的 Timestamp的 getter和 setter, 实际的 Timestamp存储 在数据库中: private Timestamp tickler = null; public Timestamp getTickler() { return (this.tickler); } public void setTickler(Timestamp tickler) { this.tickler = tickler; } 在 ActionForm 中 , 我们 可以简单的使用 String 属性,但是名称前要加上 Display前缀 : private string ticklerDisplay = null; public String getTicklerDisplay() { return this.ticklerDisplay; }; public void setTicklerDisplay(String ticklerDisplay) this.ticklerDisplay = ticklerDisplay; }; 当使用反射进行设置和组装时,这个机制等于是调用: bean.setTicklerDisplay(form.getTicklerDisplay()); 或者 form.setTicklerDisplay(bean.getTicklerDisplay()); 这取决于我们时在做哪个方向的转换。业务方法处理了 String 和 Timestamp间的转换 , 所以我们可以在 ActionForm 像通常一样使用 String 属性。 STRUTS TIP 可以在 你的 业 务 Bean中使 用显 示 助 手 来自动 转 换 复 杂类型 。 通 过使 业务 bean 处理 与字符 串之 间 的转换, 5- 5- 25 - 你可以确保业务层所要求的业务需要,并且可以在其它平台上重用它们。 在实践中,你的应用可能需要用到这些方法,因为我们很少将数据显示为其原生类型。 即使没有转换的问题, 某些象下面的 getDateDisplay 类似的 方法也通常需要用来进行对值进 行本地化和用户友好方面的加工处理。 public String getDateDisplay() { if (this.dateDisplay==null) return null; DateFormat dateFormatter = DateFormat.getDateInstance( DateFormat.default, this.getLocale() ); return dateFormatter.parse(this.dateDisplay); } 对一些类似于 timestamp的值, 你也许需要将它们分解为单个的值到几个字段之中, 以 便每个部分可以在 HTML form的下拉列表中进行选择。 下面的 setTicklerDisplay 方法从 Timestamp字 符串中 提取了几 个部 分, 以便它 可 以被不同 的控 件使 用: public void setTicklerDisplay(String ticklerDisplay) { this.ticklerDisplay = ticklerDisplay; boolean longEnough = ((this.ticklerDisplay!=null) && (this.ticklerDisplay.length()>11+2)); if (longEnough) { // Snag Components: YYYY-MM-DD HH:MM setTicklerYear(this.ticklerDisplay.substring(0,0+4)); setTicklerMonth(this.ticklerDisplay.substring(5,5+2)); setTicklerDay(this.ticklerDisplay.substring(8,8+2)); setTicklerHour(this.ticklerDisplay.substring(11,11+2)); // Parse AM/PM/EV Integer hour = null; try { hour = Integer.valueOf(getTicklerHour()); } catch (Throwable t) { hour = null; } int tod = 0; if (hour!=null) tod = hour.intValue(); setTicklerTod(AM); if (tod>12) setTicklerTod(PM); // after 1pm if (tod>16) setTicklerTod(EV); // after 5pm 5- 5- 26 - } } or displaying a date 在很多情况下, Display 方法将承担双倍的义务。他们格式化 Strings值传递给 ActionForm。 ActionForm 方法然后才可以按它需要的格式显示这些值。 5.6.7.2 结论 代码基的规模减小了; 总体维护成本减小了了; Action 类和其它组件间的耦合减小了; 此方法可能要求创建一些桥 bridge 方法或类; 某些依赖于日期信息的开发人员可能不愿使用反射。 5.6.8 使用适配器类 很多时候 ActionForm bean和模型 (业务) bean是 非常类似的。 而其它时候, 他们仅有 一点点相似。一个 ActionForm 内的字段可能需要传递给几个不同的模型 bean。或者 ActionForm的属性名称可能需要有别于业务对象使用的属性名称。在这种情况下,一个适 配器类可能有助于将业务 对象的方法映射到 ActionForm 的属性。 适配器和 ActionForm 将 共享一个或者几个具有相同方法体的方法。 适配 器类设计来就 象一个或者几个业务对象的包装类。 适配器中的 getter和 setter方法调用业务对象中的相应方 法。然后,适配器便可以用来代替业务 bean (作为代理 /proxy 或者代表 /delegate): // Get data into ActionForm DataBean dBean = new DataBean(); dBean.execute(something); AdaptorBean aBean = new AdaptorBean(dBean); aBean.populate(form); // Fetch data from ActionForm AdaptorBean adaptor = new AdaptorBean(new DataBean()); BaseForm actionForm = (BaseForm) form; adaptor.set(actionForm); DataBean model = (DataBean) adaptor.getBean(); Data.execute(); 我们已经讨论过的数据传递技术都可以用在适配器类中。适配器的关键之处在于它封装了 ActionForm 和业务类之间的差异。 这在业务模型非常复杂的时候非常有用, 因为适配器可 以清楚的去掉 Action对业务实现的耦合。 5.6.8.1 转换数据类型 适配器可以封装一些现有的数据转换方法或者根据需要实现新的转换方法。 5.6.8.2 工作区 5- 5- 27 - 如果你需要值对象是其它任何地方都是不变的, 你可以是同一个对象同时实现可变和不 变接口。 业务和数据层将通过只读接口使用这个对象, Action则可以使用可变接口, 但他们都 可以共享下面的数据字段。 5.6.8.3 结论 类是针对应用特定的,仅有一丁点儿重用性; 这个策略增加了应用中对象的数量; 这个策略对业务和表现层的改变是敏感的; 这个策略可以避免来自于其它层的改变(有利于层间的去耦合); 5.7 BaseForm Scaffold 包提供了一个基本的 ActionForm ,实现了本书讨论的几个技术 (org.apache.struts.scaffold.BaseForm)。这类包括处理本地化,分派,以及挂历自动组装的方 法。下表是 BaseForm 的方法。 分类 方法 SessionLocale public void setSessionLocale(Locale locale); public Locale getSessionLocale(); protected void resetSessionLocale (HttpServletRequest request); Dispatch public void setDispatch(String dispatch); public String getDispatch(); Mutable public void setMutable(boolean mutable); public boolean isMutable(); Autopopulation public Map describe() throws Exception; public void set(Object o) throws Exception; public void populate(Object o) throws Exception; public Map merge(Object profile) throws Exception; STRUTS TIP 如果你的应用使用几个 ActionForm类 , 定义一个基对象包括一些在应用中可能通用的属性或 者方法。 5.7.1 SessionLocale 缺省时, ActionServlet会为在会话上下文中的用户自动创建一个 Locale 对象。这些方法将 用来帮助你管理 Locale 对象 : 5- 5- 28 - public void setSessionLocale(Locale locale); public Locale getSessionLocale(); protected void resetSessionLocale(HttpServletRequest Request); resetLocale 方法被 BaseForm的 reset 方法调用。它从会话中重新取得 Locale 对象,以便 ActionForm可用。如果你有一个 HTML 控制要改变 locale,你可以在 validate 方法中进行 改变。 因为 这里 Locale 对象引用到 session 对象,改变将通过用户绘画持续。 关于 Struts 本地化的更多内容请参考第 13章。 5.7.2 分发( Dispatch) Struts 开发人员通常使用同一个 Action处理相关的操作。一个通常做法来选择操作是在 HTML表单中使用隐藏属性。 BaseForm 的 dispatch 属性也可用作这个目的: public void setDispatch(String dispatch); public String getDispatch(); 5.7.3 自动组装 ActionServlet 会处理 ActionForm的缺省自动组装。 这套方法帮助 Action 对象履行其它数 据传递任务。 describe 方法 返回一个 ActionForm bean的映射( Map)。 set 方法从另一个 JavaBean中组装 ActionForm。 populate方法设置另一个 JavaBean的属性来匹 配这个 bean: public Map describe() throws Exception; public void set(Object o) throws Exception { public void populate(Object o) throws Exception { protected Map merge(Object profile) throws Exception { merge 方法稍微有点复杂。其潜在意思是,一般情况下,你的应用中可能在会话范围内有 一个 profile bean,它用特定的设定来覆盖缺省设置。 merge 方法是你可以结合用户特定的 设置和 ActionForm中的标准设置到一个统一的 Map中。 所以,如果 ActionForm具有一个 sessionLocale 属性而且 profile bean 也具有一个 sessionLocale 属性, profile bean的设定将在 Map中返回。如果 profile bean 没有 sessionProfile 属性,或者这个属性为 null,那么 Map中返回的则是 ActionForm的属性。 5.7.4 BaseMapForm 在 Struts 1.1 或者后面版本中,我们可以使用 Map作为 ActionForm 属性。 Map中值的引 用和常规的 JavaBean 属性的引用是不同的 : // form.getValue(key); <html:text property="value(key)"/> 5- 5- 29 - 但这通常是一个公平的对灵活性的折衷,即 Map可以引入到你的 ActionForm中。 Scaffold 中的 BaseMapForm (org.apache.struts.scaffold.BaseMapForm)提供个标准方法来从 Map 属性中存储和检索字段。 这个方法扩展了 BaseForm 因此也提供了该类的所用功能。下表是 BaseMapForm 方法: 分类 方法 Value Map public void setValue(String key, public Object getValue(String public void setValues(Map values); public Map getValues(); 5.7.4.1 值映射 (Value Map) 为了使用预装的 Map来 初始化一个 ActionForm, 可以使用 setValues方 法 。 为了返回 描述 当前 BaseMapForm 值的 Map,可以调用 getValues 方法。为了添加或者改变一个特定的值,可 以使用 setValue 和 getValue 方法。 5.8 小结 通常, ActionForm是一个惹恼 Struts开发人员的源头。这可望在 Struts 1.1中的改善,因为 Map和 DynaBean可以减小 ActionForm 的维护。 在这一章,我们讨论了 ActionForm的原理,有助于明确他在框架中的角色。我们也探讨了 几种在 ActionForm和业务对象间传递数据的方式。 Scaffold BaseForm 类支持本章所述的几种数据传递策略,可以是您的 ActionForm基础的 很好的选择。 ActionForm描述了什么东西可以进入您的应用。在下一章,我们来看看 ActionForward, 则描述了“人们”可以从什么地方进入您的系统。 5- 5- 30 -