级别: 初级 Merlin Hughes (merlin@merlin.org), 密码专家, Betrusted, Inc.
2004 年 1 月 01 日 在圣诞来临之际,圣诞老人的助手 Merlin Hughes ―― 他在现实生活中的另一身份是Java 开发人员 ―― 展示了一个基于 J2EE 的 secret Santa Web 应用程序的设计和实现,并讨论了可以用来为这种应用程序的开发提供方便的工具和技术。本文(还有
第1 部分和
第 3 部分)提供了对如何用一些最新的工具和框架从头开始建立 J2EE 应用程序的一个全面综述,并详细说明了如何让这些不同的技术共同工作以得到最终结果。但是这几篇文章并不准备对每一个技术作详细分析,而是要为用J2EE 开发一个 Web 应用程序提供一个指南。这第二篇文章侧重于应用程序的控制器方面,以及使用 servlet、JavaMail 和 JakartaStruts 来支持其开发。
在本系列的
第
1 部分 中,我介绍了 secret Santa 应用程序,实现它所使用的技术以及封装其模型的企业 bean。在这部分中,我们将分析用于实际引导
Web 应用程序的操作的类的配置页 ―― 它的控制器方面。最后,在
第
3 部分 中,我们将步入应用程序的表示 ―― 它的视图。在本文中我们首先简单分析所涉及的 Web 端技术,像以往一样,有关这些技术的更详细的信息的链接,请参阅
参考资料。然后我们将分析应用程序使用的一些更重要的工具框架。最后,我们将步入应用程序的控制器类,包括它们相关的配置和支持基础设施。
对技术的简要概述
在这一节中,我们将简单回顾在我们的讨论中用到的技术:servlet 和 Struts。
Servlets
Servlet 是最基本的 Web 端 J2EE 技术。它们是处理 Web 请求的简单组件,通常是无状态的,它们依靠容器使状态与 Web 客户相关联。采用清单
1 这种模型的基本 servlet 提供检查数据结构
HttpServletRequest ,并在数据结构
HttpServletResponse
中生成响应的一个
doGet() (或者
doPost() )方法。
清单 1. 一个基本的 servlet
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class BasicServlet
extends HttpServlet {
protected void
doGet
(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
[...]
}
}
|
 |
