avatar


13.Servlet、Filter和Listener

Tomcat

什么是Tomcat

Tomcat,对外是Web服务器,对内是Servlet容器。

对外:Web服务器\textbf{对外:Web服务器}

对外:Web服务器

对内:Servlet容器\textbf{对内:Servlet容器}

对内:Servlet容器

基本使用

下载

下载地址如下:https://tomcat.apache.org/download-80.cgi

我们会看到如下的内容。

下载页面

  • Windows用户选择32-bit Windows zip64-bit Windows zip
  • macOS用户选择zip
  • Linux用户选择tar.gz

安装

Tomcat是一个绿色软件,所以直接解压即可。
理论上解压到任意的目录都可以,但最好解压到一个没有中文,没有空格的目录,因为在部署项目的时候,如果路径有中文或者空格可能会导致程序部署失败。

在Linux上的解压方式如下:

1
tar -zxvf apache-tomcat-8.5.81.tar.gz

如果是用过MacOS的Safari浏览器下载的,因为Safari浏览器的缘故,会自动对.gz的包进行解压。所以其解压命令如下:

1
tar -xvf apache-tomcat-8.5.81.tar

解压后,会得到如下内容:
目录

  • bin:可执行文件
    该目录下主要有两类文件,一种是以.bat结尾的,是Windows的可执行文件,一种是以.sh结尾的,是macOS和Linux的可执行文件。
  • conf:配置文件
  • lib:Tomcat所依赖的JAR包
  • logs:日志文件
  • temp:临时文件
  • webapps:应用发布目录
  • work:工作目录

卸载

卸载,直接删除目录即可

启动

Windows

启动方式为执行bin目录下的.\startup.bat

在Windows上,需要在环境变量中配置JAVA_HOMEJRE_HOME。不是说java.exe已经被加入环境变量(在PATH中有),而是需要在环境变量中配置JAVA_HOMEJRE_HOME

例如,我们输入java -version,返回了版本号,说明java.exe被加入了环境变量。示例代码:

1
java -version

运行结果:

1
2
3
java version "1.8.0_333"
Java(TM) SE Runtime Environment (build 1.8.0_333-b02)
Java HotSpot(TM) 64-Bit Server VM (build 25.333-b02, mixed mode)

但是执行.\startup.bat,依旧失败。示例代码:

1
.\startup.bat

运行结果:

1
2
Neither the JAVA_HOME nor the JRE_HOME environment variable is defined
At least one of these environment variable is needed to run this program

配置环境变量JAVA_HOME

JAVA_HOME

启动后,通过浏览器访问 http://localhost:8080能看到Apache Tomcat的内容就说明Tomcat已经启动成功。

在启动过程中,可能会有乱码,如图:
乱码

修改conf\logging.propertiesjava.util.logging.ConsoleHandler.encoding的值,改为GBK

macOS

在macOS上执行sh startup.sh

如果出现如下的返回

1
2
3
Cannot find ./catalina.sh
The file is absent or does not have execute permission
This file is needed to run this program

是因为没有赋予文件catalina.sh可执行的权限,执行chmod +x *.sh ,对bin目录下的所有.sh文件都赋执行权即可。

启动后,通过浏览器访问 http://localhost:8080能看到Apache Tomcat的内容就说明Tomcat已经启动成功。

Linux

在Linux上执行sh startup.sh

因为Linux系统上一般没有浏览器,我们可以通过其他系统访问,以检查是否启动成功。

如果Linux机器是以虚拟机的方式安装在本地的话,需要注意虚拟机网络连接的方式,不同的连接方式,IP地址不同。一般有三种方式:

  1. Use bridged networking(使用桥接网络)
    例如,VMware的VMnet0,ParallelDesktop的Bridged Network
    此时虚拟机相当于网络上的一台独立计算机,与主机一样,拥有一个独立的IP地址。
  2. Use network address translation(使用NAT网络)
    例如,VMware的VMnet8,ParallelDesktop的Shared Network
    此时虚拟机可以通过主机单向访问网络上的其他工作站(包括Internet网络),其他工作站不能访问虚拟机。
  3. Use Host-Only networking(使用主机网络)
    例如,VMware的VMnet1,ParallelDesktop的Host-Only Network
    此时虚拟机只能与虚拟机、主机互连,网络上的其他工作站不能访问。

在本文,采用Use network address translation(使用NAT网络),IP地址如下:

IP地址

如果无法访问,可能是因为Linux机器的8080端口未开放。
查询端口是否开放:

1
firewall-cmd --query-port=8080/tcp

永久开放8080端口:

1
firewall-cmd --permanent --add-port=8080/tcp

需要刷新,上述改动才会生效。刷新:

1
firewall-cmd --reload

关闭

对于Windows,执行shutdown.bat
对于macOS和Linux,执行sh shutdown.sh

部署

将需要部署的内容,放置在webapps目录下,即部署完成。

如果放置的是.war包,Tomcat会自动对其进行解压缩。

Web项目

结构

Web项目的结构分为两种。

  1. 开发中的项目,其目录结构如下:
    开发中的项目
  2. 开发完成可部署的项目,其目录结构如下:
    开发完成可部署的项目

对于开发中的项目:

  1. 通过执行Maven打包命令package,可以得到"开发完成可部署的项目"。
  2. 编译后的,Java字节码文件和resources的资源文件,会被放到"开发完成可部署的项目"的WEB-INFclasses目录下。
  3. pom.xml中依赖坐标对应的Jar包,会被放打"开发完成可部署的项目"的WEB-INFlib目录下。

创建

使用Maven创建Web项目,有两种方式:

  1. 使用骨架
  2. 不使用骨架

使用骨架

  1. 使用骨架的方式如下:
    使用骨架
  2. 该方式需要联网,并且需要一定的时间以创建,创建完成后的目录结构如下:
    创建完成
  3. 修改pom.xml,删除不必要的内容,只需要保留如下的内容即可。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <?xml version="1.0" encoding="UTF-8"?>

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.kakawanyifan</groupId>
    <artifactId>mv1</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    </project>
  4. main目录下新建两个Directory:javaresources

不使用骨架

  1. 不勾选Create from archetype,即不使用骨架。
    不使用骨架
  2. 创建完成之后,修改pom.xml,添加<packaging>war</packaging>
    因为,默认的<packaging>jar,我们需要指定为war
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.kakawanyifan</groupId>
    <artifactId>mv2</artifactId>
    <version>1.0-SNAPSHOT</version>

    <packaging>war</packaging>

    <properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    </properties>

    </project>
  3. 补齐webapp。步骤如下:
    1. 右键,选择Open Module Settings
      Open Module Settings
    2. 依次点击,Facets+
      Facets
    3. 选择Web
      Web
    4. 选择我们的项目:
      mv2
    5. 修改箭头所指两处的路径,然后点击Apply
      此处默认是web,需要将web修改为webapp
      修改文件路径

