IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Java technology | Web development  >

编写自己的 secret Santa Web 应用程序,第 3 部分: 视图

对工具、技术、设计和实现的分步指导

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

Merlin Hughes (merlin@merlin.org), 密码专家, Betrusted, Inc.

2004 年 1 月 01 日

在圣诞来临之际,圣诞老人的助手 Merlin Hughes ―― 他在现实生活中的另一身份是Java 开发人员 ―― 展示了一个基于 J2EE 的 secretSantaWeb 应用程序的设计和实现,并讨论了可以用来为这种应用程序的开发提供方便的工具和技术。本文(还有 第1 部分第 2 部分)提供了对如何用一些最新的工具和框架从头开始建立 J2EE 应用程序的一个全面综述,并详细说明了如何让这些不同的技术共同工作以得到最终结果。但是这几篇文章并不准备对每一个技术作详细分析,而是要为用J2EE 开发一个 Web 应用程序提供一个指南。这第三篇文章侧重于应用程序的视图方面,以及使用 JSP 技术、JSTL 和 JakartaStruts 来支持其开发。

在本系列的 第 1 部分,我介绍了 secret Santa 应用程序、实现它所使用的技术、以及封装其模型的企业 bean。在 第 2 部分,我介绍了实际响应客户请求、操纵应用程序模型以及调用展示的、基于 Struts 的控制器。作为这一系列的结束篇,我将分析用于提供其基于 Web 的表示 ―― 其视图方面 ―― 的类和 JSP 页。我们将首先简单讨论所涉及的 Web 端技术,与以往一样,有关这种技术的更详细的信息请参阅 参考资料。然后我们将讨论这个应用程序所使用的一些工具框架。最后我们将步入几个应用程序的表示页。

对技术的简要概述

在这一部分,我们将简单回顾在我们的讨论中所涉及的技术:JSP 技术和 JSTL。

JSP 技术

JavaServer Pages (JSP) 技术是一种 Web 技术,其中表示页(通常是 HTML)包含小的 Java 控制代码片段。这些 Java 代码可以提供像数据提取这样的逻辑服务,从而可以动态生成页面,同时仍然可以容易地针对不同的表示风格进行配置。清单 1 展示了一个显示请求 URI 和参数的简单 JSP 页:


清单 1. 一个基本的 JSP 页
<html>
  <head><title>Basic JSP</title></head>
  <body>
    Request URI: <%= request.getRequestURI () %><br>
<%
    java.util.Iterator entries = request.getParameterMap ().iterator (); 
    while (entries.hasNext ()) {
      java.util.Map.Entry entry = (java.util.Map.Entry) entries.next ();
%>
      <%= entry.getKey () %> = <%= entry.getValue () %><br>
<%
    }
%>
  </body>
</html>

清单 2 展示了这个 JSP 页的示例输出,这是将在一个 Web 浏览器中显示的 HTML:


清单 2. 基本 JSP 的输出
<html>
  <head><title>Basic JSP</title></head>
  <body>
    Request URI: /path/to/servlet<br>
      Host = jws.internal:13666<br>
      User-Agent = Mozilla/5.0 [...] Mozilla Firebird/0.6.1<br>
      [...]
  </body>
</html>

JSP 特别适合一个 MVC 设计的视图方面,使应用程序的表示可以与控制器逻辑分离,Web 设计者可以用标准工具利用在页面中嵌入的小段 JSP 代码生成 HTML。

通常,基于 servlet 的控制器逻辑将使用一个 EJB 模型处理客户的请求,然后利用一个 JSP 表示请求的结果。它很好地分离了应用程序设计的不同方面。

JSP 标准标签库

JSP 标准标签库(JSP Standard Tag Library JSTL)是一种 JSP 增强,它提供了类似 HTML 的标签,支持范围广泛的基本逻辑任务,包括迭代数据结构、访问 bean 字段,以及执行对文本的 HTML 格式编排。JSTL 使您可以从 JSP 源代码中去掉大部分原始 Java 代码,并以更精确和更具可读性的方式表达表示逻辑。清单 3 用 JSTL 标签取代原始 Java 代码重新编写了前面的简单 JSP:


清单 3. 一个使用 JSTL 的基本 JSP 页
<html>
  <head><title>Basic JSP</title></head>
  <body>
    Request URI: <c:out value="${request.requestURI}" /><br>
    <c:forEach var="entry" items="${request.parameterMap}">
      <c:out value="${entry.key} = ${entry.value}" /><br>
    </c:forEach>
  </body>
</html>

JSTL 表达式(例如, <c:out> 标签的 value 属性)是根据 JSTL 表达式语言(EL)进行判断的,它使用内省(introspection)和一些隐式对象提供对 JSP 页可能需要的几乎所有数据的访问。总之,在判断表达式 ${foo.bar} 时,使用内省定位 foo 对象的 getBar() 方法,调用这个方法并返回结果。在这里,调用 entry 变量的 getKey()getValue() 方法。

不要错过本系列的其余部分!

本系列的 第 1 部分详细说明了这个企业 bean 的设计和实现以及 XDoclet 是如何帮助它们的开发和部署的。

第 2 部分 详细说明了构成应用程序的操作和表单的、基于 Struts 的类的设计和实现。

还有许多第三方标签库,并且可以容易地编写自定义标签,它们可以提供现有标签不能提供的特定于应用程序的服务。在 secret Santa 应用程序中,我们使用自定义的标签,根据标准模板自动生成 HTML 图像按钮,并针对不同的目的重写 URL。





回页首


JSP 文本编码

既然对于相关的 Web 端技术有了一个简单的了解,我们将讨论在这个应用程序中使用的第一个可重用的工具框架:一个文本编码类层次结构。当一个 JSP 页将文本输出为 HTML 时,必须执行某些编码操作:小于号(<)字符必须替换为 < 和符号(&)字符必须替换为amp; 等。凭借 <c:out> 标签的 escapeXml 参数,JSTL 对这种任务提供了一些自动化支持。在设置了这个参数时(在默认情况下是设置的),在输出表达式中所有禁止的 XML 字符都自动被相应的字符引用所替换。类似地,在 HTML 页中 URL 的参数必须根据 URL 编码标准进行编码,这是由 <html:rewrite><c:url> 标签自动完成的。不过,这些只是可以为 JSP 提供帮助的诸多种文本编码中的两个,并且它们没有足够的灵活性以支持应用程序的所有需要。

JSP 编码框架

为了支持这个应用程序中的 JSP 页的编码需要,并提供一个灵活的文本编码框架,我选择利用 JSTL EL 对在映射数据结构中自动解除引用(dereference)的支持:如果一个 JSTL 表达式有 form ${foo[bar]} (也支持其他形式),并且 foo 变量是 java.util.Map 的一个实例,那么这个表达式就自动判断为 foo.get(bar) 。这就是说,对表达式 bar 进行判断并作为一个键以解除引用 foo 映射。