不要错过本系列的其余部分!
本系列的
第
1 部分详细描述了这个企业 bean 的设计和实现,以及 XDoclet 是如何帮助它们的开发和部署的。
第
3 部分 详细说明了基于 JSP 的表示页,以及如何用 Struts 和 JSTL 简化和增强它们的实现的。
|
|
Servlet 可以容易地与数据库和 EJB 组件连接,并很适合于模型-视图-控制器(MVC)设计的控制器方面,不过,它们不很适合视图方面(尽管它们可以用于这种目的),因为
Java 类中的嵌入表示(如 HTML)一般来说是很笨拙的和受到限制的。不过,通过它们,使用 struts 对于开发 Web 应用程序是一个相当简单的解决方案,这就是为什么要引入
Struts。
Jakarta Struts
Jakarta Struts 是一种 MVC 体系结构,构建在 servlet 和 JSP 技术之上(我们将在本系列的下一篇文章中讨论它们)。许多
Web 应用程序有这种形式的工作流,其中客户执行一个调用一些控制器代码的请求,调用这些控制器代码可以确定下一步向客户显示什么,以及从那里可以采取什么操作。例如,客户可能发出登录请求,在失败时,用户将返回到登录页,成功时,用户将被转发到列出了他可以请求的后续服务的页面。传统上,工作流
―― 如何请求一个操作、以及不同的结果引导到什么地方 ―― 是直接嵌入在 Java 控制器代码中的。有了 Struts,不同的请求 ―― 称为
actions ――
就是独立编码的,而工作流则在高层的 XML 控制器文件中编写脚本。这个控制器文件决定操作是如何调用的、结果会引导到什么地方以及显示哪种表示(JSP)。如果有了结构良好的操作,基于
Struts 的应用程序是非常灵活的,对工作流的改变不需要重新编码,只需要改变控制器文件。
清单 2 显示了登录操作的 Struts 配置,它是在一个
<action> XML 元素中定义的。
path
属性(在这里是
/login )是访问这个操作的路径 ―― 实际上就是它的名字。
type
属性指定实现了这个操作的类,在这里就是
LoginAction 。然后
<forward>
元素配置操作的指定退出条件:
"success" 结果将客户转发给一个
/account
操作,而
"failure" 结果就会向客户显示登录失败页面。简而言之,客户将访问像 http://host/path/login.action
这样的 URL,它会调用
/login action,而这又会执行在
LoginAction
类中的一个方法,它会返回
"success" 或者
"failure" ,而客户会被重定向或者显示一个错误页面。这个应用程序工作流和表示
JSP 页的位置和名字是在 Java 实现之外维护的,使得部署可以完全控制这些内容。
清单 2. Struts 操作配置
<action
path="/login"
type="org.example.LoginAction">
<forward
name="success"
path="/account.action"
redirect="true" />
<forward
name="failure"
path="/WEB-INF/jsp/login-failure.jsp" />
</action>
|
Struts 提供了超出编写一个应用程序工作流的体系结构之外的更多功能,其中对于当前的应用程序最重要的是自动化表单处理。 Web 应用程序常常涉及提交
HTML 表单,它通常需要控制从 servlet 请求中手工提取请求参数,根据表单的要求验证它们,然后使它们可以被控制逻辑所使用。Struts
提供了一种自动化的机制,它使用请求参数填充 Java 表单类,并带有自动客户和服务器端验证逻辑。
清单 3 展示了登录表单的 Strut 验证配置。第一个
<field> 元素声明这个表单将有一个名为
"email" 的字段(例如,
<input name="email" type="text"
/> )。这个字段是必填的(在提交时,它不能为空),并且它必须是一个有效的电子邮箱地址。第二个
<field>
元素声明这个表单将有一个名为
"password" 的字段。同样,这是必填的,但是不需要有特定的格式。Struts
将自动在登录 Web 页中生成防止用户在这个字段为无效值时提交表单的 JavaScript。它还将在提交时在服务器上验证表单,这是非常重要的,因为这意味着操作代码可以直接接受表单值,并知道它们是有效的。
清单 3. Struts 表单验证
<form name="loginForm">
<field name="email"
depends="required,email">
<arg0 key="email.displayname">
</field>
<field name="password"
depends="required">
<arg0 key="password.displayname">
</field>
</form>
|
从 Web 应用程序中发送电子邮件
有了对这种技术的简要介绍,我们现在要看几个 secret Santa 应用程序所使用的一般性基础设施:一个支持从 Web 应用程序中发送电子邮件消息的框架。JavaMail
API 提供了对发送 MIME 电子邮件消息的强大支持,不过,它没有直接解决在 Web 应用程序中生成可配置的 MIME 电子邮件消息的问题。我们需要一种让我们可以容易地用标准工具编辑消息模板以使之符合站点的
HTML 布局的技术,它要有内置的脚本编写和数据获取能力。结果证明 JSP 技术可以非常好地满足这种要求。
电子邮件过程由以下几个关键步骤组成:
- 从一个 servlet 环境中调用一个 JSP 页,并捕获 JSP 的输出。
- 生成电子邮件消息的纯文本和 HTML 表示,以支持图形和非图形邮件阅读程序。对于这个应用程序,我选择使用单独的 HTML 和纯文件
JSP 页,HTML 的自动转换效率不高并且难看。
- 重写纯文本电子邮件消息,消除一些在自动生成的 JSP 输出中常见的多余空格。
- 在 MIME 消息中直接嵌入 HTML 电子邮件消息引用的所有数据(如图像),并相应重写 HTML 图像 URI。
- 将结果打包为 MIME 消息,并将它发送出去。
下面,我们将看一下完成这些步骤的代码。
Web 应用程序电子邮件支持类
SantaMailer 实用工具类为 Web 应用程序提供了一个发送电子邮件的简单 API。清单 4 列出了它的
import 语句,这个类需要 JavaMail、Servlet 和 Struts API:
清单 4.
SantaMailer 的 import 语句
package org.merlin.santa.servlets;
import java.io.*;
import java.net.*;
import java.util.*;
import javax.activation.*;
import javax.mail.*;
import javax.mail.internet.*;
import javax.naming.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.struts.action.*;
|
清单 5 展示了这个类的构造函数。通过查询 JNDI 名
java:/Mail 获取一个 JavaMail 会话。必须在容器的配置中正确声明这个邮件会话。
email
参数是要发送的电子邮件消息的逻辑名称,构造函数用 Struts
ActionMapping 将这个名字转换为相应的
JSP 路径。这很重要,因为这意味着 Java 应用程序代码可以保持不变,比如说引用
"register-email"
电子邮件消息。然后可以在 Struts 配置文件中动态绑定这个名字的实际意义。我们将在关于
反向代理
的讨论中介绍基本的 URL 参数。
清单 5.
SantaMailer 构造函数
public class SantaMailer {
private String
_url;
private HttpServletRequest
_request;
private HttpServletResponse
_response;
private SantaServlet
_servlet;
private Session
_session;
public SantaMailer
(String email,
ActionMapping mapping,
HttpServletRequest request,
HttpServletResponse response,
SantaServlet servlet)
throws IOException, NamingException, ServletException {
ActionForward forward = mapping.findForward (email);
if (forward == null)
throw new ServletException ("Unknown e-mail name: " + email);
_url = forward.getPath ();
_request = request;
_response = response;
_servlet = servlet;
request.setAttribute ("org.merlin.santa.baseURL", _servlet.getBaseURL ());
InitialContext initialContext = new InitialContext ();
try {
_session = (Session) initialContext.lookup ("java:/Mail");
} finally {
initialContext.close ();
}
}
|
清单 6 显示了一个带有绑定名为
"register-email" 的逻辑电子邮件的示例 Struts 操作映射,在
Struts 配置中它表示为一个标准
<forward> 元素。
清单 6. 一个电子邮件 JSP 绑定
<action
path="/register"
type="org.merlin.santa.actions.family.RegisterAction"
[...]
<forward
name="success"
path="/WEB-INF/jsp/family/registered.jsp" />
<forward
name="register-email"
path="/WEB-INF/jsp/email/register.jsp" />
</action>
|
捕获 JSP 输出
下一步,我们需要捕获 JSP 页输出(通常是显示给客户的 HTML)的能力。Servlet 框架的一个核心功能是一个 servlet 调用另一个
servlet(或者 JSP 页),并将其输出直接加入到调用者的输出流中的能力。将它与包装器(用于把被调用的 servlet 的输出转移到缓存中)相结合,就得到了我们的解决方案。
清单 7 展示了
invoke() 方法,它调用目标 JSP 页并返回包含其输出的一个
StringBuffer 。
url
参数是这个 JSP 页的位置。这个 URL 必须是相对于 Web 应用程序本身的,就是说,URL /WEB-INF/email.jsp 是有效的,而
URL /web-apps/santa/WEB-INF/email.jsp 是无效的。servlet 的
ServletContext
用于提取指定 URL 的一个
RequestDispatcher ,这个调度程序提供一个
include()
方法,它通常用于将目标资料的输出加入到 servlet 的输出流中。不过,我代之以
SantaServletResponseWrapper
类(请参阅
清单
8),将资源的输出转移到一个
StringBuffer 中。
清单 7. 捕获 JSP 输出
private StringBuffer
invoke
(String url)
throws IOException, ServletException {
ServletContext servletContext = _servlet.getServletContext ();
RequestDispatcher requestDispatcher =
servletContext.getRequestDispatcher (url);
SantaServletResponseWrapper responseWrapper =
new SantaServletResponseWrapper (_response);
requestDispatcher.include (_request, responseWrapper);
return responseWrapper.getStringBuffer ();
}
|
清单 8 展示了
SantaServletResponseWrapper 类。它扩展了标准
HttpServletResponseWrapper
类,它提供我们需要的大多数方法,并覆盖了
getWriter() 方法,从而返回捕获结果的
StringWriter 。
getOutputStream()
方法被禁用,因为我们知道 JSP 页将不会使用这个方法。
getStringBuffer() 方法返回捕获的 JSP
输出。为了安全,电子邮件 JSP 页不应该调用在它们的响应数据结构中的任何方法,您应该限制它们只写出数据。
清单 8. 捕获 JSP 输出
package org.merlin.santa.servlets;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class SantaServletResponseWrapper
extends HttpServletResponseWrapper {
private StringWriter
_stringWriter;
private PrintWriter
_printWriter;
public SantaServletResponseWrapper
(HttpServletResponse servletResponse) {
super (servletResponse);
_stringWriter = new StringWriter ();
_printWriter = new PrintWriter (_stringWriter);
}
public ServletOutputStream
getOutputStream
()
throws IOException {
throw new IOException ("getOutputStream() unsupported");
}
public PrintWriter
getWriter
() {
return _printWriter;
}
public StringBuffer
getStringBuffer
() {
return _stringWriter.getBuffer ();
}
}
|
重写纯文本 JSP 输出
所有见过 JSP 页输出的人都知道它通常包含大量的空格。如果这个 JSP 页有良好的格式以使开发人员可以容易理解,那么标签周围的所有空格都将被复制到输出中。在
HTML 页中这不是一个问题,因为 HTML 会忽略这些空格,但是在一个纯文本电子邮件消息中,它是可见的并且是不希望出现的。我没有强迫纯文本
JSP 页使用背靠背的标签以消除这个问题,而是选择使用一个方法,它获取 JSP 输出并用一个空行替换所有长度的空行(或者只是空格)。并且,这个方法提取
JSP 输出的第一行并用它做为消息的主题行。通过检查这一行是否以
Subject: 开始来验证它就是主题。显然可以扩展功能使它支持段落中的自动换行,不过,就把它留给读者做为练习吧。
清单 9 展示了执行这项任务的
tidyText() 方法,它就地重写纯文本消息并返回其主题行。其实现就是做一些移动字符的工作。
清单 9. 重新编写纯文本 JSP 输出
private String
tidyText
(StringBuffer buffer)
throws ServletException {
String subject = null;
int index = 0, length = buffer.length (), old;
char[] chars = new char[length];
buffer.getChars (0, length, chars, 0);
buffer.setLength (0);
for (boolean wasBlank = true, isBlank; index < length;
wasBlank = isBlank) {
char c = chars[old = index ++];
isBlank = Character.isWhitespace (c);
while ((index < length) && (c != '\n') && (c != '\r'))
isBlank &= Character.isWhitespace (c = chars[index ++]);
if (!isBlank && (subject == null)) {
subject = new String (chars, old, index - old);
} else if (!isBlank) {
if (wasBlank && (buffer.length () > 0))
buffer.append ('\n');
buffer.append (chars, old, index - old - 1).append ('\n');
}
if ((c == '\r') && (index < length) && (chars[index] == '\n'))
++ index;
}
if ((subject == null) || !subject.startsWith ("Subject:"))
throw new ServletException
("E-mail contains no subject line: " + subject);
return subject.substring (8).trim ();
}
|
重写 HTML JSP 输出
重写 HTML 电子邮件消息的理由与重写纯文本消息的理由不同。一个 HTML 页所使用的外部资源通常是用非完全限定的 URL 引用的,例如,为了可移植性和其他原因,一个图像是用
/images/sata.gif 而不是 http://www.north-pole.int/images-pole.int/images/santa.gif
引用的。这种方法在 Web 浏览器可以工作得很好,因为浏览器以相对于 HTML 页的位置解释这个 URL。而在一个电子邮件消息中,HTML
页不在相关的 Web 服务器上,所以不能解析这种 URL。即使图像可以为客户解析(例如,如果在页中使用完全限定名,把 URI 重写为完全限定的
URL,或者指定了一个基准 URL(并遵照它)),许多邮件阅读程序出于安全和隐私原因也不会显示来自外部文档的图像。要解决这个问题,我们需要提取
HTML 电子邮件所引用的所有图像,用 MIME 附件将它们附加到电子邮件消息上,然后重写 HTML 以引用这些附件。
清单 10 展示了相应的
tidyHTML() 方法,它就地整理出一条 HTML 消息,并返回一组要包括在电子邮件消息中的
MIME 附件。代码搜索 HTML 中每一个
src="..." 实例(简陋,但是足以满足我的需要),然后它提取引用的数据,将它包装到
MIME 附件中,并改变 HTML 以指向这个附件。MIME 附件是用一个
content
ID 引用的。附件包含一个
Content-ID 头,在尖括号中是一个惟一 ID 值(content ID
必须有与电子邮件地址一样的布局,即
id@domain )。一个引用这个附件的 URI 包括前缀
cid:
,然后是 content ID,如
cid:id@domain 。
为了提取引用的数据,代码使用
ServletContext 方法
getResource() ,它取出一个
URI 字符串(例如,/images/email.gif),并返回一个对应于引用的数据的
URL 对象,如图像在其
WAR 文件中的 URL。在 HTML 页中的 URL 实际是相对于 Web 服务器而不是 Web 应用程序生成的(例如,/web-apps/santa/images/email.gif
而不是 /images/email.gif),所以首先需要从中去掉 Web 应用程序的上下文路径。
MimeBodyPart 类用于将得到的数据封装为一个 MIME 附件,它接受一个包含数据
URL
的
DataHandler 。出于某种原因,所附的图像的内容类型没有正确设置(至少在我的环境中是不正确的),所以我覆盖了其
getContentType() 方法,从资源名中猜测 MIME 类型。
清单 10. 重写 HTML JSP 输出
private List
tidyHTML
(StringBuffer buffer)
throws IOException, ServletException, MessagingException {
List resources = new ArrayList ();
String contextPath = _request.getContextPath ();
String string = buffer.toString ();
buffer.setLength (0);
int index = 0, next;
while ((next = string.indexOf ("src=\"", index) + 5) >= 5) {
buffer.append (string.substring (index, next));
String contentID =
"Resource." + resources.size () + "@north-pole.int";
buffer.append ("cid:").append (contentID);
index = string.indexOf ('"', next);
if (index < 0)
index = string.length ();
final String src = string.substring (next, index);
if (!src.startsWith (contextPath))
throw new ServletException ("Image URI is not absolute: " + src);
URL source = _servlet.getServletContext ().getResource
(src.substring (contextPath.length ()));
if (source == null)
throw new ServletException ("Error attaching image URI: " + src);
MimeBodyPart resource = new MimeBodyPart ();
resource.setDataHandler (new DataHandler (source) {
public String getContentType () {
return URLConnection.getFileNameMap ().getContentTypeFor (src);
}
});
resource.setHeader ("Content-ID", "<" + contentID + ">");
resources.add (resource);
}
buffer.append (string.substring (index));
return resources;
}
|
发送 MIME 电子邮件消息
到目前为止,我们已经看到了构建主题行、纯文本和 HTML 电子邮件正文,以及一组 MIME 附件的方法。剩下的就是将结果包装为一个兼容的 MIME
消息。这种消息的正确格式是包含一个 multipart/alternative 部分以及所有 MIME 附件的 multipart/related
正文。multipart/alternative 部分包含纯文本和 HTML 消息正文。根据其能力,一个遇到这种消息的邮件阅读程序会显示纯文本或者
HTML 消息,并会在 multipart/related 正文的子部分(children)中搜索附件。
清单 11 展示了
sendEmail() 方法,它构建并发送这样一条消息。首先,生成并整理 HTML 电子邮件正文,然后生成并整理纯文本电子邮件正文,然后构建并发送
MIME 消息。纯文本电子邮件 JSP 页的 URL 是通过在 HTML JSP 的 URL 上增加后缀
_text
构建的(例如,/WEB-INF/email.jsp 和 /WEB-INF/email_text.jsp)。
清单 11. 发送电子邮件
public void
sendEmail
(String address, String name)
throws IOException, MessagingException, ServletException {
String htmlPath = _url;
StringBuffer htmlBuffer = invoke (htmlPath);
List resources = tidyHTML (htmlBuffer);
String html = htmlBuffer.toString ();
String textPath = _url.substring (0, _url.length () - 4) + "_text.jsp";
StringBuffer textBuffer = invoke (textPath);
String subject = tidyText (textBuffer);
String text = textBuffer.toString ();
MimeBodyPart plainTextPart = new MimeBodyPart ();
plainTextPart.setContent (text, "text/plain");
MimeBodyPart htmlTextPart = new MimeBodyPart ();
htmlTextPart.setContent (html, "text/html");
MimeMultipart alternative = new MimeMultipart ("alternative");
alternative.addBodyPart (plainTextPart);
alternative.addBodyPart (htmlTextPart);
MimeBodyPart alternativePart = new MimeBodyPart ();
alternativePart.setContent (alternative);
MimeMultipart related = new MimeMultipart ("related");
related.addBodyPart (alternativePart);
for (Iterator i = resources.iterator (); i.hasNext ();)
related.addBodyPart ((MimeBodyPart) i.next ());
MimeMessage message = new MimeMessage (_session);
message.setFrom (_servlet.getEmailAddress ());
message.setRecipients (Message.RecipientType.TO,
new InternetAddress[] { new InternetAddress (address, name) });
message.setSubject (subject);
message.setContent (related);
Transport.send (message);
}
}
|
我们可以用
SantaMailer 类多次发送一条消息,不过,它不能缓存执行一个 JSP 页的结果,因为对不同的收信人可能有不同的输出。这个类的更高级的版本可以从缓存图像附件、JavaMail 会话等中受益。
支持反向代理
许多 Web 应用程序部署在所谓的
反向代理(reverse proxy) 后面。这是一个普通的 Web 服务器,它配置为将某些 HTTP 请求(如对在特定目录中的文件的请求)重定向到一个单独的内部服务器上。例如,www.north-pole.int
的一个主 Apache Web 服务器可能将对 URL http://www.north-pole.int/santa/* 的请求重定向到一个位于
http://jws.internal:13666/santa/* 的内部 Java Web 服务器。例如,清单 12 显示一个 Apache
2 配置指令启用这种反向代理(您必须为此启用
mod_proxy 模块)。一个反向代理可以有性能上的好处(主 Web
服务器可以提供静态内容)以及安全上的好处(外部世界不能直接访问 Java Web 服务器以及 ―― 更重要的 ―― 附带的基础设施,并且它可以作为一个非受信任的用户在一个非系统
TCP/IP 端口运行)。
清单 12. Apache 2 反向代理配置
<VirtualHost www.north-pole.int>
[...]
ProxyPass /santa/ http://jws.internal:13666/santa/
ProxyPassReverse /santa/ http://jws.internal:13666/santa/
[...]
</VirtualHost>
|
它为 Web 应用程序带来的复杂性是应用程序会相信是从内部 Web 地址 http://jws.internal:13666/santa/
访问它的 ―― Apache 代理放过的 HTTP 请求会自动重写以反映这一点 ―― 所以,如果它向客户返回一个完全限定 URL,那么这个
URL 将不会真正解析。例如,Web 应用程序可能指示客户进入 URL http://jws.internal:13666/santa/page2.jsp,这在外部不是有效的地址。
幸运的是,由这个 Web 应用程序返回的大多数 URL 是非完全限定的,大多数会有 /images/santa.gif 这样的形式。在这种情况下,客户将可以正确地根据它所使用的公共
Web 地址解析这些 URL(只要这个 Web 应用程序在内部服务器上是安装在与它在公共服务器上公开的相同的上下文根中)。不过,在两种特定的情况下,向客户返回完全限定的
URL:HTTP 重定向响应和电子邮件消息。
HTTP 标准规定重定向响应(HTTP 代码 301/302)必须包含
Location 头与一个完全限定的绝对目标
URL。不过,因为这个头可以容易地被反向代理看到,它可以自动重写这个头以反映 public Web 服务器地址,例如,如果它看到所缓存的响应中的一个
Location 头,它就会自动删除前缀 http://jws.internal:13666/santa/, 并用恰当的前缀
http://www.north-pole.int/santa/ 替换它。在前面 Apache 2 配置文件中显示的
ProxyPassReverse
指令用于启用这种类型的重写。结果,HTTP 重定向响应将可以工作得很好。
不过,这没有解决电子邮件消息中的 URL。这个问题归结为:电子邮件消息中的每一个 URL 都必须是完全限定的,包括主机名、端口等,可是在一个反向代理配置中,Web
应用程序将不会知道正确的 public URL 是什么。出于这个原因,secret Santa Web 部署描述符有一个
baseURL
servlet 初始化参数,它标识了在电子邮件消息中返回给客户的公共 Web 服务器地址。然后使用一个自定义 JSP 标签
<santa:emailurl>
将这个前缀自动加到电子邮件中所有 URL 上。如果没有设置
baseURL 参数,那么它就假设 Web 应用程序是直接部署在公共服务器上,这样就利用客户的请求确定在电子邮件中使用的
URL。
清单 13 显示了自定义 JSP 标签
EmailURLTag 。它取出一个参数
action,这是
Struts 操作的路径(例如
"/login" )。
doEndTag() 方法从请求对象中提取应用程序的基准
URL(它是由
清单
5 中的
SantaMailer 类设置的),或者如果没有设置它,就重新构建客户使用的 URL。然后它将这个
URL 与从 Struts
RequestUtils 类获得的指定操作的 URL 结合,再写出结果。例如,操作
/login
可能写出为 http://www.north-pole.int/santa/login.santa。其中 http://www.north-pole.int/
这一部分来自基准 URL 参数,/santa/login.santa 部分来自
RequestUtils 类。
清单 13. 一个电子邮件 URL 重写标签
package org.merlin.santa.tags;
import java.io.*;
import java.net.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
import javax.servlet.jsp.tagext.*;
import org.apache.struts.util.*;
public class EmailURLTag
extends TagSupport {
private String
_action;
public void
setAction
(String action) {
_action = action;
}
public String
getAction
() {
return _action;
}
public int
doEndTag
()
throws JspException {
try {
JspWriter out = pageContext.getOut ();
HttpServletRequest request =
(HttpServletRequest) pageContext.getRequest ();
URL baseURL =
(URL) request.getAttribute ("org.merlin.santa.baseURL");
if (baseURL == null)
baseURL = new URL (request.getScheme (), request.getServerName (),
request.getServerPort (), "/");
URL url = new URL
(baseURL, RequestUtils.getActionMappingURL (_action, pageContext));
out.print (url.toExternalForm ());
return EVAL_PAGE;
} catch (IOException ex) {
throw new JspException ("EmailURLTag error", ex);
}
}
}
|
清单 14 显示了这个标签的 TLD 描述符(摘自 web-inf/tld/santa.tld 文件)。
清单 14. 电子邮件 URL 标签 .tld 描述符
<tag>
<name>emailurl</name>
<tagclass>org.merlin.santa.tags.EmailURLTag</tagclass>
<bodycontent>empty</bodycontent>
<attribute>
<name>action</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
</tag>
|
我们将在本系列的第 3 部分讨论电子邮件 JSP 页的例子。
逐步介绍控制器
好了,我们已经看过了一些支持基础设施,那么现在让我们分析如何组装实际的 secret Santa 应用程序。特别是,我们将看一下如何在 Struts 的帮助下将类和配置文件结合到一起以控制应用程序。
Struts action 是客户可以请求的一个操作的逻辑封装,它由三个核心组件组成:在主 Sruts 配置脚本中的操作声明、一个实际实现这个操作的类,以及一个可选的相关联的表单。我们将分析在
secret Santa 中不同操作的这些组件以及一些支持基础设施。
注册操作
这个应用程序的第一个主要的操作是注册:在这里,注册者输入他的姓和电子邮件地址。系统自动向他发回包含可以让他继续完成注册过程的密码的电子邮件。一个明显的替代方式是让注册者选择一个密码,然后电子邮件只是用于确认这个电子邮件地址是有效的。对于这种应用程序,注册很可能是短期的,当前的体系结构更方便。
清单 15 展示了注册操作的配置,它摘自文件 meta/web/struts-actions.xml,XDoclet 自动将它合并到最后的
struts-config.xml 文件中。
path 属性标识这个操作的路径,而
type
属性标识真正实现它的类。在这里,操作是由客户对 URL http://host/path/register.santa 的请求调用的,.santa
后缀来自核心 Struts servlet 的配置,在
后面
将对此进行讨论。
下面几个属性声明与这个操作相关联的表单。
name 属性给出表单名(它必须匹配实际的表单声明,请参见
清单
16 的例子),
scope 属性指定这是请求范围(与会话范围相对)的表单,而
validate
属性表明在调用操作之前必须对表单值进行验证。
input 标签标识提供展示这个操作的输入的 JSP 页,它通常包含用于调用这个操作的
HTML 表单。如果出现表单验证错误,那么就重新向客户显示这个输入页,并加上错误消息。
最后,
<forward> 元素定义作为这个操作的结果展示给客户的页面。第一个元素定义当这个操作成功时(即当
RegisterAction 类返回
"success" 时)展示给客户的页面,第二个元素定义注册电子邮件
JSP 页的位置。注意没有“错误”退出。如果在这个应用程序中出现错误,那么客户不会重定向到任何地方,相反,再次向用户展示注册页(并带有所有错误消息),他可以在这里进行必要的改变再重新注册。
清单 15. 注册操作配置
<action
path="/register"
type="org.merlin.santa.actions.family.RegisterAction"
name="familyRegisterForm"
scope="request"
validate="true"
input="/WEB-INF/jsp/family/register.jsp">
<forward
name="success"
path="/WEB-INF/jsp/family/registered.jsp" />
<forward
name="email"
path="/WEB-INF/jsp/email/register.jsp" />
</action>
|
使用 Struts,您可以用自定义的类在应用程序中表示一个表单,也可以使用通用的、基于
Map 的动态表单类。我们将在
后面
讨论一个动态表单,这里,清单 16 展示了注册表单的自定义类。这是一个非常标准的 bean 类,公开
name 和
email 属性。我们在本文的
后面
分析
SantaForm 超类。
XDoclet 标签用于自动生成与这个表单关联的 Struts 配置。尽管这个表单的 Struts 配置不是特别麻烦,但是如果有将指令直接放到相应的字段旁边的能力将是非常方便的。
@struts.form
标签标识它为一个 Struts 表单类,而
name 属性为表单指定一个逻辑名。这个属性必须与使用这个表单的所有操作的
name 属性相匹配。在每一个表单属旁边,
@struts.validator 标签定义了应该如何验证这个字段,
type
参数包含一系列由豆号分隔的验证语句。对于
name 属性,验证只是简单地列为“required”。这意味着如果字段没有指定或者为空,会自动出现验证错误。对于
email 属性,值
"required,email" 表明这个字段是强制的,并且在语法上必须是一个有效的电子邮件地址。同样,自动验证会处理这个过程。在表单字段无效时,一个依赖于这个表单并且启用了验证的操作不会执行。
清单 16. 注册表单类
package org.merlin.santa.forms.family;
import org.merlin.santa.forms.*;
/**
* @struts.form
* name="familyRegisterForm"
*/
public class RegisterForm
extends SantaForm {
private String
_name;
/**
* @struts.validator
* type="required"
* @struts.validator-args
* arg0resource="form.family.register.name"
*/
public void
setName
(String name) {
_name = name;
}
public String
getName
() {
return _name;
}
private String
_email;
/**
* @struts.validator
* type="required,email"
* @struts.validator-args
* arg0resource="form.family.register.email"
*/
public void
setEmail
(String email) {
_email = email;
}
public String
getEmail
() {
return _email;
}
}
|
清单 17 展示了提供这个 Struts 表单的 JSP 片段。在
后面
我们将讨论表单标签(
<html:form> ),简单地说,它们对应于标准 HTML
<form>
和
<input type="text"> 标签。然后
<html:javascript>
标签生成客户端验证代码。其
formName 属性标识 Struts 表单(在这里为
familyRegisterForm )。然后
JavaScript 将 HTML 表单字段(由它们的
property 属性标识)与相应 Struts 表单属性的验证指令进行对应,并在提交前自动验证字段是正确的。如果用户试图提交一个具有无效字段的表单,那么就会显示一个弹出错误消息。您可以在本系列的
第
3 部分看到有关 JSP 表示代码的内容。
清单 17. 一个 JSP 注册表单
<html:form action="/register"
onsubmit="return validateFamilyRegisterForm(this);">
Family name: <html:text property="name" /><br />
Your name: <html:text property="email" /><br />
<html:submit property="register" />
</html:form>
<html:javascript formName="familyRegisterForm" /> |
清单 18 展示了Struts 验证系统的配置。这是 meta/web/struts-plugins.xml 文件,由 XDoclet 结合到
struts-config.xml 中。这个文件标识了验证支持文件在什么位置。引用的两个文件 validator-rules.xml 和 validation.xml
由编译脚本从 Struts 库中自动拷贝。
清单 18. Struts 验证器配置
<plug-in
className="org.apache.struts.validator.ValidatorPlugIn">
<set-property
property="pathnames"
value="/WEB-INF/xml/validator-rules.xml, /WEB-INF/xml/validation.xml" />
</plug-in>
|
清单 19 展示了实际的注册 action 实现,这个类扩展
SantaAction
并提供一个
santaAction() 方法。
mapping 参数包含 action 配置的一个表示(取自
清单
15),
form 参数是
RegisterForm 类的一个实例(请参阅
清单
16),而
request 和
response 参数是标准的 servlet
请求和响应数据结构。
这个操作首先根据表单值构建一个 skeleton
FamilyValue 。它不需要检查表单字段是否设置或者表单是否为正确的类型,Struts
API 保证它将是非 null 的,具有预期的类型、是有效的字段值。操作用
FamilyUtil 实用工具类查询一个
FamilyLocalHome 引用,根据注册信息创建一个新的
SantaLocal EJB
组件,然后用
SantaMailer
类向注册者发送一封电子邮件。通过在
request 数据结构中名字
"familyValue" (作为
${requestContext.familyValue} 由 JSP 访问)下放入
FamilyValue
属性提供有关注册的信息。最后,这个操作将姓存储在 HTTP 会话上下文中
"familyName" 键下,并返回
"success" (或者,更准确地说,在数据结构中
"success" 名下存储的
ActionForward )。使用
cookie 或者 URL 重写,会话上下文通过 servlet 容器提供了与客户的会话关联的持久性存储。我们将姓存储在这里,这样我们就可以在客户收到注册电子邮件后返回登录系统时自动填充姓输入字段。
在
后面 我们将更详细地讨论异常,现在,当系统配置正确时,预计这个方法惟一会抛出的异常是在姓已经存在时的
DuplicateKeyException 。我捕获这个异常并将它作为一个
SantaException
重新抛出,所有其他异常都直接通过。
SantaException 类保证在翻译为适当的语言后,可以将这个错误干净地展示给客户。
清单 19. 注册操作
package org.merlin.santa.actions.family;
import javax.ejb.*;
import javax.servlet.http.*;
import org.apache.struts.action.*;
import org.merlin.santa.*;
import org.merlin.santa.actions.*;
import org.merlin.santa.forms.family.*;
import org.merlin.santa.interfaces.*;
import org.merlin.santa.servlets.*;
import org.merlin.santa.utils.*;
import org.merlin.santa.values.*;
public class RegisterAction
extends SantaAction {
protected ActionForward
santaAction
(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
RegisterForm registerForm = (RegisterForm) form;
FamilyLocal family = null;
try {
FamilyValue value = new FamilyValue ();
value.setName (registerForm.getName ());
value.setEmailAddress (registerForm.getEmail ());
FamilyLocalHome home = FamilyUtil.getLocalHome ();
try {
family = home.create (value);
} catch (DuplicateKeyException ex) {
throw new SantaException
(SantaMessages.ERROR_FAMILY_DUPLICATE, registerForm.getName ());
}
request.setAttribute ("familyValue", getFamilyValue (family));
SantaMailer mailer = getMailer ("email", mapping, request, response);
mailer.sendEmail (registerForm.getEmail (), registerForm.getName ());
HttpSession session = request.getSession ();
session.setAttribute ("familyName", value.getName ());
return mapping.findForward ("success");
} catch (Exception ex) {
if (family != null)
family.remove ();
throw ex;
}
}
}
|
作为简单的补充,XDoclet 确实支持根据源代码中的适当标签自动生成一个操作的 Struts 配置。如清单 20 显示了对注册操作的一个
XDoclet 声明。不过,我没有使用 XDoclet 的这一功能,我认为这有些背离了用一个与实现保持分离的 XML 配置编写 Web 应用程序脚本的
Struts 概念。必须练习判断什么时候适合使用 XDoclet 功能,我倾向于不使用在源代码注释中嵌入部署特定的信息(如 JSP 路径)的功能,尽管我不能宣称自己严格遵守了任何标准。
清单 20. XDoclet 操作声明
/**
* @struts.action
* path="/register"
* name="familyRegisterForm"
* scope="request"
* validate="true"
* input="/WEB-INF/jsp/family/register.jsp"
* @struts.action-forward
* name="success"
* path="/WEB-INF/jsp/family/registered.jsp"
* @struts.action-forward
* name="email"
* path="/WEB-INF/jsp/email/register.jsp"
*/
|
消息资源
Web 应用程序通常有国际受众,所以在开发时记住这一点是有好处的。例如,直接在应用程序中嵌入消息,不能使您的应用程序被广泛的受众访问。相反,应用程序应该在内部使用抽象消息
键。然后消息资源配置文件可以提供这些键的特定于语言的翻译。例如,一个应用程序可能返回消息键“user.not.found”。一个像“That
user was not found”这样的翻译可以展示给讲英语的用户,而向其他用户展示其他合适的翻译。
清单 21 展示了一个包含 Struts 消息资源配置信息的文件,它是文件 meta/web/struts-message-resources.xml,XDoclet
会将它自动加入到最后的 struts-config.xml 配置中。这个指令告诉 Struts 有一个文件 /WEB-INF/classes/org/merlin/santa/SantaResources.properties
包含应用程序可能需要显示的各种消息键的英语翻译。对一个爱尔兰语言客户将显示相应 SantaResources_ga.properties 文件中的消息(如果这个文件存在的话),它提供了这个消息键的盖尔语翻译。
清单 21. Struts 消息资源配置
<message-resources
parameter="org.merlin.santa.SantaResources"
null="false" />
|
清单 22 展示了实际的消息资源文件的一部分,这就是文件 web-inf/classes/org/merlin/santa/SantaResources.properties。文件中每一个非注释项的格式都为
key=value,键部分定义消息资源键,而值部分是该键的语言翻译。要翻译一个应用程序,部署只需要提供相应语言的一个新属性文件,Struts
会自动使用它。
在消息翻译中,值
{0}、
{1} 等自动被生成消息键时提供的相应参数所替换。例如,如果
secret Santa 应用程序生成“error.family.duplicate”消息键,它还会提供重复的姓,可以将它展示为,例如“The
family name Featherstonehaugh is already in use”。
清单 22. secret Santa 消息资源文件
# error messages
error.global-exception=An unexpected exception was caught.
error.family.duplicate=The family name {0} is already in use.
# form field names
form.family.register.name=Your family name
form.family.register.email=Your e-mail address
# validation errors from validator-rules.xml
errors.required={0} is required.
errors.email={0} must be a valid address.
|
Struts 还使用这种消息资源机制构建验证错误消息。对每一个表单字段的消息资源键是与表单验证信息一同指定的(例如“form.family.register.name”,请参阅
清单 16),与特定验证错误关联的消息资源键是在一个内部 Struts 文件(validator-rules.xml)中指定的。生成一个验证错误后,首先翻译表单字段键(如“Your
e-mail address”),然后把它作为验证错误消息的第一个参数,生成最终的消息“Your e-mail address must be
a valid address”。
消息键通常在三个地方指定:在配置文件中,如 Struts 表单验证指令;在源文件中(如当应用程序需要向客户返回一个错误或者状态消息时);以及有些时候在 JSP 页中。重要的是跟踪应用程序中的不同消息键,这样您就可以确认提供了合适的翻译。在这方面,secret Santa 应用程序使用了清单 23 中展示的一个
SantaMessages 类,它定义了由应用程序本身生成的所有消息键。键和它们的参数都有记录,简化了翻译任务。
清单 23. secret Santa 消息键声明
package org.merlin.santa;
public interface SantaMessages {
/** Duplicate family name. {0} is the family name. */
public static final String ERROR_FAMILY_DUPLICATE =
"error.family.duplicate";
/** Unknown family name. {0} is the family name. */
public static final String ERROR_FAMILY_UNKNOWN =
"error.family.unknown";
/** Incorrect family password. {0} is the family name. */
public static final String ERROR_FAMILY_PASSWORD =
"error.family.password";
/** Duplicate family member name. {0} is the member name. */
public static final String ERROR_MEMBER_DUPLICATE =
"error.member.duplicate";
/** Unknown family member name. {0} is the member name. */
public static final String ERROR_MEMBER_UNKNOWN =
"error.member.unknown";
[...]
}
|
清单
19显示了这些消息键以及相关联的参数信息的使用。
异常处理
Web 应用程序中的异常处理是在设计的很早阶段就要考虑的另一个重要的问题,它的效果一般会在应用程序中扩散,这使后期的改变相当困难。
在一个非常基本的 Struts 应用程序中,操作可以抛出任何类型的异常。Struts 将捕捉所有发生的异常并将它们直接显示给客户。Struts
不强制使用任何特定的异常处理范例,而是让应用程序选择最适合它们需要的。
一种观点是操作应该不抛出异常,相反,每个操作都应该捕获所有可以出现的异常,并在内部对它们进行处理。我不是特别欣赏这种做法,因为它使每一操作都增加了处理在正常环境中不会发生的异常的负担。例如,考虑表达式
string.getBytes("UTF-8") 。API 声明它可能抛出一个
UnsupportedEncodingException ,不过在所有正常配置的服务器环境中这是不会发生的。我不认为让每一个操作都单独处理像这样的预计不会出现的异常有什么好处。
Struts 支持的另外一种异常处理策略是在主配置文件中编写异常处理脚本。清单 24 展示了注册 action 的配置,它带有内置的异常处理。
<exception>
元素指示 Struts 当注册操作抛出
DuplicateKeyException 时,Struts 应该重新显示带有错误消息“error.family.duplicate”的注册页。不过,我也不喜欢在这种级别上编写异常处理。它实际上将实现放到
XML 脚本中了。在这个例子中,
javax.ejb.DuplicateKeyException 是一个实现细节,如果实现转而直接使用
SQL,那么就会出现不同的异常。
清单 24. 脚本化的异常处理
<action
path="/register"
type="org.merlin.santa.actions.family.RegisterAction"
name="familyRegisterForm"
scope="request"
validate="true"
input="/WEB-INF/jsp/family/register.jsp">
<exception
key="error.family.duplicate"
type="javax.ejb.DuplicateKeyException" />
<forward
name="success"
path="/WEB-INF/jsp/family/registered.jsp" />
<forward
name="email"
path="/WEB-INF/jsp/email/register.jsp" />
</action> |
我使用的异常处理类型实际上混合了这两种方法。在 secret Santa 应用程序中的操作有内部逻辑捕获那些
预计 要发生的异常,另一方面,如果出现一个非预计的异常,就把它传递给外部处理。如果一个操作需要抛出显示给客户的异常,它就抛出有适当错误消息的
SantaException 。再使用 Struts 配置指令指示如何处理这些预期和非预期异常。清单 25 展示了
SantaException 类,它只是封装了一个错误消息键和所有相关的参数。提供了不同的构造函数,以便指定不同数量的参数。
清单 25.
SantaException 类
package org.merlin.santa;
public class SantaException
extends Exception {
private Object[]
_values;
public SantaException
(String key, Object[] values) {
super (key);
_values = (Object[]) values.clone ();
}
public SantaException
(String key, Object value0, Object value1) {
this (key, new Object[] { value0, value1 });
}
public SantaException
(String key, Object value) {
this (key, new Object[] { value });
}
public SantaException
(String key) {
this (key, new Object[0]);
}
public Object[]
getValues
() {
return (Object[]) _values.clone ();
}
}
|
清单 26 展示了应用程序的异常处理的配置指令。它就是文件 meta/web/global-exceptions.xml,XDoclet
会自动将它加入到最后的 struts-config.xml 配置中。如果一个操作抛出
SantaException ,那么就调用
SantaExceptionHandler 。否则,如果抛出任何其他类型的异常,就调用
GlobalExceptionHandler
类。
清单 26. Struts 全局异常处理程序配置
<global-exceptions>
<exception
key="unused"
type="org.merlin.santa.SantaException"
handler="org.merlin.santa.actions.SantaExceptionHandler" />
<exception
key="error.global-exception"
type="java.lang.Exception"
handler="org.merlin.santa.actions.GlobalExceptionHandler" />
</global-exceptions> |
清单 27 展示了
SantaExceptionHandler 类。当任何操作抛出
SantaException
时,就调用这个类的
execute() 方法,它创建一个包含来自异常的错误消息的
ActionError ,然后用继承的
storeException() 方法存储错误消息以供表示层使用。这个方法再指示 Struts 向客户重新显示当前操作的输入文档(由操作配置的
input 属性标识的 JSP 页),在这一页中将包含错误消息。
清单 27.
SantaExceptionHandler 类
package org.merlin.santa.actions;
import javax.servlet.http.*;
import org.apache.struts.action.*;
import org.apache.struts.config.*;
import org.merlin.santa.*;
public class SantaExceptionHandler
extends ExceptionHandler {
public ActionForward
execute
(Exception exception,
ExceptionConfig config,
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response) {
SantaException santaException = (SantaException) exception;
ActionError error = new ActionError
(santaException.getMessage (), santaException.getValues ());
ActionForward forward = mapping.getInputForward ();
String property = ActionMessages.GLOBAL_MESSAGE;
String scope = config.getScope ();
storeException (request, property, error, forward, scope);
return forward;
}
}
|
清单 28 展示了
GlobalExceptionHandler 类。调用这个类以响应由操作抛出的任何其他类型的异常。它类似于上一个处理程序,但是错误消息是从异常处理程序的配置的
key 属性中提取的。记录异常,并将它存储为请求数据结构的
"exception" 属性,使表示层在适当时可以向客户显示异常的细节。注意目前日志是很简陋的,只是将异常写到
System.err 中,应当用适当的日志 API 进行记录。
清单 28.
GlobalExceptionHandler 类
package org.merlin.santa.actions;
import javax.servlet.http.*;
import org.apache.struts.action.*;
import org.apache.struts.config.*;
import org.merlin.santa.*;
public class GlobalExceptionHandler
extends ExceptionHandler {
public ActionForward
execute
(Exception exception,
ExceptionConfig config,
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response) {
ActionError error =
new ActionError (config.getKey (), exception.getMessage ());
ActionForward forward = mapping.getInputForward ();
String property = ActionMessages.GLOBAL_MESSAGE;
String scope = config.getScope ();
storeException (request, property, error, forward, scope);
exception.printStackTrace (System.err); // should log
request.setAttribute ("exception", exception);
return forward;
}
}
|
如果 secret Santa 应用程序编写正确,正常执行应该不会抛出
SantaException 以外的其他异常。特别是,客户请求不会产生未被适当的操作处理,并且作为
SantaException 重新抛出的异常。部署了应用程序后,如果出现任何其他异常,就会记录它,管理员应该分析这种错误,因为它们表明了需要解决的部署或者系统错误。
Struts 实际上提供了一个默认的异常处理程序,它可以以类似于我刚描述的类的方式操作,标准
ModuleException
类似于
SantaException 类,使应用程序可以在它所产生的异常中指定具有键和参数的消息。使用自定义的异常处理程序
―― 如我们刚讨论的这个 ―― 使应用程序具有更大的灵活性,在需要时可以非常容易地改变它的行为。
定制 Struts 框架
在一个基本 Struts 应用程序中,应该由标准
ActionServlet 类发挥核心应用程序的作用,操作应派生自
Action 类,表单派生自
ActionForm 类。不过,对于所有实际的应用程序,我建议提供这些核心类的定制子类,用它派生出您的所有实际实现。这些定制的类可以为您的子类提供公共支持方法,并且它们可以在需要时修改默认
Struts 类的行为。
为了介绍 secret Stana 应用程序的定制框架,请考虑注册过程。注册表单有姓和电子邮件字段。Struts 验证程序自动验证这两个字段都已输入并且格式正确。如果客户直接请求注册操作(不是提交表单,比如说通过访问
http://host/path/register.santa),那么验证程序将解释这个请求并向客户展示注册页,并带有表明没有输入表单字段的错误消息。这就提出了如何让客户在一开始访问注册页的问题。
一种选择是让客户直接请求 register.jsp 页。不过,一般不推荐这种这种方法,建议所有客户请求通过 Struts 操作进行。这样,操作就可以设置由表示页所使用的数据。没有操作,JSP
页就需要直接设置其所有数据,这会导致在 JSP 页中嵌入大量代码,而这是一种不好的做法。
第二种选择是提供一个
/registration action,它不进行表单验证,它只设置所有适当的数据,然后向客户展示
register.jsp 页。这一页面将包含一个发送给
/register 操作的 HTML 表单。虽然稍微好一些,但是这种方法要求每一个操作都是重复的:一个化身只是向客户展示适当的
JSP,而第二个验证表单字段,然后采取相应的操作。可以用一个类完成所有简单的页显示操作,这样就不需要多个 action 类,不过,这并没有解决在
Struts 配置中不必要的重复问题。
第三种选择是让成功的操作向客户直接展示下一个操作的 JSP 页。例如,如果客户成功地调用
/register ,随后的操作是
/login ,那么注册操作可以直接显示 login.jsp 页。从纯粹的可视化的角度来看,客户将调用 URL http://host/path/register.santa,用户将看到登录页。这一页面将包含发送一个对
/login 操作请求的 HTML 表单。出于美学和技术方面的原因,我不喜欢这种方法。最基本的技术问题是如果 login.jsp
需要事先准备某些数据,那么
/register 操作就需要设置这些数据。这种知识实际上应该局限在
/login
操作中。通过使用一种用本地状态会话 bean 为其 JSP 页提供数据的体系结构,可以缓和这个问题,不过,仍然有像表单初始化这样的问题。
这些方法没有一种是我特别喜欢的,所以我选择对这个应用程序的操作和表单类都使用一个定制的框架。在开发 Struts 应用程序时定制框架一般来说是一个好主意
―― 即使这个框架最初什么也不做 ―― 因为这意味着通过编辑框架类,您可以容易地定制表单和 action 的操作,无需分别改变应用程序中每一个相关类。
使用自定义的 secret Santa 框架类,对
/register 操作的直接请求(没有请求参数)将只会显示操作输入页(即
register.jsp),不执行任何验证或者生成任何错误。这意味着客户可以通过访问 ttp://host/path/register.santa
来访问注册表单(它是由
RegisterAction 类展示的)。在提交这一页面的 HTML 表单时,再次调用 /register
操作,这一次,将验证表单字段而
RegisterAction 类将处理这个请求。所有与 register.jsp
页有关的处理都局限在这个类内部。这个框架目前没有解决所有潜在的问题 ―― 如出现错误后的数据准备 ―― 不过可以用很少的工作解决这些问题。
清单 29 展示了
SantaForm 类,这是 Santa 应用程序表单的超类。它派生自
ValidatorForm ,并覆盖了
validate() 方法,在客户的请求没有构成真正的操作提交时跳过验证。换句话说,如果客户只是请求操作显示其表单,那么就跳过表单验证。
清单 29.
SantaForm 类
package org.merlin.santa.forms;
import javax.servlet.http.*;
import org.apache.struts.action.*;
import org.apache.struts.validator.*;
import org.merlin.santa.actions.*;
public abstract class SantaForm
extends ValidatorForm {
public ActionErrors
validate
(ActionMapping mapping, HttpServletRequest request) {
return SantaAction.isSubmission (request) ?
super.validate (mapping, request) : null;
}
}
|
清单 30 展示了
SantaAction 类,这是 Santa 应用程序的超类。它派生自
Action ,并覆盖了
execute() 方法,将客户请求引导到适当的子类方法。
如果客户只是请求显示操作的表单(通过直接请求操作,而没有提交表单值),那么就调用
santaDisplay() 方法。在默认的实现中,这只是显示该操作的输入 JSP 页。不过,子类可以覆盖这个方法,以便准备数据并初始化表单值。
否则,客户就提交由操作处理的请求。这种请求直接转发给子类
santaAction() 方法。在这个代码的路径下,就进行表单验证(如果它启用了),这样
santaAction() 方法提供了与标准 Struts
action() 方法一样的
API 合同(contract)―― 表单将是非空的,有正确的类型,并且有有效的字段值。
其余的方法为 Santa 操作提供不同的服务,如获得对应于实体 bean 的值对象,获得一个电子邮件技术对象,或者保存消息以展示给客户。由
saveMessage() 方法保存的对象是由 JSP 页显示的,如在
这里
所讨论的。
清单 30.
SantaAction 类
package org.merlin.santa.actions;
import java.io.*;
import javax.ejb.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.naming.*;
import org.apache.struts.action.*;
import org.merlin.santa.interfaces.*;
import org.merlin.santa.utils.*;
import org.merlin.santa.values.*;
import org.merlin.santa.servlets.*;
public abstract class SantaAction
extends Action {
public ActionForward
execute
(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
if ((form != null) && !isSubmission (request)) {
return santaDisplay (mapping, form, request, response);
} else {
return santaAction (mapping, form, request, response);
}
}
protected ActionForward
santaDisplay
(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
return mapping.getInputForward ();
}
protected abstract ActionForward
santaAction
(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception;
protected FamilyValue
getFamilyValue
(FamilyLocal family)
throws NamingException, CreateException {
ValueFactoryLocalHome home = ValueFactoryUtil.getLocalHome ();
ValueFactoryLocal factory = home.create ();
return factory.getFamilyValue (family);
}
[...]
protected SantaMailer
getMailer
(String email, ActionMapping mapping,
HttpServletRequest request, HttpServletResponse response)
throws IOException, NamingException, ServletException {
return new SantaMailer
(email, mapping, request, response, (SantaServlet) getServlet ());
}
[...]
protected void
saveMessage
(HttpServletRequest request, String key, Object[] values) {
ActionMessages messages = new ActionMessages ();
ActionMessage message = new ActionMessage (key, values);
messages.add (ActionMessages.GLOBAL_MESSAGE, message);
saveMessages (request, messages);
}
public static boolean
isSubmission
(HttpServletRequest request) {
return !request.getParameterMap ().isEmpty ();
}
}
|
清单 31 展示了
SantaServlet 类,这是驱动 secret Santa Web 应用程序的主要 servlet。它扩展了
ActionServlet ―― 这是读取客户请求并将它们派送给适当操作的核心 Struts servlet,并覆盖了
init() 以获得一些 secret Santa 初始化参数。
清单 31.
SantaServlet 类
package org.merlin.santa.servlets;
import java.net.*;
import javax.mail.internet.*;
import javax.servlet.*;
import org.apache.struts.action.*;
public class SantaServlet
extends ActionServlet {
private URL
_baseURL;
private InternetAddress
_emailAddress;
public void
init
()
throws ServletException {
super.init ();
ServletConfig servletConfig = getServletConfig ();
String baseURL = servletConfig.getInitParameter
("org.merlin.santa.baseURL");
if (baseURL != null) {
try {
_baseURL = new URL (baseURL);
} catch (Exception ex) {
throw new UnavailableException ("Invalid base URL: " + baseURL);
}
}
String emailAddress = servletConfig.getInitParameter
("org.merlin.santa.emailAddress");
if (emailAddress == null)
throw new UnavailableException ("Missing e-mail address");
String emailName = servletConfig.getInitParameter
("org.merlin.santa.emailName");
if (emailName == null)
throw new UnavailableException ("Missing e-mail name");
try {
_emailAddress = new InternetAddress (emailAddress, emailName);
} catch (Exception ex) {
throw new UnavailableException
("Invalid e-mail address: " + emailAddress);
}
}
public URL
getBaseURL
() {
return _baseURL;
}
public InternetAddress
getEmailAddress
() {
return _emailAddress;
}
}
|
清单 32 展示了 servlet 部署描述符文件 web-inf/web/servlets.xml,它由 XDoclet 自动加入到应用程序的部署描述符
web.xml 中。这个文件声明了 Santa servlet 并提供了一些配置参数。
config 参数告诉 Struts
框架在哪里找它的应用程序配置脚本,其余的参数都是特定于 secret Santa 应用程序的。XDoclet 实际上提供了一个
@web.servlet
标签,可以用于将这些配置信息放入
SantaServlet 类的源文件中。不过,我选择不为此使用 XDoclet,因为我不希望在
Java 源文件中嵌入这种部署信息(配置文件的位置、基准 URL 和电子邮件地址)。
清单 32. servlet 部署描述符
<servlet>
<servlet-name>Santa</servlet-name>
<servlet-class>org.merlin.santa.servlets.SantaServlet</servlet-class>
<init-param>
<param-name>config</param-name>
<param-value>/WEB-INF/xml/struts-config.xml</param-value>
</init-param>
<init-param>
<param-name>org.merlin.santa.baseURL</param-name>
<param-value>http://santa/</param-value>
</init-param>
<init-param>
<param-name>org.merlin.santa.emailAddress</param-name>
<param-value>santa@north-pole.int</param-value>
</init-param>
<init-param>
<param-name>org.merlin.santa.emailName</param-name>
<param-value>Psychic Secret Santa</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
|
清单 33 展示了 servlet 映射文件 web-inf/web/servlet-mappings.xml,它由 XDoclet 自动加入到应用程序的部署描述符
web.xml 中。这个文件只是将 *.santa 的请求与 Santa servlet 相关联。
清单 33. servlet 映射
<servlet-mapping>
<servlet-name>Santa</servlet-name>
<url-pattern>*.santa</url-pattern>
</servlet-mapping>
|
清单 34 展示了 servlet 本地引用文件 web-inf/web/web-ejbrefs-local.xml,它由 XDoclet
自动合并到应用程序的部署描述符 web.xml 中。这个文件定义了这个 servlet 对应用程序的会话和实体 bean 所给出的本地引用。
清单 34. servlet EJB 引用
<ejb-local-ref>
<ejb-ref-name>ejb/ValueFactoryLocal</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<local-home>org.merlin.santa.interfaces.ValueFactoryLocalHome</local-home>
<local>org.merlin.santa.interfaces.ValueFactoryLocal</local>
<ejb-link>ValueFactory</ejb-link>
</ejb-local-ref>
<ejb-local-ref>
<ejb-ref-name>ejb/FamilyLocal</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<local-home>org.merlin.santa.interfaces.FamilyLocalHome</local-home>
<local>org.merlin.santa.interfaces.FamilyLocal</local>
<ejb-link>Family</ejb-link>
</ejb-local-ref>
<ejb-local-ref>
<ejb-ref-name>ejb/SantaLocal</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<local-home>org.merlin.santa.interfaces.SantaLocalHome</local-home>
<local>org.merlin.santa.interfaces.SantaLocal</local>
<ejb-link>Santa</ejb-link>
</ejb-local-ref>
|
有了这些框架类和配置文件,在 secret Santa Web 应用程序中的各个类就可以利用公共支持基础设施了。如果不同的操作有共同的需求,那么可将它们重新加入到核心类中,供所有操作或者表单使用。
认证操作
当 secret Santa 注册者收到他的确认电子邮件时,他可以在 Web 应用程序中进行认证,并开始输入有关其家庭的信息。认证操作负责验证客户的密码是正确的,并设置会话信息,以使随后的操作知道客户已经通过认证。
清单 35 展示了认证操作配置。表示页是 /WEB-INF/jsp/family/authenticate.jsp,它包含一个名为 familyAuthenticateForm
的表单。成功时,客户被转发给
/describe 表示页,它让用户编辑 secret Santa 家庭描述。注意在这种转发下(即转发给新的操作),
redirect="true"
参数意味着向客户发送一个到新操作的 HTTP 重定向,而不是展示其 JSP 页。
清单 35. 认证操作配置
<action
path="/authenticate"
type="org.merlin.santa.actions.family.AuthenticateAction"
name="familyAuthenticateForm"
scope="request"
validate="true"
input="/WEB-INF/jsp/family/authenticate.jsp">
<forward
name="success"
path="/describe.santa"
redirect="true" />
</action>
|
清单 36 展示了认证表单类。这个类有两个属性:
name 和
password ,它们都是强制性的。
清单 36. 认证表单类
package org.merlin.santa.forms.family;
import org.apache.struts.action.*;
import org.merlin.santa.forms.*;
/**
* @struts.form
* name="familyAuthenticateForm"
*/
public class AuthenticateForm
extends SantaForm {
private String
_name;
/**
* @struts.validator
* type="required"
* @struts.validator-args
* arg0resource="form.family.authenticate.name"
*/
public void
setName
(String name) {
_name = name;
}
public String
getName
() {
return _name;
}
private String
_password;
/**
* @struts.validator
* type="required"
* @struts.validator-args
* arg0resource="form.family.authenticate.password"
*/
public void
setPassword
(String password) {
_password = password;
}
}
|
清单 37 展示了认证操作实现。这个操作在客户成功认证后设置会话信息。它还展示了这个定制 Struts 框架的一个有用的功能:自动表单初始化。当然,用
Struts 可以有多种方法实现这种效果,这只是其中一种可能性。
如果客户直接请求
/authenticate 操作,而没有指定任何参数,那么就用一个空的
AuthenticateForm
实例调用
santaDisplay() 方法。所有输入到表单对象中的值都会由 Struts 作为展示给客户的 HTML
表单的初始值显示出来。之前,
/register action (请参阅
清单
19)将姓存储在客户的 HTTP 会话的
"familyName" 键下,因此在这里,我提取这个名字并将它输入到表单中作为
name 字段的初始值。
santaAction() 方法实现认证过程。首先,它删除客户 HTTP 会话中所有现有的
"family"
项(这删除了所有现有的登录会话)。然后它从提交的表单中提取姓并用它寻找特定的
Family bean。如果输入的是一个未知的名字,那么将会抛出一个
ObjectNotFoundException ,所有这种异常都被捕获,并在这个位置上抛出一个具有适当消息键的
SantaException 。姓被传递给这个异常,这样在翻译错误消息时就可以使用它。然后这个方法检查在表单中提交的密码是否与在
bean 中的匹配,如果不匹配,就会抛出一个相应的异常。如果在这个操作中出现错误,那么并且重新向用户显示注册页,那么他们在表单中以前的输入将会输入回表单中。如果是错误密码错误,我显式地清除密码字段,这样就不会输入以前的错误密码。否则,客户就成功地得到认证,通过将
Family bean 存储到客户的 HTTP 会话中的键
"family" 下完成操作。后续的操作将在会话对象中寻找这一项,确定客户是否得到认证了以及以什么身份认证的。
清单 37. 认证操作
package org.merlin.santa.actions.family;
import javax.ejb.*;
import javax.servlet.http.*;
import org.apache.struts.action.*;
import org.merlin.santa.*;
import org.merlin.santa.actions.*;
import org.merlin.santa.forms.family.*;
import org.merlin.santa.interfaces.*;
import org.merlin.santa.servlets.*;
import org.merlin.santa.utils.*;
import org.merlin.santa.values.*;
public class AuthenticateAction
extends SantaAction {
protected ActionForward
santaDisplay
(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
HttpSession session = request.getSession ();
String familyName = (String) session.getAttribute ("familyName");
if (familyName != null) {
AuthenticateForm authenticateForm = (AuthenticateForm) form;
authenticateForm.setName (familyName);
}
return super.santaDisplay (mapping, form, request, response);
}
protected ActionForward
santaAction
(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
HttpSession session = request.getSession ();
session.setAttribute ("family", null);
AuthenticateForm authenticateForm = (AuthenticateForm) form;
String familyName = authenticateForm.getName ();
FamilyLocalHome home = FamilyUtil.getLocalHome ();
FamilyLocal family;
try {
family = home.findByPrimaryKey (familyName);
} catch (ObjectNotFoundException ex) {
throw new SantaException
(SantaMessages.ERROR_FAMILY_UNKNOWN, familyName);
}
String password = authenticateForm.getPassword ();
if (!password.equals (family.getPassword ())) {
authenticateForm.setPassword ("");
throw new SantaException
(SantaMessages.ERROR_FAMILY_PASSWORD, familyName);
}
session.setAttribute ("family", family);
return mapping.findForward ("success");
}
}
|
在结束这个操作之前还有几点说明。我没有在 HTTP 会话中的认证信息中存储任何时间戳。这意味着用户登录只有在 HTTP 会话失效时才会失效,而这是由
Web 应用程序的部署描述符或者容器的配置所决定的。如果登录会话有更多的用途,您可能要对这个问题加以更多考虑。
同时,我在用户的 HTTP 会话中存储对
FamilyLocal 实体 bean 的直接引用。如果我使用远程接口并且这个应用程序是运行在一个服务器群集上,那么这就不合适了。对于这种部署,您应当只在会话对象中存储
Serializable 对象,容器将需要序列化会话上下文,以支持服务器重启和在群集计算机中共享会话信息。如果您是为这种环境做开发,那么不要将
bean 引用直接存储在会话中,您应当使用远程引用继承的
EJBObject
getHandle()
方法,获取可序列化的
Handle 并存储在会话中。如果您使用工具方法在会话中存储和获取这样的引用,那么可以试验不同的方法以优化这些引用的获取。
在这里还应该对安全性说上几句。坦白地说,这个应用程序不是安全的。密码的无序度很低,并以原始形式存储在服务器的数据库中。在认证期间它们是以纯文本电子邮件发送的,并通过一个未加密的
HTTP 连接发送回来。在确认电子邮件中的一个超链接将密码编码为 HTTP GET 请求的查询字符串,它可能被代理服务器和 Web 服务器记录。最后,没有对截取
Web 会话的行为提供保护。不过,本文的目的不在于展示一个安全的 secret Santa 应用程序,并且我已经写了够多的文字,所以我不会进一步解决这个问题。注意不告诉圣诞老人您最隐藏的秘密,可能有坏人在偷听。
在一页中有多操作
如果查看完整的 secret Santa 应用程序,您就会注意到,认证页提供了对认证操作和密码重新发送操作的访问。将多个操作与一个页面关联的情况经常会出现,这个应用程序中的另一个例子是成员关系页,它让客户可以增加新的族成员或者删除现有的成员。
一种解决这个问题的方法是提供一个复合操作,它可以执行所有页操作(例如,同时对系统进行认证和重新发送密码电子邮件,或者同时增加和删除一位族成员)。然后
HTML 页可以通过使用,比如说隐藏的表单字段(例如,一个可以设置为
"add" 或者
"remove"
的
command 字段)提供对操作的不同方面的访问,或者操作可以区别按下的不同提交按钮(例如,见
org.merlin.santa.forms.FormImage
类)并相应地继续。
不过,这种方法有几个问题:首先,得到的操作在一个类中结合了通常在逻辑上不同的操作(例如,发送电子邮件和注册)。这违背了操作范例(paradigm)的一个目标,那就是可以为独立的操作开发简单、独立的
action 类。另一个问题是它使表单验证复杂了,例如,密码字段对于注册操作是必需的,但是对于密码重发操作则是不必要的。有可能编写条件验证表达式,但是它们很复杂并且当前在客户端没有得到很好的支持。
我为这个应用程序选择的另一种方法是,让不同操作(operation)保持在不同的操作(action)中,编写客户端代码使得它分别访问不同的操作,例如,通过将字段从核心
HTML 表单拷贝到一个会发送到另一个操作的隐藏表单中。如果一个页中不同的操作共享某些公共设置,那么就可以将它放到一个共享超类中,类似地,共享的表单字段可以放到一个共享的表单超类中。这同时提供了代码重用和操作简单化的好处。
不过,这种方法也不是完全没有问题,我认为它是这两者中更好的一种。有关为支持这种方法在客户端需要做的工作,请参阅成员关系表示页中的
remove() JavaScript 函数及相关表单。
类属族相关的操作
我们已经看过了一些自定义的 Structs 框架。在进入应用程序的表示方面之前,现在我还要快速地分析一个类属控制框架。
许多族相关的操作有一些共同点。在它们可以处理一个客户的请求之前 ―― 或者,在实际向客户显示信息之前 ―― 它们需要确保客户已经通过认证。此外,许多操作只有当系统在特定的状态下才是有效的。例如,如果一个
secret Santa 中止了,那么客户就应该不能添加或者删除成员,类似地,如果其状态还没有设置,那么它们就应该不能查询它。
清单 38 没有将这些工作在每一个操作中重复,而是给出了
FamilyAction 类,这个类代表其子类执行上述检查:它覆盖了
execute() 方法,并根据客户是否经过认证以及族的状态是什么来委派不同的方法。
如果在客户的 HTTP 会话中没有
"family" 项(即客户没有经过认证),那么这个类就使用
santaDisplay()
或者
santaAction() 方法,这两个方法都将客户转送到登录页(例如
/authenticate )。转送位置实际上是通过查询
Struts 配置中名字
"family-unauthenticated" 的内容得来的。
否则,客户就是经过认证的,这样我们需要检查
Family 的状态。所有使用这个类的操作都可以用管道分隔(|)的一系列有效状态进行配置(有关例子见
描述动作)。只有当
secret Santa 是其中一种状态时 操作才会执行。如果状态是无效的,那么客户就会自动转送到在 Struts 配置中名字
"family-state-
xyz"
(其中
xyz
是当前
Family 状态)下注册的位置。
如果客户实际上是认证过并且 secret Santa 是处于有效的状态,那么就执行适当的子类方法
familyDisplay()
或者
familyAction() 以执行这个操作。为了方便,向这个方法传递
FamilyLocal
引用以及其他操作参数。
在大多数情况下,从这个类派生出的操作所得到的表示页要显示有关当前
Family 的信息。为了支持这些页,在执行了子类操作后,这个类自动在请求结构中
"familyValue" 键下存储
Family 的值对象。因而表示页可以容易地访问这个值对象以构建它们的布局。
清单 38.
FamilyAction 类
package org.merlin.santa.actions.family;
import java.util.*;
import javax.ejb.*;
import javax.servlet.http.*;
import javax.naming.*;
import org.apache.struts.action.*;
import org.merlin.santa.*;
import org.merlin.santa.actions.*;
import org.merlin.santa.interfaces.*;
import org.merlin.santa.utils.*;
import org.merlin.santa.values.*;
import org.merlin.santa.servlets.*;
public abstract class FamilyAction
extends SantaAction {
public ActionForward
execute
(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
HttpSession session = request.getSession ();
FamilyLocal family = (FamilyLocal) session.getAttribute ("family");
if (family == null) {
return super.execute (mapping, form, request, response);
} else {
ActionForward forward;
String states = mapping.getParameter ();
if ((states != null) && !checkState (states, family)) {
forward = mapping.findForward ("family-state-" + family.getState ());
} else if ((form != null) && !isSubmission (request)) {
forward = familyDisplay (family, mapping, form, request, response);
} else {
try {
forward = familyAction (family, mapping, form, request, response);
} catch (SantaException ex) {
request.setAttribute ("familyValue", getFamilyValue (family));
throw ex;
}
}
request.setAttribute ("familyValue", getFamilyValue (family));
return forward;
}
}
private boolean
checkState
(String states, FamilyLocal family)
throws Exception {
String state = family.getState ();
boolean okay = false;
StringTokenizer tokens = new StringTokenizer (states, "|");
while (!okay && tokens.hasMoreTokens ())
okay = state.equals (tokens.nextToken ());
return okay;
}
protected ActionForward
santaDisplay
(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
return santaAction (mapping, form, request, response);
}
protected ActionForward
santaAction
(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
return mapping.findForward ("family-unauthenticated");
}
protected ActionForward
familyDisplay
(FamilyLocal family,
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
return super.santaDisplay (mapping, form, request, response);
}
protected abstract ActionForward
familyAction
(FamilyLocal family,
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception;
}
|
清单 39 展示了文件 meta/web/global-forward.xml,XDoclet 会自动把它合并到 struts-config.xml
文件中。这个文件是 Struts 的一组全局转发指令,secret Santa 应用程序也是在这里标识应该采取什么操作,比如客户试图在没有事先认证的情况下执行一个操作。
"family-unauthenticated"
项说明未认证的客户应当自动转发到
/authenticate 操作。
清单 39. Struts 全局转发
<global-forwards
type="org.apache.struts.action.ActionForward">
<forward
name="family-unauthenticated"
path="/authenticate.santa"
redirect="true" />
<forward
name="family-state-created"
path="/describe.santa"
redirect="true" />
<forward
name="family-state-activated"
path="/status.santa"
redirect="true" />
[...]
</global-forwards>
|
描述操作
最后,让我们快速看一下描述操作。secret Santa 注册者可以在这里指定对他的
Family 的描述。这个
操作引入了两个新功能:使用
FamilyAction 类以及使用动态的、纯 XML 脚本的表单。
清单 40 展示了描述操作的配置。表示页是 /WEB-INF/jsp/family/describe.jsp,它包含一个名为 familyDescribeForm
的表单。如果成功,就将客户转发给
/member 操作,它让用户编辑
Family 的成员。
parameter="created" 设置指示
FamilyAction 类,只有当
Family 处于
"created" 状态时才能执行这个操作。
清单 40. 描述操作配置
<action
path="/describe"
type="org.merlin.santa.actions.family.DescribeAction"
name="familyDescribeForm"
scope="request"
validate="true"
parameter="created"
input="/WEB-INF/jsp/family/describe.jsp">
<forward
name="success"
path="/member.santa"
redirect="true" />
</action> |
清单 41 展示了描述表单的声明,它取自文件 meta/web/struts-forms.xml,XDoclet 自动将它合并到 sturts-config.xml文件中。与前面的表单不同,没有对应于这个表单的
Java 类,这个表单其实是编写到 XML 脚本中的(它有一个类型为
String 的 meta/web/struts-forms.xml
字段),并且由
DynaSantaForm 类提供给应用程序。这个类提供了一个类似
Map
的 API,用于按名字设置和提取表单字段。使用动态的、脚本化的表单减少了一些开发工作,对于没有许多操作访问的表单类来说是很适合的。另一方面,有一个专门的
java 表单类,对于有许多字段或者经常反复使用的表单来说更为方便。
清单 41. 描述表单声明
<form-bean
name="familyDescribeForm"
type="org.merlin.santa.forms.DynaSantaForm">
<form-property name="description"
type="java.lang.String" />
</form-bean>
|
清单 42 展示了描述表单的验证配置,它取自文件 meta/web/validation-global.xml,XDoclet 自动将它合并到
struts-config.xml 文件中。
depends 属性指定
description
字段的验证指令,在这里,没有指定指令,这意味着描述字段是可选的 ―― 用户如果愿意可以让这个字段为空。
清单 42. 描述表单验证配置
<formset>
<form
name="familyDescribeForm">
<field
property="description"
depends="">
<arg0
key="form.family.describe.description" />
</field>
</form>
[...]
</formset>
|
清单 43 展示了实际的描述操作的实现。
当客户第一次请求这个操作时调用
familyDisplay() 方法,它用当前
Family
描述自动填充表单。注意这个字段是通过调用
dynaForm.set() 设置的,用名字指定字段。因为我们没有使用自定义表单类,所以我们在编译时不会验证这是否是有效的表单字段名,这是动态表单类的一个主要缺点。
当客户实际提交描述表单时调用
familyAction() 方法。这个方法只是从表单中提取描述(用基于名字的
get() 方法,并将结果造型为
String ),设置新
Family
描述,并返回
"success" 。
清单 43. 描述操作
package org.merlin.santa.actions.family;
import javax.ejb.*;
import javax.servlet.http.*;
import org.apache.struts.action.*;
import org.merlin.santa.*;
import org.merlin.santa.actions.*;
import org.merlin.santa.forms.*;
import org.merlin.santa.interfaces.*;
public class DescribeAction
extends FamilyAction {
protected ActionForward
familyDisplay
(FamilyLocal family,
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
DynaSantaForm dynaForm = (DynaSantaForm) form;
dynaForm.set ("description", family.getDescription ());
return super.familyDisplay (family, mapping, form, request, response);
}
protected ActionForward
familyAction
(FamilyLocal family,
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
DynaSantaForm dynaForm = (DynaSantaForm) form;
String description = (String) dynaForm.get ("description");
family.setDescription (description);
return mapping.findForward ("success");
}
}
|
清单 44 展示了
DynaSantaForm 类。这个类派生自
DynaValidatorForm
类,它提供了大量实现,并覆盖了
validate() 方法,就像
SantaForm
类一样,以便对非提交操作请求跳过验证。
清单 44.
DynaSantaForm 类
package org.merlin.santa.forms;
import javax.servlet.http.*;
import org.apache.struts.action.*;
import org.apache.struts.validator.*;
import org.merlin.santa.actions.*;
public class DynaSantaForm
extends DynaValidatorForm {
public ActionErrors
validate
(ActionMapping mapping, HttpServletRequest request) {
return SantaAction.isSubmission (request) ?
super.validate (mapping, request) : null;
}
}
|
结束语
好了,我们已经完成了建立 secret Santa 的三分之二了。在
第
1 部分,我们分析了 secret Santa 模型:封装了应用程序状态,并提供了它的一些业务方法的企业 bean。在这第 2 部分,我们讨论了如何用
Struts 实现 secret Santa 控制器:action 类响应用户请求、处理表单参数以及响应对应用程序模型做出的相应改变。最后,在
第
3 部分,我们将讨论如何用 JSP 技术和 JSTL 将实际的应用程序表示 ―― 即最终用户看到的内容 ―― 放到一起。
在独立的类中实现不同应用程序操作的 Struts 模型是非常有用的:每一个操作都是小的、自包含的,并且易于检查和测试。有共同需要的操作,如用户认证要求,可以容易地共享在一个公共超类中的实现,从而享受代码重用和容易维护的好处。Struts
提供的更多支持 ―― 如用 XML 脚本实现的工作流和自动表单验证 ―― 对于开发可靠的、可配置的和可使用的应用程序来说也是同样重要的。总而言之,我相信您也同意,在开发基于
Java 的 Web 应用程序时,这是您可以使用的一个非常棒的工具。
参考资料
关于作者  | |  | Merlin 是全球电子安全公司 Betrusted, Inc 的密码专家和首席技术讲师,作为不断增长的 J2EE 家族成员之一,在传统的随机编码技术(hat-based
technology)无法胜任时,他用 J2EE 开发了圣诞礼物问题的解决方案。他住在纽约州纽约市(一个非常美的城市,人们给它取了两次名字),可以通过
merlin@merlin.org
与他联系。
|
对本文的评价
|