1. EL的作用:
1) 即Expression Language的缩写,即表达式语言的意思;
2) 最主要用来简化一些简单的属性、请求参数、标头、Cookie等信息的获取,在设置JavaBean方面比JSP标准标签更加简洁;
3) 可以进一步减少页面中Scriptlet的分量;
4) 语法上是使用${ }包裹的表达式;
5) 所有EL表达式在JSP中的返回值都是纯字符串;
2. 获取请求参数并进行运算:
1) 要使用EL的隐式对象param来获取,然后用.运算符来存取指定的属性,比如${param.name},就是获取请求参数中名为"name"的参数的值;
2) 可以做加法运算,例如:${param.a + param.b},这里就用到了EL的算术运算符+,得到的结果是"3"这个字符串;
!!其实param.a等表达式在EL表达式中返回的也是字符串值,在这里如果param.a是"1",param.b是"2",那么上述的EL表达式实际上是${"1" + "2"},那这样就不是两个字符串相加了吗?结果应该是"12"啊?!
!!EL的算数运算符有一个特性,就是如果运算符的两个操作数刚好都是纯数字,例如"1"、"2.3"、"1.3e-4"等(所有C语言支持的常数写法JSP的EL都支持),就是做算数运算而不是字符串连接,然后将运算结果转化为字符串返回!!
3) 字面值与字符串:
i. 在JSP中字面值和字符串是有区别的,字符串必须是以" "括起来的;
ii. 字面值特指数字等,例如1就是一个字面值(没有" "),而"1"就是一个字符串;
iii. EL运算符在进行计算时会先判断左右两边是不是字符串(即"xxx"),如果是字符串则再判断可不可以转换成纯数字字面值,如果可以则转换成字面值(例如将"1.23"转换成1.23),最后再进行运算,最后将结果再转换为字符串返回给JSP页面;
iv. 计算过程举例:${param.a + param.b} -> ${"1.2" + "3.4"} -> ${1.2 + 3.4} -> ${4.6} -> "4.6";
3) 其次EL可以优雅地处理null空值的情况:
i. 在上述例子中,如果param.a没有出现在请求参数中,而param.b给出了值,那么param.a就自动变成null值,做运算时会直接忽略param.a(计算时直接当成0来处理);
ii. 其实在内部运算时,EL就是直接把null当做0来处理的,因此像${4 / null}或者${5 / 0}得到的结果就是Infinity(也是JSP的一种常数字面值),而${null + null}的结果就是0;
iii. 在EL中允许直接使用null,null其实和Infinity一样,也是一种常数字面值,例如${3 + null}等,在运算表达是中所有的null都会被当成0来处理;
!!! 可以看到在EL中即使操作数为空值也不会抛出异常,这通常在Java中很难办到:
a. 因为${param.a + param.b}如果要用Scriptlet来实现则需要这样编写:
<% String a = request.getParameter("a"); String b = request.getParameter("b"); out.write(Integer.parseInt(a) + Integer.parseInt(b)); %>b. 在这里如果a和b中有null值则parseInt方法会直接抛出空指针异常!!!
4) EL算术运算符的要求:
i. 上述EL的算术运算符像+、-、*、/等只能用作数学运算;
ii. 不能想当然地认为+可以用作字符串连接!!像${"sle" + "12x"}等,会直接抛出异常!!!
iii. EL算数表达式只能避免null值异常,但不能避免这种无法转换成数字字面值的异常!!!
3. .运算符的更多作用:
1) 不仅可以提取某个对象的属性,也可以调用对象的getter:
i. 例如获取EL隐式对象pageContext的request,则可以这样写:${pageContext.request};
ii. 其实pageContext并没有request这一属性,但是pageContext有getRequest这一getter,因此该EL表达式会被转译成pageContext.getRequest();
2) .运算符存取属性的规则:以target.xxx为例
i. 先在target中寻找有没有名为"xxx"的属性,如果有则直接返回;
ii. 如果没有,则寻找是否有名为getXxx的getter,如果有则变成调用getXxx方法;
3) .运算符可以连续存取对象并自动完成类型转换:
i. 例如用Java代码来编写获取当前请求方法的语句就是:<%= ((HttpServletRequest)pageContext.getRequest()).getMethod() %>
ii. 但是用EL表达式可以简化成这样:${pageContext.request.method},不仅可以连续调用getter,更能完成自动的类型转换!!!
4. 关闭EL:
1) 有时候在页面中使用了某种模板,而在该模板中用到了类似${ }的语法,这就导致了和EL表达式的直接冲突,因此就需要关闭EL的功能;
2) 可以使用page指示元素的isELIgnored属性进行设置,默认是false,就是不忽略,如果为true就是忽略,就关闭了EL功能;
3) 同样也可以在web.xml的<jsp-property-group>中的<el-ignored>标签中设置,true为忽略,false为不忽略;
4) 如果在指示元素和web.xml中同时设置了,则以指示元素为准,web.xml只是一种统一的默认部署行为;
5. 存取属性简介:前面已经演示过一部分了
1) 在对JavaBean进行getProperty操作时,使用JSP标准标签<jsp:getProperty>显得还是那么的冗长,但EL对其进行了非常大的简化;
2) 比如${user.name}就是调用范围属性名为"user"的getName方法,作用等价于<jsp:getProperty name="user" property="name" />
!!但是,这么做的前提就是<jsp:useBean>标签必须存在,否则user.name的user从何而来边不得而知了!!!
3) 在存取范围属性的属性时可以指定该范围属性属于哪个范围,这是就可以从pageScope、sessionScope等中指定,这些EL隐式对象分别对应着JSP的page、session、application等隐式对象,还是使用.运算符来指定范围属性:${pageScope.user.name};
!!如果不指定范围隐式对象,则会按照默认的page、request、session、application的顺序查找范围属性,就是指${user.name}就没有指定范围,因此会先查找page....;
6. 使用.运算符和[ ]运算符进行属性存取:
1) 这两个运算符左侧都是范围属性,因此都是对范围属性的属性进行存取;
2) 两种运算符左侧可以使用的数据类型:
i. .:JavaBean、Map对象;
ii. [ ]:JavaBean、Map、数组对象、List对象;
3) 首先看一下数组:
i. 如果设置了一个数组类型的范围属性(String[] arr = {...}; request.setAttribute("arr", names););
ii. 那么就可以直接用EL表达式以C语言随机访问数组的形式来存取,例如:${arr[0]};
4) List和数组类似:
i. 设置List范围属性:
List<String> names = new ArrayList<String>(); names.add("Peter"); names.add("Tom"); request.setAttribute("arr", names);ii. 则存取里面的元素时同样只能只是用[ ]:${arr[1]}等;
5) 接着看一下Map对象:
i. 如果设置了一个Map类型的范围属性:
Map<String, String> map = new HashMap<String, String>(); map.put("user", "Peter"); map.put("role", "admin"); request.setAttribute("login".map);ii. 那么可以这样进行存取,比如:${login["user"]}或者${login.user};
6) 最后是JavaBena:
i. 如果一个JavaBean "user"有一个getName()的getter;
ii. 那么存取的时候既可以使用.也可以使用[ ],例如:${user.name}或者${user["name"]}
7) 关于[ ]运算符:
i. 该运算符里可以使用两种类型的值,一种是字符串类型(即" "括起来的字符串),一种是索引,即整数字面值(比如1、2等,不可以是浮点数字面值!!);
ii. 如果为索引,则顾名思义,就是按照顺序值进行存取,如数组和List最明显了,而Map和JavaBean都没有索引类型的访问形式;
iii. 而其实更通用的做法是使用字符串了,如果为字符串则表示直接按照键名存取,特别是Map、JavaBen最明显不过了;
!!但是,数组和List也是有键名的,其索引的字符串形式就是其键名,比如${arr["1"]}就等于${arr[1]},而且数组和List也只接受"0"、"1"、"2"类型的“键名”,如果传入非法的键名,比如"axb"等会直接抛出异常;
iv. 因此,小结:Map、JavaBean的键名就是字符串,而数组、List的键名则是索引的字符串形式!!
8) 约定俗成的规范:
!!!虽然上面介绍了.和[ ]全部可用的形式,但并非实际编写代码的时候就能随心所欲地使用它们;
!!!一般对于数组和List的访问绝不会蠢到用${arr["0"]}的形式去存取,肯定是直接用索引方便了!!
i. 数组和List都使用索引进行存取,例如${arr[0]}不要写成${arr["0"]};
!!但是用字符串键名进行存取也是有一定的应用场合的,比如:${arr[param.index]},那就是从请求参数传来的索引一开始肯定是字符串类型的,比如?name=Peter&index=15,那么param.index返回的肯定就是"15"了,所以上面的EL就是${arr["15"]};
ii. Map使用[ ]指定键名进行存取,这符合一般编程习惯,Java、Python、C++等对Map的属性进行存取都是使用的[ ],因此[ ]对Map来说更加通用;
iii. 使用.对JavaBean进行存取;
7. EL隐式对象:
1) 首先是前面讲过的pageContext,对应于JSP的pageContext隐式对象,其本身就是一个JavaBean,因此可以轻松利用EL属性存取来调用getter;
2) 接着是属性范围隐式对象:即pageScope、requestScope、sessionScope、applicationScope,分别对应于JSP隐式对象page、request、session和application所代表的范围,注意EL的这4个隐式对象并不等价于JSP的4个隐式对象,EL的4个仅仅代表一种作用范围!!
3) 请求参数相关的隐式对象:
i. param:前面已经讲过,${param.xxx}等价于<%= request.getParameter("xxx") %>;
ii. paramValues:有些请求参数具有多个值(像窗体的服务狂等组件),${paramValues.favorite_food[1]}就等价于<%= request.getParameterValues("favorite_food")[1] %>,比如窗体发送的复选框的请求参数:?favorite_food=0&favorite_food=1...;
4) 标头相关的隐式对象:
i. header:${header["User-Agent"]等价于<%= request.getHeader("User-Agent") %>; // 标头背后使用Map存取
ii. headerValues:跟请求参数一样,有些标头具有多个值,${headerValues["Accept-Language"]}就等价于<%= request.getHeaders("Accept-Language") %>;
!!在标头中多个值并不像URL请求参数一样是用&连接的,而是重复使用多个标头,例如Accept-Language有多个值(一般可接受的语系有多种的时候),标头会这样写:
Accept-Language:xxx Accept-Language:yyy!!注意!其等价于getHeaders而不是"getHeaderValues"!!!!
5) 和Cookie相关的隐式对象:隐式对象为cookie,如果用户具有名为"name"的Cookie,则可以使用${cookie.name}来获取该Cookie的值(value);
6) 初始参数相关的隐式对象:
i. 对象为initParam,代表ServletContext的初始参数(即<context-param>中设置的<init-param>初始参数);
ii. ${initParam.xxx}等价于<%= servletContext.getInitParameter("xxx") %>;
8. EL运算符:
1) EL里可以进行一些算术运算、逻辑运算和关系运算,基本包括了所有编程语言中通用的运算符;
2) 前面讲过了,EL的所有运算符实际操作的都是字符串值,再拿到字符串值后再转化成相应的类型的数据再进行计算、比较或者逻辑运算;
3) 算术运算符有+、-、*、/、%这4种,其中/可以用div表示,%可以用mod表示,例如:${4 div 5}、${5 mod 3};
!!注意:null就表示0,遇到不会抛出异常,而字面值非数字的字符串都会抛出异常!!!!
4) 逻辑运算符:and、or、not表示与或非三种,对于EL的逻辑变量,只有true和false两种字面值,其字符串形式为"true"和“false”,其中任意一个字母大小写都随意,比如"True"、"tRuE"等都表示字面值true;
!!注意:所有字面值为非true或false的都将视为false而不会抛出异常!!!比如${null and true}、${12 and false}、${"xsdws" or false},里面的null、12、"xsdws"都将被译为false!!!
5) 关系运算符:提供常规的版本和符号版本,<(lt,less than)、>(gt,greater than)、==(eq,equal)、!=(ne、not equal)、<=(le,less than or equal)、>=(ge,greater than or equal);
!!注意!!!!
a. 除非给出数字的字面值,否则比较的就是字符串的大小!!!!
b. 也就是说,像${"12" > "9.999"}或者${param.a > param.b}比较的都是字符串大小(也就是按照ASCII值和顺序进行比较),如果这里param.a和param.b和前面一样,都是"12"和"9.999",则结果为false!!!因为字符串“12”比字符串"9.9999"要小;
c. 想要比较数字大小,那么两边必须都给出字面值,比如${12 > 9.99},这样返回的就是true了,比较的就是纯数字了;
!!!使用规范:要么就比较纯字符串,关系运算符两边都要是严格的字符串,要么就比较纯数字,两边都给出数字的字面值,不要两边字符串、数字字面值和null混用,这种混用带来的后果完全未知,且不会抛出异常!
!!比较类型需要注意的地方:
a. 对于浮点数和整型数,都是精确保存,比如${100 == 100.0}的结果就是true;
b. 可以比较字符,例如${'a' > 'b'},这就和C语言一样了,背后比较的是两个ASCII码的大小,实际上比较的是两个整型数的字面值;
c. EL运算符的优先级关系和Java保持一致,也可以使用括号( )来自行决定优先级;
6) 三目运算符:即? :,和Java的三目运算符完全一样;
9. 自定义EL函数:
1) 有时可能会在JSP中需要用到一些Java的方法,特别是一些静态公有的方法,此时可能不得不使用Scriptlet编写Java代码,比如<%= new Util.Date()%>;
2) 那我们当然希望这些方法的调用也可以使用EL进行简化,也就是说是否可以使用EL来调用一些自定义的方法呢?答案是肯定的,可以自定义EL函数;
3) 虽然EL并没有提供直接在JSP中定义函数的语法,但是我们知道JSP最终是要转换成Servlet的Java代码的,因此这些函数我们可以先在.java文件中定义,然后在EL的规范下在JSP中调用这些函数;
4) 自定义EL函数的大致步骤:
i. 在.java中编写类来包装这些函数(正常的Java方法);
ii. 在.tld文件中声明上面定义过的方法(将上面的函数定义为标签),.tld是标签库描述文件(Tag Library Descriptor);
iii. 在.jsp文件中用taglib指示元素让Web容器加载上面的.tld文件中的内容;
iv. 最后就可以在.jsp文件中以标签的形式调用该函数了;
!!接下来详解上述的每一步:
5) 首先定义Java方法:
i. 能做作为EL函数的方法并非随随便便的一个方法,而是有严格规定的;
ii. 首先该方法所属的Java类必须是public的,如果不是public外界则无法调用(这里的外界是只外面的文件,.jsp和定义其的.java文件属于不同的文件,.jsp最终会被转译为.java文件);
iii. 目标方法必须是public且为静态的!!public很好理解,因为是外界调用,而静态则是因为EL并没有提供创建Java对象的语句,如果为非静态方法的话,则必须先创建Java对象才能使用其方法,因此EL函数只支持静态的方法;
iv. 例如:
package com.lirx; import java.util.Collection; public class Util { public static int length(Collection collection) { return collection.size(); } }!!这里就定义了一个自定义的Util类,里面提供了一个静态方法,用来测一个集合中元素的个数;
6) 接着是定义TLD文件:
i. .tld即Tag Library Descriptor,即标签库描述文件;
ii. 该文件用于描述一个Java类下的方法如何被定义为EL标签函数,也就是说是一个EL函数的声明文件(可以理解为C语言的函数声明);
iii. 说白了该文件的作用就是描述一个Java方法是如何被定义为标签的!!
iv. 该文件的本质是一个XML文件,因此其语法是XML的;
v. 其顶层标签是<taglib>,表示一个标签库定义的开始,接着用到的标签都是<taglib>的子标签;
vi. 接着是标签库的版本<tlib-version>,目前只有1.0,因此该项的值就写1.0即可;
vii. 然后是标签库的短名<short-name>,可以认为是该标签库名称的一种缩写,这个根据需要取就行;
viii. 有短名就必然有长名,长名就是指该文件完整的URI名称,即提供怎样的URI才会访问加载到该.tld标签库文件,该项的标签为<uri>,例如<uri>http://lirx.com/util</uril>,就表示必须通过http://lirx.com/util才能访问并加载到该标签库文件;
!!那为什么要用URI作为标签库的完整名称呢?因为.tld文件是XML文件,最后是需要部署到Web容器中的,而Web容器中的所有网络资源都必须要有自己的URI(或者URL)进行统一资源定位;
!!想想看,不仅客户端请求资源需要用URI,就连JSP/Servlet之间的调配也需要用URI(只不过是相对于环境根目录的),因此在.jsp中要加载并使用标签库中的标签同样也需要用URI来定位它;
ix. 接着就是EL函数标签的定义了,其标签为<function>,接下来要用到的标签都是<function>标签的子标签;
x. 首先是函数描述:<description>,其值仅仅是一种对函数功能的描述,起作用就是一种代码文档,并不起到实际的作用;
xi. 接着是EL函数名称:虽然EL函数对应于一个Java静态方法,静态方法有自己的名字,但是作为EL函数调用的时候需要有一个自己的名称(就是EL函数名),这个名称需要重新起,可以和相应的Java方法名一样,也可以不一样,完全随意,但不过还是建议和Java方法名一样,这样不容易出现逻辑上的错误,标签就是<name>,接着上面的例子,该EL函数名可以起为:<name>length</name>;
xii. 然后是该函数对应的Java方法所属的类:标签是<function-class>,值必须给出完整的包路径,这里应该是<function-class>com.lirx.Util</function-class>,这样标签库就知道应该到哪个包里去加载该类了;
xiii. 最后就是EL函数对应的方法的方法签名了,其中返回值、参数的类型都必须给出完整的包路径名,使用的标签为<function-signatrue>,这里应该这样写:
<function-signature> int length(java.util.Collection) </function-signature>!!注意,方法签名的语法还是Java的语法,只不过不需要在末尾加分号!
7) 写好的.tld文件可以直接放在WEB-INF目录下,这样Web容器会自动加载并解析其中的内容,当然也可以放在jar文件,这种方法会在后面详细讲解;
8) 这里给出一个.tld的完整例子,还是对应上面的自定义的Util.length方法:lirx.tld
<?xml version="1.0" encoding="UTF-8"?> <taglib version='2.1' xmlns='http://java.sun.com/xml/ns/javaee' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd'> <tlib-version>1.0</tlib-version> <short-name>lirx</short-name> <uri>http://lirx.com/util</uri> <function> <description>Get Collection Length</description> <name>length</name> <function-class>com.lirx.Util</function-class> <function-signature> int length(java.util.Collection) </function-signature> </function> </taglib>!!其中taglib的一些属性如xmlns等是标签的命名空间,先不管;
9) 最后就是在.jsp中使用EL标签函数了:
i. 首先必须要使用指示元素taglib声明当前JSP页面中要使用到的标签库;
ii. 指示元素<%@taglib%>的属性有两个,一个是uri,即标签库.tld文件的URI,Web容器在加载标签库时需要先用URI来定位它,其次是prefix,就是给该标签库取一个命名空间,在使用EL函数标签时需要先加上这个命名空间才能正常调用函数;
!!标签的命名空间就是标签中以XX:打头的字符,例如<jsp:...>、<xmlns:...>中的jsp:和xmlns:都属于标签命名空间,表示使用的标签属于jsp、xmlns命名空间下的;
!!因为可以自定义各种各样的标签库,很难避免两个标签库中的标签不发生命名冲突,因此特别是在使用自定义的标签时最好是使用命名空间前缀;
iii. 这里,我们的taglib指示元素可以这样写:<%@taglib prefix="util" uri="http://lirx.com/util"%>,该URI就指向了上面定义的.tld标签库,并且给该标签库在当前JSP文件中取一个命名空间,就叫做"util";
vi. 最后就直接在JSP中使用该EL函数了,该EL函数标签的名字就是.tld中定义的function-name,只不过调用的时候需要加上命名空间"util"而已,由于这种函数调用写的是标签,因此参数不能以Java对象的形式出现,参数也必须用EL表达式才行,这里给出例子:
<%@ page contentType="text/html" pageEncoding="UTF-8"%> <%@taglib prefix="util" uri="http://lirx.com/util" %> <%@page import="java.util.*" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> <% List<String> arrs = new ArrayList<String>(); arrs.add("haha"); arrs.add("lala"); request.setAttribute("arr", arrs); %> ${util:length(requestScope.arr)} </body> </html>!!倒数第三行就是EL函数的调用;
!!这里为了方便就先写了一段Scriptlet来添加List容器类型的范围属性,然后通过EL表达式作为参数传入EL函数中,这里可以返回容器中元素的个数;