1. Servlet 初窥
1.1 Servlet 介绍
1.1.1 Servlet 简介
Servlet 是用 Java 编写的服务器端程序。
狭义的 Servlet 是指 Java 语言实现的一个接口,广义的 Servlet 是指任何实现了这个 Servlet 接口的类。
功能:处理请求和发送响应。
- 获取用户提交的表单数据(如注册、登录等)
- 呈现后端处理结果
- 动态创建网页(很少使用)
1.1.2 案例 - HelloServlet
HelloServlet.java ```java package com.demo;
import java.io.IOException; import java.io.PrintWriter;
import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
@WebServlet(“/HelloServlet”) public class HelloServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取输出流
PrintWriter out = response.getWriter();
out.println("Hello Servlet!");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
代码分析:
- [**@WebServlet **](https://www.yuque.com/WebServlet)** 注解**(标注)
- 当请求该 Servlet 时,服务器就会自动读取当中的信息。
- 如果注解为@WebServlet("/HelloServelt"),则表示访问该 Servlet 的请求路径为 http://localhost:8080/HelloServelt/**HelloServelt**([http://服务器地址:8080/项目名/Servlet的url)。](http://xn--zfru1gfr6bz63i:8080/%E9%A1%B9%E7%9B%AE%E5%90%8D/Servlet%E7%9A%84url%EF%BC%89%E3%80%82)
- 代码中省略了 urlPatterns 属性名,完整的写法应该是:[@WebServlet(urlPatterns ](https://www.yuque.com/WebServlet(urlPatterns) = “/HelloServelt”),如果在 [@WebServlet ](https://www.yuque.com/WebServlet) 中需要设置多个属性,必须给属性值加上属性名称,中间用逗号隔开,否则会报错。
- 以前在使用 Servlet 时,需要在 web.xml 中对 Servlet 进行配置。Servlet 3.0 开始支持已注解的形式对 Servlet 进行配置。
- **HelloServlet**:类名,Servlet 本质为一个 Java 类,所以首字母大写。Servlet 的命名方式一般为 _Servlet _或者_ _Controller。
- **HttpServlet**
- HttpServlet 是 Servlet 接口的一个实现类,同时又是 GenericServlet 的子类,是在 GenericServlet 的基础上做了增强。
- HTTP 的请求方式包括 DELETE,GET,OPTIONS,POST,PUT 和 TRACE,在HttpServlet 类中分别提供了相应的服务方法, 它们是分别是:doDelete(),doGet(),doOptions(),doPost(),doPut() 和 doTrace()
- 如果继承了 HttpServlet 没有实现任何的 doXxx 方法则会抛出一个异常405。
- **Serialize**
- 含义
- **序列化**:将对象写入到 IO 流中
- **反序列化**:从 IO 流中恢复对象
- **意义**:序列化机制允许将实现序列化的 Java 对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。
- **使用场景**:所有可在网络上传输的对象都必须是可序列化的,比如 RMI(remote method invoke,即远程方法调用),传入的参数或返回的对象都是可序列化的,否则会出错;所有需要保存到磁盘的 Java 对象都必须是可序列化的。
- **建议**:程序创建的每个 JavaBean 类都实现 Serializeable 接口。
- serialVersionUID
- serialVersionUID 是 Java 为每个序列化类产生的版本标识,可用来保证在反序列时,发送方发送的和接受方接收的是可兼容的对象。
- 如果接收方接收的类的 serialVersionUID 与发送方发送的 serialVersionUID 不一致,进行反序列时会抛出 InvalidClassException。
- 序列化的类可显式声明 serialVersionUID 的值,如上面代码中所示。
- 当显式定义 serialVersionUID 的值时,Java 根据类的多个方面(具体可参考 Java 序列化规范)动态生成一个默认的 serialVersionUID 。
- 尽管这样,还是建议在每一个序列化的类中显式指定 serialVersionUID 的值,因为不同的 JDK 编译很可能会生成不同的 serialVersionUID 默认值,进而导致在反序列化时抛出 InvalidClassExceptions 异常。
- 所以,为了保证在不同的 JDK 编译实现中,其 serialVersionUID 的值也一致,可序列化的类必须显式指定 serialVersionUID 的值。
- 另外,serialVersionUID 的修饰符最好是 private,因为 serialVersionUID 不能被继承,所以建议使用 private 修饰 serialVersionUID。
- 案例在本文档的最后:案例 - Person 序列化
- doGet() 和 doPost()
- doGet() 和 doPost() 方法都有两个参数,分别为 HttpServletRequest 和 HttpServletResponse 类型。
- 这两个参数分别用于表示客户端的请求和服务器的响应。
- 通过 HttpServletRequest,可以从客户端中获得发送过来的信息;通过 HttpServletResponse,可以让服务器对客户端做出相应,最常用的就是向客户端发送信息。
- 如果在浏览器中直接输入地址来访问 Servlet 资源,属于使用 Get 方式访问。
<a name="ioVBv"></a>
## 1.2 HTTP GET 请求剖析
**login.html**
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form action="LoginServlet" method="get">
<div>
<label for="username">用户名: </label>
<input type="text" id="username" name="username" placeholder="请输入用户名">
</div>
<div>
<label for=""password"">密码: </label>
<input type="text" id="password" name="password" placeholder="请输入密码">
</div>
<button type="submit">提交</button>
</form>
</body>
</html>
LoginServlet
package com.demo;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=UTF-8");
String username = request.getParameter("username");
String password = request.getParameter("password");
PrintWriter out = response.getWriter();
out.print("<h1>" + username + "</h1>");
out.print("<h1>" + password + "</h1>");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
运行系统,进行以下操作:
- 在地址栏中输入 login.html 进行访问:http://localhost:8080/HelloServlet/login.html
打开浏览器的 Console,查看 Network 标签页,发现以下信息:
其中,Request Method:GET,表示该请求的类型为 Get 请求,即当我们在地址栏中输入资源路径直接访问时,发起的请求类型为 Get 请求。
- 在 login.html 页面中输入用户名“guo”和密码“123”,点击“提交”按钮后,地址栏显示的信息如下:
http://localhost:8080/HelloServlet/LoginServlet?username=guo&password=123
打开浏览器的 Console,查看 Network 标签页,发现以下信息:
其中的 Request URL 与 地址栏中的值一致,分析如下:
- http://localhost:8080/HelloServlet/LoginServlet:表示访问的资源路径。
- ?:表示要向该资源传递参数。
- 参数跟在?后面,以“键-值”对的形式传递,“=”左边是键名,“=”右边是键值,不同的参数之间用“&”分隔。
1.3 HTTP POST 请求剖析
修改 login.html
LoginServlet.java 保持不变。<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>登录</title> </head> <body> <form action="LoginServlet" method="post"> <div> <label for="username">用户名: </label> <input type="text" id="username" name="username" placeholder="请输入用户名"> </div> <div> <label for=""password"">密码: </label> <input type="text" id="password" name="password" placeholder="请输入密码"> </div> <button type="submit">提交</button> </form> </body> </html>
进行如下操作:
- 在地址栏中输入 login.html 进行访问:http://localhost:8080/HelloServlet/login.html。
在 login.html 页面中输入用户名“guo”和密码“123”,点击“提交”按钮后,打开浏览器的 Console,查看 Network 标签页,发现以下信息:
- Request Method:POST,与 form 表单的 action 属性值一致。
- Post 请求所携带的参数显示在最下面的“Form Data”中。
考点:GET 请求和 POST 请求有什么区别?
第一层理解:
参考链接:https://www.w3school.com.cn/tags/html_ref_httpmethods.asp
第二层理解:
- GET 和 POST 本质上没有区别。
- GET 和 POST 是什么?HTTP 协议中的两种发送请求的方法。
- HTTP 是什么?HTTP 是基于 TCP/IP 的关于数据如何在万维网中如何通信的协议。
HTTP 的底层是 TCP/IP。所以 GET 和 POST 的底层也是 TCP/IP,也就是说,GET/POST 都是 TCP 链接。GET 和 POST 能做的事情是一样一样的。你要给 GET 加上 request body,给 POST 带上 url 参数,技术上是完全行的通的。
那么,“第一层理解”里的那些区别是怎么回事?
在我大万维网世界中,TCP 就像汽车,我们用 TCP 来运输数据,它很可靠,从来不会发生丢件少件的现象。但是如果路上跑的全是看起来一模一样的汽车,那这个世界看起来是一团混乱,送急件的汽车可能被前面满载货物的汽车拦堵在路上,整个交通系统一定会瘫痪。为了避免这种情况发生,交通规则 HTTP 诞生了。HTTP给汽车运输设定了好几个服务类别,有 GET, POST, PUT, DELETE 等等,HTTP 规定,当执行 GET 请求的时候,要给汽车贴上 GET 的标签(设置 method 为GET),而且要求把传送的数据放在车顶上(url 中)以方便记录。如果是 POST 请求,就要在车上贴上POST的标签,并把货物放在车厢里。当然,你也可以在 GET 的时候往车厢内偷偷藏点货物,但是这是很不光彩;也可以在 POST 的时候在车顶上也放一些数据,让人觉得傻乎乎的。HTTP 只是个行为准则,而 TCP 才是 GET 和 POST 怎么实现的基本。
但是,我们只看到 HTTP 对 GET 和 POST 参数的传送渠道(url还是requrest body)提出了要求。“第一层理解”里关于参数大小的限制又是从哪来的呢?
在我大万维网世界中,还有另一个重要的角色:运输公司。不同的浏览器(发起 http 请求)和服务器(接受 http 请求)就是不同的运输公司。 虽然理论上,你可以在车顶上无限的堆货物(url 中无限加参数)。但是运输公司可不傻,装货和卸货也是有很大成本的,他们会限制单次运输量来控制风险,数据量太大对浏览器和服务器都是很大负担。业界不成文的规定是:超过的部分,恕不处理。如果你用GET服务,在 request body 偷偷藏了数据,不同服务器的处理方式也是不同的,有些服务器会帮你卸货,读出数据,有些服务器直接忽略,所以,虽然 GET 可以带 request body,也不能保证一定能被接收到哦。
好了,现在你知道,GET 和 POST 本质上就是 TCP 链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。
- GET和POST还有一个重大区别。
简单说:
GET产生一个TCP数据包;POST产生两个TCP数据包。
长的说:
- 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
- 而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
也就是说,GET只需要汽车跑一趟就把货送到了,而POST得跑两趟,第一趟,先去和服务器打个招呼“嗨,我等下要送一批货来,你们打开门迎接我”,然后再回头把货送过去。
因为POST需要两步,时间上消耗的要多一点,看起来GET比POST更有效。因此Yahoo团队有推荐用GET替换POST来优化网站性能。但这是一个坑!跳入需谨慎。为什么?
(1)GET与POST都有自己的语义,不能随便混用。
(2)据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。
(3)并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。
参考链接:https://www.jianshu.com/p/678ff764a253
第三层理解:https://zhuanlan.zhihu.com/p/38217343
1.4 HTTP 响应剖析,到底什么是“MIME类型”
HTTP 响应包括一个首部和一个体:
- 首部信息告诉浏览器使用了什么协议,请求是否成功,以及体中包括何种类型的内容。
- 体包含了让浏览器显示的具体内容。
案例:
·
- 地址栏:http://localhost:8080/HelloServlet/LoginServlet (POST 和 GET 请求的响应头一样)
结论:
- 响应首部中 Content-Type(内容类型)的值称为 MIME 类型。
- MIME 类型告诉浏览器要接收的数据是什么类型,这样浏览器才能知道如何显示这些数据。
注意:MIME 类型值与 HTTP 请求“Accept”首部中所列的值相关。
1.5 URL 剖析
http://localhost:8080/HelloServlet/login.html
- http:协议,告诉服务器使用什么通信协议。
- localhost:服务器,所请求物理服务器的唯一域名。这个域名会映射到一个唯一的 IP 地址(现代域名解析中,为了负载均衡,一个域名可能会对应多个 IP 地址)。IP 地址是一串数字,数字之间用“.”隔开,形式为“xx.xx.xx.xx”。在这里也可以指定一个 IP 地址而不是域名,不过域名更容易记忆。(127.0.0.1)
- 8080:端口。一个服务器可以支持多个端口。一个服务器应用由端口标识。如果在 URL 中没有指定端口,默认端口则是端口80,这正是 Web 服务器的默认端口,即对于80端口,可以不写;但是对于其他端口,必须填写。
- HelloServlet:路径,所请求的资源在服务器上的路径。因为 Web 上大多数较早的服务器都采用 Unix 系统,因此还是 Unix 语法来描述 Web 服务器的目录层次结构(Windows使用“\”来表示目录层级结构)。
- login.html:资源,所请求的内容的名字。可以是一个 HTML 页面,一个 Servlet,一个图像、PDF、音频、视频,或者服务器能够提供的任何资源。这部分是可选的,如果 URL 中没有这一部分,大多数 Web 服务器都会默认地查找 index.html(依据 web.xml)。
1.6 端口解析
TCP 端口只是一个数字而已。
端口是一个16位数,标识服务器硬件上一个特定的软件程序。
问题:一台服务器,即可以接收用户的普通的HTTP请求,也可以接收 Telnet(远程连接)请求、FTP(文件传输)请求和 POP3(邮件)请求,服务器该如何区分每种不同的请求?
答案:区分的依据就是端口号。
端口表示与服务器硬件上运行的一个特定软件的逻辑连接,可以把端口看作是唯一的标识符。
- 服务器上有65536个端口(0~65535);
- 端口并不表示一个可以插入物理设备的位置,端口只是表示服务器应用的“逻辑”数而已。
- 如果没有端口号,服务器就没有办法知道客户想连接哪个应用。
- 每个应用可能有自己特定的协议。
从0到1023的 TCP 端口号已经保留,由一些众所周知的服务使用(包括我们最关心的“NO.1”—端口80)。我们自己的定制服务器程序不要使用这些端口。
2. Servlet 生命周期
2.1 什么是生命周期
问题:为什么 Servlet 中没有 Main() 方法?
回答:Servlet 是运行在服务器上的,其生命周期由 Servlet 容器(Tomcat)负责管理。
问题:生命周期长什么样子?
回答:Servlet 生命周期是指 Servlet 实例从创建到响应客户请求直至销毁的过程。
Servlet 生命周期中最重要的3个阶段,由3个 Servelt API管理并体现。
- init():用于 Servlet 初始化。当容器创建 Servlet 实例后,会自动调用此方法。
- service():
- 用于服务处理。当客户端发出请求时,容器会自动调用此方法进行处理,并将结果返回到客户端。
- 有2个参数,分别使用 ServletRequest 接口 和 ServletResponse 接口的对象来处理请求和响应。
- destory():用于销毁 Servlet。当容器销毁 Servlet 实例时自动调用此方法,释放 Servlet 实例,清除当前 Servlet 所持有的资源。
Servlet 生命周期:
- 装载 Servlet:该项操作一般是动态执行的,有些服务器提供了相应的管理功能,可以在启动的时候就装在 Servlet。
- 创建一个 Servlet 实例:容器创建 Servlet 的一个实例对象,即调用构造器。
- 初始化:init()。
- 服务:service()。
- 销毁:destory()。
2.2 案例
LoginServlet
package cn.edu.qfnu.servlet;
import java.io.IOException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/LifeCycleServlet")
public class LifeCycleServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final String METHOD_GET = "GET";
private static final String METHOD_POST = "POST";
public LifeCycleServlet() {
super();
System.out.println("LifeCycleServlet Constructor...");
}
public void init(ServletConfig config) throws ServletException {
System.out.println("LifeCycleServlet init...");
}
public void destroy() {
System.out.println("LifeCycleServlet destroy...");
}
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("LifeCycleServlet service...");
String method = request.getMethod();
if (method.equals(METHOD_GET)) {
doGet(request, response);
} else if (method.equals(METHOD_POST)) {
doPost(request, response);
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("LifeCycleServlet doGet...");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("LifeCycleServlet doPost...");
}
}
3. 请求转发和重定向
请求转发和重定向是 Servlet 处理完数据后进行跳转的两种主要方式。
3.1 请求转发
3.1.1 请求转发的概念及特点
- 将请求再转发到另一个页面或者 Servlet(页面和 Servlet 统一称为资源)。
- 此过程依然在 request 对象作用域范围之内。
- 转发后的地址栏内容不变。
- 请求转发使用 RequestDispatcher 接口中的 forward() 方法来实现,该方法可以把请求转发到另一个资源,并让该资源对浏览器的请求进行响应。
RequestDispatcher 接口有两个方法:
- forward() 方法:请求转发,可以从当前 Servlet 跳转到其他资源(页面或者 Servlet)。
- include() 方法:引入其他 Servlet。
注意:RequestDispatcher 是一个接口,那么如何使用接口的中的方法呢?
接口实例化!
通过使用 HttpRequest 对象的 getRequestDispatcher() 方法可以获得 RequestDispatcher 接口的实例对象。
3.1.2 案例
通过 LoginServlet 获取用户名和密码,交由 ValidationServlet 进行校验,输出校验结果。
LoginServlet.java
package cn.edu.qfnu.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取用户提交的用户名
String uname = request.getParameter("user_name");
// 获取用户提交的密码
String upwd = request.getParameter("pass_word");
request.setAttribute("username", uname);
request.setAttribute("password", upwd);
request.getRequestDispatcher("ValidationServlet").forward(request, response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
doGet(request, response);
}
}
ValidationServlet.java
package cn.edu.qfnu.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/ValidationServlet")
public class ValidationServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = (String) request.getAttribute("username");
String password = (String) request.getAttribute("password");
if ("admin".equals(username)) {
if ("123".equals(password)) {
PrintWriter out = response.getWriter();
out.print("Welcome!");
}
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
doGet(request, response);
}
}
3.2 重定向
3.2.1 重定向的概念及特点
- 页面重新定位到某个新地址。
- 之前的 request 失效,进入一个新的 request。
- 跳转后地址栏的内容将变为新的指定地址。
- 通过 HttpServletResponse 对象的 sendRedirect() 来实现。
- 该方法用于生成 302 响应码和 Location 响应头,从而通知客户端去重新访问 Location 响应头中指定的 URL。
3.2.2 案例
通过 LoginServlet 获取用户名和密码,交由 ValidationServlet 进行校验,如果用户和密码正确,则跳转到首页,否则跳转到错误信息提示页面。
LoginServlet.java ```java package cn.edu.qfnu.servlet;
import java.io.IOException; import java.io.PrintWriter;
import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
@WebServlet(“/LoginServlet”) public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 1L;
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取用户提交的用户名
String uname = request.getParameter("user_name");
// 获取用户提交的密码
String upwd = request.getParameter("pass_word");
request.setAttribute("username", uname);
request.setAttribute("password", upwd);
response.sendRedirect("index.html");
// request.getRequestDispatcher(“ValidationServlet”).forward(request, response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
doGet(request, response);
}
}
**ValidationServlet.java**
```java
package cn.edu.qfnu.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/ValidationServlet")
public class ValidationServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = (String) request.getAttribute("username");
String password = (String) request.getAttribute("password");
String path = "login.html";
if ("admin".equals(username)) {
if ("123".equals(password)) {
path = "index.html";
}
}
response.sendRedirect(path);
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
doGet(request, response);
}
}
4. 会话状态
HTTP 是一种无状态的协议,这就意味着 Web 服务器并不了解同一用户以前请求的信息,即 Web 服务器是没有记忆的。一旦发送了响应(即完成了一次通信过程,请求+响应),Web 服务器就会忘了你是谁,服务器上不会保留任何客户端的信息。换句话说,Web 服务器不记得你曾经做过请求,也不记得它们曾经给你发出过响应。
但是,对于现在的 Web 应用而言,需要记录特定客户端的一系列请求之间的关系,以便于对客户的状态进行管理,以便于对客户的状态进行跟踪。
比如在电商网站中,Web 服务器回味每个客户配置一辆购物车,购物车需要一直跟随客户,以便于客户将商品放入购物车中,而且,不同客户的购物车,绝对不能混肴,即客户 A 添加的商品,不能出现在客户 B 的购物车中。
常用的会话跟踪技术:
- Cookie 技术
- Session 技术
-
4.1 Cookie
4.1.1 Cookie 的概念
Cookie 是服务器发送给客户端(一般是浏览器)的一小段文本,保存在浏览器所在机器的内存或磁盘上。
- Cookie 通过 HTTP Header 从服务器端返回到客户端,即 Cookie 首先由服务器创建,然后在服务器向客户端发送响应时,把 Cookie 一起发送给客户端。
Cookie 产生过程.pptx
Cookie 是会话跟踪的一种解决方案,典型应用就是记住用户登录状态。
Cookie 常用的方法:
方法 | 说明 |
---|---|
Cookie uCookie = new Cookie(“user”, username); | 创建保存用户名的 Cookie |
Cookie[] cookies = request.getCookies(); | 获取客户端所保存的该网站的所有 Cookie |
getMaxAge() / setMaxAge() | 读取/设置 Cookie 的过期时间。 对于 setMaxAge(): 1. 参数为一个负值,表示这个 Cookie 在用户退出浏览器后马上过期; 1. 参数为 0,表示删除此 Cookie |
getName() / getValue() | 获取 Cookie 的名字 / 值 |
response.addCookie(uCookie) | 将 Cookie 发送到客户端 |
4.1.2 案例
login.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form action="CookieServlet" method="get">
<div>
<label for="username">用户名: </label>
<input type="text" id="username" name="username" placeholder="请输入用户名">
</div>
<div>
<label for="password">密码: </label>
<input type="text" id="password" name="password" placeholder="请输入密码">
</div>
<div>
<input type="checkbox" name="saveCookie" value="yes">记住登录状态
</div>
<button type="submit">提交</button>
</form>
</body>
</html>
CookieServlet.java
package com.demo;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/CookieServlet")
public class CookieServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=UTF-8");
String username = request.getParameter("username");
String password = request.getParameter("password");
// String saveCookie = request.getParameter("saveCookie");
String[] saveCookie = request.getParameterValues("saveCookie");
Cookie usernameCookie = new Cookie("username", username);
Cookie passwordCookie = new Cookie("password", password);
if (saveCookie!=null && "yes".equals(saveCookie[0])) {
usernameCookie.setMaxAge(60);
passwordCookie.setMaxAge(60);
} else {
usernameCookie.setMaxAge(0);
passwordCookie.setMaxAge(0);
}
response.addCookie(usernameCookie);
response.addCookie(passwordCookie);
PrintWriter out = response.getWriter();
out.print("Welcome: " + username);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
LoginHtmlServlet.java
package com.demo;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/LoginHtmlServlet")
public class LoginHtmlServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=UTF-8");
String username = "";
String password = "";
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if ("username".equals(cookie.getName())) {
username = cookie.getValue();
}
if ("password".equals(cookie.getName())) {
password = cookie.getValue();
}
}
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE html>\r\n"
+ "<html>\r\n"
+ "<head>\r\n"
+ "<meta charset=\"UTF-8\">\r\n"
+ "<title>登录</title>\r\n"
+ "</head>\r\n"
+ "<body>\r\n"
+ " <form action=\"CookieServlet\" method=\"get\">\r\n"
+ " <div>\r\n"
+ " <label for=\"username\">用户名: </label>\r\n"
+ " <input type=\"text\" value=\"" + username + "\" id=\"username\" name=\"username\" placeholder=\"请输入用户名\">\r\n"
+ " </div>\r\n"
+ " <div>\r\n"
+ " <label for=\"password\">密码: </label>\r\n"
+ " <input type=\"text\" value=\"" + password + "\" id=\"password\" name=\"password\" placeholder=\"请输入密码\">\r\n"
+ " </div>\r\n"
+ " <div>\r\n"
+ " <input type=\"checkbox\" name=\"saveCookie\" value=\"yes\">记住登录状态\r\n"
+ " </div>\r\n"
+ " <button type=\"submit\">提交</button>\r\n"
+ " </form>\r\n"
+ "</body>\r\n"
+ "</html>");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
Cookie 的本质就是键值对儿,即通过键值对儿的形式保存值,一个想要保留的值,对应一个 Cookie。这就带来了两个问题:
(1)安全
客户端相对于服务器来说,更容易被黑客攻破,但目前还没有发送应为 Cookie 所带来的重大安全问题,这主要也是由 Cookie 的安全机制所决定的:
- Cookie 不会以任何方式在客户端被执行
- 浏览器会限制来自同一个网站的 Cookie 数目
- 单个 Cookie 的长度是有限制的
- 浏览器限制了最多可以接受的Cookie数目
(2)效率
使用 Cookie 可以将请求的状态信息传递到下一次请求中,但是如果传递的状态信息较多,将极大地降低网络传输效率,并且回增大服务器端程序地处理难度,为此,各种服务器端技术都提供了一种将会话状态保存在服务器端的方案,即 Session(会话)技术。
4.2 Session
4.2.1 Session 的概念
- Servlet 容器为每一个 HttpSession 对象分配一个唯一的标识符,称为 SessionID,同时将 SessionID 发送到客户端,由浏览器负责保存此 SessionID。
- 当客户端再发送请求时,浏览器会同时发送 SessionID,Servlet 容器可以从请求对象中读取 SessionID,根据 SessionID 的值找到相应的 HttpSession 对象。
通常服务器借助于 Cookie 把 SessionID 存储在浏览器进程中,在该浏览器进程下一次访问服务器时,服务器就可以从请求中的 Cookie 里获取 SessionID。此外,Session 还可以借助 URL 重写的方式在客户端保存 SessionID。
方法名 | 描述 |
---|---|
public void setAttribute(String name,Object value) | 将value对象以name名称绑定到会话 |
public object getAttribute(String name) | 获取指定name的属性值,如果属性不存在则返回null |
public void removeAttribute(String name) | 从会话中删除name属性,如果不存在不会执行,也不会抛处错误 |
public Enumeration getAttributeNames() | 返回和会话有关的枚举值 |
public void invalidate() | 使会话失效,同时删除属性对象 |
public Boolean isNew() | 用于检测当前客户是否为新的会话 |
public long getCreationTime() | 返回会话创建时间 |
public long getLastAccessedTime() | 返回在会话时间内web容器接收到客户最后发出的请求的时间 |
public int getMaxInactiveInterval() | 返回在会话期间内客户请求的最长时间.秒 |
public void setMasInactiveInterval(int seconds) | 允许客户客户请求的最长时间 |
ServletContext getServletContext() | 返回当前会话的上下文环境,ServletContext对象可以使Servlet与web容器进行通信 |
public String getId() | 返回会话期间的识别号 |
4.2.2 案例
login.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form action="SessionServlet" method="get">
<div>
<label for="username">用户名: </label>
<input type="text" id="username" name="username" placeholder="请输入用户名">
</div>
<div>
<label for="password">密码: </label>
<input type="text" id="password" name="password" placeholder="请输入密码">
</div>
<div>
<input type="checkbox" name="saveCookie" value="yes">记住登录状态
</div>
<button type="submit">提交</button>
</form>
</body>
</html>
SessionServlet.java
package com.demo;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@WebServlet("/SessionServlet")
public class SessionServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=UTF-8");
String username = request.getParameter("username");
String password = request.getParameter("password");
String[] saveCookie = request.getParameterValues("saveCookie");
HttpSession session = request.getSession();
if (saveCookie!=null && "yes".equals(saveCookie[0])) {
session.setAttribute("uname", username);
session.setAttribute("upwd", password);
} else {
session.removeAttribute("uname");
session.removeAttribute("upwd");
}
PrintWriter out = response.getWriter();
out.print("Welcome: " + username);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
LoginHtmlServlet.java
package com.demo;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/LoginHtmlServlet")
public class LoginHtmlServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=UTF-8");
String username = "";
String password = "";
HttpSession session = request.getSession();
Object unameAttribute = session.getAttribute("uname");
Object upwdAttribute = session.getAttribute("upwd");
if ( (unameAttribute != null) && (upwdAttribute != null) ) {
username = "" + unameAttribute;
password = "" + upwdAttribute;
}
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE html>\r\n"
+ "<html>\r\n"
+ "<head>\r\n"
+ "<meta charset=\"UTF-8\">\r\n"
+ "<title>登录</title>\r\n"
+ "</head>\r\n"
+ "<body>\r\n"
+ " <form action=\"SessionServlet\" method=\"get\">\r\n"
+ " <div>\r\n"
+ " <label for=\"username\">用户名: </label>\r\n"
+ " <input type=\"text\" value=\"" + username + "\" id=\"username\" name=\"username\" placeholder=\"请输入用户名\">\r\n"
+ " </div>\r\n"
+ " <div>\r\n"
+ " <label for=\"password\">密码: </label>\r\n"
+ " <input type=\"text\" value=\"" + password + "\" id=\"password\" name=\"password\" placeholder=\"请输入密码\">\r\n"
+ " </div>\r\n"
+ " <div>\r\n"
+ " <input type=\"checkbox\" name=\"saveCookie\" value=\"yes\">记住登录状态\r\n"
+ " </div>\r\n"
+ " <button type=\"submit\">提交</button>\r\n"
+ " </form>\r\n"
+ "</body>\r\n"
+ "</html>");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
4.3 URL 重写
有时,用户由于某些原因禁止了浏览器的 Cookie 功能,Servlet 规范中还引入了一种补充的会话管理机制,它允许不支持 Cookie 的浏览器也可以与 Web 服务器保持连续的会话。这种补充机制要求在需要加入同一会话的每个 URL 后附加一个特殊参数,其值为会话标识号(SessionID) 。
LoginHtmlServlet.java
package com.demo;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@WebServlet("/LoginHtmlServlet")
public class LoginHtmlServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=UTF-8");
String username = "";
String password = "";
HttpSession session = request.getSession();
Object unameAttribute = session.getAttribute("uname");
Object upwdAttribute = session.getAttribute("upwd");
if ( (unameAttribute != null) && (upwdAttribute != null) ) {
username = "" + unameAttribute;
password = "" + upwdAttribute;
}
System.out.println("session_id: " + session.getId());
System.out.println("url_ecode: " + response.encodeURL("SessionServlet"));
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE html>\r\n"
+ "<html>\r\n"
+ "<head>\r\n"
+ "<meta charset=\"UTF-8\">\r\n"
+ "<title>登录</title>\r\n"
+ "</head>\r\n"
+ "<body>\r\n"
+ " <form action=\"SessionServlet\" method=\"get\">\r\n"
+ " <div>\r\n"
+ " <label for=\"username\">用户名: </label>\r\n"
+ " <input type=\"text\" value=\"" + username + "\" id=\"username\" name=\"username\" placeholder=\"请输入用户名\">\r\n"
+ " </div>\r\n"
+ " <div>\r\n"
+ " <label for=\"password\">密码: </label>\r\n"
+ " <input type=\"text\" value=\"" + password + "\" id=\"password\" name=\"password\" placeholder=\"请输入密码\">\r\n"
+ " </div>\r\n"
+ " <div>\r\n"
+ " <input type=\"checkbox\" name=\"saveCookie\" value=\"yes\">记住登录状态\r\n"
+ " </div>\r\n"
+ " <button type=\"submit\">提交</button>\r\n"
+ " </form>\r\n"
+ "</body>\r\n"
+ "</html>");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
SessionServlet.java
package com.demo;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@WebServlet("/SessionServlet")
public class SessionServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=UTF-8");
String username = request.getParameter("username");
String password = request.getParameter("password");
String[] saveCookie = request.getParameterValues("saveCookie");
HttpSession session = request.getSession();
if (saveCookie!=null && "yes".equals(saveCookie[0])) {
session.setAttribute("uname", username);
session.setAttribute("upwd", password);
} else {
session.removeAttribute("uname");
session.removeAttribute("upwd");
}
PrintWriter out = response.getWriter();
out.print("Welcome: " + username);
out.print("<a href='" + response.encodeRedirectURL("TestServlet") + "'>跳转</a>");
// out.print("<a href='TestServlet'>跳转</a>");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
TestServlet.java
package com.demo;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@WebServlet("/TestServlet")
public class TestServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=UTF-8");
PrintWriter out = response.getWriter();
HttpSession session = request.getSession();
out.print(session.getAttribute("uname"));
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
5. 补充
5.1 Servlet 中文乱码
5.1.1 乱码产生的原因
背景知识:HTTP 协议中规定,数据的传输采用字节编码(Unicode)方式,即无论浏览器所提交的数据中,包含的中文是什么字符编码格式,一旦由浏览器经过 HTTP 协议传输,那么这些数据均以字节的形式上传给服务器。因为 HTTP 协议的底层使用的是 TCP 传输协议。TCP(Transmission Control Protocol:传输控制协议)是一种面向连接的、可靠的、基于字节流的、端对端的通信协议。在请求中,这些字节均以 % 开头,并以十六进制形式出现。如%5A%3D等。
乱码产生的原因:当用户通过浏览器提交一个包含 UTF-8 编码格式的两个字的中文请求时,浏览器会将这两个中文字符变为六个字节(一般一个 UTF-8 汉字占用三个字节),即形成六个类似 %8E 的字节表示形式,并将这六个字节。上传至 Tomcat 服务器。
Tomcat 服务器在接收到这六个字节后,并不知道它们原始采用的是什么字符编码。而 Tomcat 默认的编码格式为 IS0-8859-1。所以会将这六个字节按照【IS0-8859-1】的格式进行编码,编码后在控制台显示,所以在控制台会显示乱码。
Servlet 乱码分为 request 乱码和 response 乱码。
通俗地说,不管是request乱码还是response乱码,其实都是由于客户端(浏览器)跟服务器端采用的编码格式不一致造成的。
5.1.2 request 乱码
requst 乱码:浏览器向服务器发送的请求参数中包含中文字符,服务器获取到的请求参数的值是乱码。
requst 请求分为 GET 请求和 POST 请求,GET 和 POST 请求是不一样的,当发送 GET 请求时,其传递给服务器的数据是附加在 URL 地址之后的;而发送 POST 请求时,其传递给服务器的数据是作为请求体的一部分传递给服务器。所以两者处理乱码的方式是不一样的。下面分别对 GET 请求和 PSOT 请乱码问题进行处理。
- 如果浏览器以 GET 方式发送请求,请求参数在请求头存放,在请求协议包到达服务端之后,请求头内容是由 Tomcat 负责解析,Tomcat8、9 在解析数据时,默认采用的字符集UTF-8,所以如果浏览器以GET方式发送中文参数,此时在服务端不会出现中文乱码问题。也就是说,Tomcat 8、9 版本中文乱码问题已解决,但是 Tomcat7 依然存在这个问题。
如果浏览器以 POST 方式发送请求,请求参数在请求体存放,在请求协议包到达服务端之后,请求体内容是由对应请求对象 request 负责解码的。request 对象默认使用 IS0-8859-1 字符集。所以如果浏览器以 POST 方式方式发送中文参数,此时在服务端必会出现中文乱码问题。解决方法是改变 HTTP 请求体中的字符编码(对于 GET 无效,因为 GET 提交的信息在请求头中)。
request.setCharacterEncoding("UTF-8");
5.1.3 reponse 乱码
response乱码:服务器向浏览器发送的数据包含中文字符,浏览器中显示的是乱码。
response 有一个缓冲区编码,默认值为ISO-8859-1。
对于 response 乱码,只需要在服务器端指定一个编码字符集,然后通知浏览器按照这个字符集进行解码就可以了。对服务器的响应进行编码:
① response.setCharacterEncoding(“utf-8”);
② response.setContentType(“text/html;charset=utf-8”);
- 通知浏览器,服务器发送的数据格式(UTF-8)
① response.setHeader(“contentType”, “text/html;charset=utf-8”);
② response.setContentType(“text/html;charset=utf-8”);
问题:选择哪种结合方式?
①+①?
总结:
- 设置服务器端编码:response.setCharacterEncoding(“utf-8”);
- 通知浏览器,服务器发送的数据格式:response.setContentType(“text/html;charset=utf-8”);
注意:
response.setCharacterEncoding() 方法的使用前提是:之前必须要先使用 response.setContentType() 方法,response.setCharacterEncoding() 方法用于修改 ContentType 的 MIME 类型字符编码,如果之前使用response.setContentType() 设置了编码格式,则使用 response.setCharacterEncoding() 会覆盖之前的设置,二者结合使用方法如下:
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");
最终解决方案:
response.setContentType(“text/html;charset=utf-8”)
5.2 ServletConfig
ServletConfig 代表当前 Servlet 在 web.xml 中的配置信息(用的不多)。
在运行 Servlet 程序时,可能需要一些辅助信息,例如,文件使用的编码、使用 Servlet 程序的共享信息等,这些信息可以在 web.xml 文件中使用一个或多个
当 Tomcat 初始化一个 Servlet 时,会将该 Servlet 的配置信息封装到 ServletConfig 对象中(不能自己去创建 ServletConfig 对象),此时可以通过调用 init(ServletConfig config)方法将 ServletConfig 对象传递给 Servlet。进而,我们通过 ServletConfig 对象就可以得到当前 Servlet 的初始化参数信息。
这样做的好处是:如果将数据库信息、编码方式等配置信息放在web.xml中,如果以后数据库的用户名、密码改变了,则直接很方便地修改web.xml就行了,避免了直接修改源代码的麻烦。
ServletConfig 接口中定义了一系列获取配置信息的方法:
方法 | 说明 |
---|---|
String getInitParameter(String name) | 根据初始化参数名返回对应的初始化参数值 |
Enumeration getInitParameterNames() | 返回一个 Enumeration 对象,其中包含了所有的初始化参数名 |
ServletContext getServletContext() | 返回一个代表当前 Web 应用的 ServletContext 对象 |
String getServletName() | 返回 Servlet 的名字,即 web.xml 中 |
web.xml
<servlet>
<servlet-name>MyServletConfigServlet</servlet-name>
<servlet-class>com.demo.MyServletConfigServlet</servlet-class>
<init-param>
<param-name>username</param-name>
<param-value>admin</param-value>
</init-param>
<init-param>
<param-name>password</param-name>
<param-value>123</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>MyServletConfigServlet</servlet-name>
<url-pattern>/MyServletConfigServlet</url-pattern>
</servlet-mapping>
MyServletConfigServlet.java
package com.demo;
import java.io.IOException;
import java.util.Enumeration;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
//@WebServlet("/MyServletConfigServlet")
public class MyServletConfigServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
public void init(ServletConfig config) throws ServletException {
String paramValue = config.getInitParameter("username");
System.out.println("username = " + paramValue);
String paramName = null;
Enumeration<String> paramNames = config.getInitParameterNames();
while (paramNames.hasMoreElements()) {
paramName = paramNames.nextElement();
paramValue = config.getInitParameter(paramName);
System.out.println(paramName + " : " + paramValue);
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=UTF-8");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
5.3 ServletContext
5.3.1 简介
ServletContext 称为 Servlet 上下文,当 Tomcat 启动时,Tomcat 会为每个 Web 应用创建一个唯一的 ServletContext 对象,该对象代表当前的 Web 应用,并封装了当前 Web 应用的所有信息,这个对象全局唯一,而且该 Web 应用内部的所有 Servlet 都共享这个对象,所以叫全局应用程序共享对象。可以利用该对象获取 Web 应用程序的初始化信息、读取资源文件等。
5.3.2 ServletContext 生命周期
- 新 Servlet 容器启动的时候,服务器端会创建一个 ServletContext 对象。
- 在容器运行期间,ServletContext 对象一直存在。
- 当容器停止时,ServletContext 的生命周期结束。
6. 补充
6.1 案例 - Pserson 序列化
Person.java - 普通类
package com.demo;
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public Person() {}
public Person(String name) {
super();
this.name = name;
}
@Override
public String toString() {
return "Person [name=" + name + "]";
}
}
PersonTest.java - 测试类
package com.demo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import org.junit.jupiter.api.Test;
class PersonTest {
@Test
void test() throws Exception {
File file = new File("person.out");
// 序列化
ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
Person person = new Person("小明");
oout.writeObject(person);
oout.close();
// 反序列化
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
Object newPerson = oin.readObject();
oin.close();
System.out.println(newPerson);
}
}
测试发现没有什么问题。有一天,因业务发展,需要在 Person 中增加了一个属性 age,如下:
package com.demo;
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
public Person() {}
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
这时我们假设和之前序列化到磁盘的 Person 类是兼容的,便不修改版本标识 serialVersionUID。再次测试如下:
@Test
void deserialize1LWithAge() throws Exception {
File file = new File("person.out");
// 反序列化
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
Object newPerson = oin.readObject();
oin.close();
System.out.println(newPerson);
}
将以前序列化到磁盘的旧 Person 反序列化到新 Person 类时,没有任何问题。
可当我们增加 age 属性后,不作向后兼容,即放弃原来序列化到磁盘的 Person 类,这时我们可以将版本标识提高,如下:
private static final long serialVersionUID = 2L;
再次进行反序列化,则会报错,如下:
java.io.InvalidClassException: com.demo.Person; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
到这里,大致可以清楚,serialVersionUID 就是控制版本是否兼容的,若我们认为修改的 Person 是向后兼容的,则不修改 serialVersionUID;反之,则提高 serialVersionUID 的值。再回到一开始的问题,为什么 Eclipse 会提示声明 serialVersionUID 的值呢?
因为若不显式定义 serialVersionUID 的值,Java 会根据类的细节自动生成 serialVersionUID 的值,如果对类的源代码作了修改,再重新编译,新生成的类文件的 serialVersionUID 的取值有可能也会发生变化。
类的 serialVersionUID 的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的 Java 编译器编译,也有可能会导致不同的 serialVersionUID。所以 Eclipse 才会提示声明 serialVersionUID 的值。