使用 JSP 2.0 EL API
作者:Andrei Cioroianu
了解如何动态求解 JSP 表达式,如何在 XML 配置文件中使用表达式语言 (EL),以及如何在显示 SQL 结果集时优化 EL 的使用
下载本文的源代码
JSP 2.0 表达式语言 (EL) 是希望创建无脚本 JSP 页面的 Web 开发人员所急需的功能。它被设计为 JSP 标准标记库 (JSTL) 1.0 的一部分,随后,JavaServer Pages (JSP) 2.0 采用并增强了 EL。
在其当前格式中,EL 定义了一个易于使用的语法,用于访问 JavaBean 属性、Java 集合、范围属性、初始化和请求参数、HTTP 标头和 cookie,而不必在 JSP 页面中使用 Java scriptlet。这提高了代码的可读性并增强了 Web 页面的可维护性。 此外,EL 还提供了一个完整的运算符集,用于创建算术、逻辑和条件表达式。JSP 2.0 新增了一个名为 EL 函数的功能,可用于调用 Web 页面中的静态 Java 方法而不必使用 Java 代码。JSP EL 的全部功能通过简单的应用程序编程接口 (API) 提供给 Java 程序员,使他们能够以非常规的方式使用 EL。例如,在 web.xml 配置文件中使用 EL 可以增强 Web 应用程序的可自定义性。EL API 用于求解 XML 文件中的表达式。
本文提供了 EL API,并在几个实用程序类(使用 EL 函数从 JSP 页中调用它们的静态方法)中使用它。本文还演示了 EL API 在 JSP 页面以及基于简单标记 API(JSP 2.0 的另一个新特性)的自定义标记处理类中的实际用法。其中的示例之一演示了如何在 XML 配置文件中使用 JSP EL。
我的上一篇 Oracle 技术网 (OTN) 文章—《创建 JSP 2.0 标记文件》—包含一组完整的 JSP 页面和标记文件,它们使用 EL、JSTL 和 SQL 创建表、查询该表,并插入、更新和删除行。查询数据库的页面使用 EL 显示结果集。本文对该页面进行了优化,即借助于 EL API 分析表达式一次,然后在一个循环中对其多次求解。
表达式语言 API 概述
javax.servlet.jsp.el API 由两个类(Expression 和 ExpressionEvaluator)、两个接口(VariableResolver 和 FunctionMapper)以及两个异常(ELException 和 ELParseException)组成。其中的每个类和接口只包含一个或两个方法。虽然简易,但 EL API 提供了在 JSP 页面外部使用表达式语言所需的任何功能。以下用法说明介绍了使用 EL API 求解 Java 代码中的表达式所必须的操作。
第 1 步:获取 ExpressionEvaluator。如果开发 Java 标记处理类,则调用 getJspContext() 所返回对象的 getExpressionEvaluator() 方法。在标记文件和 JSP 页面中,可以使用 jspContext 和 pageContext 隐含对象提供的同一名称调用该方法。
第 2 步:获取 VariableResolver。调用 JSP 环境的 getVariableResolver() 方法。该方法返回一个用于访问 JSP 变量和隐含对象的对象。必要时,还可以开发您自己的 VariableResolver 实现。
第 3 步(可选):提供 FunctionMapper。如果要在表达式中使用 EL 函数,则必须实现 FunctionMapper 接口。EL API 的 evaluate() 方法接受 null 函数映射接口。因此,该参数是可选参数。
第 4 步:求解表达式。调用表达式求解类的 evaluate() 方法,并传递以下参数:表达式(String)、其预期类型(Class)、变量解析类和可能为 null 的函数映射接口。evaluate() 方法返回的值将具有预期类型或其子类。如果不知道预期的类型,则可以指定 Object.class。
创建实用类。我们的 ELUtils 类(参见源代码)提供的实用方法可以将 EL API 的使用减少为一行代码。ELUtils.evaluate() 方法执行以上所述的第 1 步、第 2 步和第 4 步的操作。这些方法使用它们的 JspContext 参数获取表达式求解类和变量解析类,然后求解给定的表达式并返回所得的值。我们将在本文的另一个示例中实现可选的第 3 步的函数映射接口。
调用表达式求解类的 evaluate() 方法时,应用服务器的 JSP 容器分析该表达式,获取变量值,调用 EL 函数,应用运算符,获得一个值(该值被转换为预期的类型)。请注意,如果表达式存在语法错误,或因类型转换、无效数组索引、bean 方法抛出异常或其他事由产生错误,则 evaluate() 可能抛出 ELException。
本文稍后展示了如何从 JSP 页面使用 EL 函数调用 ELUtils 类的方法。为简化函数的用法,expectedType 参数可以是 String 或 Class 实例。如果为 String,则 ELUtils 的 getClass() 方法使用 Class.forName() 获得给定名称的 Class 对象。
在自定义标记处理类中使用 EL API
如上一部分所述,EL API 需要 JspContext 来求解表达式。每个 JSP 页面中都有这样一个环境对象,该对象被传送给处理该页面中所用自定义标记的 Java 类。因此,自定义标记处理类最适合使用 EL API。
为简单标记 API 添加 EL 支持javax.servlet.jsp.tagext 包包含构建标记处理类所需的 API。这些类的大部分均继承自 JSP 1.x,并用于构建所谓的传统标记。JSTL 是使用传统标记 API(包括 Tag、BodyTag、IterationTag 和 TryCatchFinally 接口,以及 TagSupport 和 BodyTagSupport 类)的许多标记库之一。javax.servlet.jsp.tagext 包还包含 SimpleTag 接口和 SimpleTagSupport 类,它们构成简单标记 API。这是 JSP 2.0 中引入的新 API,用于替换早期的 JSP 1.x 类和接口。ELTagSupport 类使用一个方便方法扩展了 SimpleTagSupport,该方法接受 JSP 表达式和预期的类型,将它们与 getJspContext() 方法(从 SimpleTagSupport 继承)返回的 JspContext 对象一起传递给 ELUtils.evaluate():
public class ELTagSupport extends SimpleTagSupport { protected Object evaluate(String expression, Object expectedType) throws JspException { return ELUtils.evaluate( expression, expectedType, getJspContext()); } ... }
解决 API 限制问题。从 Web 开发人员的角度而言,所有自定义 JSP 标记看上去都一样,无论它们是基于简单标记 API 还是基于传统标记 API。新的 JSP 2.0 API 是专门为抱怨原有 API 过于复杂的 Java 开发人员而设计的。简单标记 API 的使用非常简单,但它有一个限制:JspContext 类没有用于获得 JSP 隐含对象(如 request、response、session 和 application)的方法。从理论上讲,该限制为在 Servlet/JSP 环境的外部使用简单标记 API 创造了机会。而实际上,所有自定义标记都用于 JSP 页面中,且其中的很多标记都需要访问 JSP 隐含对象。
幸运地是,对于许多 JSP 容器(包括 Oracle Application Server Containers for J2EE (OC4J) 10g),可以将 JspContext 转换为 PageContext 来获得 JSP 隐含对象。该过程非常有效,但可能并不适用于每个应用服务器。对于那些不支持 PageContext 转换的 JSP 容器,可以使用 EL API,它一般均可用于 Servlet/JSP 环境,不过效率不高。ELTagSupport 类显示了如何组合这两个过程,以确保您的代码适用于每个 J2EE 应用服务器并尽可能地高效。
ELTagSupport 类的 getRequest()、getResponse()、getSession() 和 getApplication() 方法尝试将 JspContext 对象转换为 PageContext,以便使用 PageContext 类提供的方法获得 JSP 隐含对象。如果该情况无法实现,ELTagSupport 将使用表达式语言查询 pageContext 隐含对象。在非 Servlet 环境中,我们的方法将返回 null。下表包含被调用的方法以及为获得第一列中的 JSP 隐含对象而需要求解的表达式。第二列指出了这些对象的 Java 类。
JSP 对象 | Java 类 | PageContext 方法和 JSP 表达式 |
request | HttpServletRequest | PageContext.getRequest() ${pageContext.request} |
response | HttpServletResponse | PageContext.getResponse() ${pageContext.response} |
session | HttpSession | PageContext.getSession() ${pageContext.session} |
application | ServletContext | PageContext.getServletContext() ${pageContext.servletContext} |
表达式语言函数
EL 函数允许您使用以下语法在 EL 表达式中调用静态 Java 方法:
libraryPrefix:functionName(param1, param2, ...)
函数在可能也包含自定义标记的 JSP 库中定义。必须使用 <%@taglib%> 指令在 JSP 页面中声明所使用的库:
<%@taglib prefix="libraryPrefix" uri="/WEB-INF/.../libraryDescriptor.tld" %>
或
<%@taglib prefix="libraryPrefix" uri="http://.../libraryDescriptor.tld" %>
定义 EL 函数。EL 函数和静态 Java 方法之间的映射必须定义在 .tld 文件中。例如,在 el.tld 库描述文件中,ELUtils 的第一个 evaluate() 方法通过以下声明映射到 EL 函数(参见源代码):
<function> <name>evaluate</name> <function-class>jsputils.el.ELUtils</function-class> <function-signature> java.lang.Object evaluate( java.lang.String, java.lang.Object, javax.servlet.jsp.JspContext) </function-signature> </function>
el.tld 文件为源代码中的 ELUtils、FNMapper 和 PEWrapper 类定义相似的映射。由于每个 EL 函数的名称在 .tld 文件中必须唯一,因此我们对 ELUtils 的第二个 evaluate() 方法以及 PEWrapper 提供的相同名称的方法分别使用 evaluate2() 和 evaluate3()。
使用 EL 函数。ELTest.jsp 页面包含一个表单,该表单只有一个输入域,用户可以在该域中键入一个 EL 表达式。单击“求解”按钮时,JSP 页面收到该表达式,并用先前定义的函数求解。el:evaluate() 返回的值通过 JSTL 的 <c:set> 标记存储在 JSP 变量 (exprValue) 中:
<c:set var="exprValue" value="${el:evaluate(param.expr, 'java.lang.Object', pageContext)}"/>
然后,使用 <c:out> 标记打印该表达式及其值,该标记执行任何必要的 HTML 编码(用 < 替换 <,用 > 替换 >,依此类推):
<c:out value="${param.expr}"/> = <c:out value="${exprValue}"/> <br>
如果您刚刚学习 EL,则可以使用下面的 ELTest.jsp 验证表达式的语法并查看它们生成的结果。不要忘了用 ${ 和 } 围住 EL 结构。请注意,EL 表达式可能包含多个 ${...},它们的值被连成一串。
<%@ taglib prefix="el" uri="/WEB-INF/el.tld" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <body> <form method="post"> <c:if test="${!empty param.expr}"> <c:set var="exprValue" value="${el:evaluate(param.expr, 'java.lang.Object', pageContext)}"/> <c:out value="${param.expr}"/> = <c:out value="${exprValue}"/> <br> </c:if> <input type="text" name="expr" size="40" value="<c:out value='${param.expr}'/>"> <input type="submit" value="Evaluate"> <c:if test="${empty param.expr}"> <br> Example: \${ 1 + 2 } </c:if> </form> </body> </html>
实现函数映射接口
当要在用 EL API 求解的表达式中使用 EL 函数时,需要使用函数映射接口。FunctionMapper 接口只有一个名为 resolveFunction() 的方法,该方法接受两个参数(一个库前缀和一个函数名),并返回 java.lang.reflect.Method 对象,该对象提供有关使用给定名称映射到 EL 函数的静态 Java 方法的信息以及对该方法的访问。
FNMapper 类将函数-方法映射保存在 java.util.HashMap 中。resolveFunction() 方法使用 prefix 和 localName 参数构建一个关键字,该关键字被传递给 java.util.HashMap 对象的 get() 方法,该对象返回相应的 Method 实例:
public class FNMapper implements FunctionMapper { private HashMap functionMap; ... public Method resolveFunction( String prefix, String localName) { return (Method) functionMap.get(prefix+':'+localName); } }
构建函数映射。FNMapper 的 buildMap() 方法使用 Java 反射 API 构建函数映射。给定类的每个公共静态方法均映射到同名的 EL 函数。只要类没有多个同名的静态方法,该映射将运作正常。
private void buildMap(String prefix, Class clazz) { Method methods[] = clazz.getMethods(); for (int i = 0; i < methods.length; i++) { Method m = methods[i]; if (Modifier.isStatic(m.getModifiers())) functionMap.put(prefix+':'+m.getName(), m); } }
buildMap() 方法由 FNMapper() 构造方法调用,后者使用 Class.forName() 获得 Class 实例。由于 FNMapper 类管理它自己的实例,因此该构造方法被声明为专用。公共 getInstance() 方法接受 id 参数并返回请求的函数映射接口。在其当前实现中,FNMapper 只支持 JSTL 函数库,但您可以轻松地修改它即可支持其他函数库。请注意,同一函数映射接口实例可用于多个具有不同前缀的函数库。这对于在同一表达式中使用不同库中的函数来说确实很必要。
由于类名是硬编码的,因此 FNMapper 只适用于 JSTL 1.1 的 Apache 实现。一个比较通用的函数映射接口必须分析 .tld 文件,提取包含静态方法的类的名称。还应从 .tld 文件中获得 EL 函数的名称和映射信息。而简单的 FNMapper 类足以满足测试和学习目的。
测试函数映射接口。FNTest.jsp 页面类似于 ELTest.jsp。为启用对 JSTL 函数的支持,FNTest.jsp 使用 el:evaluate2(),并传递一个使用 el:getFNMapper('fn') 获得的函数映射接口:
<c:set var="exprValue" value="${el:evaluate2(param.expr, 'java.lang.Object', pageContext, el:getFNMapper('fn'))}"/>
evaluate2() 函数被映射到 ELUtils 的第二个 evaluate() 方法,该方法接受函数映射接口参数。getFNMapper() 函数被映射到 el.tld 文件中的 FNMapper.getInstance() 方法。
请注意,FNTest.jsp 不必使用 <%@taglib%> 声明 JSTL 函数库,这是因为用户键入的 EL 表达式是在 ELUtils 的 Java 代码中通过 EL API 求解的,且 FNMapper 类执行 JSTL 函数-方法映射。
在 XML 配置文件中使用 EL
在《创建 JSP 2.0 标记文件》中,我演示了如何使用 JSP、JSTL 和 SQL 更新和查询数据库。在本文的余下部分,我们将使用 JSP 2.0 EL API 改进上篇文章中的某些 JSP 示例。
修改 Web 应用程序描述文件 (web.xml)。在《创建 JSP 2.0 标记文件》中,数据源名称 (jdbc/dbtags) 在 web.xml 配置文件中提供并由标记文件片段 (init.tagf) 使用 ${initParam.tags_db_dataSource} 获得。我们现在拥有一个名为 debug_mode 的附加初始化参数,它指示应用程序是在测试环境还是在生产环境中运行:
<context-param> <param-name>debug_mode</param-name> <param-value>true</param-value> </context-param>
假设根据 debug_mode 标志的值,我们要从具有相同结构的两个数据库中选择一个数据库。可以将该选择在 JSP 页面中进行硬编码,但也可以使用 JSP EL 在 web.xml 文件中指定该选择:
<context-param> <param-name>tags_db_dataSource</param-name> <param-value>jdbc/${ initParam.debug_mode ?"dbtags" :"production" }</param-value> </context-param>
此配置更改后,${initParam.tags_db_dataSource} 将返回一个必须用 EL API 求解的表达式。
获得数据源名称。XMLConfig.jsp 页面使用 el:evaluate() 函数获得数据源名称,该函数返回 jdbc/dbtags 或 jdbc/production,具体取决于 debug_mode 的值。数据源名称通过 <c:set> 存储到 JSP 变量 (evaluated_tags_db_dataSource) 中:
<c:set var="evaluated_tags_db_dataSource" value="${el:evaluate(initParam.tags_db_dataSource, 'java.lang.String', pageContext)}"/>
XMLConfig.jsp 页面使用以下代码输出 debug_mode 参数、表达式和它的值:
debug_mode:${initParam.debug_mode} <br> expression:${initParam.tags_db_dataSource} <br> value:${evaluated_tags_db_dataSource}
最终输出如下:
debug_mode:true expression:jdbc/${ initParam.debug_mode ?"dbtags" :"production" } value:jdbc/dbtags
init.tagf 文件获得数据源名称(如 XMLConfig.jsp)。init.tagf 使用 JSTL 的 <sql:setDataSource> 标记创建 javax.sql.DataSource 变量,而非输出某些信息:
<sql:setDataSource dataSource="${evaluated_tags_db_dataSource}" var="tags_db_dataSource" scope="application"/>
init.tagf 片段包括在 select.tag 文件中,本文的下一部分将提供该文件。
通过分析过的表达式优化 EL
在求解表达式之前,JSP 容器必须对其进行分析以便验证它的语法,并获得有关已用变量、函数、运算符等的信息。该过程可能涉及许多字符串运算并可能创建大量临时对象,JVM 的垃圾收集器稍后必须从内存中删除这些临时对象。在 JSP 页面、标记文件或标记处理类的循环中使用表达式时,在循环外部只分析该表达式一次,然后根据需要在循环中多次求解分析过的表达式是很有意义的。以下用法说明介绍了如何使用 EL API 完成此优化。
第 1 步:获得 ExpressionEvaluator。调用 JspContext 对象的 getExpressionEvaluator() 方法。
第 2 步:分析该表达式。调用表达式求解类的 parseExpression() 方法,并传递以下参数:表达式(String)、它的预期类型(Class)以及可选的函数映射接口。parseExpression() 方法返回 Expression 对象,它的 evaluate() 方法将在第 4 步中被调用。
第 3 步:获得 VariableResolver。使用 JSP 环境的 getVariableResolver() 方法获得必须在下一步中传递给 evaluate() 的对象。
使用以下资源测试示例并了解有关 JSP 2.0 EL API 的更多信息。 下载源代码。jspelapi_src.zip 文件包含本文的示例:jsputils 目录将 Java 类归在一起,jspelapi 是一个 Java Web 应用程序。为运行这些示例,您需要 J2SE、J2EE 1.4 应用服务器、JSTL 1.1 以及数据库服务器。 阅读《创建 JSP 2.0 标记文件》。Andrei Cioroianu 展示了如何创建和使用标记文件,以及如何将现有页面片段变换为标记文件。他使用 JSTL 和几个高级 JSP 特性构建用于更新和查询数据库的标记文件。 下载 OC4J 10g。OC4J 10g 完全贯彻了 J2EE 1.4 规范,该规范包括 JSP 2.0。可以使用 OC4J 10g (10.0.3) 来测试这些示例。它可用于所有主要的数据库服务器 — 当然 — 包括 Oracle 数据库。此外,不要忘记配置 dbtags 数据源,并确保可以提供正确的数据库驱动程序。 下载 JSTL 1.1。部署 jspelapi Web 应用程序之前,下载 JSTL 并将文件 jstl.jar 和 standard.jar 复制到 jspelapi/WEB-INF/lib 目录中。 阅读 JSP 2.0 规范。JSP 2.0 规范中有一整章专门介绍 EL API(“第二部分:第 JSP.14 章 表达式语言 API”)。表达式语言在另一章中介绍(“第一部分:第 JSP.2 章 表达式语言”)。 JSP 示例和教程。 |
第 4 步:求解表达式。调用 Expression 对象的 evaluate() 方法。每当要求解分析过的表达式时均可重复第 3 步和第 4 步。请注意,在不同的时刻,根据其变量值的不同,同一表达式可能具有不同的值。
包装分析过的表达式。PEWrapper 类有两个字段,用于维护对分析过的 Expression 及其 JspContext 的引用。这使我们能够将 Expression 对象与 JspContext 实例(稍后用于获得变量解析类)保存在一起。getInstance() 方法执行上述的第 1 步和第 2 步,并返回一个 PEWrapper 对象。非静态 evaluate() 方法执行第 3 步和第 4 步,并返回表达式的值。我们还需要一个可以映射到 EL 函数(在 el.tld 文件中命名为 evaluate3())的静态 evaluate() 方法。getInstance() 方法也映射到 EL 函数(在 el.tld 中命名为 getPEWrapper())。
使用分析过的表达式。《创建 JSP 2.0 标记文件》提供了一个 JSP 示例 (select.jsp),该示例使用标记文件 (select.tag) 查询数据库。该标记文件构建 SQL 语句并使用 JSTL 的 <sql:query> 标记执行该语句。然后,该标记文件使用 <c:forEach> 迭代结果集的行,并使用 <jsp:doBody> 执行标记主体。因此,调用标记文件 (<db:select>) 的自定义标记执行一个循环,并在每个迭代中执行 <db:select> 与 </db:select> 之间的 JSP 代码。在我们的示例中,该代码反复分析和求解 EL 表达式,输出 HTML 表的行:
<db:select var="row" table="People" orderBy="name"> <tr> <td> ${row.userID} </td> <td> ${row.name} </td> <td> ${row.email} </td> </tr> </db:select>
我们可以将 <db:select> 的主体看作是单个 JSP 表达式,可以使用 <c:set> 将它存储在 JSP 变量 (rowExpr) 中。为了避免 JSP 容器求解该表达式,每个 $ 字符使用反斜杠进行转义。因此,rowExpr 保留表达式的文本而不是它的值:
<c:set var="rowExpr"> <tr> <td> \${row.userID} </td> <td> \${row.name} </td> <td> \${row.email} </td> </tr> </c:set>
该表达式使用我们的 PEWrapper 类(el:getPEWrapper() 返回它的实例)进行分析。对 PEWrapper 对象的引用保存在名为 parsedRowExpr的 JSP 变量中:
<c:set var="parsedRowExpr" value="${el:getPEWrapper(rowExpr, 'java.lang.String', pageContext, null)}"/>
现在,我们创建了一个可以使用 el:evaluate3() 函数(调用 PEWrapper 的静态 evaluate() 方法)更快速求解的分析过的表达式:
<db:select var="row" table="People" orderBy="name"> ${el:evaluate3(parsedRowExpr)} </db:select>
优化通常意味着更多的编程,并且在很多情况下,意味着代码的可读性降低。而使用分析过的表达式,JSP 容器不必反复重复相同的操作。大部分 JSP 表达式不需要优化,但如果循环有很多迭代并使用复杂的 EL 表达式,则应考虑优化代码。当然,JSP 容器本身可以缓存分析过的表达式,但您无法确定是否发生此情况,除非它已被记录。PEWrapper 类的源代码之后为 select.tag 文件和 select.jsp 页面的优化版本。
结论
JSP 2.0 表达式语言可以显著增强 Web 页面的可维护性。使用 EL API,您可以将同一语言与其他技术(如基于 XML 的技术)集成在一起。EL 只有一个缺点:JSP 表达式比编译的 Java 代码速度慢,但在大多数情况下,EL 开销并不明显。出现此情况时,可以使用 EL API 优化表达式求解过程。