在IDEA使用Tomcat

在IDEA中使用Tomcat有两种方式:

  1. 集成本地Tomcat
  2. tomcat7-maven-plugin

其中第二种方法,只支持7版本的Tomcat,而且相关的插件,最近一次更新还是在2013年11月。

我们主要讨论如何集成本地的Tomcat

  1. 点击Add Configuration
    Add Configuration
  2. 选择Tomcat ServerLocal
    TomcatServer-Local
  3. 点击Configure,配置Tomcat目录:
    Configure
  4. 选择Tomcat的目录:
    选择Tomcat的目录
  5. 依次点击,Deployment+
    Deployment+
  6. 选择我们的项目
    选择我们的项目
    war模式是将WEB⼯程打成war包,把war包发布到Tomcat服务器上;部署成功后,Tomcat的webapps⽬录下会有部署的项⽬内容。
    war exploded模式是将WEB⼯程以当前⽂件夹的位置关系发布到Tomcat服务器上;部署成功后,Tomcat的webapps⽬录下没有,⽽使⽤的是项⽬的target⽬录下的内容进⾏部署。
  7. 添加完成后,我们还可以在下方,自定义项目的名称:
    自定义项目的名称
  8. 配置完成之后,即可以在IDEA的上方看到
    IDEA

特别的,我们还可以设置热部署,方法如下:

  1. 选择Deployment的方式为war exploded
  2. 修改On Update actionOn frame deactivationUpdate classes and resources

热部署

有时候,如果我们通过IDEA启动Tomcat一直报错(例如,404),可以在第五步的时候,选择External Source,看看我们已有的外部的应用(例如,Tomcat安装时自带的在webapps目录下的)是否正常。
这样有助于我们排查是Tomcat配置问题,还是IDEA中应用的问题。

Servlet

什么是Servlet?

Servlet=Server+applet\text{Servlet} = \text{Server} + \text{applet}

其中,Server指服务器,applet指小程序,Servlet含义是服务器端的小程序。
(当然,就规模而言,早已不是小程序了。)

例如,在我们查询员工列表的过程中,Servlet的角色如下
Servlet

在整个Web应用中,Servlet主要负责处理请求、协调调度。

案例

我们创建一个Web项目web-demo,并在pom.xml中导入Servlet依赖坐标

1
2
3
4
5
6
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>

<scope>provided,表示只在编译和测试过程中有效;这么设置的原因是,Tomcat的lib目录中也有servlet-api这个Jar包,如果在运行时生效,可能会和Tomcat中的Jar包冲突。

然后,创建一个类,该类实现Servlet接口,重写接口中所有方法。并为该类加上@WebServlet注解,配置访问路径。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.kakawanyifan;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/demo")
public class ServletDemo implements Servlet {

public void init(ServletConfig servletConfig) throws ServletException {

}

public ServletConfig getServletConfig() {
return null;
}

public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("Servlet Demo Hello");

// 返回响应字符串
// 1、获取能够返回响应数据的字符流对象
PrintWriter writer = servletResponse.getWriter();

// 2、向字符流对象写入数据
writer.write("Hello,I am Servlet");
}

public String getServletInfo() {
return null;
}

public void destroy() {

}
}

然后,我们启动Tomcat,在浏览器中输入地址。

Servlet

同时,我们会发现控制台也打印了Servlet Demo Hello

执行过程

但是,好像有问题?我们创建的是一个类ServletDemo,没创建对象啊。
那么是谁执行了System.out.println("Servlet Demo Hello")?还让我们看到了在网页看到了Hello,I am Servlet

这就涉及到Servlet的执行过程了。

执行流程

  1. 浏览器发出请求:http://localhost:8080/web_demo_war/demo
    根据localhost:8080找到要访问的Tomcat Web服务器
    根据web_demo_war找到部署在Tomcat服务器上的web_demo_war项目
    根据/demo找到被访问的Servlet类(通过@WebServlet注解进行匹配)
  2. 找到ServletDemo这个类后,Tomcat会为ServletDemo这个类创建一个对象,然后调用对象中的service方法。
    那么,Tomcat是怎么知道调用service方法呢?
    这是一种约定,我们自定义的Servlet类,必须实现Servlet接口并重写其方法,包括service方法。

生命周期

通过上文的讨论,我们知道了Servlet对象是由Tomcat创建的。
那么,Tomcat是什么时候创建的Servlet对象的?
这就涉及到Servlet的生命周期了,即一个对象从被创建到被销毁的整个过程。

Servlet对象是由Servlet容器(Tomcat)创建的,其生命周期也由Servlet容器(Tomcat)来管理,分为4个阶段:

  1. 加载和实例化
    默认情况,Servlet对象会在第一次被访问的时候,由容器创建,但是如果创建Servlet对象比较耗时的话,那么第一个访问的人等待的时间就比较长,用户体验较差。也可以在服务启动的时候,就创建Servlet对象。
    1
    @WebServlet(urlPatterns = "/demo1",loadOnStartup = 1)
    loadOnstartup的取负整数表示第一次访问时创建Servlet对象,取0或正整数表示服务启动时创建Servlet对象(数字越小优先级越高)。
  2. 初始化
    在Servlet实例化之后,容器将调用Servlet的init()方法初始化这个对象,完成一些如加载配置文件、创建连接等初始化的工作。该方法只调用一次。
  3. 请求处理
    每次请求时,Servlet容器都会调用Servlet对象的service()方法对请求进行处理。
  4. 服务终止
    当需要释放内存或者容器关闭时,容器就会调用Servlet对象的destroy()方法完成资源的释放,随后会被Java的垃圾收集器回收。

我们可以验证一下,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.kakawanyifan;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(urlPatterns = "/demo",loadOnStartup = 1)
public class ServletDemo implements Servlet {

public ServletDemo(){
System.out.println("构造...");
}

public void init(ServletConfig servletConfig) throws ServletException {
System.out.println("初始化...");
}

public ServletConfig getServletConfig() {
return null;
}

public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("Servlet Demo Hello");

// 返回响应字符串
// 1、获取能够返回响应数据的字符流对象
PrintWriter writer = servletResponse.getWriter();

// 2、向字符流对象写入数据
writer.write("Hello,I am Servlet");
}

public String getServletInfo() {
return null;
}

