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 {

  1. private static final long serialVersionUID = 1L;
  2. protected void doGet(HttpServletRequest request, HttpServletResponse response)
  3. throws ServletException, IOException {
  4. // 获取输出流
  5. PrintWriter out = response.getWriter();
  6. out.println("Hello Servlet!");
  7. }
  8. protected void doPost(HttpServletRequest request, HttpServletResponse response)
  9. throws ServletException, IOException {
  10. doGet(request, response);
  11. }

}

代码分析:

- [**@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);
    }
}

运行系统,进行以下操作:

  1. 在地址栏中输入 login.html 进行访问:http://localhost:8080/HelloServlet/login.html

打开浏览器的 Console,查看 Network 标签页,发现以下信息:
二、Servlet - 图1
其中,Request Method:GET,表示该请求的类型为 Get 请求,即当我们在地址栏中输入资源路径直接访问时,发起的请求类型为 Get 请求
二、Servlet - 图2

  1. 在 login.html 页面中输入用户名“guo”和密码“123”,点击“提交”按钮后,地址栏显示的信息如下:

http://localhost:8080/HelloServlet/LoginServlet?username=guo&password=123
打开浏览器的 Console,查看 Network 标签页,发现以下信息:
二、Servlet - 图3
其中的 Request URL 与 地址栏中的值一致,分析如下:

  • http://localhost:8080/HelloServlet/LoginServlet:表示访问的资源路径。
  • ?:表示要向该资源传递参数。
  • 参数跟在?后面,以“键-值”对的形式传递,“=”左边是键名,“=”右边是键值,不同的参数之间用“&”分隔。

    1.3 HTTP POST 请求剖析

    修改 login.html
    <!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>
    
    LoginServlet.java 保持不变。
    进行如下操作:
  1. 在地址栏中输入 login.html 进行访问:http://localhost:8080/HelloServlet/login.html
  2. 在 login.html 页面中输入用户名“guo”和密码“123”,点击“提交”按钮后,打开浏览器的 Console,查看 Network 标签页,发现以下信息:

    二、Servlet - 图4
    二、Servlet - 图5

  • Request Method:POST,与 form 表单的 action 属性值一致。
  • Post 请求所携带的参数显示在最下面的“Form Data”中。

考点:GET 请求和 POST 请求有什么区别?
第一层理解
二、Servlet - 图6
参考链接:https://www.w3school.com.cn/tags/html_ref_httpmethods.asp
第二层理解

  1. 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 参数,技术上是完全行的通的。

    那么,“第一层理解”里的那些区别是怎么回事?
    二、Servlet - 图7
    在我大万维网世界中,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)提出了要求。“第一层理解”里关于参数大小的限制又是从哪来的呢?
    二、Servlet - 图8
    在我大万维网世界中,还有另一个重要的角色:运输公司。不同的浏览器(发起 http 请求)和服务器(接受 http 请求)就是不同的运输公司。 虽然理论上,你可以在车顶上无限的堆货物(url 中无限加参数)。但是运输公司可不傻,装货和卸货也是有很大成本的,他们会限制单次运输量来控制风险,数据量太大对浏览器和服务器都是很大负担。业界不成文的规定是:超过的部分,恕不处理。如果你用GET服务,在 request body 偷偷藏了数据,不同服务器的处理方式也是不同的,有些服务器会帮你卸货,读出数据,有些服务器直接忽略,所以,虽然 GET 可以带 request body,也不能保证一定能被接收到哦。
    好了,现在你知道,GET 和 POST 本质上就是 TCP 链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。

  1. 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 响应包括一个首部和一个体:

  • 首部信息告诉浏览器使用了什么协议,请求是否成功,以及体中包括何种类型的内容。
  • 体包含了让浏览器显示的具体内容。

案例

  1. 地址栏:http://localhost:8080/HelloServlet/login.html

·二、Servlet - 图9

  1. 地址栏:http://localhost:8080/HelloServlet/LoginServlet (POST 和 GET 请求的响应头一样)

二、Servlet - 图10
结论

  • 响应首部中 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(邮件)请求,服务器该如何区分每种不同的请求?
    答案:区分的依据就是端口号。
    端口表示与服务器硬件上运行的一个特定软件的逻辑连接,可以把端口看作是唯一的标识符。

二、Servlet - 图11

  • 服务器上有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 生命周期

  1. 装载 Servlet:该项操作一般是动态执行的,有些服务器提供了相应的管理功能,可以在启动的时候就装在 Servlet。
  2. 创建一个 Servlet 实例:容器创建 Servlet 的一个实例对象,即调用构造器
  3. 初始化init()
  4. 服务service()
  5. 销毁destory()

二、Servlet - 图12

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 技术
  • URL 重写

    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 的概念

image.png

  • 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);
    }
}

问题:如何通过 ServletConfig 设置编码方式?

5.3 ServletContext

5.3.1 简介

ServletContext 称为 Servlet 上下文,当 Tomcat 启动时,Tomcat 会为每个 Web 应用创建一个唯一的 ServletContext 对象,该对象代表当前的 Web 应用,并封装了当前 Web 应用的所有信息,这个对象全局唯一,而且该 Web 应用内部的所有 Servlet 都共享这个对象,所以叫全局应用程序共享对象。可以利用该对象获取 Web 应用程序的初始化信息、读取资源文件等。

5.3.2 ServletContext 生命周期

  1. 新 Servlet 容器启动的时候,服务器端会创建一个 ServletContext 对象。
  2. 在容器运行期间,ServletContext 对象一直存在。
  3. 当容器停止时,ServletContext 的生命周期结束。

二、Servlet - 图14

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 的值。