无脚本的JSP
Web页面设计人员没必要懂Java,所以我们使用EL表达式。(这一章中所说到的性质,是指对象中的字段)
使用表达式语言(EL)和标准动作构建JSP页面
7.1 使用EL中的顶级变量编写一个代码片段。包括以下隐式变量:pageScope,requestScope,sessionScope和applicationScope;param和paramValues;header和headerValues;cookies;以及initParam。
7.2 使用以下EL操作符编写一个代码片段:性质访问操作符(.)和集合访问操作符([])。
7.3 使用以下EL操作符编写一个代码片段:算术操作符、关系操作符和逻辑操作符。
7.4 对于EL函数:使用EL函数编写一个代码片段;明确或创建用于声明EL函数的TLD文件结构;明确或创建一个代码示例来定义EL函数。
8.1 给定一个设计目标,使用以下标准动作创建一个代码片段:jsp:useBean(有以下属性:id、scope、type和class),jsp:getProperty和jsp:setProperty(包括所有属性组合)。
8.2 给定一个设计目标,使用以下标准动作创建一个代码片段:jsp:include,jsp:forward和jsp:param。
6.7 给定一个特定的设计目标,要求将一个JSP片段包含在另一个页面中,编写JSP代码使用最合适的包含机制(include指令或< jsp:include >标准动作)
使用与bean相关的标准动作
我们先假设有一个名为person的属性,它的值是一个Person类,这个类在foo包下并且有一个属性是name和一个方法是getName()。这个person对象在request作用域中,则如下。
使用脚本时:
<html><body>
<% foo.Persion p = (foo.Person) request.getAttribute("person");%>
Person is: <%= p.getName()%>
</body></html>
使用标准动作
<html><body>
<jsp:useBean id="person" class="foo.Person" scope="request"/>
Person is: <jsp:getProperty name="person" property="name">
</body></html>
上面两段代码是等价的,不过我们说过不准使用脚本代码。
分析jsp:useBean和jsp:getProperty
先看jsp:userBean,id是声明bean对象的标识符,class是声明对象的类型,scope说明对象的作用域(从request中拿出一个属性名为”person”的对象,这对象的类型是foo.Person类型的)。
在看jsp:getProperty,name表示具体的bean对象(与useBean中的id相匹配),property是只Person中的属性(从useBean中拿出一个id名为”person”的对象,获取这个对象的”name”属性)。
jsp:useBean还能创建一个bean
jsp:useBean可以有体
正如看到的,useBean可以有体。当然下面还有一个jsp:setProperty
,它可以出现在体外面,功能与getProperty相反。setProperty是设置属性。
<jsp:useBean id="person" calss="foo.Person" scope="page">
<jsp:setProperty name="person" property="name" value="Fred">
</jsp:useBean>
放在体内的代码,转换成servlet将会在这个对象为null的时候为这个对象new完之后再执行相应的操作。如果这个对象不为null的话就不会执行体内的操作。
jsp:useBean有体时生成的serlvet:
可以看出最后一行写的好复杂,但它的作用和person.setName("Fred")
等价。
多态的bean引用
我们先看看我们原来jsp:useBean生成的servlet代码:
继承的UML图:
如果我们使用跟原来一样的代码(很明显,抽象类不能实例化):
我们增加一个type属性(可以是一个class类型、抽象类或者一个接口):
这里指出:如果只有一个type属性,没有class属性的话,结果也会报错!
这里指出scope属性的默认值是”page”。
直接从请求到JSP,没有经过servlet
假设我们的表单是:
想要获得请求的参数就需要如下:
param属性
要获取请求的参数时,如果要像上面那样做,明显是不好的。
利用param属性,可以把bean的性质值设置为一个请求参数的值。只需指定请求参数!如下:
更好的方法
这样就方便了许多!!
还有更好的···
真的特别酷!一句话搞定所有事情。
自动转换基本类型的性质
如果是纯数字,那么String会自动转换成int。
如果属性不是String或基本类型
例如以下这种场景:
我需要打印出Person的Dog的name,又该如何呢?
这时候我们就需要表达式语言(EL)${person.dog.name}
。
它等价于<%= (foo.Person) request.getAttribute("person").getDog().getName()%>
JSP表达式语言(EL)剖析
如图中所说明的,除了pageContext比较特殊以外,其他都是映射。
使用点号.和括号[]
我们可以用${person.name}
访问一个bean和map。同样的用${person["name"]}
也可以访问一个bean和map。
那如果person是一个数组或者是一个List呢?又该如何?
可以使用括号。如下:
这里指出:数组和List中的String索引会被强制转换为int(如musicList[“1”]等价于musicList[1])。
这里还指出:如果[ ]中不是String直接量,那么就会进行计算(例如:musicList[0]=1,那么musicList[musicList[0]] = musicList[1])。
EL隐式对象
我们再次注意到最后一个pageContext对象不是一个map。那它拿来做什么呢?(这很重要!)
EL中的请求参数
想要从请求中获取对应的参数(param),如下:
看上去不错,我们在试试获取”host”首部(header):
我们再来获取一个HTTP请求方法:
好像不太行,获取不到request的方法。
requestScope不是请求对象
隐式的requestScope只是请求作用域属性的一个Map,而不是request对象本身!
想要得到HTTP方法,就需要先得到一个request对象。所以在于我们如何获取这个request对象呢?
通过pageContext来获得其他的一切。例如:${pageContext.request.method}
这样就可以通过request对象获取到HTTP方法。
需要注意的:
作用域隐式对象能救你
会出现这样一种情况:request.setAttribute("foo.person",p);
。我们如何在JSP中获取这个键所对应的值呢?
显然,用${foo.person.name}
是不行的,所以我们的隐式作用域对象派上用场了!可以使用:${requestScope["foo.person"].name}
,完美“救场”!
得到Cookie和初始化参数
我们除了cookie和初始化参数的相应隐式对象外,其他隐式对象我们都已经了解了。接下来我们说明cookie和初始化参数的隐式对象。
打印“userName” cookie的值
用脚本来完成:
<%
Cookie[] cookies = request.getCookies();
for(int i=0;i<cookies.length;i++){
if((cookies[i].getName().equals("userName"))){
out.println(cookies[i].getValue());
}
}
%>
用EL:${cookie.userName.value}
就可以了。
打印一个上下文初始化参数的值
在DD中配置:
<context-param>
<param-name>mainEmail</param-name>
<param-value>[email protected]</param-value>
</context-param>
然后再用脚本:<%= application.getInitParameter("mainEmail")%>
就可以了。
用EL:${initParam.mainEmail}
就可以了。
该注意的地方:
EL函数
如果我们需要在JSP中用EL表达式去调用一个方法来返回一个值,那又该如何呢?
这里指出:EL函数可以有参数,与普通方法一样,只是定义TLD时参数列表是(包名.类型)。例如:int function(java.util.Map)
需要做4步事情:
一幅图解释工作过程:
部署环境:
需要注意:(函数名不一定要和EL中的函数名一致,EL的函数名必须与function标签下的name标签的内容处于一致)
另一些EL操作符
EL能妥善处理null值
假设没有一个名为“foo”的属性,但确实有一个名为“bar”的属性,而且这个“bar”没有名为“foo”的性质或键。
如下(注意,foo是null,bar是一个对象,bar[foo]不是bar[“foo”],这里指出:如果是bar[“foo”]则将报错!):
JSP表达式语言(EL)复习
- EL表达式总是用大括号括起,而且前面有一个美元符($)前缀(例如:
${expression}
)。 - 表达式中第一个命名变量要么是一个隐式对象,要么是某个作用域(页面、请求、会话、应用作用域)中的一个属性。
- 点号操作符允许你使用一个Map键或一个bean(点号操作符的第一个字符后面可以有数字,但是不能有其他符号)。
- 点号右边只能放合法的Java标识符(例如:
${foo.1}
是不允许的)。 - [ ]操作符比点号功能更强大,因为利用[ ]可以访问数组和List,可以把包含命名变量的表达式放在括号里(例如:
${musicList["name"]}
),而且可以作任意层次的嵌套(例如:${musicList[musicList["name"]]}
)。 - 例如,如果musicList是一个ArrayList,可以是
${musicList[0]}
或${musicList["0"]}
来访问列表中的第一个值。EL并不关心列表索引加不加引号。 - 如果括号里的内容没有用引号引起来,容器就会进行计算。如果确实放在引号里,而不是一个数组或List的索引,容器就会把它看做是性质或键的直接量名(也就是bean或者Map)。
- 除了一个EL隐式对象(PageContext)外,其他隐式对象都是Map。从这些Map隐式对象可以得到任意4个作用域的属性(requestScope,responseScope,sessionScope,applicationScope)、请求参数值(param)、首部值(header)、cookie值和上下文初始化参数(initParam)。非映射的隐式对象pageContext,它是PageContext对象的一个引用。
- 不要把隐式EL作用域对象(属性的Map)与属性所绑定的对象混合一谈(例如:requestScope和request对象不是同一个东西,这种作用域对象有4个)。
- EL函数允许你调用一个普通Java类中的公共静态方法(函数名不一定与具体的方法名匹配,这个根据TLD文件来做)。
- 使用TLD(标记库描述文件)将函数名映射到一个具体静态方法。
- 要在JSP中使用函数,必须使用taglib指令声明一个命名空间(例如:
<%@ taglib prefix="mime" uri="/WEB-INF/foo.tld"%>
)。 - EL表达式调用函数的方式:
${前缀:方法()}
(例如:${mime:function()}
)。
可重用的模板部件
如果我有234个JSP文件并且它们都有同一个导航栏,如果我需要改变导航栏的代码,那么234个JSP都要改。这简直是噩梦。
JSP中有一个对应的处理机制,这就是包含(include)。
形式如下:
<html><body>
<!-- 在这里插入页眉文件 -->
Welcome to our size...
blah blah blah more stuff here...
<!-- 在这里插入页脚文件 -->
</html></body>
include指令
include指令告诉容器:复制所包含文件中所有内容,在把它粘贴到这个文件中,而且就放在这里…
jsp:include标准动作
上述两者的内部原理并不相同
<jsp:include page="Header.jsp" />
标准动作和<%@ include file="Header.jsp" %>
指令的内部原理并不同。
include指令在转换(.jsp变成.java)时发生jsp:include标准动作在运行时发生
很容易就能明白,指令是将其变为out.write()
输出,而标准动作是调用方法。
这里指出:指令的效率会比标准动作的高。
对应第一个请求的include指令
执行过程如下:
对应第一个请求的jsp:include标准动作
执行过程如下:
两个注意的地方
属性名不一样:
位置是否敏感(include指令是位置敏感的):
这里还说明了一个问题:不要把开始和结束HTML和body标记放在可重用部件中!设计和编写布局模板时,要假设它们会包含在其他页面中。
使用jsp:param定制包含的内容
如果我们希望页眉上有一个与上下文相关的子标题,它要依相应的页面而定,该怎么做呢?
有两种方法:
- 笨方法:把子标题的信息放在主页面上,作为页眉之后的第一个内容,也就是紧挨着放在页面中包含页眉的include的后面。
- 更聪明的办法:把子标题信息作为新的请求参数传递给所包含的页面!
第二种办法如下所示:
我们注意到jsp:include标准动作的体可以增加(或替换)请求参数,供被包含的片段使用。
jsp:forward标准动作
有这样一个问题,如果我的客户访问我的页面,但是还没有登录,我想让他转跳到登录页面,该怎么做呢?
虽然这不是MVC中视图的任务(很明显应该给控制器处理),但我们还是提供了这么一种操作,就是<jsp:forward>
。
有条件的转发
代码如下:
结果如下:
这里指出:利用<jsp:forward>
,缓冲区会在转发之前清空。
看下面这个有趣的问题:
是的,毫无疑问的你收货了一个异常。
初识JSTL标记
由于我们不能在JSP页面写脚本,那么如果我们需要对页面测试又该怎么办呢?
JSTL标记解决这个问题!如下:
我们看到,有一个taglib指令,并且它的prefix的属性值是”c”。我们先放着!
Bean相关标准动作复习(要点)
<jsp:useBean>
标准动作会定义一个变量,它可能是一个现有bean属性的引用,如果还不存在这样一个bean,则会创建一个新的bean,这个变量就是新bean的引用。<jsp:useBean>
必须有一个“id”属性,这个属性声明了JSP中引用bean时所用的变量名。- 如果
<jsp:useBean>
中没有“scope”属性,作用域默认为页面(page)作用域。 - “class”属性是可选的,它声明了创建一个新bean时使用的类类型。这个类型必须是公共的、非抽象的,而且有一个公共的无参数构造函数。
- 如果在
<jsp:useBean>
中放了一个“type”属性,bean必须能强制转换为这种类型。 - 如果有一个“type”属性,但是没有“class”属性,bean必须已经存在,因为你没有指定为这个新bean实例化哪个类类型。
<jsp:useBean>
标记可以有一个体,体中的内容会有条件的运行,只有当创建一个新的bean作为<jsp:useBean>
的结果时,才会运行体中的内容(这说明,指定或默认作用域中不存在有该“id”的bean)。<jsp:useBean>
体的主要作用是使用<jsp:setProperty>
设置新bean的性质。<jsp:userProperty>
必须有一个name属(它要与<jsp:useBean>
的“id”匹配),还有一个“property”属性必须有一个具体的性质名,或者是通配符“*”。- 如果没有包含“value”属性,只有当一个请求参数的名字与性质名匹配时,容器才会设置性质值。如果“property”属性使用通配符(*),容器会为所有与某个请求参数名匹配的性质设置值。(其他性质不受影响)
- 如果请求参数名与性质名不同,但是你想把性质的值设置为请求参数值,可以在
<jsp:setProperty>
标记中使用“param”属性。 <jsp:setProperty>
动作使用自省将“性质”匹配到一个JavaBean设置方法。如果性质是“*”,JSP将迭代处理所有请求参数来设置JavaBean性质。- 性质值可以是String或基本类型,
<jsp:setProperty>
标准动作会自动转换。
包含复习(要点)
- 通过使用两种包含机制之一(include指令或
<jsp:include>
标准动作),可以利用可重用的组件建立页面。 - include指令在转换时完成包含,只发生一次。所以如果包含的内容已经部署后不太可能改变,使用include指令就很合适。
- include指令实际上只是复制被包含文件中的所有内容,把它粘贴到有include指令的页面中。容器把所有被包含文件的内容合并起来,只编译一个文件来生成servlet。在运行时,有include指令的页面将成为一个“大”页面,就像是你自己把所有源代码键入一个文件中一样。
<jsp:include>
标准动作在运行时把包含页面的响应包含到原页面中。所以如果包含的内容在部署之后可能更新,include标准动作就很实用,此时不适用include指令。- 这两种机制都能包含静态html页面,也可以包含动态元素(例如,有EL表达式的JSP代码)。
- include指令是唯一一个对位置敏感的指令,所包含的内容会插入到页面中include指令所在的位置。
- include指令和include标准动作的属性命名不一致,指令使用“file”属性,而标准动作使用“page”属性。
- 在你的可重用组件中,一定要去掉开始和结束标记(html和body)。否则,生成的输出会有嵌套的开始和结束标记,对于这种嵌套的开始和结束标记,并非所有浏览器都能处理。设计和构造可重用部件时,要知道它们会包含/插入到别的页面中。
- 可以在
<jsp:include>
的体中使用<jsp:param>
标准动作设置(或替换)请求参数,用来定制所包含的文件。 - 尽管在这一章没有介绍,但是要知道,
<jsp:param>
也可以用在<jsp:forward>
标记的体中。 <jsp:param>
只能放在<jsp:include>
或<jsp:forward>
标准动作中。- 如果
<jsp:param>
中使用的参数名已经有一个值(作为请求参数),新值会覆盖原来的值。否则,就会向请求增加一个新的请求参数。 - 对被包含资源有一些限制:它不能改变响应状态码或设置首部。
<jsp:forward>
标准动作可以把请求转发到同一个Web应用中的另一个资源(就像使用RequestDispatcher一样)。- 发送转发时,会首先清空响应缓冲区!请求转发到目标资源会先清空输出。所以转发前挟制响应的所有内容都会清掉。
- 如果在转发之前先提交响应(例如,通过调用out.flush()),会把刷新输出的内容发送给客户,但是仅此而已。不会发送转发,原页面的余下部分不会得到处理。