public void destroy() {
System.out.println("销毁...");
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

【部分运行结果略】

构造...
初始化...

【部分运行结果略】

Servlet Demo Hello
Servlet Demo Hello
Servlet Demo Hello
Servlet Demo Hello

【部分运行结果略】

销毁...

题外话: 除了Servlet有生命周期,一些基金公司的产品管理系统,也被命名为生命周期系统,指一个产品从创建到退出的整个过程。

HttpServlet

通过上文的讨论,我们知道要想编写一个Servlet就必须要实现Servlet接口,重写接口中的5个方法。但其实,我们更关注的其实只有service方法,有更简单方式来创建Servlet。直接继承HttpServlet。

HttpServlet

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.kakawanyifan;

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 java.io.IOException;

@WebServlet(urlPatterns = "/demo2")
public class HttpServletDemo extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("get");
resp.getWriter().write("get");
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("post");
resp.getWriter().write("post");
}
}

对于GET方法,通过浏览器访问即可。对于POST,可以通过Postman等工具。

特别的,我们还可以看看HttpServlet的源码,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package javax.servlet.http;

【部分代码略】

public abstract class HttpServlet extends GenericServlet {

【部分代码略】

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
long lastModified;
if (method.equals("GET")) {
lastModified = this.getLastModified(req);
if (lastModified == -1L) {
this.doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader("If-Modified-Since");
if (ifModifiedSince < lastModified) {
this.maybeSetLastModified(resp, lastModified);
this.doGet(req, resp);
} else {
resp.setStatus(304);
}
}
} else if (method.equals("HEAD")) {
lastModified = this.getLastModified(req);
this.maybeSetLastModified(resp, lastModified);
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
} else if (method.equals("DELETE")) {
this.doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if (method.equals("TRACE")) {
this.doTrace(req, resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[]{method};
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}


}

【部分代码略】

}

调用HttpServletRequest对象的getMethod()以获取方法,然后根据方法的不同,分别调用doGetdoPost

访问路径

注解的方式

Servlet类编写好后,要想被访问到,就需要配置其访问路径(urlPattern)。我们可以为一个Servlet,配置多个urlPattern,示例代码:

1
2
@WebServlet(urlPatterns = {"/demo2","/demo3"})
public class HttpServletDemo extends HttpServlet {

如果我们只配置urlPatterns,而且只配置一个urlPatterns,也可以采用@WebServlet("/request")这种的简化方式。

web.xml

还可以通过web.xml进行配置。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.kakawanyifan;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class HttpServletDemoXML extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
System.out.println("XML");
resp.getWriter().write("XML");
}
}

web.xml的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>

<servlet>
<!-- servlet的名称,名字任意 -->
<servlet-name>what-ever</servlet-name>
<!--servlet的全限定名-->
<servlet-class>com.kakawanyifan.HttpServletDemoXML</servlet-class>
</servlet>

<servlet-mapping>
<!-- servlet的名称,要和上面的名称一致-->
<servlet-name>what-ever</servlet-name>
<!-- servlet的访问路径 -->
<url-pattern>/xml</url-pattern>
</servlet-mapping>
</web-app>

匹配方式

精确匹配

形如/user/select,示例代码:

1
2
3
4
5
6
7
8
9
@WebServlet(urlPatterns = "/user/select")
public class HttpServletDemo2 extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
System.out.println("/user/select");
resp.getWriter().write("/user/select");
}
}

访问/user/select,运行结果:

1
/user/select

目录匹配

