JSP 2.0 最重要的特性之一就是表达式语言(EL),JSP 用户可以用它来访问应用程序数据。由于受到 ECMAScript 和 XPath 表达式语言的启发,EL 也设计成可以轻松地编写免脚本的 JSP 页面。也就是说,页面不使用任何 JSP 声明、表达式或者 scriptlet。第 11 章会进一步介绍为何无脚本的的JSP 页面是一个最佳实践。
本章介绍如何使用 EL 表达式在 JSP 页面中显示数据和对象属性。它涵盖了最新的 EL 3.0
版本技术。
本章中的所有示例都可以在本书附带的zip 文件中的 el-demo 项目中找到。
JSP 2.0 最初是将 EL 应用在 JSP 标准标签库(JSTL)1.0 规范中。JSP 1.2 程序员将标准库导入到他们的应用程序中,就可以使用EL。JSP 2.0 及其更高版本的用户即使没有 JSTL, 也能使用 EL,但在许多应用程序中,还是需要 JSTL 的,因为它里面还包含了与 EL 无关的其他标签。
JSP 2.1 和JSP 2.2 中的 EL 要将JSP 2.0 中的EL 与 JSF(JavaServer Faces)中定义的 EL 统一起来。JSF 是在 Java 中快速构建 Web 应用程序的框架,并且是构建在 JSP 1.2 之上。由于JSP 1.2 中缺乏整合式的表达式语言,并且JSP 2.0 EL 也无法满足JSF 的所有需求,因此为JSF 1.0 开发出了一款 EL 的变体。后来这两种语言变体合二为一。
2013 年 5 月发布了EL 3.0 版本(JSR 341),EL 不再是 JSP 或任何其他技术的一部分,而是一个独立的规范。EL 3.0 添加了对 lambda 表达式的支持,并允许集合操作。其 lambda 支持不需要 Java SE 8,Java SE 7 即可。
EL 表达式以 ${ 开头,并以 } 结束。EL 表达式的结构如下:
${expression} #{expression}
${x+y}
#{x+y}
两个表达式可以连接在一起。对于一系列的表达式,它们的取值将是从左到右进行,计算结果的类型为 String,并且连接在一起。假如 a+b 等于 8,c+d 等于 10,那么这两个表达式的计算结果将是 810:
${a+b}${c+d}
如果在定制标签的属性值中使用 EL 表达式,那么该表达式的取值结果字符串将会强制变成该属性需要的类型:
以下是关键字,它们不能用作标识符:
and eq gt true instanceof
8.2 表达式语言的语法
or ne le false empty not lt ge null div mod
EL 表达式可以返回任意类型的值。如果 EL 表达式的结果是一个带有属性的对象,则可以利用[ ]或者.运算符来访问该属性。[ ]和.运算符类似;[ ]是比较规范的形式,.运算符则比较快捷。
为了访问对象的属性,可以使用以下任意一种形式:
${object["propertyName"]}
${object.propertyName}
${header["host"]}
${header.host}
如果对象的属性碰巧返回带有属性的另一个对象,既可以用[ ],也可以用. 运算符来访问第二个对象的属性。例如,隐式对象 pageContext 是表示当前 JSP 的 PageContext 对象。它有 request 属性,表示 HttpServletRequest。HttpServletRequest 带有 servletPath 属性。那么, 下列几个表达式的结果相同, 均能得出 pageContext 中 HttpServletRequest 的
servletPath 属性值:
${pageContext["request"]["servletPath"]}
${pageContext.request["servletPath"]}
${pageContext.request.servletPath}
${pageContext["request"].servletPath}
${pageContext.session}
${pageContext.session.id}
8.2.3 取值规则
取值方法如下:
(1)先计算 expr-a 得到 value-a。
(2)如果 value-a 为 null,则返回 null。
(3)然后计算 expr-b 得到 value-b。
(4)如果 value-b 为 null,则返回 null。
(5)如果 value-a 为 java.util.Map,则会查看 value-b 是否为 Map 中的一个 key。若是,则返回 value-a.get(value-b),若不是,则返回 null。
(6)如果 value-a 为 java.util.List,或者假如它是一个 array,则要进行以下处理: a.强制 value-b 为 int,如果强制失败,则抛出异常。
b.如果 value-a.get(value-b)抛出 IndexOutOfBoundsException,或者假如 Array.get (value-a, value-b)抛出 ArrayIndexOutOfBoundsException,则返回 null。
c.否则,若 value-a 是个 List,则返回 value-a.get(value-b);若 value-a 是个 array, 则返回 Array.get(value-a, value-b)。
(7)如果 value-a 不是一个 Map、List 或者 array,那么,value-a 必须是一个 JavaBean。在这种情况下,必须强制value-b 为 String。如果 value-b 是 value-a 的一个可读属性,则要调用该属性的 getter 方法,从中返回值。如果 getter 方法抛出异常,该表达式就是无效的,否则, 该表达式有效。
8.3 访问JavaBean
利用. 或[]运算符,都可以访问 bean 的属性,其结构如下:
${beanName["propertyName"]}
${beanName.propertyName}
${myBean.secret}
8.4 EL 隐式对象
在JSP 页面中,可以利用 JSP 脚本来访问 JSP 隐式对象。但是,在免脚本的 JSP 页面中, 则不可能访问这些隐式对象。EL 允许通过提供一组它自己的隐式对象来访问不同的对象。EL 隐式对象见表 8.1。
表 8.1 EL 隐式对象
对象 |
描述 |
pageContext |
这是当前 JSP 的 javax.servlet.jsp.PageContext |
initParam |
这是一个包含所有环境初始化参数并用参数名作为 key 的 Map |
param |
这是一个包含所有请求参数并用参数名作为 key 的 Map。每个 key 的值就是指定名称的第一个参数值。因此,如果两个请求参数同名,则只有第一个能够利用 param 获取值。要想访问同名参数的所有参数值,可用 params 代替 |
paramValues |
这是一个包含所有请求参数并用参数名作为 key 的 Map。每个 key 的值就是一个字符串数组,其中包含了指定参数名称的所有参数值。就算该参数只有一个值,它也仍然会返回一个带有一个元素的数组 |
header |
这是一个包含请求标题并用标题名作为 key 的 Map。每个 key 的值就是指定标题名称的第一个标题。换句话说,如果一个标题的值不止一个,则只返回第一个值。要想获得多个值的标题,得用 headerValues 对象代替 |
headerValues |
这是一个包含请求标题并用标题名作为key 的Map。每个 key 的值就是一个字符串数组, 其中包含了指定标题名称的所有参数值。就算该标题只有一个值,它也仍然会返回一个带有一个元素的数组 |
cookie |
这是一个包含了当前请求对象中所有 Cookie 对象的 Map。Cookie 名称就是 key 名称, 并且每个 key 都映射到一个 Cookie 对象 |
applicationScope |
这是一个包含了 ServletContext 对象中所有属性的 Map,并用属性名称作为 key |
sessionScope |
这是一个包含了 HttpSession 对象中所有属性的 Map,并用属性名称作为 key |
requestScope |
这是一个 Map,其中包含了当前 HttpServletRequest 对象中的所有属性,并用属性名称作为 key |
pageScope |
这是一个 Map,其中包含了全页面范围内的所有属性。属性名称就是 Map 的 key |
8.4.1 pageContext
pageContext 对象表示当前 JSP 页面的 javax.servlet.jsp.PageContext。它包含了所有其他的
JSP 隐式对象,见表 8.2。
表 8.2 JSP 隐式对象
对象 |
EL 中的类型 |
request |
javax.servlet.http.HttpServletRequest |
response |
javax.servlet.http.HttpServletResponse |
out |
javax.servlet.jsp.JspWriter |
session |
javax.servlet.http.HttpSession |
application |
javax.servlet.ServletContext |
config |
javax.servlet.ServletConfig |
PageContext |
javax.servlet.jsp.PageContext |
page |
javax.servlet.jsp.HttpJspPage |
exception |
java.lang.Throwable |
${pageContext.request}
${pageContext["request"]
${pageContext["request"]["method"]}
${pageContext["request"].method}
${pageContext.request["method"]}
${pageContext.request.method}
表 8.3 pageContext.request 中一些有用的属性
属性 |
说明 |
characterEncoding |
请求的字符编码 |
contentType |
请求的 MIME 类型 |
locale |
浏览器首先 locale |
locales |
所有 locale |
protocol |
HTTP 协议,例如:HTTP/1.1 |
remoteAddr |
客户端 IP 地址 |
remoteHost |
客户端 IP 地址或主机名 |
scheme |
请求发送方案,HTTP 或 HTTPS |
serverName |
服务器主机名 |
serverPort |
服务器端口 |
secure |
请求是否通过安全链接传输 |
两个隐式对象。
8.4.2 initParam
隐式对象 initParam 用于获取上下文参数的值。例如,为了获取名为 password 的上下文参数值,可以使用以下表达式:
${initParam.password}
${initParam["password"]
${param.userName}
${param["userName"]}
${paramValues.selectedOptions[0]}
${paramValues.selectedOptions[1]}
名称作为 key。例如,为了获取 accept-language 这个 header 值,可以使用以下表达式:
${header["accept-language"]}
${header.connection}
${headerValues["accept-language"][0]}
cookie 的值。例如,为了获取名为 jsessionid 的 cookie 值,要使用以下表达式:
${cookie.jsessionid.value}
${cookie.jsessionid.path}
${applicationScope.myVar}
ServletRequest 、HttpSession 或者 ServletContext 。隐式对象 sessionScope 、requestScope 和
pageScope 与 applicationScope 相似。但是,其范围分别为 session、request 和 page。
有界对象也可以通过没有范围的 EL 表达式获取。在这种情况下,JSP 容器将返回
PageContext、ServletRequest、HttpSession 或者 ServletContext 中第一个同名的对象。执行顺序是从最小范围(PageContext)到最大范围(ServletContext)。例如,以下表达式将返回 today引用的任意范围的对象。
${today}
8.5 使用其他EL 运算符
符、条件运算符,以及 empty 运算符。使用这些运算符时,可以进行不同的运算。但是,由于 EL 的目的是方便免脚本 JSP 页面的编程,因此,除了关系运算符外,这些 EL 运算符的用处都很有限。
8.5.1 算术运算符
算术运算符有 5 种。
— 加法(+)。
— 减法(−)。
— 乘法(*)。
— 除法(/和 div)。
— 取余/取模(%和 mod)。
除法和取余运算符都有两种形式,与 XPath 和 ECMAScript 是一致的。
注意,EL 表达式的计算按优先级从高到低、从左到右进行。下列运算符是按优先级递减顺序排列的:
— */div%mod
— +-
这表示*、/、div、%以及 mod 运算符的优先级相同,+与−的优先级相同,但第二组运算符的优先级小于第一组运算符。因此,表达式
${1+2*3}
8.5.2 关系运算符
下面是关系运算符列表:
— 等于(==和 eq)。
— 不等于(!=和 ne)。
— 大于(>和 gt)。
— 大于或等于(>=和 ge)。
— 小于(<和 lt)。
— 小于或等于(<=和 le)。
例如,表达式${3==4}返回 False,${“b”<“d”}则返回 True。
8.5.3 逻辑运算符
下面是逻辑运算符列表:
— 和(&&和 and)。
— 或(|| 和 or)。
— 非(!和 not)。
8.5.4 条件运算符
EL 条件运算符的语法如下:
${statement? A:B}
例如,利用下列 EL 表达式可以测试 HttpSession 中是否包含名为 loggedIn 的属性。如果找到这个属性,就显示“You have logged in(您已经登录)”。否则显示“You have not logged in
(您尚未登录)”。
${(sessionScope.loggedIn==null)? "You have not logged in" : "You have logged in"}
8.5.5 empty 运算符
${empty X}
是一个空 Map、空数组或者空集合,它也将返回 True。否则,将返回 False。
8.5.6 字符串连接运算符
+ =运算符用于连接字符串。 例如,以下表达式打印 a + b 的值。
8.6 引用静态属性和静态方法
$ {a + = b}
{ comma-delimited-elements }
${{1, 2, 3, 4, 5}}
[ comma-delimited-elements ]
${["Aster", "Carnation", "Rose"]}
{ comma-delimited-key-value-entries}
${{"Canada": "Ottawa", "China": "Beijing", "France": "Paris"}}
${cities[0]}
${map[key]}
${{"Canada": "Ottawa", "China": "Beijing"} ["Canada"]}
下面展示如何将列表转换为流,假设 myList 是一个 java.util.List:
${myList.stream()}
$ {MyList.stream().operation-1().operation-2().toList()}
8.9.1 toList
toList 方法返回一个 List,它包含与当前流相同的成员。调用此方法的主要目的是轻松地打印或操作流元素。下面是一个将列表转换为流并返回列表的示例:
$ {[100, 200, 300].stream().toList()}
8.9.2 toArray
与 toList 类似,但返回一个 Java 数组。同样,在数组中呈现元素通常是有用的,因为许多Java 方法将数组作为参数。这里是一个 toArray 的例子:
$ {["One","Two","Three"].stream().toArray()}
8.9.3 limit
limit 方法限制流中元素的数量。名为 cities 的 List 包含 7 个城市:
[Paris, Strasbourg, London, New York, Beijing, Amsterdam, San Francisco]
$ {cities.stream().limit(3).toList()}
[Paris, Strasbourg, London]
8.9.4 sort
此方法对流中的元素进行排序。例如,这个表达式
$ {cities.stream().sorted().toList()}
[Amsterdam, Beijing, London, New York, Paris, San Francisco, Strasbourg]
8.9.5 average
此表达式返回 4.0:
$ {[1,3,5,7].stream().average().get()}
$ {[1,3,100,1000].stream().min().get()}
$ {[1,3,100,1000].stream().max().get()}
例如,此映射方法使用 lambda 表达式 x - > 2 * x,这实际上将每个元素乘 2,并将它们返回到新的流中。
$ {[1,3,5].stream().map(x - > 2 * x).toList()}
[2,6,10]
$ {cities.stream().map(x - > x.toUpperCase()).toList()}
[PARIS,STRASBOURG,LONDON,NEW YORK,BEIJING,AMSTERDAM,SAN FRANCISCO]
$ {cities.stream().filter(x - > x.startsWith("S")).toList()}
[Strasbourg, San Francisco]
$ {cities.stream().forEach(x - > System.out.println(x))}
如果你是 EL 3.0 的新手,但熟悉 JSP,那么格式化集合最简单的方法就是使用第 6 章中讨论的 JSTL。然而,具有强大的功能的 EL 3.0 应足以解决这些问题,并允许我们完全抛弃
JSTL。
例如,你可以尝试使用 forEach,类似于 JSTL 的 forEach。以下代码可以在 Tomcat 8 上运行。
$ {cities.stream().forEach(x - > pageContext.out.println(x))}
我最终想出的两个解决方案不像 forEach 那样优雅,但在所有主要的 servlet 容器上都可用。第一个解决方案适用于 Java SE 7。第二个解决方案可能比第一个更优雅,但只适用于 Java SE 8。
8.10.1 使用 HTML 注释
List 的字符串表示形式如下所示:
8.10 格式化集合
[element-1,element-2,...]
...
$ {myList.stream().map(x - >"
[
所以,这里是一个例子:
"-->
图 8.2 显示了页面显示结果。
这里有另一个例子,这个例子用表格格式化显示地址信息。
图 8.2 使 用 HTML 注 释 来格式化集合
Street City
"+=a.streetName+=" "+=a.city+="
8.10.2 使用 String.join()
public static String join(CharSequence delimiter, Iterable <?extends CharSequence> elements)
Collection 接口正好扩展了 Iterable。因此,你可以将 Collection 传递给 join 方法。
例如,下面是如何将列表格式化为HTML 有序列表:
$ {"
${empty cities? "" : "
+= String.join("
+= ""}
例如,以下表达式返回带有两个小数点位的数字。
${String.format("%-10.2f%n", 125.178)}
8.12 格式化日期
可以通过 String.format()来格式化一个 date 或 time。例如:
${d = LocalDate.now().plusDays(2); String.format("%tB %te, %tY%n", d, d, d)}
首先计算 LocalDate.now().plusDays(2),并将结果复制给变量 d,然后用 String.format()方法来格式化 LocalDate,引用了 3 次变量 d。
更多的格式化规则见:
https://docs.oracle.com/javase/tutorial/java/data/numberformat.html
另一方面,在有些情况下,可能还会需要在应用程序中取消 EL。例如,正在使用与 JSP 2.0 兼容的容器,却尚未准备升级到 JSP 2.0,那么就需要这么做。在这种情况下,可以关闭 EL 表达式的计算。
8.13.1 实现免脚本的JSP 页面
为了关闭 JSP 页面中的脚本元素,要使用 jsp-property-group 元素以及 url-pattern 和scripting- invalid 两个子元素。url-pattern 元素定义禁用脚本要应用的 URL 样式。下面展示如何将一个应用程序中所有 JSP 页面的脚本都关闭:
在部署描述符中只能有一个 jsp-config 元素。如果已经为禁用 EL 而定义了一个
jsp-property-group,就必须在同一个 jsp-config 元素下,为禁用脚本而编写 jsp- property-group。
8.13.2 禁用EL 计算
在某些情况下,比如,当需要在 JSP 2.0 及其更高版本的容器中部署 JSP 1.2 应用程序时, 可能就需要禁用 JSP 页面中的 EL 计算了。此时,出现的 EL 结构,就不会作为 EL 表达式进
行计算。目前有两种方式可以禁用JSP 中的 EL 计算。
第一种,可以将 page 指令的 isELIgnored 属性设为 True,像这样:
<%@ page isELIgnored="true" %>
第二种,可以在部署描述符中使用 jsp-property-group 元素。jsp-property-group 元素是 jsp-
config 元素的子元素。利用 jsp-property-group 可以将某些设置应用到应用程序中的一组 JSP
页面中。
为了利用 jsp-property-group 元素禁用 EL 计算,还必须有 url-pattern 和 el-ignored 两个子元素。url-pattern 元素用于定义EL 禁用要应用的URL 样式。el-ignored 元素必须设为 True。
下面举一个例子,展示如何在名为noEI.jsp 的JSP 页面中禁用 EL 计算。
页面的 EL 计算:
JSP 页面的模式匹配,那么该页面的 EL 计算也将被禁用。
此外,如果使用的是与 Servlet 2.3 及其更低版本兼容的部署描述符,那么 EL 计算已经默认关闭,即便使用的是JSP 2.0 及其更高版本的容器,也一样。
8.14 小结
8.14 小结
EL 是 JSP 2.0 及其更高版本中最重要的特性之一。它有助于编写更简短、更高效的 JSP 页面,还能帮助编写免脚本的页面。本章介绍了如何利用EL 来访问JavaBeans 和隐式对象, 还介绍了如何使用EL 运算符。本章的最后一个小节介绍了如何在与JSP 2.0 及其更高版本相关的容器中使用与EL 相关的应用程序设置。
算术运算符有 5 种。
— 加法(+)。
— 减法(−)。
— 乘法(*)。
— 除法(/和 div)。
— 取余/取模(%和 mod)。
除法和取余运算符都有两种形式,与 XPath 和 ECMAScript 是一致的。
注意,EL 表达式的计算按优先级从高到低、从左到右进行。下列运算符是按优先级递减顺序排列的:
— */div%mod
— +-
这表示*、/、div、%以及 mod 运算符的优先级相同,+与−的优先级相同,但第二组运算符的优先级小于第一组运算符。因此,表达式
${1+2*3}
8.5.2 关系运算符
下面是关系运算符列表:
— 等于(==和 eq)。
— 不等于(!=和 ne)。
— 大于(>和 gt)。
— 大于或等于(>=和 ge)。
— 小于(<和 lt)。
— 小于或等于(<=和 le)。
例如,表达式${3==4}返回 False,${“b”<“d”}则返回 True。
8.5.3 逻辑运算符
下面是逻辑运算符列表:
— 和(&&和 and)。
— 或(|| 和 or)。
— 非(!和 not)。
8.5.4 条件运算符
EL 条件运算符的语法如下:
${statement? A:B}
${x+y}
#{x+y}
以下是关键字,它们不能用作标识符:
and eq gt true instanceof
${pageContext.session}
${pageContext.session.id}