清单 4 展示了得到的 JSP 文本编码框架的超类:它扩展 java.util.HashMap 并覆盖了 get() 方法,从而返回子类 encode() 方法的结果。我也可以手工实现 java.util.Map 及其所有声明的方法,不过目前的实现更容易一些。


清单 4. Encoder 超类
package org.merlin.santa.encoders;
import java.util.HashMap;
public abstract class Encoder
    extends HashMap {
  protected Encoder
  () {
    super (1);
  }
  public Object
  get
  (Object o) {
    return encode (o);
  }
  /**
   * Encode the specified parameters.
   *
   * @param o The parameter to encode.
   *
   * @return The encoded value.
   */
  protected abstract String
  encode
  (Object o);
}

要在 JSP 页中使用文本编码器,必须首先创建编码器的一个实例并将它存储为一个页变量,然后在 JSTL 表达式中用这个变量解除引用所有需要编码的值。

清单 5 展示了显示一组用这个框架编码的名字的 JSP 代码的一部分。第一个标签导入 JSTL 标签库,下一个标签设置页上下文中的 xyzEnc 变量为新的 XYZEncoder ,然后一个 JSTL <c:forEach> 循环编码一系列的名字。这个简单紧凑的表达式 ${xyzEnc[name]} 自动调用在 XYZEncoder 类中的 encode() 实现。在这个例子中,编码器生成已经过 HTML 编码的输出,因而 <c:out> 标签的 escapeXml 参数必须设置为 false ,禁止由 JSTL 执行的普通 HTML 编码。


清单 5. 一个文本编码器
<%@
  taglib prefix="c" uri="/tags/jstl-core"
%><%
  pageContext.setAttribute ("xyzEnc", new org.xyz.encoders.XYZEncoder ());
%>
<html>
  [...]
  <c:forEach items="${names}" var="name">
    <c:out value="${xyzEnc[name]}" escapeXml="false" />
  </c:forEach>
  [...]
</html>

一个 HTML 编码器

清单 6 展示了一个非常基本的 HTML 编码器的源代码。这段代码非常简陋,但是却有高效率,这对于处理大量文本很重要。 encode() 方法取其参数的字符串值并用相应的转义字符替换小于( < )、大于( > )及和( & )字符。提供了一个 singleton instance 变量,这样所有 JSP 页都可以共享这个类的一个实例。


清单 6. 一个 HTML 编码器
package org.merlin.santa.encoders;
public class HTMLEncoder
    extends Encoder {
  public static final HTMLEncoder
  instance
  = new HTMLEncoder ();
  private static final String[]
  __escapes
  = new String[64];
  static {
    __escapes['<'] = "<";
    __escapes['>'] = ">";
    __escapes['&'] = "&";
  }
  protected String
  encode
  (Object o) {
    String s = (o == null) ? "" : o.toString ();
    StringBuffer r = new StringBuffer ();
    char[] chars = s.toCharArray ();
    int i = 0, j = 0;
    try {
      do {
        char c = chars[i ++];
        String escape = (c < 64) ? __escapes[c] : null;
        if (escape != null) {
          r.append (chars, j, i - j - 1).append (escape);
          j = i;
        }
      } while (true);
    } catch (IndexOutOfBoundsException ex) {
      r.append (chars, j, chars.length - j);
    }
    return r.toString ();
  }
}

显然,JSTL <c:out> 标签已经可以完成这种 HTML 编码任务,不过在某些情况下,在 JSTL 正常执行它之前就需要进行编码。

JavaScript 编码器