形如/user/*,示例代码:

1
2
3
4
5
6
7
8
9
@WebServlet(urlPatterns = "/user/*")
public class HttpServletDemo3 extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
System.out.println("/user/*");
resp.getWriter().write("/user/*");
}
}

访问/user/【除select外的任意内容】,运行结果:

1
/user/*

注意,【除select外的任意内容】,因为:精确匹配的优先级高于目录匹配

后缀名匹配

形如*.do,示例代码:

1
2
3
4
5
6
7
8
9
@WebServlet(urlPatterns = "*.do")
public class HttpServletDemo4 extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
System.out.println("*.do");
resp.getWriter().write("*.do");
}
}

访问what-ever.do,运行结果:

1
*.do

但是,如果我们访问user/what-ever.do呢?运行结果:

1
/user/*

因为目录匹配的优先级高于后缀名匹配

任意匹配

任意匹配有两种。

形如/,示例代码:

1
2
3
4
5
6
7
8
9
@WebServlet(urlPatterns = "/")
public class HttpServletDemo5 extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
System.out.println("/");
resp.getWriter().write("/");
}
}

形如/*,示例代码:

1
2
3
4
5
6
7
8
9
@WebServlet(urlPatterns = "/*")
public class HttpServletDemo6 extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
System.out.println("/*");
resp.getWriter().write("/*");
}
}

//*的区别:

  1. /*的优先级会高于/
  2. 如果我们的项目中的Servlet配置了/,会覆盖掉Tomcat中的DefaultServlet,DefaultServlet用来处理静态资源。
    例如,如果我们配置了/,就会导致项目中webapp目录下的a.html访问不到。

优先级

五种配置的优先级,从高到低为:

  1. 精确匹配
  2. 目录匹配
  3. 后缀匹配
  4. /*
  5. /

转发和重定向

转发

在请求的处理过程中,Servlet将请求转交给下一个资源处理,是在服务器端完成的。

转发

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.kakawanyifan;

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 java.io.IOException;

@WebServlet(urlPatterns = "/dispatcher")
public class HttpServletDemoDispatcher extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
req.getRequestDispatcher("/a.html").forward(req,resp);
}
}

此时,通过访问/dispatcher会得到/a.html的内容。

重定向

重定向,本质是一种特殊的响应,是Servlet以一个响应的方式告诉浏览器:需要你另外再访问下一个资源。在浏览器端完成的。

重定向

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.kakawanyifan;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(urlPatterns = "/redirect")
public class HttpServletDemoRedirect extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.sendRedirect("/web_demo/a.html");
}
}

我们通过浏览器访问/web_demo/a.html,会发现浏览器实际上发了两个请求。

两个请求

我们看到,第一个请求的状态码是302,这里解释一下:

  • 1XX:服务器就收客户端消息,但没有接受完成,等待一段时间后,发送1XX状态码
  • 2XX:成功。代表:200。
  • 3XX:重定向。代表:302(重定向),304(访问缓存)。
  • 4XX:客户端错误。代表:404(请求路径没有对应的资源)。
  • 5XX:服务器端错误。

比较

转发 重定向
一次请求 两次请求
浏览器地址栏显示的是第一个资源的地址 浏览器地址栏显示的是第二个资源的地址
全程使用的是同一个request对象 全程使用的是不同的request对象
在服务器端完成 在浏览器端完成
目标资源地址由服务器解析
所以在代码中不需要加虚拟目录)
目标资源地址由浏览器解析
所以在代码中需要加虚拟目录(项目访问路径)
目标资源可以在WEB-INF目录下 目标资源不能在WEB-INF目录下
目标资源仅限于本应用内部 目标资源可以是外部资源

在具体的代码实现中,转发是由Request实例(请求对象)完成的,重定向是由Response实例(响应对象)完成的。
而这两个,将是我们接下来讨论的重点。

Request

什么是Request

浏览器会发送HTTP请求到后台服务器(Tomcat),HTTP的请求中会包含很多请求数据(请求行+请求头+请求体),后台服务器(Tomcat)会对HTTP请求中的数据进行解析并把解析结果存入到一个对象中,所存入的对象就是Request对象。我们可以从Request对象中获取请求的相关参数,以完成后续操作。

我们再看看上文的代码,我们的Servlet类实现的是Servlet接口的时候,service方法中的参数是ServletRequest和ServletResponse,示例代码:

1
2
3
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {

}

当我们的Servlet类继承的是HttpServlet类的时候,doGet和doPost方法中的参数就变成HttpServletRequest和HttpServletReponse,示例代码:

1
2
3
4
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

}

如果我们点开看,会发现ServletRequest是接口,HttpServletRequest继承了ServletRequest,也是接口。

《2.面向对象》,我们讨论过,接口不能实例化,但可以通过实现类对象实例化,这叫接口多态。

那么,接口的实现类在哪里呢?

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.kakawanyifan;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(urlPatterns = "/find")
public class HttpServletDemoFind extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
System.out.println(req);
}
}

运行结果:

1
org.apache.catalina.connector.RequestFacade@6e160913

org.apache.catalina.connector.RequestFacade,这就是其实现类。

即,其继承体系如下:

继承体系

获取请求数据

HTTP请求数据总共分为三部分内容:请求行请求头请求体

获取请求行

请求行包含三块内容:请求方式请求资源路径HTTP协议及版本

方法如下:

  1. 获取请求方式:String getMethod()
  2. 获取虚拟目录(项目访问路径):String getContextPath()
  3. 获取URL(统一资源定位符):StringBuffer getRequestURL()
  4. 获取URI(统一资源标识符):String getRequestURI()
  5. 获取请求参数(GET方式):String getQueryString()(POST方式的请求参数在请求体中)。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.kakawanyifan;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/request")
public class RequestDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 获取请求方式
String method = req.getMethod();
System.out.println(method);
// 获取虚拟目录(项目访问路径)
String contextPath = req.getContextPath();
System.out.println(contextPath);
// 获取URL(统一资源定位符)
StringBuffer url = req.getRequestURL();
System.out.println(url.toString());
// 获取URI(统一资源标识符)
String uri = req.getRequestURI();
System.out.println(uri);
// 获取请求参数(GET方式)
String queryString = req.getQueryString();
System.out.println(queryString);
}
}

访问/request?u=123&p=abc,运行结果:

1
2
3
4
5
GET
/web_demo
http://localhost:8080/web_demo/request
/web_demo/request
u=123&p=abc

获取请求头

请求头是key-value的形式。

  • 获取所有的keygetHeaderNames()
  • 获取具体的valuegetHeader

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.kakawanyifan;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;

@WebServlet("/request")
public class RequestDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
System.out.println(key + " : " + req.getHeader(key));
}
}
}

访问/request,运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
host : localhost:8080
connection : keep-alive
cache-control : max-age=0
sec-ch-ua : " Not A;Brand";v="99", "Chromium";v="102", "Google Chrome";v="102"
sec-ch-ua-mobile : ?0
sec-ch-ua-platform : "macOS"
upgrade-insecure-requests : 1
user-agent : Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36
accept : text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
sec-fetch-site : none
sec-fetch-mode : navigate
sec-fetch-user : ?1
sec-fetch-dest : document
accept-encoding : gzip, deflate, br
accept-language : zh-CN,zh;q=0.9
cookie : JSESSIONID=4E40FFD71F9F301FDA16532C13397229

获取请求体

GET请求中是没有请求体的,我们需要把请求方式变更为POST。
有两种方式获取请求体中的数据:

  1. 获取字符输入流
    BufferedReader getReader()
  2. 获取字节输入流
    ServletInputStream getInputStream()

获取字符输入流

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.kakawanyifan;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;

@WebServlet("/request")
public class RequestDemo extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 获取post 请求体:请求参数
// 1. 获取字符输入流
BufferedReader br = req.getReader();
// 2. 读取数据
String line = br.readLine();
System.out.println(line);
}
}

我们在Postman中,通过x-www-form-urlencoded的格式调用。
x-www-form-urlencoded

运行结果:

1
u=123&p=abc

获取字节输入流

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.kakawanyifan;

import javax.servlet.ServletInputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileOutputStream;
import java.io.IOException;

@WebServlet("/request")
public class RequestDemo extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 获取post 请求体:请求参数
// 获取字符输入流
ServletInputStream servletInputStream = req.getInputStream();
// 文件输出流
FileOutputStream fileOutputStream = new FileOutputStream("程序都是一样的-副本.jpg");

//读写数据,复制图片(一次读取一个字节数组,一次写入一个字节数组)
byte[] bys = new byte[1024];
int len;
while ((len=servletInputStream.read(bys))!=-1) {
fileOutputStream.write(bys,0,len);
}

fileOutputStream.close();
}
}

我们在Postman中,通过binary的格式调用。
binary

会在Tomcatbin目录下找到程序都是一样的-副本.jpg

程序都是一样的-副本

统一的方式

通过上文的讨论,我们知道:

  • 对于GET请求,可以通过String getQueryString()获取参数
  • 对于POST方式,可以通过BufferedReader getReader()获取参数

那么,有没有统一的方式呢?

  1. 获取所有参数Map集合
    1
    Map<String,String[]> getParameterMap()
    因为参数的值可能是一个,也可能有多个,所以Map的值的类型为String数组。
  2. 根据名称获取参数值(数组)
    1
    String[] getParameterValues(String name)
  3. 根据名称获取参数值(单个值)
    1
    String getParameter(String name)

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.kakawanyifan;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Map;

@WebServlet("/request")
public class RequestDemo extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Map<String, String[]> parameterMap = req.getParameterMap();

Enumeration<String> parameterNames = req.getParameterNames();
while (parameterNames.hasMoreElements()) {
String key = parameterNames.nextElement();
System.out.println(key + ":");
System.out.println(req.getParameter(key));

String[] parameterValues = req.getParameterValues(key);
for (String parameterValue: parameterValues) {
System.out.println(parameterValue);
}
}

System.out.println("------");

for (String key: parameterMap.keySet()) {
System.out.println(key + ":");
String[] vArr = parameterMap.get(key);
for (String v: vArr) {
System.out.println(v);
}
}
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
doPost(req, resp);
}
}

我们在Postman中,通过POST请求的x-www-form-urlencoded格式调用。
x-www-form-urlencoded

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
u:
123
123
p:
abc
abc
a:
1
1
2
------
u:
123
p:
abc
a:
1
2

我们在Postman中,通过GET请求调用。
GET

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
u:
123
123
p:
abc
abc
a:
1
1
2
------
u:
123
p:
abc
a:
1
2

application/json

还有一种请求方式是application/json,以JSON的形式提交。
关于该方式,在《14.HttpClient》有讨论。

其实也是从请求体中拿数据,只是关于该方式,不会将请求体的内容解析成"getParameterMap"的形式,而是解析成"JSON"的形式。

乱码

现象

我们传入中文。
中文乱码问题

运行结果:

1
2
3
4
5
6
ch:
中文
中文
------
ch:
中æ

为什么会乱码?
《5.IO流》,我们讨论过乱码以及字符集编码。乱码是因为编码和解码所采用的字符集不对。

方法

因为客户端将数据发送给服务端后,并没有告诉服务端需要采取何种编码方式,这时候服务端会采取默认的ISO-8859-1

所以,我们可以像如下这样操作,先按照ISO-8859-1解码,再按照utf-8重新编码。

1
new String(【需要解码的内容】.getBytes("ISO-8859-1"),"utf-8")

第二种方法,我们直接为Request对象指定编码方式是utf-8

1
req.setCharacterEncoding("utf-8");

第三种方法为采用过滤器Filter。
在7版本的Tomcat中,已经将这个Filter加入Tomcat内置了,具体位置:Tomcat目录下的conf/web.xml,我们直接复制一下代码到项目的web.xml中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<filter>
<filter-name>setCharacterEncodingFilter</filter-name>
<filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<async-supported>true</async-supported>
</filter>

<filter-mapping>
<filter-name>setCharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

关于过滤器,我们会在下文做更多的讨论。

Get请求中的乱码

这个其实在8版本的Tomcat的默认配置中,已经不会复现了。
我们先讨论为什么会有乱码?为什么8版本的Tomcat没有复现,最后我们试图修改8版本的参数,让其复现。

对于Post请求,获取请求参数的方式是request.getReader(),所以我们通过request.setCharacterEncoding("utf-8")这种方法,指定编码方式可以解决。但是在Get请求中,获取参数的方式是request.getQueryString(),根本就没有经过流。

在发送的过程中,客户端(浏览器)会将中文以UTF-8的方式进行URL编码,然后这时候服务端(Tomcat)应该将收到的内容,以UTF-8的形式进行解码。
在8版本的Tomcat中,的确是以UTF-8的形式进行解码,但是在7版本的Tomcat中,是以ISO-8859-1的方式进行解码的,所以就有乱码了。

Get请求的乱码-原因

我们修改Tomcat的配置文件conf/server.xml,在原有的

1
2
3
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />

加上URIEncoding="ISO-8859-1"

然后试一下

Get请求的乱码-现象

运行结果:

1
2
3
4
5
6
ch:
中文
中文
------
ch:
中文

当然,如下的方式可以解决。

1
System.out.println(new String(req.getParameter(key).getBytes("ISO-8859-1"),"utf-8"));

更好的方法是修改Tomcat的配置文件conf/server.xml,去除URIEncoding="ISO-8859-1",采取默认的UTF-8

特殊字符被拒

再来看一下现象,如果我们通过Get请求传递的参数中有特殊字符,请求就会报错

特殊字符被拒

同时,控制台会打印

1
2
3
29-Jun-2022 22:17:29.453 INFO [http-nio-8080-exec-10] org.apache.coyote.http11.Http11Processor.service Error parsing HTTP request header
Note: further occurrences of HTTP request parsing errors will be logged at DEBUG level.
java.lang.IllegalArgumentException: Invalid character found in the request target [/web_demo_war/request?json={%22k%22:%22%E5%80%BC%22} ]. The valid characters are defined in RFC 7230 and RFC 3986

这是因为,Tomcat在7.0.738.0.398.5.7版本后,在HTTP解析时做了严格限制。根据RFC3986文档规定,请求的URL中只允许包含英文字母(a-zA-Z)、数字(0-9)、-``_``.``~4个特殊字符以及所有保留字符。
不允许出现:空格、反斜杠、#^|<>{}等。

解决办法是在conf/catalina.properties的最后如下的两行,并重启Tomcat以生效。

1
2
tomcat.util.http.parser.HttpParser.requestTargetAllow=|{}
org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true

requestTargetAllow只能配置|{}这三个字符,对于其他的(例如[]),在请求时,仍然拦截。
如果使用了|{}之外的其他字符,需要在conf/server.xml中的<Connector>节点中,添加2个属性:

1
2
relaxedPathChars="|{}[],"
relaxedQueryChars="|{}[],"
  • 这2个属性,可以接收任意特殊字符。

Response

什么是Response

如何接受请求我们已经讨论过了,在接受到请求之后,就开始各种业务处理,在业务处理之后,就需要把结果返回给调用方。

怎么返回呢?return?不是。service方法都是void的,不能返回值的,我们需要将将响应数据封装到Response对象中,后台服务器(Tomcat)会解析Response对象,按照HTTP的格式(响应行+响应头+响应体)拼接结果。

而Response对象,是调用service时的一个入参。所以其返回方式是通过修改入参。
类似的还有《12.MyBatis》获取自增主键,将自增的主键塞在我们入参中,也是修改入参。

Reponse的继承体系和Request的继承体系也非常相似:

继承体系

响应

HTTP响应数据总共分为三部分内容:响应行响应头响应体

响应行

对于响应行,比较常用的就是设置响应状态码:

1
void setStatus(int sc);

响应头

设置响应头键值对:

1
void setHeader(String name,String value);

响应体

对于响应体,是通过字符、字节输出流的方式往客户端写,

  1. 获取字符输出流:
    1
    PrintWriter getWriter();
  2. 获取字节输出流:
    1
    ServletOutputStream getOutputStream();

响应字符数据

步骤

  1. 通过Response对象获取字符输出流:
    1
    PrintWriter writer = resp.getWriter();
  2. 通过字符输出流写数据:
    1
    writer.write("aaa");

响应简单字符串

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.kakawanyifan;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/response")
public class ResponseDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
PrintWriter writer = resp.getWriter();
writer.write("Simple");
}
}

响应简单字符串

一次请求响应结束后,response对象就会被销毁掉,所以没有主动关闭流。

响应HTML字符串

示例代码:

1
2
3
4
5
6
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// content-type,告诉浏览器返回的数据类型是HTML类型数据,这样浏览器才会解析HTML标签
resp.setHeader("content-type","text/html");
PrintWriter writer = resp.getWriter();
writer.write("<h1>Title</h1>");
}

如果没有设置"content-type","text/html",访问结果如图:

没有设置

如果设置了"content-type","text/html",访问结果如图:

设置了

在一些浏览器,例如Chrome浏览器中,即使不设置"content-type","text/html",也会解析,这和浏览器自身的特性有关。

响应中文

返回一个中文的字符串,需要注意设置响应数据的编码为utf-8

示例代码:

1
2
3
4
5
6
7
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// content-type,告诉浏览器返回的数据类型是HTML类型数据,这样浏览器才会解析HTML标签
resp.setHeader("content-type","text/html;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.write("<h1>大标题</h1>");
writer.close();
}

大标题

设置方法还有:

  1. response.setCharacterEncoding("utf-8");
  2. response.setContentType("text/html;charset=UTF-8");

注意,如果我先PrintWriter writer = resp.getWriter(),再resp.setHeader("content-type","text/html;charset=utf-8"),则不会生效。具体因为在resp.getWriter()的时候,就会去设置字符集的编码。

响应字节数据

响应字节数据的方法为:

  1. 通过Response对象获取字节输出流:
    1
    ServletOutputStream outputStream = resp.getOutputStream();
  2. 通过字节输出流写数据:
    1
    outputStream.write(字节数据);

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.kakawanyifan;

import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;

@WebServlet("/response")
public class ResponseDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
//1. 读取文件
FileInputStream fis = new FileInputStream("../../jpg.jpg");
//2. 获取response字节输出流
ServletOutputStream os = resp.getOutputStream();
//3. 完成流的copy
byte[] buff = new byte[1024];
int len = 0;
while ((len = fis.read(buff))!= -1){
os.write(buff,0,len);
}
fis.close();
}
}

响应数据

ServletContext

在上文,我们读取文件的代码是new FileInputStream("../../jpg.jpg");,这个其实是相对于Tomcat的bin的相对路径。我们也可以采取相对于项目的路径,这就是涉及到ServletContext。

ServletContext,代表整个web应用,可以和程序的容器(服务器)来通信。

获取方式:

  1. 通过request对象获取
    1
    request.getServletContext();
  2. 通过HttpServlet获取
    1
    this.getServletContext();

作用:

  1. 域对象:共享数据,所有用户所有请求的数据。
    设置属性:setAttribute(String name,Object value)
    获取属性:getAttribute(String name)
    删除属性:removeAttribute(String name)
  2. 获取文件的真实(服务器)路径
    方法:String getRealPath(String path)

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/response")
public class ResponseDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
//1. 读取文件
FileInputStream fis = new FileInputStream(this.getServletContext().getRealPath("/jpg.jpg"));
//2. 获取response字节输出流
ServletOutputStream os = resp.getOutputStream();
//3. 完成流的copy
byte[] buff = new byte[1024];
int len = 0;
while ((len = fis.read(buff))!= -1){
os.write(buff,0,len);
}
fis.close();
}
}

jpg.jpg,在项目的根目录下。

jpg.jpg

Cookie

会话技术

我们知道,HTTP协议是无状态的,每次浏览器向服务器请求时,服务器都会将该请求视为新的请求。

那么,现在问题来了:我访问淘宝,登录,一个HTTP请求;登录成功后,我把商品加入购物车,一个HTTP请求;然后我支付,一个HTTP请求。这三个HTTP请求之间,是互相独立的,那么我加入购物车的时候,服务端是怎么知道是哪位用户要把商品加入购物车呢?支付的时候,服务端怎么知道是哪位用户要支付哪一笔订单呢?

客户端告诉服务端不就够了吗?那么客户端怎么知道呢?客户端先记下来!\begin{aligned} & \text{客户端告诉服务端不就够了吗?} \\ & \text{那么客户端怎么知道呢?} \\ & \text{客户端先记下来!} \\ \end{aligned}

这就是基于Cookie的客户端会话跟踪技术。

《基于JavaScript的前端开发入门:3.DOM和BOM》的最后,讨论本地存储的时候,我们也讨论过Cookie,当时主要从客户端的角度讨论Cookie。
《爬虫及其Python实现:1.发送请求获取响应》,也有讨论Cookie。当时Cookie在客户端(爬虫)的应用。
现在我们会从服务端以及服务端和客户端的交互的角度,讨论Cookie。

还有一种服务端会话跟踪技术,基于Session,而Session也是基于Cookie。

操作

一共有四个Cookie操作:

  1. 创建Cookie
    1
    Cookie cookie = new Cookie("key","value");
  2. 发送Cookie
    1
    response.addCookie(cookie);
  3. 接收Cookie
    1
    Cookie[] cookies = request.getCookies();
  4. 解析Cookie
    1
    2
    3
    4
    // 获取名称
    cookie.getName();
    // 获取值
    cookie.getValue();

我们创建ServletA,用于创建发送Cookie;再创建ServletB,用于接收和解析Cookie。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.kakawanyifan;

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;
import java.io.IOException;

@WebServlet("/a")
public class A extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Cookie cookie1 = new Cookie("uid","123");
resp.addCookie(cookie1);
Cookie cookie2 = new Cookie("pwd","abc");
cookie2.setValue("xyz");
resp.addCookie(cookie2);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.kakawanyifan;

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;
import java.io.IOException;

@WebServlet("/b")
public class B extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Cookie[] cookies = req.getCookies();
for (Cookie cookie : cookies) {
System.out.println(cookie.getName());
System.out.println(cookie.getValue());
}
}
}

我们访问/a,会看到客户端(浏览器)中已经有Cookie了。

cookie-1

再访问http://localhost:8080/web_demo_war/b,控制台会打印:

1
2
3
4
5
6
uid
123
pwd
xyz
JSESSIONID
180FB1E8DC7F1A789D2F1233E8E5DF1B

其中JSESSIONID由Tomcat创建,Session就基于这个,在下文我们会继续讨论。

存活时间

默认情况下,Cookie存储在浏览器内存中,当浏览器关闭,内存释放,则Cookie被销毁。
如果需要将Cookie持久化的存储,可以设置Cookie的存活时间。

1
setMaxAge(int seconds)

参数值为:

  • 正数:将Cookie进行持久化存储(客户端电脑的硬盘中),到时间自动删除。
  • 负数:Cookie在当前浏览器内存中,浏览器关闭,Cookie被销毁。
    默认值
  • :对应Cookie立即失效。

存储中文

在8版本的Tomcat中,Cookie中不能直接存储中文数据(需要经过UrlEncode)。
在8版本的Tomcat之后,cookie支持中文数据,但特殊字符还是不支持。

但是,我们来看一个现象。

Safari不支持

Chrome中可以存储中文,Safari中不能存储中文,但是Safari对于存储英文和数字却没有问题。
所以,建议,对于8版本的Tomcat,依然进行UrlEncode。

我们在ServletA中进行编码,在ServletB中进行解码。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.kakawanyifan;

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;
import java.io.IOException;
import java.net.URLEncoder;

@WebServlet("/a")
public class A extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Cookie cookie1 = new Cookie("uid","123");
Cookie cookie2 = new Cookie("pwd","abc");
Cookie cookie3 = new Cookie("desc","China");
cookie1.setValue(URLEncoder.encode("中国","utf-8"));
cookie2.setValue(URLEncoder.encode("中华","utf-8"));
resp.addCookie(cookie2);
resp.addCookie(cookie1);
resp.addCookie(cookie3);
System.out.println(cookie1.getMaxAge());
System.out.println(cookie2.getMaxAge());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.kakawanyifan;

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;
import java.io.IOException;
import java.net.URLDecoder;

@WebServlet("/b")
public class B extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Cookie[] cookies = req.getCookies();
for (Cookie cookie : cookies) {
System.out.println(cookie.getName());
System.out.println(URLDecoder.decode(cookie.getValue(),"utf-8"));
}
}
}

通过Safari浏览器,先访问a,再访问b
先访问a 再访问b

运行结果:

1
2
3
4
5
6
desc
China
pwd
中华
uid
中国

共享问题

假设在一个Tomcat服务器中,部署了多个web项目,那么在这些web项目中cookie能不能共享?
默认情况下cookie不能共享
但是可以通过setPath(String path),设置cookie的获取范围。默认情况下,设置当前的虚拟目录。如果要共享,则可以将path设置为/

不同的Tomcat服务器间cookie共享,当然也不能。
但可以通过setDomain(String path),设置cookie的获取域名。如果设置一级域名相同,那么多个服务器之间cookie可以共享。
例如:setDomain("baidu.com"),那么tieba.baidu.comnews.baidu.com中cookie可以共享。

Session

概述

Session:服务端会话跟踪技术,将主要数据保存到服务端。
(注意,是主要数据,因为有些数据,还是需要保存在客户端。)

操作

  1. 获取Session对象,通过Request对象:
    1
    HttpSession session = request.getSession();
  2. 存储数据到session域中:
    1
    void setAttribute(String name, Object o)
  3. 根据key,获取值:
    1
    Object getAttribute(String name)
  4. 根据key,删除该键值对
    1
    void removeAttribute(String name)

我们在ServletA中存储数据,在ServletB中获取数据,在ServletC中移除数据。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.kakawanyifan;

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("/a")
public class A extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// 获取Session对象
HttpSession httpSession = req.getSession();
// 存储数据
httpSession.setAttribute("uid","123");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.kakawanyifan;

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("/b")
public class B extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// 获取Session对象
HttpSession httpSession = req.getSession();
Object uid = httpSession.getAttribute("uid");
System.out.println(uid);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.kakawanyifan;

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("/c")
public class C extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// 获取Session对象
HttpSession httpSession = req.getSession();
httpSession.removeAttribute("uid");
}
}

我们依次访问/a/b/c/d,运行结果:

1
2
123
null

原理

Session是基于Cookie实现的。

Session要想实现一次会话多次请求之间的数据共享,就必须要保证多次请求获取Session的对象是同一个。
那么,是怎么做到是同一个的呢?

JSESSIONID

基于JSESSIONID,而这个存储在Cookie中。

存活时间

默认情况下,无操作,30分钟自动销毁。

设置存活时间的方法有:

  1. 可以在Web容器中设置,通过Tomcat的\conf\web.xml
    1
    2
    3
    4
    5
    6
    7
    <!-- ==================== Default Session Configuration ================= -->
    <!-- You can set the default session timeout (in minutes) for all newly -->
    <!-- created sessions by modifying the value below. -->

    <session-config>
    <session-timeout>30</session-timeout>
    </session-config>
    • 默认值30分钟;可以根据需要修改,负数或0为不限制session失效时间。
  2. 可以在工程的web.xml中设置,通过工程的\WEB-INF\web.xml
    1
    2
    3
    4
    <!-- 时间单位为分钟   -->
    <session-config>
    <session-timeout>30</session-timeout>
    </session-config>
    • 工程和Web容器一样,默认值30分钟;可以根据需要修改,负数或0为不限制session失效时间。
  3. 可以通过通过java代码设置
    1
    session.setMaxInactiveInterval(30*60)
    • 特别注意!这里的单位是 ,负数或0为不限制session失效时间。

上述三种方法,如果都设置,优先级最高的是在Java代码中设置,其次是工程,最后是在Tomcat中。

钝化和活化

  1. 钝化:在服务器正常关闭后,Tomcat会自动将Session数据写入硬盘的文件中。
  2. 活化:再次启动服务器后,从文件中加载数据到Session中

Filter

概述

Filter,过滤器,是JavaWeb三大组件(Servlet、Filter、Listener)之一。
Filter,可以把对资源的请求,进行拦截过滤,从而实现一些通用功能,例如:权限控制、统一编码处理等。

案例

  1. 定义类,实现 Filter接口,并重写其方法。
  2. 配置Filter拦截资源的路径:在类上定义 @WebFilter 注解。而注解的 value 属性值 /* 表示拦截所有的资源
  3. 在doFilter方法中打印内容,并放行。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.kakawanyifan;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/s")
public class HttpServletDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().write("123456");
}
}

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.kakawanyifan;


import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter("/*")
public class FilterDemo implements Filter {

public void init(FilterConfig filterConfig) throws ServletException {

}

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Filter");

// 放行
filterChain.doFilter(servletRequest,servletResponse);
}

public void destroy() {

}
}

然后我们访问/s,会发现注释filterChain.doFilter(servletRequest,servletResponse);,不放行的时候,看不到内容。

放行后

放行后

如图是Filter的流程,我们需要关注的是,不但有放行前的流程,还是放行后的流程。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 放行前
System.out.println("Filter");

// 放行
filterChain.doFilter(servletRequest,servletResponse);

// 放行后
servletResponse.getWriter().write("abcdef");

}

路径配置

使用 @WebFilter 注解进行配置。如:@WebFilter("拦截路径")

和Servlet中的路径一样,Filter的拦截路径有如下四种配置方式,

  1. 精确匹配
    形如/user/select
  2. 目录匹配
    形如/user/*
  3. 后缀名匹配
    形如*.do
  4. 任意匹配
    /*

(与Servlet不同的是,在Filter中,任意匹配只有/*/无效。)

