在本系列的 第 1 部分,我介绍了 secret Santa 应用程序、实现它所使用的技术、以及封装其模型的企业 bean。在 第 2 部分,我介绍了实际响应客户请求、操纵应用程序模型以及调用展示的、基于 Struts 的控制器。作为这一系列的结束篇,我将分析用于提供其基于 Web 的表示 ―― 其视图方面 ―― 的类和 JSP 页。我们将首先简单讨论所涉及的 Web 端技术,与以往一样,有关这种技术的更详细的信息请参阅 参考资料。然后我们将讨论这个应用程序所使用的一些工具框架。最后我们将步入几个应用程序的表示页。
在这一部分,我们将简单回顾在我们的讨论中所涉及的技术:JSP 技术和 JSTL。
JavaServer Pages (JSP) 技术是一种 Web 技术,其中表示页(通常是 HTML)包含小的 Java 控制代码片段。这些 Java 代码可以提供像数据提取这样的逻辑服务,从而可以动态生成页面,同时仍然可以容易地针对不同的表示风格进行配置。清单 1 展示了一个显示请求 URI 和参数的简单 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:
<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 Standard Tag Library JSTL)是一种 JSP 增强,它提供了类似 HTML 的标签,支持范围广泛的基本逻辑任务,包括迭代数据结构、访问 bean 字段,以及执行对文本的 HTML 格式编排。JSTL 使您可以从 JSP 源代码中去掉大部分原始 Java 代码,并以更精确和更具可读性的方式表达表示逻辑。清单 3 用 JSTL 标签取代原始 Java 代码重新编写了前面的简单 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() 方法。
|
还有许多第三方标签库,并且可以容易地编写自定义标签,它们可以提供现有标签不能提供的特定于应用程序的服务。在 secret Santa 应用程序中,我们使用自定义的标签,根据标准模板自动生成 HTML 图像按钮,并针对不同的目的重写 URL。
既然对于相关的 Web 端技术有了一个简单的了解,我们将讨论在这个应用程序中使用的第一个可重用的工具框架:一个文本编码类层次结构。当一个 JSP 页将文本输出为 HTML 时,必须执行某些编码操作:小于号(<)字符必须替换为 < 和符号(&)字符必须替换为amp; 等。凭借 <c:out> 标签的 escapeXml 参数,JSTL 对这种任务提供了一些自动化支持。在设置了这个参数时(在默认情况下是设置的),在输出表达式中所有禁止的 XML 字符都自动被相应的字符引用所替换。类似地,在 HTML 页中 URL 的参数必须根据 URL 编码标准进行编码,这是由 <html:rewrite> 和 <c:url> 标签自动完成的。不过,这些只是可以为 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 编码。
<%@
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>
|
清单 6 展示了一个非常基本的 HTML 编码器的源代码。这段代码非常简陋,但是却有高效率,这对于处理大量文本很重要。 encode() 方法取其参数的字符串值并用相应的转义字符替换小于( < )、大于( > )及和( & )字符。提供了一个 singleton instance 变量,这样所有 JSP 页都可以共享这个类的一个实例。
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 正常执行它之前就需要进行编码。
清单 7 展示了一个 JavaScript 编码器的源代码。它将一个值编码为适合于作为 JavaScript 字符串文本使用的形式,如将这个值嵌入到由 JSP 页生成的 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。这个应用程序的非英语部署应当为这个特定的任务选择特定于区域的编码器。
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 转义,因为已经有其他机制可以完成这项工作。
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 ();
}
}
|
清单 10 显示了一个对值进行 URL 编码的类,它只是使用 java.net.URLEncoder 类,使用的是 UTF-8 编码。
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 而对它进行编码。
清单 11 展示了进行垂直 HTML 编码的类的源代码。代码对提供的字符串进行 HTML 编码、在每一个字符之间插入 <br> 换行,比如,将 “Job” 转换为
J
o
b
我们将用垂直 HTML 生成 secret Santa 应用程序中表的列标题。
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 (" ");
else r.append (c);
}
return r.toString ();
}
}
|
一般来说,一个 Web 应用程序应当从不向最终用户显示异常,它降低了使用性并对安全性有不利影响。不过,我常常发现在应用程序开发和测试阶段,这样一个功能是相当有用的。
清单 12 展示了一个 JSP 片段,它使用我们刚刚讨论过的一些编码器在一个弹出窗口显示异常堆栈跟踪。这个异常存储在请求范围变量 exception 中,JSP 页显示异常细节消息,后面一行代码调用 JavaScript details() 函数来显示堆栈跟踪。
JavaScript details() 函数使用 JSTL 表达式 ${jsEnc[htmlEnc[traceEnc[requestScope.exception]]]} 在一个新窗口显示堆栈跟踪。这个表达式生成包含可以写入窗口的 HTML 代码的 JavaScript 字符串文字。这个表达式实际上用到三个编码器: traceEnc 将异常堆栈跟踪编码为字符串、然后 htmlEnc 将它编码为 HTML 以在弹出窗口中显示、而 jsEnc 将结果编码为适合作为 JavaScript 字符串文字使用的格式。JSTL EL 的灵活性使我们可以容易地访问这个编码框架。
<%@
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 片段的实际使用。