Spring MVC -- 表达式于语言(EL)
JSP 2.0最重要的特性之一就是表达式语言(EL),JSP用户可以用它来访问应用程序数据。由于受到ECMAScript和XPath表达式语言的启发,EL也设计成可以轻松地编写免脚本(就是不用在jsp文件中嵌入脚本)的JSP页面。也就是说页面中不使用任何JSP声明、表达式或者scriptlet。
本篇博客将会介绍如何使用EL表达式在JSP页面中显示数据和对象属性,它涵盖了最新的EL3.0版本技术。
一 表达式语言简史
JSP 2.0最初是将EL应用在JSP标准标签库(JSTL)1.0规范中。
JSP 1.2程序员将JSTL库导入到他们的应用程序中,就可以使用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.0EL也无法满足JSF的所有需求,因此为JSF 1.0开发了一款EL的变体,后者这两种语言变体合二为一。
2013年5月发布了EL 3.0版本,EL不再是JSP或任何其它技术的一部分,而是一个独立的规范。EL 3.0添加了对lambda表达式的支持,并允许集合操作,其lambda支持不需要Java SE8,Java SE7即可。
二 表达式语言的语法
EL表达式以${开头,并以}结束。EL表达式的结构如下:
${expression}
#{expression}
例如,表达式x+y,可以写成:
${x+y}
或者:
#{x+y}
\${exp}和#{exp}结构都由EL引擎以相同的方式进行计算。然而,当EL未被用作独立引擎而是使用诸如JSF或JSP的底层技术时,该技术可以不同地解释构造。例如,在JSF中,\${exp}结构用于立即计算,#{expr}结构用于延迟计算(即表达式直到系统需要它的值时,才进行计算)。另一方面,立即计算的表达式,会在JSP页面编译时同时编译,并在执行JSP页面时被执行。在JSP 2.1和更高版本中,#{exp}表达式只能在接受延迟表达式的标签属性中使用。
两个表达式可以连接在一起。对于一系列的表达式,它们的取值将是从左到右进行,计算结构的类型为String,并且连接在一起。假设$a+b=8$,$c+d=10$,那么这两个表达式的计算结果将是810:
${a+b}${c+d}
表达式\${a+b}and\${c+d}的取值结果则是8and10。
注意:在EL表达式中的"+"只有数学运算的功能,没有连接符的功能,它会试着将运算符两边的操作数转换为数值类型,进而进行数学加法运算,然后将结果输出。若出现\${"a"+"b"}则会出现异常。如果想将两个字符串连接可以使用\${"a"+="b"}。
如果在定制标签的属性值中使用EL表达式,那么该表达式的取值结果字符串将会强制变成该属性需要的类型:
<my:tag someAttribute="${expression}"/>
像\${这样的字符顺序就表示是一个EL表达式的开头,如果需要的只是文本\${,则需要在它前面加一个转义符\\\${。
1、关键字
以下是关键字,它们不能用作标识符:
and eq gt true instanceof
or ne le false empty
not lt ge null div mod
2、[]和.运算符
EL表达式可以返回任意类型的值。如果object是一个带有属性的对象,则可以利用[]或者.运算符来object的属性。[]和.运算符类似;[]是比较规范的形式,.运算符则比较快捷。
为了访问对象的属性,可以使用以下任意一种形式:
${object["propertyName"]}
${object.propertyName}
但是,如果propertyName不是有效的Java变量名, 即属性名称中包含一些特殊字符,如. 或 – 等并非字母或数字的符号,则只能使用[]运算符。
例如,下面这两个EL表达式可以用来访问隐式对象header中的host属性(EL表达式的隐式对象是指不需要new,就可以使用的对象,即JSP容器为每个页面中的开发人员提供的Java对象):
${header["host"]}
${header.host}
但是,要访问accept-language属性,只能使用[]运算符。
如果对象的属性碰巧返回带有属性的另一个对象,即可以使用[],也可以用.运算符来访问第二个对象的属性。例如隐式对象pageContext是表示当前JSP的PageContext对象,它有request属性,表示HttpServlertRequest。HttpServlertRequest自带servlertPath属性,那么下列几个表达式结果相同,均能得出pageContext中HttpServlertRequest的servlertPath属性:
${pageContext["request"]["servletPath"]}
${pageContext.request["servletPath"]}
${pageContext.request.servletPath}
${pageContext.["request"].servletPath}
要访问HttpSession,可以使用以下语法:
${pageContext.session}
例如,以下表达式会得出session标识符:
${pageContext.session.id}
3、自动转变类型
EL 提供方一个方便的功能就是:自动转变类型,我们来看下面这个范例:
${param.count + 20}
假若窗体传来count的值为10时,那么上面的结果为30。之前没接触过JSP 的读者可能会认为上面的例子是理所当然的,但是在JSP 1.2 之中不能这样做,原因是从窗体所传来的值,它们的类型一律是String,所以当你接收之后,必须再将它转为其他类型。如:int、float 等等,然后才能执行一些数学运算,下面是之前的做法:
<% String str_count = request.getParameter("count"); int count = Integer.parseInt(str_count); count = count + 20; %>
所以,注意不要和java的语法(当字符串和数字用“+”链接时会把数字转换为字符串)搞混淆。
4、取值规则
EL表达式的取值是从左到右进行的,对于exp-a[exp-b]形式的表达式,其EL表达式的取值方法如下:
- 先计算exp-a得到value-a;
- 如果value-a为null,则返回null;
- 然后计算exp-b得到value-b;
- 如果value-b为null,则返回null;
- 如果value-a为java.util.Map,则会查看value-b是否为Map的一个key。若是,则返回value-a.get(value-b),若不是,则返回null;
- 如果value-a为java.util.List或者假设它是一个Array,则要进行一下处理:
a.强制value-b为int,如果强制失败,则抛出异常;
b.如果value-a.get(value-b)抛出IndexOutOfBoundsException,或者假设Array.get(value-a,value-b)抛出ArrayIndexOfBoundsException,则返回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方法抛出异常,该表达式就是无效的,否则,该表达式有效。
三 使用EL访问数据
1、访问变量
EL 存取变量数据的方法很简单:例如:
${username}
它的意思是取出某一范围中名称为username的变量。因为我们并没有指定哪一个范围的username,所以它的默认值会先从Page 范围找,假如找不到,再依序到Request、Session、Application范围。假如途中找到username,就直接回传,不再继续找下去,
但是假如全部的范围都没有找到时,就回传null,当然EL表达式还会做出优化,页面上显示空白,而不是打印输出null。
属性范围 |
EL中的名称 |
Page |
PageScope |
Request |
RequestScope |
Session |
SessionScope |
Application |
ApplicationScope |
我们也可以指定要取出哪一个范围的变量:
范例 |
说明 |
${pageScope.username} |
取出Page范围的username变量 |
${requestScope.username} |
取出Request范围的username变量 |
${sessionScope.username} |
取出Session范围的username变量 |
${applicationScope.username} |
取出Application范围的username变量 |
其中,pageScope、requestScope、sessionScope和applicationScope都是EL 的隐含对象,由它们的名称可以很容易猜出它们所代表的意思,
例如:${sessionScope.username}是取出Session范围的username 变量。这种写法是比之前JSP的写法容易、简洁许多.:
<% String username = session.getAttribute("username"); %>
2、访问JaveBean
利用.或[]运算符,都可以访问bean的属性,其结构如下:
${beanName["propertyName"]}
${beanName.propertyName}
例如,访问myBean的secret属性,可以使用以下表达式:
${myBean.secret}
3、访问List、Array和Map
可以通过索引来访问List和Array,如下表达返回hobbies(Array或List)中的3个元素:
//Lits或Array
${requestScope.hobbies[0]}
${requestScope.hobbies[1]}
${requestScope.hobbies[2]}
可以通过如下方式访问Map:
${map[key]}
例如:
//Map
${requestScope.map["china"]}
${requestScope.map.china}
${{"Canada":"Ottawa","China":"Beijing"}["Canada"]}
四 EL隐式对象
在JSP页面中,可以利用JSP脚本来访问JSP隐式对象。如下:
<% //设置register.jsp注册信息提交内容编码方式 只对表单post提交方式有效 tomcat8以后默认提交内容编码为utf-8 request.setCharacterEncoding("UTF-8"); String name = request.getParameter("uname"); String pwd = request.getParameter("upwd"); //要求年龄输入必须是数字 int age = Integer.valueOf(request.getParameter("uage")); String[] hobbies = request.getParameterValues("uhobbies"); %>
但是,在免脚本的JSP页面中(不使用JSP脚本),则不可能访问这些隐式对象。EL允许通过提供一组它自己的隐式对象来访问不同的对象。EL隐式对象如下表:
隐式对象 |
类型 |
说明 |
pageContext |
javax.servlet.jsp.PageContext |
表示此JSP的PageContext |
pageScope |
java.util.Map |
取得Page范围的属性名称所对应的值 |
pageRequest |
java.util.Map |
取得Request范围的属性名称所对应的值 |
sessionScope |
java.util.Map |
取得Session范围的属性名称所对应的值 |
applicationScope |
java.util.Map |
取得Application范围的属性名称所对应的值 |
param |
java.util.Map |
如同ServletRequest.getParameter(String name)。返回String类型的值 |
paramValues |
java.util.Map |
如同ServletRequest.getParameterValues(String name)。返回String[]类型的值 |
header |
java.util.Map |
如同ServletRequest.getHeader(String name)。返回String类型的值 |
headerValues |
java.util.Map |
如同ServletRequest.getHeaders(String name)。返回String[]类型的值 |
cookie |
java.util.Map |
如同HttpServletRequest.getCookies()。返回String[]类型的值 |
initParam |
java.util.Map |
如同ServletContext.getInitParameter(String name)。返回String类型的值 |
1、pageContext
pageContext隐式对象表示当前JSP页面的javax.servlet.jsp.PageContext。它包含了所有其它的JSP隐式对象,如下表:
对象 | EL中的类型 |
request | javax.servlet.http.HttpServlertRequest |
response | javax.servlet.http.HttpServlertResponse |
out | javax.servlet.jsp.JspWriter |
session | javax.servlet.http.HttpServlertRequest |
application | javax.servlet.ServletContext |
config | javax.servlet.ServletConfig |
PageContext | javax.servlet.jsp.PageContext |
page | javax.servlet.jsp.HttpJspPage |
exception | javax.lang.Throwable |
例如,可以利用以下任意一个表达式来获取当前的ServlertRequest:
${pageContext.request}
${pageContext["request"]}
并且,还可以利用以下任意一个表单时来获取请求方法:
${pageContext["request"]["method"]}
${pageContext["request"].method}
${pageContext.request["method"]}
${pageContext.request.method}
下表列出${pageContext.request}中一些有用的属性:
属性 | 说明 |
characterEncoding | 请求的字符编码 |
contentType | 请求的MIME类型 |
locale | 浏览器首先loale |
locales | 所有locale |
protocol | HTTP协议,例如:HTTP/1.1 |
remoteAddr | 客户端IP地址 |
remoteHost | 客户端IP地址或主机名 |
scheme | 请求发送方案,HTTP或HTTPS |
serverName | 服务器主机名 |
serverPort | 服务器端口 |
secure | 请求是否通过安全链接传输 |
对请求参数的访问比对其它隐式对象更加频繁;因此,这里提供了param和paramValues两个隐式对象。
2、作用于访问对象(EL域对象)
与范围有关的EL隐含对象包含以下四个:pageScope、requestScope、sessionScope 、applicationScope,它们基本上就和JSP的pageContext、request、session和application一样。
不过必须注意的是,这四个隐含对象只能用来取得范围属性值,即JSP中的getAttribute(String name),却不能取得其他相关信息。例如:JSP中的request对象除可以存取属性之外,还可以进行设置属性、请求转发等:
request.setAttribute("name", "郑洋"); //请求转发 request.getRequestDispatcher("rq.jsp").forward(request,response);
但是在EL中,它就只能单纯用来取得对应范围的属性值。例如:我们要在session 中储存一个属性,它的名称为username,在JSP 中使用session.getAttribute("username")来取得username 的值,但是在EL中,则是使用${sessionScope.username}来取得其值的。
注意:如果不指定域对象,则默认根据从小到大的顺序依次取值,即返回pageScope、requestScope、sessionScope、applicationScope中第一个同名的对象。
3、param
隐式对象param用于获取请求参数值(主要是表单数据)。这个对象表示一个包含所有请求参数的Map。例如,要获取userName参数,可以使用以下任意一种表达式:
${param.userName}
${param["userName"]}
等价于JSP脚本:
<% String username = request.getAttribute("username"); %>
4、paramValues
利用隐式对象可以获取一个请求参数的多个值。这个对象表示一个包含所有请求参数,并以参数名称作为key的Map,每个key的值时一个字符串数组,其中包含了指定参数名称的所有值。即使该参数只有一个值,它也仍然返回一个带有一个元素的数组。例如,为了获取selectedOptions参数的第一个值和第二个值,可以使用以下表达式:
${paramValues.selectedOptions[0]}
${paramValues.selectedOptions[1]}
5、header
隐式对象header表示一个包含所有请求标题的Map,主要包含以下属性:
为了获取header值,要利用header属性名称作为key。例如,为了获取accept-language这个header,可以使用以下表达式:
${header["accept-language"]}
如果header名称是一个有效的Java变量,如connection,那么也可以使用.运算符:
${header.connnection}
6、headerValues
隐式对象headerValues表示一个包含所有请求标题并以header属性名称作为key的Map。但是与header不同的是,隐式对象headerValues返回的Map是一个字符串数组。例如,为了获取标题accept-language的第一个值,要使用以下表达式:
${headerValues["accept-language"][0]}
7、cookie
隐式对象cookie可以用来获取一个cookie。这个对象表示当前HttpServlertRequest中所有cookie的值。例如为了获取名为jsessionid的cookie值,要使用以下表达式:
${cookie.jsessionid.value}
为了获取jsessionid的路径值,要使用以下表达式:
${cookie.jsessionid.path}
8、initParam
隐式对象initParam用于获取上下文参数的值。例如,为了获取名为password的上下文参数值,可以使用以下表达式:
$[initParam.password}
$[initParam["password"]}
看到这里,大家应该很明确EL表达式只能通过内置对象取值,也就是只读操作,如果想进行写操作的话就让后台代码去完成,毕竟EL表达式仅仅是视图上的输出标签罢了。
五 使用其他EL运算符
除了.和[]运算符,EL还提供了其它运算符:算术运算符、关系预算法、逻辑运算符、条件运算符、以及empty运算符。使用这些运算符,可以进行不同的运算,但是由于EL的目的是方便免脚本JSP页面的编程,因此,除了关系运算符外,这些EL运算符的用处都很有限。
1、算术运算符
算术运算符有5种:
- 加法(+);
- 减法(-);
- 乘法(*);
- 除法(/或div)
- 取余/取模(%和mod)
除法和取余运算符都有两种形式,与XPath和ECMAScript是一致的。
注意:EL表达式的计算按优先级从高到低、从左到右进行。下列运算符是按优先级递减顺序排列的:
- */div%mod;
- +-;
这表示*、/、div、%以及mode运算符的优先级是同级的,+与-的优先级是同级的,但第二组运算符的优先级小于第一组运算符。因此,表达式:
${1+2*3}
的运算结果是7,而不是9。
注意:在EL表达式中的"+"只有数学运算的功能,没有连接符的功能,它会试着将运算符两边的操作数转换为数值类型,进而进行数学加法运算,然后将结果输出。若出现\${"a"+"b"}则会出现异常。如果想将两个字符串连接可以使用\${"a"+="b"}。
2、关系运算符
下面是关系运算符列表:
关系运算符 |
说明 |
范例 |
结果 |
== 或 eq |
等于 |
\${5==5}或\${5eq5} |
true |
!= 或 ne |
不等于 |
\${5!=5}或\${5ne5} |
false |
< 或 lt |
小于 |
\${3<5}或\${3lt5} |
true |
> 或 gt |
大于 |
\${3>5}或\${3gt5} |
false |
<= 或 le |
小于等于 |
\${3<=5}或\${3le5} |
true |
>= 或 ge |
大于等于 |
\${3>=5}或\${3ge5} |
false |
表达式语言不仅可在数字与数字之间比较,还可在字符与字符之间比较,字符串的比较是根据其对应UNICODE值来比较大小的。
注意:在使用EL 关系运算符时,不能够写成:
${param.password1} = =${param.password2}
或者
${ ${param.password1 } = = ${param.password2 } }
而应写成
${ param.password1 = =param.password2 }
3、逻辑运算符
下面是逻辑运算符列表:
逻辑运算符 |
范例 |
结果 |
&&或and |
交集\${A && B}或\${A and B} |
true/false |
||或or |
并集\${A || B}或\${A or B} |
true/false |
!或not |
非\${! A }或\${not A} |
true/false |
4、条件运算符
E条件运算符的语法如下:
${statement?A:B}
如果statement的计算结果为true,那么该表达式的输出结果就是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"}
5、empty运算符
empty 运算符主要用来判断值是否为空(NULL,空字符串,空集合)。下面是一个empty运算符的使用范例:
${empty X}
如果X为null,或者说X是一个长度为0的字符串,那么该表达式将返回true。如果X是一个空Map、空数组或者空集合,它也将返回true。否则,将返回false。
6、字符串连接运算符
+=运算符用于练级字符串,例如,以下表达式打印a+b的值:
${a+=b}
7、分号操作符
;操作符用于分割两个表达式。
六 引入静态属性和静态方法
我们可以使用EL表达式引用在任何Java类中定义的静态字段和方法。但是,在JSP页面中引用静态字段或方法之前,必须使用page伪指令导入包或类包。java.lang包是一个例外,因为它是自动导入的。
我们可以使用page指令导入java.time包:
<%@ page import="java.time.*"%>
或者,导入单个类:
<%@ page import="java.time.LocalDate"%>
然后,就可以使用EL表达式引用LocalDate类的静态now()方法:
Today is ${LocalDate.now()}
七 创建Set、List和Map
使用EL表达式可以动态的创建Set、List和Map。创建一个Set的语法如下:
{comma-delimited-elements}
例如,如下表达式创建一个5个数字的Set:
${{1,2,3,4,5}}
创建一个List语法如下:
[comma-delimited-elements]
例如,如下表达式创建一组花名的List:
${["Aster","Carnation","Rose"]}
最后,创建一个Map的语法为:
[comma-delimited-key-value-entries]
如下为一组国家及其首都的Map:
${{"Cannada":"Ottawa","China":"Beijing","France":"Paeis"}}
八 操作集合
EL 3.0带来了很多新特性。其中一个主要的贡献是操纵集合的能力。你可以通过调用流方法将集合转换为流来使用此功能。
下面展示如何将列表转换为流,假设myList是一个java.util.List:
${myList.stream()}
大部分流的操作会返回另一个流,因为可以形成链式操作:
${myList.stream().operation-1().operation-2().toList()}
在链式操作的末尾,通常调用toList()方法,以便打印或格式化结构,以下小结介绍了你可以对流执行的一些操作。
1、toList
toList()方法返回一个List,它包含与当前流相同的成员。调用此方法的主要目的是轻松地打印或操作流元素。下面是一个将列表转换为流,并返回列表的示例:
${[100,200,300].stream().toList()}
当然这个例子也没什么用,稍后在接下来的小节中,你将看到更多的例子。
2、toArray
与toList()类似,但返回的是一个Java数组,同样,在数组中呈现元素通常是有用的,因为许多Java方法将数组作为参数。这里是一个toArray()的例子:
${["one","two","three"].stream().toArray()}
与toList()不同,toArray()不打印元素,因此次,toList()更经常使用。
3、limit
limit()方法限制流中元素的数量。
名为cities的List包含7个城市:
[Paris, Strasbourg, London, New York, Beijing, Amsterdam, San Francisco]
下面的代码将元素的数量限制为3:
${cities.stream().limit(3).toList()}
执行时,表达式将返回此列表:
[Paris, Strasbourg, London]
如果传递给limit()方法的参数大于元素的数量,则返回所有的元素。
4、sorted
此方法对流中的元素进行排序,例如,这个表达式:
${cities.stream().sorted().toList()}
返回如下排序后的列表:
[Amsterdam, Beijing, London, New York, Paris, San Francisco, Strasbourg]
5、average
此方法返回流中所有元素的平均值。其返回值是一个Optional对象,它可能为null。需要调用get()获取实际值。
此表达式返回4.0:
${[1,3,5,7].stream().average().get()}
6、sum
才方法计算流中所有元素的总和。例如,此表达式返回16:
${[1,3,5,7].stream().sum()}
7、count
此方法返回流中元素的数量。例如,次表达式返回7:
${[1,3,5,7].stream().count()}
8、min
此方法返回流中元素中的最小值。同average()方法一样,其返回值是一个Optional对象,因此你需要调用get()方法来获取实际值。
例如,此表达式的返回值为1;
${[1,3,100,1000].stream().min().get()}
9、max
此方法返回流中元素中的最大值。同average()方法一样,其返回值是一个Optional对象,因此你需要调用get()方法来获取实际值。
例如,此表达式的返回值为1000;
${[1,3,100,1000].stream().max().get()}
10、map
此方法将流中的每一个元素映射到另一个流中的另一个元素,并返回该流。此方法接受一个lambda表达式。
例如,此映射方法使用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]
11、filter
此方法根据lambda表达式过滤流中的所有元素,并返回包含结果的新流。
例如,以下表达式测试城市是否以“S”开头,并返回所有的结果:
${cities.stream().filter(x->x.startsWith("S").toList())}
它产生的列表如下所示:
[Strasbourg, San Francisco]
12、forEach
此方法对流中的所有元素执行操作,它返回void。
例如,此表达式将城市中的所有元素打印到控制台:
${cities.stream().forEach(x->System.out.println(x))}
部分代码:
<% List<String> cities= Arrays.asList("Paris","Strasbourg","London","New York","Beijing","Amsterdam","San Francisco"); out.print(cities); //只在当前页面有效 pageContext.setAttribute("cities", cities); %> <br/> 1:${cities}<br/> 2:${cities.stream().map(x->x.toUpperCase()).toList()}<br/> 3:${cities.stream().sorted().toList()}<br/> 4:${cities.stream().filter(x->x.startsWith("S")).toList()}<br/> 5:${cities.stream().forEach(x->System.out.println(x))}<br/>
注意:需要使用tag伪指令导包:
<%@ page import="java.util.*" %>
输出:
九 格式化集合
由于EL定义了如何写表达式而不是函数,因此无法直接打印或格式化集合,毕竟,打印和格式化不是EL负责的领域。然而,打印和格式化是两个不能忽视的重要任务。
最简单的方法就是使用forEach()方法,以下代码可以再tomcat 8以上运行:
<ul> ${cities.stream().forEach(x->pageContext.out.println("<li>"+=x+="</li>"))} </ul>
遗憾的是,这在GlassFish4中不起作用,所以forEach()不能通用。
但是我们可以使用以下两种解决方案,虽然不像forEach()那么优雅,但是在所有主要的servlet容器上都可以使用。下面我们将介绍两种格式化集合的方法。
1、使用HTML注释
该解决方案适用于Java SE7版本以上。
List字符串表示形式如下:
[element-1,element-2,...]
现在,如果我想在HTML中呈现列表元素,需要这么写:
<ul> <li>element-1</li> <li>element-2</li> ... </ul>
现在,你可能已经注意到了每个元素必须转向 <li>element-n</li>。那么我们应该如何做?
我们还记得map()方法么,它可以将流中的每一个元素映射到另一个流中的另一个元素,并返回该流。因此我们可以利用map()方法转换每个元素。所以,代码可以这么写:
${myList.stream().map(x->"<li>"+=x+="</li>").toList()}
这样,我们将会得到如下的List:
[<li>element-1</li>,<li>element-2</li>,...]
足够接近,但任然需要删除括号和逗号。遗憾的是,我们无法控制列表的字符串表示。但是好在我们可以使用HTML注释。
所以,下面这个例子:
<ul> <!-- ${cities.stream().map(x->" --> <li>"+=x+="</li> <!--").toList()}--> </ul>
上面代码${cities.stream().map(x->"<li>"+=x+="</li>").toList()}中,把<li>"+=x+="</li>以外的代码全部使用<!-- -->注释掉,即只保留列表元素,其他EL语法注释掉。
结果如下所示:
<ul> <!-- [ --> <li>Paris</li> <!--, --> <li>Strasbourg</li> <!--, --> <li>London</li> <!--, --> <li>New York</li> <!--, --> <li>Beijing</li> <!--, --> <li>Amsterdam</li> <!--, --> <li>San Francisco</li> <!--]--> </ul>
可以看到这段代码有效的注释掉了括号和逗号。虽然结果看起来有点混论,但是它是有效的HTML,更重要的是,它能工作。下面是页面的显示效果:
2、使用String.join()
这第二个解决方案适用于Java SE8以上版本,该方法之所以有效,因为EL 3.0允许引入静态方法。在Java SE8中,String类新增了一些静态方法,其中一个方法就是join()。String类中有两个join()重载方法。但是这里需要使用到的一个方法如下所示:
public static String join(CharSequence delimiter,Iterable<? extends CharSequence> elements)
此方法返回用指定分隔符连接在一起的CharSequence元素组成的字符串。而java.util.Collection接口正好实现了Iterable接口,因此,可以讲Collection传递给join()方法。
例如,下面是如何将列表格式化成HTML有序列表:
<ol> ${"<li>"+=String.join("</li><li>",cities)+="</li>"} </ol>
此表达式适用于至少有一个元素的集合,如果你可能要处理一个空集合,这里有一个更好的表达式:
<ol> ${empty cities?"":"<li>" +=String.join("</li><li>",cities.stream().sorted().toList()) +="</li>"} </ol>
十 格式化数字
要格式化数字,你可以利用EL 3.0中允许引用静态方法的能力。String类的format()静态方法可以用来格式化数字。
例如,以下表达式返回带有两个小数点的数字:
${String.format("%-10.2f%n",125.178) }
更多格式化规则可以查阅java.text.DecimalFormat的javadoc文档。
十一 格式化日期
可以通过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。输出如下:
十二 在JSP 2.0及更高版本中配置EL
有了EL,JavaBean和定制标签,就可以编写免脚本的JSP页面了。JSP2.0及更高的版本中还提供了一个开关,可以使所有的JSP页面都禁用脚本。现在,软件架构师可以强制编写免脚本的JSP页面了。
另一方面,在有些情况下,可能还会需要在应用程序中取消EL。例如,正在使用与JSP 2.0兼容的容器(tomcat),却尚未准备将JSP应用程序升级到JSP 2.0,那么就需要这么做。在这种情况下,可以关闭EL表达式的计算。
注意:最初,JSP 2.0版本才开始支持EL,也就是说JSP 2.0版本之前的应用程序默认是不支持EL的。
1、实现免脚本的JSP页面
为了关闭JSP页面中的脚本元素,要在部署描述符中(web.xml文件)使用jsp-property-group元素以及url-pattern和scripting-invalid两个子元素。url-pattern元素定义禁用脚本要应用的URL样式。下面展示如何将一个应用程序中所有JSP页面的脚本都关闭:
<jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <scripting-invalid>true</scripting-invalid> </jsp-property-group> </jsp-config>
所以当执行带有JSP脚本的代码时:
<% out.println("在免脚本jsp页面中,可以使用这个jsp脚本么?"); %>
注意:在部署描述符中只能有一个jsp-config元素。如果已经为禁用EL而定义了一个jsp-property-group,就必须在同一个jsp-config下,为禁用脚本编写jsp-property-group。
2、禁用EL计算
在有些情况下,比如,当需要在JSP 2.0及更高版本的容器中部署JSP 1.2应用程序(该版本默认不支持EL)时,可能就需要禁用JSP页面中的EL计算了。此时,出现的EL结构,就不会作为EL表达式进行计算。目前有两种方式可以禁用JSP中的EL计算。
(1) 可以将page指令的isELIgnored属性设置为true,像这样:
<%@ page isELIgnored="true"%>
isELIgnored属性的默认值为false,如果想在一个或者几个JSP页面中关闭EL表达式计算,建议使用isELIgnored属性。
(2) 可以在部署描述符中使用jsp-property-group元素。jsp-property-group元素是jsp-config元素的子元素。利用jsp-property-group可以将某些设置应用到应用程序中的一组JSP页面中。
为了利用jsp-property-group元素禁用EL运算,还必须有url-patter和el-ignored两个子元素。url-pattern元素用于定义EL禁用要应用的URL样式。el-ignored元素必须设置为true。
下面举一个例子,展示如何在名为noEl.jsp的jsp页面中禁用EL计算。
<jsp-config> <jsp-property-group> <url-pattern>/noEl.jsp</url-pattern> <el-ignored>true</el-ignored> </jsp-property-group> </jsp-config>
此时,noEl.jsp页面<body>内容如下:
<body> ${1+3} </body>
输出如下:
无论是将page指令的isELIgnored属性设置为true,还是其URL与el-ignored为true的jsp-property-group的URL模式向匹配,都将禁用JSP页面中的EL计算。假设将一个JSP页面中的page指令的isELIgnored属性设置为false,但其URL与在部署描述符中禁用了EL计算的JSP页面的模式匹配,那么该页面的EL计算也将被禁用。
此外,如果使用的是与Servlet2.3及更低版本兼容的部署描述符,那么EL计算已经默认关闭,即使使用的是JSP 2.0及更高版本的容器,也一样。
参考博客
[1]Spring MVC学习指南