过滤器链

过滤器链是指在一个Web应用,可以配置多个过滤器,这多个过滤器称为过滤器链。

通过注解无法排序

不少资料说,通过注解@WebFilter配置的Filter过滤器,是按照过滤器类名(字符串)的自然排序。

实际上,绝非如此。

通过注解@WebFilter配置的Filter过滤器,无法进行排序,若需要对Filter过滤器进行排序,建议使用web.xml进行配置。

例如,现在有FilterAFilterBFilterC。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@WebFilter("/*")
public class FilterA implements Filter {

public void init(FilterConfig filterConfig) {

}

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 放行前
System.out.println("FilterA 放行前");

// 放行
filterChain.doFilter(servletRequest,servletResponse);

// 放行后
System.out.println("FilterA 放行后");

}

public void destroy() {

}
}

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@WebFilter("/*")
public class FilterB implements Filter {

public void init(FilterConfig filterConfig) {

}

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 放行前
System.out.println("FilterB 放行前");

// 放行
filterChain.doFilter(servletRequest,servletResponse);

// 放行后
System.out.println("FilterB 放行后");

}

public void destroy() {

}
}

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@WebFilter("/*")
public class FilterC implements Filter {

public void init(FilterConfig filterConfig) {

}

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 放行前
System.out.println("FilterC 放行前");

// 放行
filterChain.doFilter(servletRequest,servletResponse);

// 放行后
System.out.println("FilterC 放行后");

}

