# Java Web 进阶
# 1. HttpSession
一个用户有且最多有一个 HttpSession,并且不会被其他用户访问到。HttpSession 对象在用户第一次访问网站时自动被创建,你可以通过调用 HttpServeltRequest 的 getSession()
方法获得该对象。
会话(Session)是一个比请求(Request)更“大”的概念。一个会话中可以包含一个或多个请求;一个请求必定是在某个会话中。
getSession()
方法会返回当前的 HttpSession,若当前没有,则创建一个返回。
可以通过 HttpSession 的 setAttribute()
方法将值放入 HttpSesion 中。
WARNING
- 放到 HttpSesion 中的值不仅限于 String 类型,可以是任意实现了 java.io.Serializable 接口的 java 对象。
- 其实,你也可以将不支持序列化的对象放入 HttpSession,只不过这样做会有隐患。
调用 setAttribute()
方法时,若传入的 name 参数此前已经使用过,则会用新值覆盖旧值。通过调用 HttpSession 的 getAttribute()
方法可以取回之前放入的对象。
所有保存在 HttpSession 的数据不会发送到客户端。容器为每个 HttpSession 生成唯一的表示,并将该标识发送给客户端,或创建一个名为 JSESSIONID 的 cookie,或者在 URL 后附加一个名为 jsessionid 的参数。在后续的请求总,浏览器会将该标识发送给客户端,这样服务器就可以识别该请求是由哪个用户发起的(这个过程无须开发人员介入)。
默认情况下,HttpSession 会在用户不活动一段时间之后自动过期,该时间由 web.xml 中的 session-timeout
元素配置,单位为分钟(如果不设置,则过期时间由容器自行决定)。此外,HttpSession还定义可一个 invalidate()
方法强制会话立即过期失效。
<session-config>
<session-timeout>2</session-timeout>
</session-config>
# 2. EL 表达式
JSP 2.0 的最重要特性就是表达式语言(EL),EL 的目的是帮助程序员编写无脚本的 JSP 页面。
最初 EL 表达式被创造出来是为了 JSTL 服务,配合 JSTL 使用的。不过从 JSP 2.0 开始即便项目中没有引入 JSTL,也可以(单独)使用 EL 。
# EL 的默认关闭
在 Servlet 3.0 以下版本中,EL 表达式的功能默认是关闭的。
如何判断你的项目使用的是哪个 Servlet ?可以查看你的 web.xml 配置文件的头部声明。
通常情况下,我们一般不会使用 3.0 以下(甚至是 3.0)版本,最常见的至少是 3.1 版本起。但是,有时我们通过开发工具去自动创建 Web 项目时,很有被创建出来的项目默认的 Servlet 版本会偏低。因为这些工具的模板会偏老旧。
如果因为某些原因,你使用的是低版本的 Servlet,记得要将 EL 表达式的功能打开。
在 JSP 的 page
指令中,通过 isELIgnored
属性可以在当前页面 启用/禁止 EL 表达式。
或者在 web.xml 作出全局性设置:
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<el-ignored>false</el-ignored>
</jsp-property-group>
</jsp-config>
# EL 语法
EL 表达式以 ${
开头,并以 }
结束,其结构为 ${ 表达式 }
,其计算结果的类型是一个『字符串』。
EL 表达式的结果值可以是任何类型,但是浏览器会将其值以字符串形式(toString 方法)的形式将其“替换”到 EL 表达式所处位置。
EL 表达式可以返回任意类型的值。如果 EL 表达式的结果是一个带有属性的对象,则可以利用 [ ]
或者 .
运算符来访问该属性。如果是使用 [ ]
,属性名需要用引号括起来。例如:
${object["propertyName"]}
${object.propertyName}
如果,propertyName 不是一个有效的 Java 变量名,例如:accept-language
,那么,此时只能使用 [ ]
语法,而不能使用 .
语法。
如果,对象的属性碰巧又是另一个对象,那么可以用 [ ]
,也可以使用 .
运算符来访问第二个对象的属性。例如:
${pageContext["request"]["servletPath"]}
${pageContext.request["servletPath"]}
${pageContext.request.servletPath}
${pageContext["request"].servletPath}
如果 object 的类型是一个 Map,那么,这里使用的是键值对的键:${object["key"]}
如果 object 的类型是一个 Array 或 List,那么这里使用的是其下标索引:${object[0]}
。这里的下标索引 0
没有使用 ""
,它必须是一个数字。
# EL 隐式对象
在 JSP 页面中,可以利用 JSP 脚本来访问 JSP 隐式对象,
注意,在页面上显示 EL 表达式的值时,不需要 out.print()
或者 <%= %>
,容器会执行 EL 表达式并将其结果“写在”它所在的位置。
隐含对象 | 描述 |
---|---|
pageContext | 当前 JSP 页面的 pageContext 对象 |
initParam | Application 的初始化参数,初始化参数通常是在 web.xml 中通过 <context-param> 及其子元素 <param-name> 和 <param-value> 配置项配置 |
param | 一个包含了所有请求参数的 Map,其中请求参数名为 key。不过,它无法处理一个请求参数,多个值的情况。通过 key 始终只有第一个值返回。 |
paramValues | 和 param 类似,不过它可以处理一个参数名有多个参数值的情况。不过,如果参数只有一个值,它的返回值仍然是一个数组 |
applicationScope | 包含了 ServletContext 对象中所有属性的 map,并用属性名作 key。 |
sessionScope | 包含了 HttpSession 对象中所有属性的 Map,并用属性名作 key。 |
requestScope | 包含了 HttpServletRequest 对象中所有属性的Map,并用属性名作key。 |
pageScope | 包含了全页面范围内的属性的 Map,并用属性名作 Key。 |
cookie | 包含了当前请求对象中所有 Cookie 对象的Map。以 Cookie 的名称作为 key,并且每一个 key 都映射到一个 Cookie 对象。 |
header | HTTP 请求信息头,字符串 |
headerValues | HTTP 信息头,字符串数组。对应一个请求名,多个请求值的情况。通过 key 取出的始终是数组。 |
E L表达式所提供的隐式对象中,并没有 request、response、session、application、out 等这些JSP中所存在的隐式对象。这是 EL 隐式对象与 JSP 隐式对象的区别。
不过 EL 的隐式对象中有 pageContext(和 JSP 中一样),通过它我们依旧可以访问到上述这些 JSP 中直接提供,但 EL 中没有直接提供的对象。
EL 表达式中可以使用 + 、- 、*、/、% 五种算数运算符。
EL 表达式中可以使用 &&、||、! 三种逻辑运算符。
EL 表达式中可以使用 ==、!=、>、>=、<、<= 六种关系运算符。
EL 表达式中提供了一个 empty 运算符专门用于 空和非空 的判断(当然,你也可以用 == null 判断)。例如:
${empty X}
X 为 null,或者 X 是一个空字符串,或者 X 是一个空数组、空List、空Map、空Set,它都将返回 true 。
# 3. JSTL 标签库
JSP 标准标签库(JSTL)是一个定制标签库的集合,它的出现是为了实现呈现层与业务层的分离功能。使用 JSTL(结合EL表达式)在绝大多数情况下,JSP 页面中不再需要“嵌入”Java 代码(scriplet)。
使用 JSTL 需要额外导入 jstl 库。
使用 JSTL 需要额外导入 jstl 库。
使用 JSTL 需要额外导入 jstl 库。
根据 JSTL 标签所提供的功能,可以将其分为 5 个类别:核心(Core)标签、格式化标签、SQL 标签、XML 标签、JSTL 函数。其中以核心(Core)标签最为常用。
使用不同类别的 JSTL 库,需要在 JSP 页面的头文件中做出相应的“声明”。例如:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
如果忘记引入 jstl 库,上述声明会报错。
# c:out
<c:out>
标签用来显示一个表达式的结果,与 <%= ... %>
作用相似,它们的区别就是 <c:out>
标签可以直接通过 "." 操作符来访问属性。
out 的语法有两种形式:
形式一:
<c:out value = "VALUE" [escapeXml = {true | false}] [default = "默认值"] />
形式二:
<c:out value = "VALUE" [escapeXml = {true | false}]> 默认内容 </c:out>
其中 value 属性是必要部分。
# c:set
set 标签常见 2 种形式/作用:
- 第一种用于创建一个有界变量,并用 value 属性在其中定义一个要创建的字符串或者现存的有界对象:
<c:set value="VALUE"
var="VAR NAME"
[scope="{ page | request | session | application }"]/>
- 第二种形式是设置有界变量的属性。
例如:
<%
request.setAttribute("username", "tom");
session.setAttribute("", "");
application.setAttribute("", "");
%>
<c:set var="password" value="123" scope="request"></c:set>
<c:set var="key" value="value" scope="session"></c:set>
<c:set var="key" value="value" scope="application"></c:set>
${requestScope.username}, ${requestScope.password}
<c:set target="TARGET"
property="PROPERTY NAME"
value="VALUE"/>
注意,这种形式中,target 属性中 必须使用一个 EL 表达式 来引用这个有界变量。
例如:
<%
Department dept = new Department(10, "Testing","BeiJing");
request.setAttribute("dept", dept);
%>
<c:set target="${requestScope.dept}" property="dname" value="System"></c:set>
<p> ${requestScope.dept.deptno} </p>
<p> ${requestScope.dept.dname} </p>
<p> ${requestScope.dept.loc} </p>
# c:remove
remove 标签用于删除有界变量。
<c:remove var = "VAR NAME"
[scope="{ page | request | session | application }"] />
# c:if
if 标签是对某一个条件进行测试,假如结果为 true,就处理它的 body content 。另外,测试的结果可以保存在一个 Boolean 对象中,并创建有界变量来引用这个 Boolean 对象。
if 的语法有两种形式。第一种是没有 body content:
<c:if test="bool 型 EL 表达式"
[var="VAR NAME"]
[scope="{ page | request | session | application }"]/>
第二种形式使用了一个 body content:
<c:if test="bool 型 EL 表达式" [var="变量名"] [scope="{ page | request | session | applicationi }"]>
body content
</c:if>
# c:choose、c:when 和 c:otherwise
choose-when-otherwise 标签的作用与 Java 中的 switch-case-default 类似。
choose 标签中必须嵌有一个或多个 when 标签,并且每个 when 标签都有一种可以计算和处理的情况。otherwise 标签则用于默认的条件块。
choose 和 otherwise 标签没有属性。when 标签必须带有定义的测试条件 test 属性,来决定是否应该处理 body content 。
<c:choose>
<c:when test="${boolean 表达式}"> ... </c:when>
<c:when test="${boolean 表达式}"> ... </c:when>
...
<c:otherwise> ... </c:otherwise>
</c:choose>
# c:forEach
forEach 标签会无数次反复便利 body content 或者对象集合。
forEach 标签的语法有两种。第一种形式是固定次数地重返 body content:
<c:forEach [var="VAR NAME"] begin="BEGIN" end="END" step="STEP">
body content
</c:forEach>
这种形式与集合对象无关。类似于 Java 代码中的 for (int i = 0; i < 10; i++)
例如:
<c:forEach var="item" begin="1" end="5" step="2">
<p>hello world ${item} </p>
</c:forEach>
第二种形式用于遍历对象集合。类似于 Java 代码中的 for (String string : list)
<c:forEach items="COLLECTIONS" [var="变量名"] [varStatus="变量名"]>
body content
</c:forEach>
对于每一次遍历,forEach 标签都将创建一个有界变量,变量名通过 var 属性定义,可在 body content 中使用。 该有界变量只能在 body content 部分使用。
forEach 标签有一个类型为 javax.servlet.jsp.jstl.core.LoopTagStatus 的变量 varStatus,这个变量有一个 count 属性,其中记录了当循环遍历的次数,该数值从1开始。
例如:
<c:forEach items="${requestScope.depts}" var="dept" varStatus="loop">
<p>第 ${loop.count} 个:${dept.deptno}, ${dept.dname}, ${dept.loc}</p>
</c:forEach>
# fmt 进行日期格式化
引入申明:
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<fmt:formatDate value="<%=new Date()%>" type="date" pattern="yyyy-MM-dd"%/>
<fmt:formatDate value="${date}" type="date" pattern="yyyy-MM-dd"//>
# 4. 乱码问题
# 判断字符串的编码格式
注意,由于存在重码现象,以下方案并不严谨 。
// 由于字符编码存在重叠区,所以一个字符/字符串有多种编码可能,是完全正常合理的。
public static String getEncoding(String str) {
String encode[] = new String[]{
"ASCII",
"ISO-8859-1",
"GB2312", // GB2312 是 GBK 的一种“具体情况”。
"GBK",
"UTF-8", // UTF-8 是 Unicode 的一种“具体情况”。
"UTF-16", // UTF-16 是 Unicode 的一种“具体情况”。
"Unicode",
};
String ret = "";
for (int i = 0; i < encode.length; i++) {
try {
if (str.equals(new String(str.getBytes(encode[i]), encode[i]))) {
ret = ret + encode[i] + " ";
}
} catch (Exception ex) {
}
}
return ret.equals("") ? "Other" : ret;
}
乱码出现在三种场景:
GET 请求提交的中文数据,在 Servlet 中输出乱码。
POST 请求提交的中文数据,在 Servlet 中输出乱码。
返回给浏览器的中文数据,在页面显示乱码。
提前声明,虽然同样是提交的数据在 Servlet中 显示为乱码,但是 GET / POST 发生乱码的原因和解决乱码的方案并不相同,不能混用 。
# 获取 GET 请求中的数据,打印乱码
默认情况下,浏览器发送给 Tomcat 的数据都是以 ISO-8859-1
进行编码后的字节流。
在 get 请求中,Tomcat 以何种方式看待、解析接收的这些字节流取决于 server.xml 中的一个配置:<Connector port="8080" ... URIEncoding="xxx"
。
默认情况下 URIEncoding="ISO-8859-1"
,即 Tomcat 默认以 ISO-8859-1 编码解析所收到的 get 请求发送来的字节流,从而转换为字符流,即字符串。
但是这里有一个关键性问题:Java 字符串是 UTF-8 编码。所以,当你通过 req.getParameter("");
获得一个中文字符串时,这个字符串是 ISO-8859-1 编码,但一旦你使用 System.out.println()
打印时,Java 会当它是一个 UTF-8 编码的字符串。这就是出现乱码原因。
解决问题的办法有两个:
修改配置文件,让 Tomcat 以 UTF-8 格式 看待/解析 接收到的字节流。
不改变配置文件。获得 Tomcat 的 ISO-8859-1 字符串后,生成对应的 UTF-8 字符串,再进行输出打印。
# 获取 POST 请求中的数据,打印乱码
serverl.xml 中 URIEncoding="xxx"
设置影响不到 post 请求提交来的数据!适用于 get 请求的修改配置方案,对 post 请求无效!
Tomcat 如何 看待/解析 post 请求发送来的数据,取决于 req.setCharacterEncoding("");
。如果未曾调用该方法,Tomcat 以 ISO-8859-1 编码解析接受的post请求数据。同上,随后一旦调用 System.out.println()
就会出现乱码。
解决问题的办法有两个:
在调用
req.getParameter()
前调用req.setCharacterEncoding("UTF-8");
, 让 Tomcat 以 UTF-8 格式 看待/解析 接收到的字节流不设置req字符编码。获得 Tomcat 的 ISO-8859-1 字符串后,生成对应的 UTF-8 字符串,再进行输出打印。
如同 get 的方案一对 post 无效,post 的方案一同样解决不了 get 的乱码问题。
但是,显而易见,两者方案二是相同的,这也是后续 过滤器 的主要使用场景。
# 中文字符串,输出到页面显示乱码
以何种编码将字符转换为字节流后,发送给浏览器,取决于 resp.setCharacterEncoding("xxx");
。没有调用时,默认使用 ISO-8859-1 编码格式。
但是 ISO-8859-1 格式最大的问题在于:它是一个单字节编码,不支持中文。
所以,使用 Tomcat 默认的 ISO-8859-1 编码向浏览器发送的字节流中,如果含有中文信息,浏览器无法正确显示成对应中文。
解决办法只有一个,设置 Tomcat 发送数据的编码格式,并且,resp.setCharacterEncoding()
必须在 PrintWriter out = resp.getWriter();
之前,否则设置无效(因为你已经得到了使用 ISO-8859-1 编码的 out 对象了)。
# setContentType 和 setCharacterEncoding
在 servletResponse.setCharacterEncoding() 方法的注释中写到了它和 setContentType 之间的关系:
- 如果 response 的字符集已经在 setContentType(或 setLocale)方法中指定,那么你再调用 setCharacterEncoding 方法则会覆盖掉之前的设置。
.setContentType("text/html; charset=UTF-8")
等同于.setContentType("text/html")
+.setCharacterEncoding("UTF-8")
。
另外,在使用 setCharacterEncoding 会出现失效的情况,通常是因为 2 点原因:
- 你在
.getWriter()
之后才调用.setCharacterEncoding()
,这样对字符集的设置太晚,自然是无效的。 - 你有
.setCharacterEncoding()
,但是没有调用.setContentType()
,这样对字符集的设置也是无效的。