清单 7 展示了一个 JavaScript 编码器的源代码。它将一个值编码为适合于作为 JavaScript 字符串文本使用的形式,如将这个值嵌入到由 JSP 页生成的 JavaScript 片段中时。在这里,单引号(')、引号(")、反斜线(\)、回车以及换行字符必须加上反斜线字符前缀。


清单 7. 一个 JavaScript 编码器
package org.merlin.santa.encoders;
public class JavaScriptEncoder
    extends Encoder {
  public static final JavaScriptEncoder
  instance
  = new JavaScriptEncoder ();
  private static final String[]
  __escapes
  = new String[96];
  static {
    __escapes['\''] = "\\'";
    __escapes['"'] = "\\\"";
    __escapes['\\'] = "\\\\";
    __escapes['\r'] = "\\r";
    __escapes['\n'] = "\\n";
  }
  protected String
  encode
  (Object o) {
    String s = (o == null) ? "" : o.toString ();
    StringBuffer r = new StringBuffer ();
    char[] chars = s.toCharArray ();
    int i = 0, j = 0;
    try {
      do {
        char c = chars[i ++];
        String escape = (c < 96) ? __escapes[c] : null;
        if (escape != null) {
          r.append (chars, j, i - j - 1).append (escape);
          j = i;
        }
      } while (true);
    } catch (IndexOutOfBoundsException ex) {
      r.append (chars, j, chars.length - j);
    }
    return r.toString ();
  }
}

所有格编码器

清单 8 显示了对名词进行英语所有格格式编码的类的源代码,它根据这个名词的结尾,在名词后面加上一个引号或者引号和“s”。我知道 Strunk 规则反对这样做,不过,我不喜欢 “Achilles's heel”,也不喜欢“heel of Achilles”(不是针对个人),所以我在这个例子中没有遵守 Strunk。这个应用程序的非英语部署应当为这个特定的任务选择特定于区域的编码器。


清单 8. 一个所有格编码器
package org.merlin.santa.encoders;
public class PossessionEncoder
    extends Encoder {
  public static final PossessionEncoder
  instance
  = new PossessionEncoder ();
  protected String
  encode
  (Object o) {
    String s = (o == null) ? "" : o.toString (), suffix;
    if (s.endsWith ("s") || s.endsWith ("ce") || s.endsWith ("x")) {
      suffix = "'";
    } else {
      suffix = "'s";
    }
    return s + suffix;
  }
}

堆栈跟踪编码器

清单 9 展示了一个对异常的堆栈跟踪进行编码的类,JSP 页可以用它显示在请求处理过程中出现的错误的细节。 encode() 方法将堆栈跟踪转储到 StringWriter 并返回得到的字符串,没有进行内部 HTML 转义,因为已经有其他机制可以完成这项工作。


清单 9. 一个堆栈跟踪编码器
package org.merlin.santa.encoders;
import java.io.PrintWriter;
import java.io.StringWriter;
public class StackTraceEncoder
    extends Encoder {
  public static final StackTraceEncoder
  instance
  = new StackTraceEncoder ();
  protected String
  encode
  (Object o) {
    if (!(o instanceof Throwable))
      return "Not an exception: " + o;
    Throwable throwable = (Throwable) o;
    StringWriter stringWriter = new StringWriter ();
    throwable.printStackTrace (new PrintWriter (stringWriter));
    return stringWriter.toString ();
  }
}

URL 编码器

清单 10 显示了一个对值进行 URL 编码的类,它只是使用 java.net.URLEncoder 类,使用的是 UTF-8 编码。


清单 10. 一个 URL 编码器
package org.merlin.santa.encoders;
public class URLEncoder
    extends Encoder {
  public static final URLEncoder
  instance
  = new URLEncoder ();
  protected String
  encode
  (Object o) {
    String s = (o == null) ? "" : o.toString ();
    try {
      return java.net.URLEncoder.encode (s, "UTF-8");
    } catch (java.io.UnsupportedEncodingException ex) {
      return s;
    }
  }
}

像前面一样,它重复了一些现有的标签可以完成的编码工作。不过同样,这个类可以在现有标签不支持的情况下使用,例如,不重写一个 URL 而对它进行编码。

垂直 HTML 编码器

清单 11 展示了进行垂直 HTML 编码的类的源代码。代码对提供的字符串进行 HTML 编码、在每一个字符之间插入 <br> 换行,比如,将 “Job” 转换为
J
o
b

我们将用垂直 HTML 生成 secret Santa 应用程序中表的列标题。


清单 11. 一个垂直 HTML 编码器
package org.merlin.santa.encoders;
public class VHTMLEncoder
    extends Encoder {
  public static final VHTMLEncoder
  instance
  = new VHTMLEncoder ();
  protected String
  encode
  (Object o) {
    String s = (o == null) ? "" : o.toString ();
    StringBuffer r = new StringBuffer ();
    for (int i = 0, n = s.length (); i < n; ++ i) {
      if (i > 0)
        r.append ("<br>");
      char c = s.charAt (i);
      if (c == '<') r.append ("<");
      else if (c  == '>') r.append (">");
      else if (c == '&') r.append ("&");
      else if (c == ' ') r.append ("&nbsp;");
      else r.append (c);
    }
    return r.toString ();
  }
}

使用这个编码框架显示一个 Web 应用程序异常

一般来说,一个 Web 应用程序应当从不向最终用户显示异常,它降低了使用性并对安全性有不利影响。不过,我常常发现在应用程序开发和测试阶段,这样一个功能是相当有用的。

清单 12 展示了一个 JSP 片段,它使用我们刚刚讨论过的一些编码器在一个弹出窗口显示异常堆栈跟踪。这个异常存储在请求范围变量 exception 中,JSP 页显示异常细节消息,后面一行代码调用 JavaScript details() 函数来显示堆栈跟踪。

JavaScript details() 函数使用 JSTL 表达式 ${jsEnc[htmlEnc[traceEnc[requestScope.exception]]]} 在一个新窗口显示堆栈跟踪。这个表达式生成包含可以写入窗口的 HTML 代码的 JavaScript 字符串文字。这个表达式实际上用到三个编码器: traceEnc 将异常堆栈跟踪编码为字符串、然后 htmlEnc 将它编码为 HTML 以在弹出窗口中显示、而 jsEnc 将结果编码为适合作为 JavaScript 字符串文字使用的格式。JSTL EL 的灵活性使我们可以容易地访问这个编码框架。


清单 12. 用这个编码框架显示 Web 应用程序异常
<%@
  taglib prefix="c" uri="/tags/jstl-core"
%><%
  pageContext.setAttribute 
    ("traceEnc", org.merlin.santa.encoders.StackTraceEncoder.instance);
  pageContext.setAttribute 
    ("htmlEnc", org.merlin.santa.encoders.HTMLEncoder.instance);
  pageContext.setAttribute 
    ("jsEnc", org.merlin.santa.encoders.JavaScriptEncoder.instance);
%>
[...]
<blockquote class="error">
  <c:out value="${requestScope.exception.message}" />
  <small>(<a href="javascript:details()">details</a>)</small>
  <script language="javascript">
    <!--
    function details () {
      var win = window.open ("", null, "height=480,width=640");
      win.document.open ();
      win.document.write 
        ("<html><head><title>exception details</title></head><body><pre>"
        + "<c:out value="${jsEnc[htmlEnc[traceEnc[requestScope.exception]]]}" 
              escapeXml="false" />"
        + "</pre></body></html>");
      win.document.close ();
    }
    // -->
  </script>
</blockquote>

在本文的 后面 展示了这个 JSP 片段的实际使用。





回页首


逐步介绍表示

我们现在将逐步介绍这个 secret Santa 应用程序的表示层,它由生成 Web 页及电子邮件消息的 JSP 页提供。这些页面使用 JSTL、Struts 和自定义 JSP 标签以及 Struts 的 JavaScript 表单验证能力。我们将只讨论其中的几个页面,其余的页使用的是同样的技巧和技术。

欢迎页

在一个 Web 应用程序中,欢迎页是显示给客户的第一页。这一页是作为对应用程序的“主页”的请求的响应而显示的。对于性能有严格要求的 Web 应用程序可能会考虑使用静态的欢迎页,或者使用缓存的动态欢迎页。这个应用程序没有这种要求,不过,它有 J2EE 1.4 下欢迎页不能是一个匹配模式的 servlet 这一限制,即将欢迎页配置为 /index.santa 不起作用。为了避免这个问题,用一个简单的公共 index.jsp 页将客户立即引导到 /index.santa。

清单 13 展示了欢迎文件声明。这是源代码发布中的文件 meta/web/welcomefiles.xml ,它由 XDoclet 自动合并到最终的 web.xml 部署描述符中。它将 index.jsp 标识为当客户访问 Web 应用程序的上下文根时(例如,当他们访问 http://www.north-pole.int/santa/ 时)所显示的那一页。


清单 13. 欢迎文件声明
<welcome-file-list>
  <welcome-file>
    index.jsp
  </welcome-file>
</welcome-file-list>

下面是欢迎页本身:

<% response.sendRedirect ("index.santa"); %>

这是源代码发布中的 web/index.jsp 文件。 web/ 目录中的所有文件(如 web/imanges/*)都打包为 Web 应用程序的 WAR 文件中的一个公共文档。这个 JSP 页只是向客户发送一个到 index.santa 的 HTTP 重定向。您可能说这是不必要的,可以将主 index 页放在这里。不过,重定向可以维护 Web 应用程序的页面的统一性,并且它使真正的 index 页可以与其他 JSP 页放在同一位置,下一步的 /index 操作可能会完成一些有用的工作。而且,我相信在下一版本的 J2EE 中,可能会正式支持欢迎文件规范 index.santa,从而不需要再绕这个小弯子了。

下面是 /index 操作声明:

<action
   path="/index"
   forward="/WEB-INF/jsp/index.jsp" />

这个操作只是显示 /WEB-INF/jsp/index.jsp 页。注意在这里“forward”不是一个 HTTP 转发,它是一个内部 Web 应用程序转发。JSP 页作为对 URL http://www.north-pole.int/santa/index.santa 的响应直接显示给客户。

清单 14 展示了通过 /index 操作显示给用户的实际 JSP。注意这个 JSP 页以及所有其他 JSP 页是部署在 /WEB-INF/ 目录中的。这很重要,因为客户请求不能直接使用在这个目录中的文件。这意味着,比如,客户不能发送对 http://www.north-pole.int/santa/WEB-INF/index.jsp 的请求以直接访问这个或者任何其他 JSP 页(一个潜在的安全性或者错误风险)。相反,它必须通过 Struts 操作,这个操作在内部访问 JSP 页并将结果展示给客户。J2EE 平台目前不支持这种功能(内部访问 /WEB-INF/ 中的页面),需要重新组织 Struts 配置文件并做不多的改变以部署在这种系统上(不需要编码或者 JSP 改变)。

实际的 JSP 页是非常直观的。第一个标记设置页的标题,下一个标记加入 header JSP(它使用标题,参见 下面 的讨论),然后是一些 HTML 布局,最后,加入 footer JSP(参见 下面 对它的讨论)。


清单 14. /WEB-INF/jsp/index.jsp 页
<%
  String title = null;
%><%@
  include file="header.jsp"
%>
<p>
  <santa:button action="/learn" src="/images/learnMore.gif"
     border="0" hspace="32" alt="learn more" />
</p>
<p>
  <santa:button action="/register" src="/images/signUp.gif"
     border="0" hspace="32" alt="sign up" />
</p>
<%@
  include file="footer.jsp"
%>

图 1 显示了这个 JSP 的结果。在这个特定的应用程序中,header 和 footer JSP 页提供了基本的页面布局,各个页只需要提供它们相应的内容。其他工具,如 Jakarta Tiles,对以这种方式组织 JSP 页提供了更多支持。


图 1. 欢迎页
欢迎页

index 页所使用的 <santa:button> 标签是一个自定义标签,它生成简单的三状态按钮,在鼠标悬停在它们上面时会突出显示、在单击时会再次改变其状态。目前,不同的图像是用一个外部绘图程序手工创建的,可以提供一个动态生成并缓存这些图像的 servlet。我在这里不给出这个标记的详细代码,在源代码发布中它是在 src/web/org/merlin/santa/tags/ButtonTag.java 中提供的,在这里它生成的 HTML 有如清单 15 所示的形式。


清单 15. <santa:button> 标签输出
<a href="/learn.santa"><img src="/images/learnMore.gif"
  alt="learn more" border="0" hspace="32"
  onmouseover="this.src='/images/learnMore_hi.gif'"
  onmousedown="this.src='/images/learnMore_lo.gif'"
  onmouseout="this.src='/images/learnMore.gif'" /></a>

在这里值得一提的是我没有直接解决您将看到的 JSP 页的翻译问题。可以完全基于资源文件消息键、使用 <bean:message> 标签及其同伴编写页。不过,结果通常是不易使用的,并且这种技术不能普遍适用。我宁愿写出独立的翻译过的 JSP 页并让容器选择适合的客户展示。在应用程序代码中消息键最有用,因为在这里翻译器是不能改变文本的。

加入 header.jsp

每一个 HTML JSP 页(即每一个 Web 页和 HTML 电子邮件页)都以加入这个 JSP header 开始,它设置 JSP 状态并初始化 HTML 页布局(请参阅清单 16)。首先,导入 JSTL Struts 和应用程序标记库,然后在页上下文中安装所支持的不同文本编码器,然后开始 HTML 页。HTML 包括一个由 title 变量给出的标题,每一个 JSP 页在加入这个 header、一个样式表和打开 HTML 正文之前必须设置它。注意打开图像是用 <santa:imgurl> 标签记引用的,这个标签自动重写指定的 URL 以加入 Web 应用程序上下文。例如,如果这个应用程序部署在 URL /web-apps/santa/ 中,那么得到的 HTML 页会有形式为 /web-apps/santa/images/xxx.gif 的图像 URL。 图 1显示了得到的页面布局。


清单 16. 加入 header.jsp
<%@
  taglib prefix="c" uri="/tags/jstl-core"
%><%@
  taglib prefix="html" uri="/tags/struts-html"
%><%@
  taglib prefix="logic" uri="/tags/struts-logic"
%><%@
  taglib prefix="santa" uri="/tags/santa"
%><%
  pageContext.setAttribute 
    ("traceEnc", org.merlin.santa.encoders.StackTraceEncoder.instance);
  pageContext.setAttribute 
    ("vhtmlEnc", org.merlin.santa.encoders.VHTMLEncoder.instance);
  pageContext.setAttribute 
    ("htmlEnc", org.merlin.santa.encoders.HTMLEncoder.instance);
  pageContext.setAttribute 
    ("urlEnc", org.merlin.santa.encoders.URLEncoder.instance);
  pageContext.setAttribute 
    ("jsEnc", org.merlin.santa.encoders.JavaScriptEncoder.instance);
  pageContext.setAttribute 
    ("sEnc", org.merlin.santa.encoders.PossessionEncoder.instance);
%><html>
  <head>
    <title>Psychic Secret Santa<%= 
      (title != null) ? " - " + title : "" %></title>
    <meta name="description" content="Psychic Secret Santa" />
    <meta name="keywords" content="psychic, secret, santa" />
    <style type="text/css">
      body {
        margin: 2em 1em 2em 2em;
        font-family: sans-serif;
        color: #FFC;
        background: black;
        text-align: justify;
      }
      a:link { color: #FF3; text-decoration: none; }
      a:visited { color: #F00; text-decoration: none; }
      a:active { color: #C00; text-decoration: none; }
      p.first:first-letter { font: 140% sans-serif; }
      p.error { color: #F00; }
      p.copyright { font: 70% sans-serif; }
      table.grid { border-collapse: collapse; }
      td.grid, th.grid { 
        border-bottom: 1px solid #554; 
        border-right: 1px solid #554; }
      td.gridx, th.gridx { 
        border-top: 1px solid #554; 
        border-left: 1px solid #554; }
      td.gridv, th.gridv { 
        border-bottom: 1px solid #554; 
        border-right: 1px solid #554; line-height: 80%; }
    </style>
  </head>
  <body bgcolor="#000" text="#ffffcc" link="#ffff33" vlink="#ff0000" 
     alink="#cc0000">
    <img src="<santa:imgurl page="/images/psychicSecretSanta1.gif" />" 
       width="533" height="76" border="0" align="left">&nbsp;
    <br clear="left"><img
       src="<santa:imgurl page="/images/psychicSecretSanta2.gif" />" 
       width="180" height="188" border="0" align="left">

重用一个 header 页以定义整个 Web 应用程序的布局极大地简化了应用程序的维护。对这个文件的改变可以扩散到所有 Web 页和电子邮件消息中。

清单 17 展示了 ImgURLTag 类,它实现了 <santa:imgurl> 标签。这段代码接受一个 page 参数,它再输出这个参数并在前面加上 Web 应用程序上下文路径。它与 JSTL <c:url> 标签或者 Struts <html:rewrite> 标签的区别是如果禁用了 cookie,那么其他标签会将会话 ID 信息附加到得到的 URI 后面(例如,生成 URL /santa/images/image.gif;jsessionid=ABCDEFGHIJKL)。这个会话 ID 信息会妨碍使用外部 Web 服务器提供静态图像。


清单 17. 一个图像 URL 重写标签
package org.merlin.santa.tags;
import java.io.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
import javax.servlet.jsp.tagext.*;
public class ImgURLTag
    extends TagSupport {
  private String
  _page;
  public void
  setPage
  (String page) {
    _page = page;
  }
  public String
  getPage
  () {
    return _page;
  }
  
  public int
  doEndTag
  ()
  throws JspException {
    try {
      JspWriter out = pageContext.getOut ();
      out.print (rewriteImgURL (_page, pageContext));
      return EVAL_PAGE;
    } catch (IOException ex) {
      throw new JspException ("ImgURLTag error", ex);
    }
  }
  public static String
  rewriteImgURL
  (String page, PageContext pageContext) {
    HttpServletRequest request =
      (HttpServletRequest) pageContext.getRequest ();
    return request.getContextPath () + page;
  }
}

这个标签实际上等于 JSTL 表达式 <c:out value="${request.contextPath}/images/xxx.gif}" escapeXml="false" /> 。不过,与对每一个图像重复这个表达式相比,使用自定义标签更优雅、更有表现力、并更容易更新。

清单 18 显示了这个标签的 TLD 描述(摘自文件 web-inf/tld/santa.tld)。


清单 18. 图像 URL 标签的 .tld 描述
  <tag>
    <name>imgurl</name>
    <tagclass>org.merlin.santa.tags.ImgURLTag</tagclass>
    <bodycontent>empty</bodycontent>
    <attribute>
      <name>page</name>
      <required>true</required>
      <rtexprvalue>true</rtexprvalue>
    </attribute>
  </tag>

由 header 页导入的这个标签库 URI 的绑定是在 meta/web/taglibs.xml 文件中给出的,XDoclet 会自动将它结合到 web.xml 部署描述符中。清单 19 显示了这个文件的一个片段。每一个 <taglib> 元素告诉 JSP 容器由一个 URI (如 /tags/santa)标识的特定标签库的定义是在一个特定 TLD 文件(如,/WEB-INF/tld/santa.tld)提供的。然后 TLD 文件告诉 JSP 容器这个库支持什么标签、它们的参数是什么,以及哪些类实现了这些标签。


清单 19. taglibs.xml 描述符
<taglib>
  <taglib-uri>
    /tags/jstl-core
  </taglib-uri>
  <taglib-location>
    /WEB-INF/tld/c.tld
  </taglib-location>
</taglib>
[...]
<taglib>
  <taglib-uri>
    /tags/santa
  </taglib-uri>
  <taglib-location>
    /WEB-INF/tld/santa.tld
  </taglib-location>
</taglib>

加入 footer.jsp

每一个 HTML JSP 在结束时都加入这个 JSP footer,它显示一个版权信息并关闭由 header 打开的 HTML 页(您可以在上面 图 1中看到它的效果)。


清单 20. 加入 footer.jsp
    <br clear="left" />
    <p class="copyright">
      Copyright &copy; 2002-2003 <a href="http://merlin.org/">The Merlin
      Foundation for Psychic Freedom</a>; All Rights Reserved.
    </p>
  </body>
</html>

注册页

欢迎页和伴随它的所有基础设施是相当简单的。我们要讨论的下一个页面 ―― 注册页 ―― 结合了这个基础设施与 Struts 表单的使用,以使让客户可以在 secret Santa 应用程序上注册一个族。

清单 21 显示了注册页。这一页面的基本布局与 index 页一样:它设置一个标题、加入标准 header、提供 HTML 展示(包括一个填写表单)、加入标准 footer。此外,这一页加入 messages.jsp 文件(在 这里 讨论,它显示了从上一个客户请求得到的所有错误或者消息。例如,如果客户试图注册一个已经存在的姓,那么会向客户重新显示带有适当的错误消息的注册页)。

这一页对表单没有使用标准 HTML 标签,相反,用 Struts <html:form><html:text> 标签声明表单及其字段,而在 index 页中使用的 <santa:button> 标签用于提交按钮。使用这个 Struts 标签很重要,因为它意味着 Struts 可以自动填入字段的默认值,例如,如果出现错误,表单将重新显示并带有以前输入的值。 <html:form> 标签的 focus 属性标识哪个表单字段应该自动取得焦点,不同的 property 将匹配在相应的 Struts 表单类中的字段。标签 <html:javascript> 自动生成 JavaScript 验证方法 validateFamilyRegisterForm(),formName 属性标识表单需要进行验证。


清单 21. /WEB-INF/jsp/family/register.jsp 页
<%
  String title = "Register";
%><%@
  include file="../header.jsp"
%>
<p class="first">
  To create a psychic secret Santa you must provide a family name
  and an email address which will be used to confirm the validity
  of your registration. A password will be sent to this email
  address, allowing you to continue with the registration.
</p>
<%@
  include file="../messages.jsp"
%>
<p>
  <table border="0">
    <html:form action="/register" focus="name"
       onsubmit="return validateFamilyRegisterForm(this);">
      <tr>
        <th align="right">Family&nbsp;name:&nbsp;</th>
        <td><html:text property="name" /></td>
        <td><em>(e.g: Jebus' Family)</em></td>
      </tr>
      <tr>
        <th align="right">Your&nbsp;email&nbsp;address:&nbsp;</th>
        <td><html:text property="email" /></td>
        <td><em>(e.g: jebus@merlin.org)</em></td>
      </tr>
      <tr>
        <td align="right" colspan="3">
          <santa:button property="register" src="/images/register.gif"
             alt="register" />
        </td>
      </tr>
    </html:form>
  </table>
</p>
<html:javascript formName="familyRegisterForm" />
<%@
  include file="../footer.jsp"
%>

图 2 显示了这个 JSP 的结果、以及当试注册一个无效的电子邮件地址时,由验证方法生成的 JavaScript 错误。


图 2. 注册页
注册页

加入 messages.jsp

清单 22 展示了被大多数其他应用程序页包括、并用于显示从客户的请求得到的所有消息或者错误的 JSP 页。 <logic:messagesPresent> 标签只在要显示特定类型的 Struts 消息时才显示其内容。 message 属性定义了消息类型:如果为 true ,那么就考虑正常消息,否则就考虑错误消息。在 <logic:messagesPresent> 标签中, <html:message> 标签枚举并显示相应类型的每一个消息。因而在下面 JSP 页中,第一部分显示所有正常应用程序消息,第二部分所有显示错误。最后, <c:if> 部分(实际的 JSP 代码请参阅 清单 12)显示发生的所有异常的细节。


清单 22. 加入 messages.jsp
<logic:messagesPresent message="true">
  <p>
    <html:messages id="message" message="true">
      <c:out value="${message}" />
    </html:messages>
  </p>
</logic:messagesPresent>
<logic:messagesPresent>
  <p>
    There was a problem with your request:
  </p>
  <blockquote class="error">
    <html:messages id="error">
      <li><c:out value="${error}" /></li>
    </html:messages>
  </blockquote>
  <c:if test="${not empty requestScope.exception}">
    [...]
  </c:if>
  <p>
    Please correct the problem and try again.
  </p>
</logic:messagesPresent>

图 3 显示出现非预期异常时,加入 message JSP 的效果以及弹出的堆栈跟踪窗口。


图 3. 显示一个非预期异常
非预期异常

族成员页

我们要分析的最后一个 Web 页是族成员页。secret Santa 注册者可以在这里添加和删除族成员。清单 23 展示了 JSP 代码,它建立在我们到目前为止讨论过的所有技术的基础上,并对 JSTL 迭代标签加入了一些变化。

这个页面首先做的事件之一是用 <c:set> 标签将值 ${requestScope.familyValue.santaList} 赋予名为 santas 的页上下文变量。它的效果是调用存储在请求数据结构中的 FamilyValuegetSantaList() 方法,并将结果存储为名为 santas 的本地变量。这个变量将包含这个 secret Santa 族的排序过的成员列表,可以在页面中的其他地方有效地使用这个变量。显示这一页的 FamilyAction 类将 FamilyValue 对象存储在请求对象中。

然后这一页面显示一个添加新族成员的表单和一个列出当前族成员的表。只有当 Santa 列表不是空的( ${not empty santas} )时候这个表才会显示,而对每一位成员都提供了一个“删除”链接。每一个链接调用一个 JavaScript 方法 remove(0)remove(1) 等等。在 <c:forEach> 循环中,属性 varStatus="ptr" 意味着在循环中会有一个可访问的本地变量,它提供了一种获得有关当前循环索引的信息的方法。例如,像 isFirst() isLast() (可作为 ${ptr.first} ${ptr.last} 访问)这样的方法表明当前项是否是列表中的第一项或者最后一项,而 getIndex() getCount() (分别为 ${ptr.index}${ptr.count} )方法提供当前项的 0-offset 和 1-offset 索引。这些索引属性对于编排数据格式以及提取有关列表的信息是非常有用的。例如,JSTL 没有提供直接确定列表长度的方法( size() 方法不符合 bean 命名规范,所以不能以 ${santas.size} 使用它)。为了解决这个问题,我们可以在循环的每一次迭代中(或者最后)将值 ${ptr.count} 赋予 size 变量。在循环完成时,这个变量就将包含列表的真正大小。在这个例子中使用了这个技巧,以便于在注册了至少三个族成员后才显示“next”按钮。

这一页面的最后一部分与一个用于删除一位族成员 /dismember 操作的隐藏表单有关。JavaScript remove() 方法在一个 santas 数组(由一个 <c:forEach> 循环填充)中查询成员名、设置相应的隐藏表单字段、并提交表单以删除这个 Santa。


清单 23. /WEB-INF/jsp/family/member.jsp 页
<%
  String title = "Members";
%><%@
  include file="../header.jsp"
%>
<p class="first">
  Please add all your secret Santas; enter each name and email
  address and hit the <strong>add</strong> button.
  When you are finished, hit the <strong>next</strong> button.
</p>
<p>
  Don't forget to add yourself if appropriate!
</p>
<%@
  include file="../messages.jsp"
%>
<c:set var="santas" value="${requestScope.familyValue.santaList}" />
<p>
  <table>
    <html:form action="/member" focus="name"
       onsubmit="return validateFamilyMemberForm(this);">
      <tr>
        <td>
          <table>
            <tr>
              <th align="right">Name:&nbsp;</th>
              <td><html:text property="name" /></td>
              <td><em>(e.g: Jubas)</em></td>
            </tr>
            <tr>
              <th align="right">Email&nbsp;address:&nbsp;</th>
              <td valign="top"><html:text property="email" /></td>
              <td><em>(e.g: jubas@merlin.org)</em></td>
            </tr>
            <tr>
              <td colspan="3" align="right">
                <santa:button property="add" src="/images/add.gif"
                   alt="add" />
              </td>
            </tr>
            <c:if test="${not empty santas}">
        <tr>
          <td colspan="3">
            <table class="grid">
              <tr>
                <th class="grid" align="left">Name</th>
                <th class="grid" align="left">Email address</th>
                <th>&nbsp;</th>
              </tr>
              <c:forEach items="${santas}" var="santa" varStatus="ptr">
                <tr>
                  <td<c:if test="${not ptr.last}"> class="grid"</c:if>>
                    <c:out value="${santa.name}" />
                  </td>
                  <td class="gridx">
                    <tt><c:out value="${santa.emailAddress}" /></tt>
                  </td>
                  <td class="gridx">
                    <a href="javascript:remove(<c:out value="${ptr.index}"
                      />)"><font color="green" size="-1">remove</font></a>
                  </td>
                </tr>
                <c:set var="size" value="${ptr.count}" />
              </c:forEach>
            </table>
          </td>
        </tr>
            </c:if>
          </table>
        </td>
      </tr>
    </html:form>
    <c:if test="${size > 2}">
      <tr>
        <td colspan="3" align="right">
          <santa:button action="/partners" src="/images/next.gif"
             border="0" alt="next" />
        </td>
      </tr>
    </c:if>
  </table>
</p>
<html:javascript formName="familyMemberForm" />
<html:form action="/dismember">
  <input type="hidden" name="name" />
</html:form>
<script language="javascript">
  <!--
  var santas = new Array
    (<c:forEach items="${santas}" var="santa" varStatus="ptr">
       "<c:out value="${jsEnc[santa.name]}" escapeXml="false" />"
       <c:if test="${not ptr.last}">,</c:if>
     </c:forEach>);
  function remove (i) {
    document.familyDismemberForm.name.value = santas[i];
    document.familyDismemberForm.submit ();
  }
  // -->
</script>
<%@
  include file="../footer.jsp"
%>

图 4 显示了这个 JSP 页的结果。


图 4. 成员页
成员页

HTML 注册电子邮件

最后,让我们简单分析生成其电子邮件消息的页来结束这个 Web 应用程序的表示过程。

清单 24 显示生成 HTML 注册电子邮件的 JSP 页,它用于验证族电子邮件地址确实是有效的。这一页面完全没有什么特别的,它使用我们已经见过的框架以及普通 JSTL、Struts 和 Santa JSP 标签生成要显示的 HTML。唯一的特定于电子邮件的功能是使用 <santa:emailurl> 标签提供完全限定的 URI。这通常是很有好处的,这意味着可以使用与核心 Web 应用程序的展示完全一样的方法和同样的工具开发电子邮件消息,并且 Web 应用程序的电子邮件支持框架将自动把它转换为 MIME 消息。


清单 24. /WEB-INF/jsp/email/register.jsp 页
<%
  String title = "Registration information";
%><%@
  include file="../header.jsp"
%>
<p class="first">
  A Psychic Secret Santa has been created for your family,
  <strong><c:out value="${requestScope.familyValue.name}" /></strong>.
</p>
<p>
  Please follow the directions in this e-mail to continue
  your registration.
</p>
<p>
  To log in automatically, simply
  <a href="<santa:emailurl action="/authenticate" /><c:out
     value="?name=${urlEnc[requestScope.familyValue.name]}"
     escapeXml="false" /><c:out
     value="&password=${urlEnc[requestScope.familyValue.password]}"
     escapeXml="false" />">click here</a>.
</p>
<p>
  Alternatively, you can log in manually by pointing your
  browser at the following URL and entering the family
  name and password below:
</p>
<p>
  <table class="grid" hspace="16">
    <tr>
      <th align="right" class="grid">URL:&nbsp;</th>
      <td align="left"><a href="<santa:emailurl action="/authenticate"
	 />"><santa:emailurl action="/authenticate" /></a></td>
    </tr>
    <tr>
      <th align="right" class="grid">Family&nbsp;name:&nbsp;</th>
      <td align="left" class="gridx"><c:out
	 value="${requestScope.familyValue.name}" /></td>
    </tr>
    <tr>
      <th align="right">Password:&nbsp;</th>
      <td align="left" class="gridx"><c:out
	 value="${requestScope.familyValue.password}" /></td>
    </tr>
  </table>
</p>
<p>
  If you did not set up a Psychic Secret Santa then please
  ignore this message and we will not contact you again.
</p>
<%@
  include file="../footer.jsp"
%>

图5 显示了得到的 HTML 电子邮件消息。


图 5. HTML 注册电子邮件
HTML 注册电子邮件

纯文本注册电子邮件

清单 25 展示了生成纯文本注册电子邮件的 JSP 页。这一页面将不继承 Web 页框架的任何表示,相反,它是一个非常直观的文本页,使用普通的 JSP 标签生成所需要的输出。Web 应用程序自动提取第一行做为电子邮件的主题并压缩连续的空行,使 JSP 标签可以整齐地展示。


清单 25. /WEB-INF/jsp/email/register.jsp 页
<%@
  taglib prefix="c" uri="/tags/jstl-core"
%><%@
  taglib prefix="santa" uri="/tags/santa"
%><%
  pageContext.setAttribute
    ("urlEnc", new org.merlin.santa.encoders.URLEncoder ());
%>
Subject: Psychic Secret Santa registration for <c:out value=
    "${requestScope.familyValue.name}" escapeXml="false" />
A Psychic Secret Santa has been created for your family,
<c:out value= "${requestScope.familyValue.name}" escapeXml="false" />.
Please follow the directions in this e-mail to continue
your registration.
To log in automatically, simply point your browser at
the following URL:
  <santa:emailurl action="/authenticate" /><c:out
     value="?name=${urlEnc[requestScope.familyValue.name]}"
     escapeXml="false" /><c:out
     value="&password=${urlEnc[requestScope.familyValue.password]}"
     escapeXml="false" />
Alternatively, you can log in manually by pointing your
browser at the following URL and entering the family
name and password below:
  URL: <santa:emailurl action="/authenticate" />
  Family name: <c:out value="${requestScope.familyValue.name}" 
                  escapeXml="false" />
  Password: <c:out value="${requestScope.familyValue.password}" 
               escapeXml="false" />
If you did not set up a Psychic Secret Santa then please
ignore this message and we will not contact you again.
-- 
Copyright (c) 2002-2003 The Merlin Foundation for Psychic Freedom
All Rights Reserved                            http://merlin.org/

图 6 显示了得到的纯文本电子邮件消息。


图 6. 纯文本电子邮件消息
纯文本电子邮件消息

在提供的源代码中,HTML 和 纯文本电子邮件 JSP 页的 username/password 部分实际上分离到了一个单独的文件中,使展示登录信息的不同电子邮件 JSP 页加入可以它们。

在这篇文章中讨论这个应用程序的所有 JSP 页是不可能的,我的篇幅只能让我选择一小部分。如果您希望看到更多有用的 JSP 技术,我建议您查看 web-inf/jsp/family/partners.jsp (以及相关的表单和操作类)。它展示了垂直文本编码和 Struts 支持带有任意数量的选择框的表单的能力 ―― 在这里,一个 n * n 网格表示所有可能的伙伴关系。另一个让人感兴趣的页面是 web-inf/jsp/family/activate.jsp,它使用 JSP 注释以编排 JSP 标签的格式,使输出不再有大量的空格,并显示了如何测试数据集合中的一个旗标。





回页首


编译和部署 Web 应用程序

最后,让我们分析 secret Santa 应用程序的编译环境:源代码结构、库和编译脚本。我用 Ant 作为编译工具,它与 XDoclet 完全集成、提供了编译、打包并部署一个 Web 应用程序需要的所有支持、并且总的来说使开发更容易了。

源代码结构

EJB 组件、Web 组件和共享工具库的源代码文件都是分开的,而不同的支持文件是根据它们的使用和类型进行安排的:

License.txt
LGPL 许可证。

Readme.txt
一些说明和指导。

build.xml
Ant 编译脚本。

lib/
支持库。

lib.properties
一个描述库及其版本的属性文件。
j2ee1.4/
一个到 J2EE 安装的符号链接(针对 lib/j2ee.jar)。
jakarta-struts-1.1/
Jakarta Struts,版本 1.1。
jakarta-taglibs-standard-1.0.4/
Jakarta's JSTL 发布,版本 1.0.4。
xdoclet-lib-1.2b3/
XDoclet 库,版本 1.2b3。有关指它的补丁的说明见主要 ReadMe.txt 文件。


meta/
XML 配置文件和文件片段。

app/application.xml
主要应用程序部署描述符。
ejb/
EJB 组件描述符(当前没有)。
web/
Web 组件描述符,Struts 配置文件等。


src/
Java 源代码文件。

ejb/
EJB 组件类(org.merlin.santa.beans、org.merlin.santa.interfaces 和 org.merlin.santa.values 包)。
util/
共享工具类(org.merlin.santa 包)。
web/
Web 组件类(org.merlin.santa.actions、org.merlin.santa.encoders、org.merlin.santa.forms、org.merlin.santa.servlets、org.merlin.santa.tags 包和一些子包)。


web/
Public Web 文档。

index.jsp
index 重定向页。
images/
图像文件。


web-inf/
Private Web 文档。

classes/org/merlin/santa/SantaResources.properties
资源属性文件(它将消息键与相关的英语文本相关联,可以在其他属性文件中提供其他语言翻译)。
jsp/
Private JSP 文件。它们打包在 WEB-INF 目录中以避免外部客户非法访问它们,只有内部 Web 组件可以显示它们。现在还不是所有容器都支持这个功能。


index.jsp learn.jsp
index 和信息页。

header.jsp footer.jsp exception.jsp messages.jsp
由其他 JSP 页加入的 JSP 片段。

email/
电子邮件 JSP 文件,包括文本和 HTML。

family/
与 secret santa 族工作流相关的 JSP 页。

member/
与 secret santa 成员工作流相关的 JSP。

tld/
应用程序标签库描述符。

xdoclet/
修改过的 XDoclet .xsd 脚本。

编译脚本

build.xml 文件改编自 Erik Hatcher 很棒的编译脚本(请参阅 参考资料),它可以很好地支持库版本、编译不同的组件、打包为 JAR、WAR 和 EAR 并部署结果。下面是主要的编译目标:

clean
删除编译和发布文件。

compile-util package-util
编译和打包工具库。

compile-ejb package-ejb
编译和打包 EJB 组件,它隐式地在 EJB 组件上运行 XDoclet,并编译工具库。

compile-web package-web
编译并打包 Web 组件,它隐式地在 Web 组件上运行 XDoclet,并编译 EJB 组件。

package-ear
打包 Web 应用程序,它隐式地编码所有组件。

deploy undeploy
部署或者反部署 Web 应用程序,它需要设置环境变量 JBOSS_HOME

编译结构

编译 secret Ssanta 应用程序时,在最后的 EAR 包之前要生成许多中间源代码、部署和类文件。观察中间文件,特别是自动生成的源代码文件通常是有启发性的。这些文件是在下面的目录结构中生成的:

../build/ejb/
EJB 组件编译文件。

gen/
自动生成的文件,包括 ejb-jar.xml 和 the org.merlin.santa.beans、org.merlin.santa.interfaces、org.merlin.santa.utils 和 org.merlin.santa.values 包。


../build/santa/
特定于部署的描述符文件,在这里,是 JBoss 相关的 XML 文件。

../build/web/
Web 组件文件,大量部分编译的类文件和 XML/TLD 文件是从支持库中拷贝的。

../build/util/
工具库类文件。

../dist/
最终的 JAR、WAR 和 EAR 文件。

注意所有这些文件都是在工作目录的父目录中生成的。我这样做是为了保持所有生成的文件与我的原始源代码文件分离。如果您不喜欢这种安排,可以改变 build.xml 中的 build.dirdist.dir 属性。

部署应用程

在 Readme.txt 文件中给出了编译和部署这个应用程序的分步细节,作为参考,下面是关键的步骤:

  1. 通过编辑 server/default/deploy/mail-service.xml 文件并设置 mail.smtp.host 属性为您的 SMTP 中继而启用 JBoss 的 JavaMail 支持。
  2. 取得 secret Santa Web 应用程序源代码包、下载所需要的库、将它们解开到 lib/ 目录中、并使用修改过的 XDoclet .xsd 脚本。
  3. 编译 meta/app/application.xml 并选择一个合适的上下文根。
  4. 编译 meta/web/servlets.xml 并根据您的部署的情况配置 org.merlin.santa.baseURL、org.merlin.santa.emailAddress 和 org.merlin.santa.emailName 参数。
  5. 定制 web-inf/jsp/ 目录中的 JSP 文件以满足您所需要的表示,或者保持默认的表示不变。您需要的所有图像都应该放到 web/images/ 目录中。
  6. 进入 secret Santa 源代码发布的根,设置环境变量 JBOSS_HOME 并用 Ant 执行部署目标:
    JBOSS_HOME=/path/to/jboss-3.2 ant deploy


  7. 用您常用的 Web 浏览器查看这个 Web 应用程序,它将部署在 application.xml 文件中配置的路径中的 Web 服务器上,如 http://localhost:8080/santa/。




回页首


结束语

如果您完成了这些步骤,那么我要衷心地祝贺您。本文章系列具有相当多的内容。在我们共同度过的时间里,我们以一些经过挑选的 J2EE 技术为基础,从头至尾分析了 secret Santa Web 应用程序。尽管动机是虚构的(真的要通过与从随机编码中取出名字比较来判断这些工作吗?),设计远非是完美的,但是我认为这个应用程序清楚地展示了 J2EE 的长处,特别是现在有了许多强大的支持工具。Java Web 应用程序体系结构使它自己变得特别适合于 MVC 体系结构,EJB组件提供了模型、Struts 提供了很好的控制器框架,而 JSP 技术是理想的表示层。这个体系结构的灵活性甚至让我们可以利用 JSP 技术生成我们的电子邮件消息。

如果不指出我的应用程序中的一些问题,那就是我不负责任。异常处理和事件日志留下了一点遗憾,我也许应当对族和成员相关的操作使用单独的 Struts 模块,没有处理电子邮件退回,应用程序在廉价群集硬件上不能伸缩(它是完全用本地 EJB 接口构建的,并且没有提供会话 facade,不过,XDoclet 使转换到远程接口成为小事一桩),而且,也许还有一些可以提供帮助的技术(也许用 Jakarta Tiles 管理 JSP 布局)。不过,尽管有这些缺点,但是 secret Santa 应用程序可以工作得很好,并且它可以达到其目的,它使我的家庭不用相信一个戴有帽子的人 …… 并且总是有时间在第 2 版中改进这些缺点的!



参考资料



关于作者

Merlin 是全球电子安全公司 Betrusted, Inc 的密码专家和首席技术讲师,作为不断增长的 J2EE 家族成员之一,在传统的随机编码技术(hat-based technology)无法胜任时,他用 J2EE 开发了圣诞礼物问题的解决方案。他住在纽约州纽约市(一个非常美的城市,人们给它取了两次名字),可以通过 merlin@merlin.org 与他联系。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款