public void destroy() {

}
}

我们访问/s,运行结果:

1
2
3
4
5
6
FilterB 放行前
FilterC 放行前
FilterA 放行前
FilterA 放行后
FilterC 放行后
FilterB 放行后

即!通过注解配置的过滤器,无法排序!

通过web.xml设置

通过web.xml设置,谁写在上面,谁先。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>

<!-- 过滤顺序:谁的写在上面,谁先 -->
<filter>
<filter-name>FilterA</filter-name>
<filter-class>com.kakawanyifan.FilterA</filter-class>
</filter>
<filter-mapping>
<filter-name>FilterA</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<filter>
<filter-name>FilterB</filter-name>
<filter-class>com.kakawanyifan.FilterB</filter-class>
</filter>
<filter-mapping>
<filter-name>FilterB</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<filter>
<filter-name>FilterC</filter-name>
<filter-class>com.kakawanyifan.FilterC</filter-class>
</filter>
<filter-mapping>
<filter-name>FilterC</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

</web-app>

运行结果:

1
2
3
4
5
6
FilterA 放行前
FilterB 放行前
FilterC 放行前
FilterC 放行后
FilterB 放行后
FilterA 放行后

Listener

Listener,监听器,是JavaWeb三大组件(Servlet、Filter、Listener)之一。
可以在监听到applicationsessionrequest三个对象创建、销毁或者添加修改删除属性时;自动执行代码

JavaWeb中有八大监听器,三大域对象各有一个生命周期监听器和属性操作监听器,共计6个;还有2个与HttpSession相关的感知型监听器。
三大域:

  1. ServletContext
    1. 生命周期监听器:ServletContextListener,有两个方法,一个在出生时调用,一个在死亡时调用。
      1. void contextInitialized(ServletContextEvent sce):创建ServletContext时调用
      2. void contextDestroyed(ServletContextEvent sce):销毁ServletContext时调用
    2. 属性监听器:ServletContextAttributeListener,它有三个方法:
      1. void attributeAdded(ServletContextAttributeEvent event):添加属性时调用;
      2. void attributeReplaced(ServletContextAttributeEvent event):替换属性时调用;
      3. void attributeRemoved(ServletContextAttributeEvent event):移除属性时调用;
  2. HttpSession
    1. 生命周期监听器:HttpSessionListener,有两个方法,一个在出生时调用,一个在死亡时调用;
      1. void sessionCreated(HttpSessionEvent se):创建session时调用
      2. void sessionDestroyed(HttpSessionEvent se):销毁session时调用
    2. 属性监听器:HttpSessioniAttributeListener,它有三个方法
      1. void attributeAdded(HttpSessionBindingEvent event):添加属性时调用
      2. void attributeReplaced(HttpSessionBindingEvent event):替换属性时调用
      3. void attributeRemoved(HttpSessionBindingEvent event):移除属性时调用
  3. ServletRequest
    1. 生命周期监听器:ServletRequestListener,有两个方法,一个在出生时调用,一个在死亡时调用;
      1. void requestInitialized(ServletRequestEvent sre):创建request时调用
      2. void requestDestroyed(ServletRequestEvent sre):销毁request时调用
    2. 属性监听器:ServletRequestAttributeListener,它有三个方法
      1. void attributeAdded(ServletRequestAttributeEvent srae):添加属性时调用
      2. void attributeReplaced(ServletRequestAttributeEvent srae):替换属性时调用
      3. void attributeRemoved(ServletRequestAttributeEvent srae):移除属性时调用

还有2个感知型监听器,都与HttpSession相关:

  1. HttpSessionBindingListener:监听对象与session的绑定。
  2. HttpSessionActivationListener:监听session的钝化与活化。

我们定义一个类,实现ServletContextListener接口,重写所有的抽象方法,使用@WebListener进行配置,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.kakawanyifan;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class ContextLoaderListener implements ServletContextListener {

@Override
public void contextInitialized(ServletContextEvent sce) {
//加载资源
System.out.println("ContextLoaderListener...");
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
//释放资源
System.out.println("contextDestroyed");
}
}

运行结果:

1
2
ContextLoaderListener...
contextDestroyed

也可以通过web.xml进行配置,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>

<listener>
<listener-class>com.kakawanyifan.ContextLoaderListener</listener-class>
</listener>

</web-app>
文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/10813
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

留言板