JavaWeb_07_Servlet


前面讲解了Tomcat服务器的配置和初步使用,那么它怎么和Java程序交互呢?本文讲解二者的交互标准Servlet规范。

1. 模拟Servlet本质

Servlet和JDBC一样,是SUN公司制定的一套标准规范,为了更容易理解Servlet,首先手动模拟一下Servlet本质。前面文章中提到,Servlet本质上是Java提供的接口,而WEB Server厂商负责调用这个接口为浏览器提供服务,这时候,我们只实现这个接口所需要的功能,以及请求路径和实现类之间的对应关系,用于WEB服务器的动态资源定位所以,模拟Servlet涉及到的角色:

  • 充当SUN公司的角色,指定Servlet规范;
    • public interface Servlet
  • 充当Tomcat服务器的开发者;
    • public class Tomcat
  • 充当webapp的开发者
    • public class UerListServlet implements Servlet
    • public class UserLoginServlet implements Servlet
    • public class BankServlet implements Servlet
    • 请求路径和动态资源文件的对应关系 配置文件。

1.1 充当SUN公司的角色

SUN公司主要是制定Servlet接口。

1
2
3
4
5
6
7
8
9
10
package servlet;

/**
* 充当SUN公司,制定接口,提供服务
*/
public interface Servlet {

void service();
}

1.2 充当Tomcat服务器的角色

Tomcat服务器负责接收浏览器传过来的请求,根据不同的请求,定位不同的资源文件(这里以动态资源文件为例),对于动态资源,会创建类对象,然后执行程序,获得结果,并返回给浏览器。

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
package servlet;

import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
import java.util.Scanner;

/**
* 充当Tomcat服务器的开发者
*/
public class Tomcat {

public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
System.out.println("Tomcat 服务器启动成功,开始接收用户的访问。");

// 使用Scanner模拟用户的请求
// 用户访问服务器是通过浏览器上的“请求路径”
// 也就是说用户请求路径不同,后台执行的Servlet不同。
// /bank BankServlet
// /login UserLoginServlet
// /list UserListServlet
System.out.print("请输入您的访问路径:");
Scanner scanner = new Scanner(System.in);
String path = scanner.nextLine();
System.out.println(path);

// 此时Tomcat已经获取到了用户的请求路径
// Tomcat服务器应该通过用户的请求路径找对应的Servlet实现功能类
// 请求路径和实现功能类之间的关系应该由谁来指定呢?二者之间应该有对照关系。
// 由webapp的开发人员通过配置文件来指定对应关系
FileReader reader = new FileReader("src/servlet/web.properties");
Properties pro = new Properties();
pro.load(reader);
reader.close();

// 获取对应的动态资源文件名
String ClassName = pro.getProperty(path);

// 通过反射机制创建类对象
Class clazz = Class.forName(ClassName);
Object obj = clazz.newInstance();

// 实现类都实现了Servlet,并且实现了的抽象方法,所以Tomcat直接创建类对象,然后调用该接口,获得结果即可。
Servlet servlet = (Servlet)obj;
servlet.service();

}
}

1.3 充当webapp的开发者

webapp的开发者主要是实现具体的功能,这些功能类都实现了Servlet接口,另外,也需要将请求路径和功能实现类对应起来。

三个功能类如下所示:

1
2
3
4
5
6
7
8
9
10
11
package servlet;

/**
* 充当webapp开发者,写程序,实现用户登录功能
*/
public class UserLoginServlet implements Servlet{

public void service(){
System.out.println("UserLoginServlet service...");
}
}
1
2
3
4
5
6
7
8
9
10
11
package servlet;

/**
* 充当webapp开发者,写程序,实现查询用户列表功能
*/
public class UerListServlet implements Servlet{

public void service(){
System.out.println("UserListServlet...");
}
}
1
2
3
4
5
6
7
8
9
10
11
package servlet;

/**
* 充当webapp开发者,写程序,实现银行功能。
*/
public class BankServlet implements Servlet{

public void service(){
System.out.println("Bank service...");
}
}

对应关系的配置文件(web.properties)如下所示:

1
2
3
/bank=servlet.BankServlet
/login=servlet.UserLoginServlet
/list=servlet.UserLoginServlet

1.4 总结

通过上述分析,对于webapp程序员来说,只需要编写功能类实现Servlet接口,将编写的类和请求路径对应起来(设置配置文件)。

注意,因为Tomcat已经是写死的,即上面的读取文件的文件名以及解析格式对于我们来说,都是不能改动的,所以Tomcat给程序员提供了配置文件的具体路径、名称以及具体的配置格式。

  • 配置文件的文件名是固定的
  • 配置文件的存放路径是固定的
  • 格式也是固定的

严格以上来说Servlet其实并不是简单的一个接口,Servlet规范中规定了:

  • 一个合格的webapp应该是一个怎样的目录结构
  • 一个合格的webapp应该有一个怎样的配置文件
  • 一个合格的webapp配置文件路径应该放在哪里
  • 一个合格的webapp中Java程序应该放在哪里
  • 这些都是Servlet规范中规定的。

Tomcat服务器要遵循Servlet规范,JavaEE程序员也要遵循这个Servlet规范,这样二者才能解耦合,即webapp可以随意放在任何一个WEB服务器上。

2. 开发第一个Servlet

上面模拟了Servlet的本质,接下来具体开发一个带有Servlet的webapp。建议的开发步骤如下所示:

  1. 在Tomcat的安装目录下的webapps目录下新建一个目录,起名crm(这个crm就是项目的名字)。注意,这个crm就是这个项目的根目录。

  2. 在这个根目录下新建一个目录:WEB-INF,注意,这个目录的名字是Servlet规范中规定的,必须全部大写,必须一模一样。(注意,静态资源文件html必须放在WEB-INF目录之外。

  3. WEB-INF目录下新建一个目录:classes,注意,这个目录的名字也是Servlet规范中规定的,另外这个目录下存放的是Java程序编译之后的class文件。

  4. WEB-INF目录下新建一个目录:lib,注意,这个目录不是必须的,但是如果一个webapp需要第三方的jar包的话,这个jar包必须存放在这个目录下,这个目录的名字也是Servlet规范中规定的。例如Java语言连接数据库所需要的Jar包,就一定要放到这个lib目录下。

  5. WEB-INF目录下新建一个文件:web.xml,注意,这个文件是必须的,文件名也是Servlet规范中规定的。这个文件就是配置文件,描述了请求路径和Servlet类之间的对照关系。另外,这个文件最好是从被的项目中拷贝,否则自己写比较麻烦,只需要修改一些对应关系即可。

  6. 编写Java程序,必须实现Servlet接口。这个接口不是Java SE中的,所以不再JDK中,属于Java EE。但是因为Tomcat需要使用这个接口,所以Tomcat中应该就已经包含了这个接口,在安装目录中的lib文件夹内有对应的Jar包,将该Jar包配置到ClassPath中即可。注意,配置这个Jar包和Tomcat运行没有任何关系,只是为了编译生成class文件,Tomcat自身有配置文件,可以自己找到自己安装目录下的jar包。

    注意,这个Java程序可以在任意位置编写,只需要将对应的编译后的class文件放到WEB-INF目录下的classes子文件夹下即可。

  7. 将编译后的class文件拷贝到classes子文件夹下。

  8. 编写web.xml文件,将请求路径和Servlet实现类关联在一起。学名叫注册

  9. 启动Tomcat,启动浏览器,请求上述的URL并测试。

注意:

JavaEE目前最高版本是JavaEE8,JavaEE被Oracle捐献给Apache了,Apache将JavaEE改名为JakartaEE,所以JavaEE8版本升级后的JaavEE9编程了JakartaEE9。所以最新版本的Tomcat10,在lib目录下的Jar包解压后,不再是Javax了,而是Jakarta。所以,包名需要修改。javax.servlet包修改为jakarta.servlet,这时候Tomcat9及以前的版本部署的项目就不能部署到Tomcat10上了,需要修改导入的包名。

Servlet接口主要有5个抽象方法,具体的描述后续再说,这里先简单实现一下。

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 servlet;

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

/**
* 第一个Servlet
*/
public class HelloServlet implements Servlet{

public void init(ServletConfig config) throws ServletException{
}

public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException{
System.out.println("My First Servlet: Hello Servlet");
}

public void destroy(){

}

public String getServletInfo(){
return "";
}

public ServletConfig getServletConfig(){
return null;
}
}

将编译好后的class文件(包括其所在包)复制到classes文件夹下。接下来配置映射文件web.xml。其关键内容如下所示:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0"
metadata-complete="true">

</web-app>

和html一样,根标签是<web-app>,只需要在其内部嵌套映射标签即可。这步目的是将class文件和请求路径对应起来。所以需要有class文件的路径以及请求路径,二者需要一个名字,将两个映射起来。完整的配置如下所示(注意,似乎运行的时候,配置文件中不能有中文,所以下面的中文是为了注释。):

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
<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0"
metadata-complete="true">

<!-- Servlet描述信息 -->
<!-- 一个servlet对应一个servlet-mapping -->
<servlet>
<servlet-name>servlet_hello</servlet-name>

<!-- 带有包名的全限定类名 -->
<servlet-class>servlet.HelloServlet</servlet-class>
</servlet>

<!-- Servlet映射信息 -->
<servlet-mapping>

<!-- 这里的name也是随便写,就是将描述信息和映射信息对应起来,所以要和描述信息中的name一致 -->
<servlet-name>servlet_hello</servlet-name>

<!-- 这里的路径就是将来浏览器的请求路径,随便写,但是要以 / 开始 -->
<url-pattern>/asdf/qwer/zxcv</url-pattern>
</servlet-mapping>
</web-app>

配置好后打开Tomcat进行测试,如下所示,在Tomcat的命令行中确实打印了对应的内容(注意,URL中的请求路径后缀一定和web.xml中的url-pattern中的路径一致):

servlet_001.png (948×678) (gitee.io)

为了方便测试,也可以新建一个静态资源文件html(注意,是在WEB-INF外面),里面的超链接中设置对应的请求路径即可,如下所示:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<title>first-servlet</title>
<meta charset="utf-8">
</head>
<body>
<a href="/FirstServlet/asdf/qwer/zxcv">测试第一个Servlet</a>
</body>
</html>

servlet_002.png (817×791) (gitee.io)

综上所述,一个合法的webapp项目目录结构应该如下所示:

1
2
3
4
5
6
7
8
9
10
webapproot
|------WEB-INF
|------classes(存放字节码)
|------lib(第三方Jar包)
|------web.xml(注册Servlet)
|------html
|------css
|------javascript
|------image
......

上述是将信息打印到Tomcat服务器的控制台上,那么怎么将信息输出到浏览器上呢?需要调用ServletResponse对象的相关方法,如下所示:

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
package servlet;

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

/**
* 第一个Servlet
*/
public class HelloServlet implements Servlet{

public void init(ServletConfig config) throws ServletException{
}

public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException{
// System.out.println("My First Servlet: Hello Servlet");

// response表示响应,从服务器向浏览器发送数据叫做响应,该接口的getWriter方法返回一个PrintWriter对象。
// 这个对象是一个IO流,Tomcat负责该流的刷新和关闭,我们不需要关注。
// 设置响应内容的类型,可以是文本,也可以是html,注意,设置响应内容的类型的语法一定要在获取流对象前之前有效,另外也可以设置文本的编码格式
response.setContentType("text/html");

// 获取响应流对象
PrintWriter out = response.getWriter();

out.print("HelloServlet, you are my first servlet");

}

public void destroy(){

}

public String getServletInfo(){
return "";
}

public ServletConfig getServletConfig(){
return null;
}

}

servlet_003.png (685×131) (gitee.io)

上述是服务器向浏览器返回了一段文本,但是浏览器实际上是可以识别html代码的,所以可以测试一下返回一段代码,查看浏览器是否会编译。修改内容如下所示:

1
out.print("<br><input type='button' value='button from tomcat'>");

servlet_004.png (629×130) (gitee.io)

2.1 servlet编写JDBC连接数据库

注意,要将JDBC的Jar包放入webapp项目的lib中。简单例子如下所示:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package servlet;

import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.*;

/**
* Servlet编写JDBC连接数据库
*/
public class ConnectJDBC implements Servlet{

public void init(ServletConfig config) throws ServletException{
}

public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException{

// response表示响应,从服务器向浏览器发送数据叫做响应,该接口的getWriter方法返回一个PrintWriter对象。
// 这个对象是一个IO流,Tomcat负责该流的刷新和关闭,我们不需要关注。
// 设置响应内容的类型,可以是文本,也可以是html
response.setContentType("text/html");
PrintWriter out = response.getWriter();

Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;

try{
// 注册驱动
Class.forName("com.mysql.jdbc.Driver");

// 获取连接
String user = "user";
String password = "password";
String url = "jdbc:mysql://ip:3306/mysql_test";
conn = DriverManager.getConnection(url, user, password);

// 获取预编译的数据库操作对象
String sql = "select * from t_user";
ps = conn.prepareStatement(sql);

// 执行SQL
rs = ps.executeQuery();

// 处理结果

while(rs.next()){
out.print("no: " + rs.getString("no") + ";" +
"login: " + rs.getString("login") + ";" + "<br>");
}
}catch(Exception e){
e.printStackTrace();
}finally{
// 释放资源
if(rs != null){
try{
rs.close();
}catch(Exception e){
e.printStackTrace();
}
}

if(ps != null){
try{
ps.close();
}catch(Exception e){
e.printStackTrace();
}
}

if(conn != null){
try{
conn.close();
}catch(Exception e){
e.printStackTrace();
}
}
}

// out.print("HelloServlet, you are my first servlet");

}

public void destroy(){

}

public String getServletInfo(){
return "";
}

public ServletConfig getServletConfig(){
return null;
}

}

新增xml映射如下所示:

servlet_006.png (641×217) (gitee.io)

数据库中的表如下所示:

servlet_007.png (448×236) (gitee.io)

结果如下所示:

servlet_005.png (528×127) (gitee.io)

2.2 IDEA编写Servlet

上述只是手动在文本编辑器中实现了Servlet,但是这样做很麻烦,每次都需要手动编译java文件,然后将class文件放到classes目录中,可以采用IDEA工具进行Servlet开发。之前在上一篇文章中已经叙述了IDEA的配置过程,这里不再赘述。

这里还需要的就是:

  • 编写Servlet程序
  • 手动将需要的Jar包放入到新建的lib目录中;
  • 在web.xml中注册配置信息;
  • 手动创建一些静态资源,比如HTML(这一步需要在Web目录下,即webapp根目录下)。
  • 但是真正的项目名在哪里呢?(项目名需要在Edit Configuration中写)

3. Servlet接口及其对象的生命周期

什么是Servlet对象生命周期?

  • Servlet对象什么时候被创建
  • Servlet对象什么时候被销毁
  • Servlet对象创建了几个?
  • Servlet对象的生命周期表示:一个Servlet对象从出生到最后的死亡,整个过程是怎样的。

其实在上面的例子中可以看到,程序员只需要编写Servlet的实现类即可,至于对象的创建、方法的调用以及对象的销毁都是由Tomcat自己操作的(可参考第一部分,模拟Servlet本质)。

  • Servlet对象的生命周期是由Tomcat服务器(WEB Server)全权负责的。
  • Tomcat服务器通常我们又称为:WEB容器

我们自己new的Servlet对象受WEB容器的管理吗?

  • 我们自己new的Servlet对象是不受WEB容器管理的,只是一个普通的对象,不会被放到Tomcat的集合中的;WEB容器创建的Servlet对象会被放到一个集合当中(HashMap),只有放到这个集合中的Servlet对象才能够被放到WEB容器管理。

servlet_008.png (395×479) (gitee.io)

3.1 Servlet对象的创建

对象什么时候被创建呢?首先看一下Tomcat服务器启动的时候会不会创建Servlet对象,可以在Servlet实现类中添加构造方法,输出一段文字进行测试,经过测试,默认情况下Tomcat服务器在启动的时候,Servlet对象并不会被创建。在用户没有发起请求之前,如果提前创建出对象,必定耗费资源的。

不过可以手动更改,让Tomcat在启动的时候创建Seevlet对象,在web.xml中配置如下所示,即在想要启动的类中添加标签<load-on-startup>0</load-on-startup>,标签的内容是整数,整数越小,优先级越高,对象越先被创建

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
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<servlet>
<servlet-name>servlet_A</servlet-name>
<servlet-class>servlet.AServlet</servlet-class>
<load-on-startup>0</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>servlet_A</servlet-name>
<url-pattern>/servletA</url-pattern>
</servlet-mapping>

<servlet>
<servlet-name>servlet_B</servlet-name>
<servlet-class>servlet.BServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>servlet_B</servlet-name>
<url-pattern>/servletB</url-pattern>
</servlet-mapping>
</web-app>

可以看到控制台确实输出了AServlet对象的构造方法输出。

servlet_010.png (586×193) (gitee.io)

servlet_009.png (1094×197) (gitee.io)

3.2 Servlet接口的几个方法

Servlet接口主要有5个方法,对其主要的几个方法进行测试:

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
public class AServlet implements Servlet {

public AServlet(){
System.out.println("AServlet的无参构造方法执行了。。。");
}

@Override
public void init(ServletConfig servletConfig) throws ServletException {
System.out.println("AServlet的init方法执行了");
}

@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("AServlet的srvice方法执行了");
}

@Override
public void destroy() {
System.out.println("Aervlet的destroy方法执行了");
}

@Override
public ServletConfig getServletConfig() {
return null;
}

@Override
public String getServletInfo() {
return null;
}
}

先恢复Tomcat服务器启动不创建对象,然后进行测试。浏览器发送请求的时候,以下的方法执行了:

servlet_011.png (564×185) (gitee.io)

即:

  • 用户在第一次发送请求的时候Servlet对象被实例化(AServlet的构造方法被执行了,并且执行的是无参构造方法)

  • AServlet对象被创建出来之后,Tomcat服务器马上调用了init方法,也就是init方法在执行的时候,AServlet对象已经存在了。

  • 因为用户发送了请求,所以在init方法执行之后,Tomcat马上调用service方法。

  • 继续在浏览器刷新,即再次发送请求,可以看到Tomcat只执行了service方法,即使用前面创建好的对象

    • 说明Servlet对象是单实例的,但Servlet对象不符合单实例的,所以是假单例。(即使后续有很多个用户访问,Servlet对象也只有一个。)
    • 所以 无参构造方法、init方法 只被调用一次。
    • service方法,只要被请求一次,就会被执行一次。

    servlet_012.png (508×289) (gitee.io)

  • 关闭服务器,Servlet对象的destroy方法被执行,即只有关闭服务器的时候,销毁对象的内存之前,会自动调用destroy方法,只执行一次。

可以看到,Servlet对象的生命周期更像是人的一生。另外,如果提供有参构造方法的话,必须要手动提供无参构造方法。否则会返回500错误,没有无参构造方法,实例化对象失败。所以一般情况下不建议写构造方法。

实际上Servlet接口提供init方法,就是为了代替有参构造方法,使得程序更加安全,可以在该方法中手动编写要执行的操作。Servlet接口中的方法如下所示:

方法 描述
void init(ServletConfig servletConfig) 该方法很少用,通常在init方法中做初始化操作,并且这个初始化操作只需要执行一次,例如初始化数据库连接池、初始化线程池等等。
void service(ServletRequest servletRequest, ServletResponse servletResponse) 该方法用的最多,用来处理用户请求的核心方法。
void destroy() 该方法很少用,通常进行资源的关闭。
ServletConfig getServletConfig() 获得ServletConfig对象,包括Servlet对象的初始化和启动参数。
String getServletInfo() 获得Servlet的相关信息,比如作者、版本和版权。

3.3 适配器模式修改Servlet

上面讲到,Servlet中的5个方法,只有service方法是常用的,其余的几个方法基本上不用。那么这样,我们就没必要在每个Servlet实现类中都实现这4个方法,太冗余,代码也不美观。此时可以采用适配器模式,创建抽象类实现Servlet中不常用的方法,将常用的方法保留为抽象方法,等待真正的Servlet实现类继承该抽象类再实现该方法。简单例子如下所示:

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
package adapter;

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


public abstract class UserAdapter implements Servlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {

}

@Override
public ServletConfig getServletConfig() {
return null;
}

// 保留该方法为抽象方法,用于后续继承类来实现
@Override
public abstract void service(ServletRequest servletRequest, ServletResponse servletResponse);

@Override
public String getServletInfo() {
return null;
}

@Override
public void destroy() {

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

import adapter.UserAdapter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;


public class CServlet extends UserAdapter {

// 只需要实现该抽象方法即可。
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
System.out.println("适配器模式下的Servlet实现类");
}
}

上面只是最基本的适配器,其实可以对适配器进行改造。首先Servlet接口中的ServletConfig getServletConfig()方法是为了获得ServletConfig对象,这个对象是哪个对象呢?可以看到void init(ServletConfig servletConfig)方法在执行的时候,会传入一个ServletConfig对象,那么是谁传入的呢?因为Servlet对象是Tomcat自己创建的,所以这个对象实际上也是Tomcat自己创建好的,然后作为实参传入init方法。也就是上面的ServletConfig getServletConfig()实际上就是要获得这个ServletConfig对象,那么怎么在适配器中操作可以更方便的获得该对象呢?加入成员变量即可。但是此时子类可能会重写该方法,这就使得父类方法中的代码被重写,影响父类的逻辑,而且子类又必须重写,这该怎么办呢?,解决办法就是父类将init设为final方法,再重载一个无参的init方法,有参init方法调用无参init方法,然后子类重写无参init方法。如下所示:

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
public abstract class UserAdapter implements Servlet {

private ServletConfig servletConfig;

@Override
public final void init(ServletConfig servletConfig) throws ServletException {
// 成员变量赋值,用于下面的方法返回值
this.servletConfig = servletConfig;
init();
}

public void init(){
System.out.println("父类重载的init方法");
}

@Override
public ServletConfig getServletConfig() {
return servletConfig;
}

// 保留该方法为抽象方法,用于真正的Servlet实现类来实现该方法
@Override
public abstract void service(ServletRequest servletRequest, ServletResponse servletResponse);

@Override
public String getServletInfo() {
return null;
}

@Override
public void destroy() {

}
}
1
2
3
4
5
6
7
8
9
10
public class CServlet extends UserAdapter {
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
System.out.println("适配器模式下的Servlet实现类");
}

public void init(){
System.out.println("子类重写无参init方法");
}
}

注意,上述的Adapter抽象类已经实现了,在javax.servlet.GenericServlet,该抽象类的大致源码如下所示,即包括了Servlet接口的5个方法、ServletConfig接口的4个方法,1个无参构造方法,1个无参init方法,和2个自定义的log日志方法:

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
52
53
54
55
56
57
58
package javax.servlet;

import java.io.IOException;
import java.io.Serializable;
import java.util.Enumeration;

public abstract class GenericServlet implements Servlet, ServletConfig, Serializable {
private static final long serialVersionUID = 1L;
private transient ServletConfig config;

public GenericServlet() {
}

public void destroy() {
}

public String getInitParameter(String name) {
return this.getServletConfig().getInitParameter(name);
}

public Enumeration<String> getInitParameterNames() {
return this.getServletConfig().getInitParameterNames();
}

public ServletConfig getServletConfig() {
return this.config;
}

public ServletContext getServletContext() {
return this.getServletConfig().getServletContext();
}

public String getServletInfo() {
return "";
}

public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}

public void init() throws ServletException {
}

public void log(String message) {
this.getServletContext().log(this.getServletName() + ": " + message);
}

public void log(String message, Throwable t) {
this.getServletContext().log(this.getServletName() + ": " + message, t);
}

public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

public String getServletName() {
return this.config.getServletName();
}
}

测试类如下所示:

1
2
3
4
5
6
7
8
9
10
11
public class DServlet extends GenericServlet {

@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("利用javax.servlet.GenericServlet包实现的service方法");
}

public void init(){
System.out.println("这是重写GenericServlet类下的init方法");
}
}

结果如下所示:

servlet_013.png (799×187) (gitee.io)

4. ServletConfig接口

Tomcat在创建Servlet实现类对象之后,会先创建ServletConfig接口的实现类对象,然后将其作为实参,传入init方法并调用该方法。接下来看一下ServletConfig接口的相关内容。

  • ServletConfig是Servlet规范中的一员,javax.servlet.ServletConfig

实际上,创建的ServletConfig实现类是org.apache.catalina.core.StandardWrapperFacade,该类实现了ServletConfig接口,即Tomcat实现了ServletConfig的接口,并创建了该类对象,传入Servlet实现类对象的init方法中。那么如果有多个Servlet类对象被创建,ServletConfig对象会不会创建多个呢?答案是肯定的,编写两个Servlet实现类,启动服务器并分别请求,查看其ServletConfig对象地址,二者是不一样的。也就是说,一个Servlet对象就会关联一个ServletConfig对象。

servlet_014.png (1190×147) (gitee.io)

4.1 ServletConfig接口的作用

实际上,这个对象是关于Servlet对象的配置信息形成的对象,也被称为Servlet对象的配置信息对象。不同的Servlet对象的配置信息不同,所以需要不同的ServletConfig对象。那么ServletConfig对象中到底有什么信息呢?实际上就是web.xml中对应的<servlet></servlet>标签中的信息。即根据标签中的信息将请求路径和示例对象关联起来。

4.2 ServletConfig接口的方法

ServletConfig接口提供了4个方法,另外JavaEE提供了该接口的两个实现类GenericServlet和HttpServlet,这两个类也都实现了这4个方法。4个方法如下所示:

方法名 描述
String getInitParameter(String name) 获取指定name的初始化配置信息。(注意,在web.xml中是可以通过<init-para>标签来设置servlet的初始化信息的
Enumeration<String> getInitParameterNames() 获取所有初始化信息的name。
ServletContext getServletContext() 获得ServletContext对象
String getServletName() 获得Servlet对象的名字

设置web.xml中的指定Servlet实现类的初始化参数信息,部分信息如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<servlet>
<servlet-name>ServletConfigTest01</servlet-name>
<servlet-class>servletConfig.ServletConfigTest01</servlet-class>

<!-- 设置该类对象的初始化信息 -->
<init-param>
<param-name>driver</param-name>
<param-value>com.mysql.jdbc.Driver</param-value>
</init-param>
<init-param>
<param-name>user</param-name>
<param-value>root</param-value>
</init-param>
<init-param>
<param-name>password</param-name>
<param-value>root</param-value>
</init-param>
<init-param>
<param-name>url</param-name>
<param-value>jdbc:mysql://localhost:3306/mysql_test</param-value>
</init-param>
</servlet>

在Servlet类对象中利用ServletConfig对象获取该信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ServletConfigTest01 extends GenericServlet {
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
ServletConfig sc = this.getServletConfig();
System.out.println("ServletConfigTest01类对象中的ServletConfig对象:" + sc);

Enumeration<String> initParameterNames = sc.getInitParameterNames();
while(initParameterNames.hasMoreElements()){
String name = initParameterNames.nextElement();
System.out.println( name + ":" + sc.getInitParameter(name));
}

}
}

servlet_015.png (1204×274) (gitee.io)

注意,因为GenericServlet抽象类已经实现了ServletConfig接口,而自己编写的Servlet实现类也继承了GenericServlet抽象类,所以自己的这个实现类就相当于有了上面ServletConfig的4个方法,直接用this来调用即可。

注意,ServletConfig对象是Tomcat实现接口类并创建类对象的,所以它里面的4个方法是Tomcat实现的,而GenericServlet抽象类实现了ServletConfig接口,并且通过init方法使得自身的成员变量ServletConfig类对象有了值,所以采用this来调用,底层也是通过获取成员变量来调用该方法的。

如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ServletConfigTest01 extends GenericServlet {
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
ServletConfig sc = this.getServletConfig();
System.out.println("ServletConfigTest01类对象中的ServletConfig对象:" + sc);

Enumeration<String> initParameterNames = this.getInitParameterNames();
while(initParameterNames.hasMoreElements()){
String name = initParameterNames.nextElement();
System.out.println( name + ":" + this.getInitParameter(name));
}

}
}

5. ServletContext接口

前面讲解了ServletConfig接口及其几个方法,还有一个方法返回的是ServletContext接口对象,本节讲解一下ServletContext接口的内容。

5.1 ServletContext接口的作用

Tomcat服务器实现了该接口,类名为org.apache.catalina.core.ApplicationContextFacade。ServletContext接口也是Servlet规范中的一员。

ServletContext对象是WEB服务器启动的时候由WEB服务器创建的(注意,该对象是WEB服务器启动的时候创建的,而Servlet对象以及ServletConfig对象则是第一次被请求的时候才创建的),通过编写两个Servlet实现类进行测试,发现ServletContext类对象只有一个。实际上该对象对应的内容是web.xml文件中的内容,即对于一个webapp来说,ServletContext对象只有一个。该对象也被称为Servlet对象的环境对象(Servlet对象的上下文对象)。而上面的ServletConfig对象,则是web.xml中某个servlet的配置信息。

5.2 ServletContext接口的方法

ServletContext接口的常用方法如下表所示:

方法名 描述
String getInitParameter(String name) 获得指定name的全局配置信息
Enumeration<String> getInitParameterNames() 获得全局配置信息的所有name
String getContextPath() 获取所在webapp项目的根路径
String getRealPath(String path) 获取path的绝对路径,这里的path指的是在webapp中的路径,以web为根路径,如/path.html.
void log(String msg) 将指定的信息写入日志文件,日志在Tomcat安装目录下的logs目录下
void log(String message, Throwable throwable) 在上述方法的基础上,也将异常信息写入进去
void setAttribute(String name, Object object) 向应用域中存数据(应用域的概念可参考5.3节)
void getAttribute(String name) 向应用域中取数据
void removeAttribute(String name) 删除应用域中的数据

注意,除了在<servlet></servlet>标签中设置该类对象的初始化信息,也可以在<web></web>标签中设置整个webapp的初始化信息,注意是在servlet外面的进行设置,不属于某个servlet的配置,是全局的,而ServletContext中的那两个方法就是获得的是全局的配置信息。所以,如果webapp中的所有servlet都共有的配置信息可以配到全局上。如下所示:

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
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<!--设置整个webapp的初始化信息-->
<context-param>
<param-name>pageSize</param-name>
<param-value>10</param-value>
</context-param>

<context-param>
<param-name>startIndex</param-name>
<param-value>0</param-value>
</context-param>

<servlet>
<servlet-name>ServletConfigTest01</servlet-name>
<servlet-class>servletConfig.ServletConfigTest01</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>ServletConfigTest01</servlet-name>
<url-pattern>/ServletConfig</url-pattern>
</servlet-mapping>

</web-app>

测试方法如下所示,和ServletConfig类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ServletContextTest02 extends GenericServlet {
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
ServletContext servletContext = this.getServletContext();
System.out.println(servletContext);

System.out.println(servletContext.getInitParameter("pageSize"));

Enumeration<String> initParameterNames = servletContext.getInitParameterNames();
while (initParameterNames.hasMoreElements()){
System.out.println(initParameterNames.nextElement());
}

System.out.println(servletContext.getContextPath());

// 不加斜杠也可以
System.out.println(servletContext.getRealPath("/path.html"));

}
}

servlet_016.png (916×187) (gitee.io)

注意,如果采用传统的方法来运行Tomcat服务器的话,日志默认是在Tomcat安装目录的logs下的,但是如果采用IDEA来运行的话,会默认在自己的备份目录下来保存日志的。可以在启动服务器的时候,看到CATALINA_BASE采用的是IDEA中目录的。日志种类及文件名如下所示

  • catalina.*.log,服务器端Java程序运行的控制台信息
  • localhost.*.log,ServletContext对象的log方法记录的日志信息
  • localhost_access_log.*.log,用户访问日志

5.3 应用域

该接口对象也被称为应用域,这个应用域相当于一种数据结构,可以存储数据,相当于缓存,存取比IO速度快。除了应用域,还有一些其他的域,比如请求域、会话域等等。如果所有的用户共享一份数据,并且这个数据很少的被修改,并且这个数据量很少,那么可以将这些数据放到ServletContext这个应用域中。

  • 因为ServletContext对象只有一个,只有所有Servlet对象共享的数据放进去才有意义;
  • 数据量比较大的话,太占用堆内存,并且这个对象的生命周期比较长,服务器关闭的时候,这个对象才会被销毁。
  • 如果所有用户共享的数据频繁修改,必然会存在线程并发所带来的线程安全问题,所以ServletContext对象中的数据一般都是只读的。

在一个Servlet实现类中servletContext.setAttribute("1", "zhangSan");,在另一个Servlet实现类中System.out.println(servletContext.getAttribute("1"));,可以看到控制台中的确输出了”zhangSan”。

实际上向应用域当中绑定数据,就相当于把这个数据放到了缓存(cache)当中,然后用户访问的时候直接从缓存中取,减少IO的操作(比如提前将数据库的数据查询出来,或者提交将数据库连接对象连接好),大大提升了系统的性能,所以缓存技术是挺高性能的重要手段。

目前为止接触过的缓存机制:

  • 堆内存中的字符串常量池
    • 如一个字符串“abc”,现在字符串常量池中查找,如果有,直接拿来用。如果没有则新建,然后再放入字符串常量池。
  • 堆内存中的整数型常量池
    • [-128~127]一共256个Integer类型的引用,放在整数型常量池中,没有超出这个范围的数字,直接从常量池中取。
  • 连接池
    • 这里所说的连接池是Java连接数据库的连接对象:java.sql.Connection。
    • JVM是一个进程。MySQL数据库是一个进程,进程和进程之间建立连接,打开通道是很费劲的,是很耗费资源的。可以提前先创建好N个Connection连接对象,将连接对象放到一个集合当中,我们把这个存放对象的集合称为连接池。每一次用户连接的时候不需要再新建连接对象,省去了新建的环节,直接从连接池中取连接对象,大大提升了访问效率。另外,连接池中保证了最大连接数,也保证了系统的安全性。
  • 线程池
    • Tomca服务器本身就支持多线程的。
    • Tomcat服务器是在用户发送一次请求,就新建一个Thread线程对象吗?
      • 当然不是,实际上是在Tomcat服务器启动的时候,会先创建好N多个线程Thread对象,然后将线程对象放到集合当中,称为线程池。用户发送请求过来之后,需要有一个对象的线程来处理这个请求,这个时候就会直接从线程池中来线程对象来处理。
  • redis
    • NoSQL数据库,非关系型数据库,缓存数据库
  • 向ServletContext应用域中存储数据,也等于是将数据存放到缓存cache中。

6. 中间总结

本质上Tomcat服务器调用我们编写的Servlet实现类来为浏览器提供服务,但是Servlet接口中的方法有很多方法不需要,所以JavaEE利用适配器模式创建了Servlet的一个抽象实现类GenericServlet,但是我们B/S架构的系统是基于HTTP超文本传输协议的,所以,为了更方便的编程,以后我们在编写Servlet类要继承JavaEE提供的另一个Servlet实现类HttpServlet,它是专门为HTTP协议准备的一个Servlet类。我们编写的类要继承HttpServlet,使用HttpServlet处理HTTP协议更便捷。HttpServlet类也继承了GenericServlet类。继承关系如下所示:

  • javax.servlet.Servlet(接口)【爷爷】
    • javax.servlet.GenericServlet(抽象类)【儿子】
      • javax.servlet.http.HttpServlet(抽象类)【孙子】

7. HTTP协议

因为B/S架构是基于HTTP协议传输数据的,所以HttpServlet可以很好的处理HTTP协议数据包。在讲解HttpServlet类之前,先了解一下HTTP协议。

协议就是某种规范,某种规定。比如汉语、英语等等,这些都是制定好的协议,大家按照某种协议来,可以沟通无障碍。那么计算机之间的通信也需要某种协议,HTTP协议是W3C制定的一种超文本传输协议,这种协议游走在B/S之间,是通信协议,即发送消息的模板提前被制定好的。HTTP协议包括请求协议和响应协议:

  • 请求协议
    • 浏览器向WEB服务器发送数据的时候,这个发送的数据需要遵循一套标准,这个标准中规定了发送的数据具体格式。
  • 响应协议
    • WEB服务器向浏览器发送数据的时候,这个发送的数据需要遵循一套标准,这个标准中规定了发送的数据具体格式。

为了获得请求协议和响应协议的具体内容,编写form表单和对应的servlet实现类来测试一下。网页源码如下所示:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>post and get</title>
</head>
<body>
<h1>get请求</h1>
<form action="/servlet03/getServlet" method="get">
用户名:<input type="text" name="username"/><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="login">
</form>

<br>

<h1>post请求</h1>
<form action="/servlet03/postServlet" method="post">
用户名:<input type="text" name="username"/><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="login">
</form>

</body>
</html>

get和post请求servlet实现类如下所示:

1
2
3
4
5
6
7
8
9
public class GetServlet extends GenericServlet {
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
servletResponse.setContentType("text/html");
PrintWriter out = servletResponse.getWriter();

out.print("this response if from get");
}
}
1
2
3
4
5
6
7
8
9
public class PostServlet extends GenericServlet {
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
servletResponse.setContentType("text/html");
PrintWriter out = servletResponse.getWriter();

out.print("this response if from post");
}
}

配置文件修改好后,运行网页的时候点击F12,查看网络流。

servlet_017.png (1720×850) (gitee.io)

7.1 HTTP请求协议

HTTP请求协议包括四部分:

  • 请求行

    包括三部分

    • 请求方式(get(常用)、post(常用)、delete、head、put、options、trace)
    • URI
      • 统一资源标识符,代表某个资源的名字,但是通过这个URI是无法定位资源的。
      • URL是统一资源定位符,可以定位资源,URL是包含URI的。如下所示:
    • 协议版本号(HTTP/1.1)
  • 请求头

    • 请求主机
    • 主机端口
    • 浏览器信息
    • cookie
  • 空白行

    用来区分请求头和请求体。

  • 请求体

    向服务器发送的具体数据

get请求报文如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET /servlet03/getServlet?username=fuyun&password=asdf HTTP/1.1						(请求行)
Host: localhost:8080 (请求头)
Connection: keep-alive
sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="97", "Chromium";v="97"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 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: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://localhost:8080/servlet03/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: _xsrf=2|4625403a|23df939a35c378e3490d78b3c8ed5792|1641540047; username-localhost-8888="2|1:0|10:1641541087|23:username-localhost-8888|44:ZTQ1NDEwMTlkOTU3NDIwNjhmNjc2MDcwYmMzMDFmYTk=|cee758225322d1fb51d837825cb244f7490e0cbee520215a9b15111d87a74fd6"; username-localhost-8889="2|1:0|10:1641541104|23:username-localhost-8889|44:Mjg1ZGI1NTFiZDc0NGU2MDkyNTE1NTJmYmNjZDUxZjM=|de7019faea91f6ccfc5fc6fb7a8fa25dc8c7b50ac6634fe82e5ed2bd895e0df0"
(空白行)
(请求体)

post请求报文如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST /servlet03/postServlet HTTP/1.1												(请求行)
Host: localhost:8080 (请求头)
Connection: keep-alive
Content-Length: 28
Cache-Control: max-age=0
sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="97", "Chromium";v="97"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: http://localhost:8080
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 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: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://localhost:8080/servlet03/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: _xsrf=2|4625403a|23df939a35c378e3490d78b3c8ed5792|1641540047; username-localhost-8888="2|1:0|10:1641541087|23:username-localhost-8888|44:ZTQ1NDEwMTlkOTU3NDIwNjhmNjc2MDcwYmMzMDFmYTk=|cee758225322d1fb51d837825cb244f7490e0cbee520215a9b15111d87a74fd6"; username-localhost-8889="2|1:0|10:1641541104|23:username-localhost-8889|44:Mjg1ZGI1NTFiZDc0NGU2MDkyNTE1NTJmYmNjZDUxZjM=|de7019faea91f6ccfc5fc6fb7a8fa25dc8c7b50ac6634fe82e5ed2bd895e0df0"
(空白行)
username=fuyun&password=asdf (请求体)

7.1.1 GET请求和POST请求的发送

到目前为止,只有一种情况可以发送POST请求:就是使用form表单,并且form标签中的method属性值为:method=”post”,其他所有情况一律都是GET请求:

  • 在浏览器地址栏上直接输入URL,敲回车,属于GET请求;
  • 在浏览器上直接点击超链接,属于GET请求;
  • 使用form表单提交数据时,没有使用method或者method为get,属于GET请求;

7.1.2 GET请求和POST请求的区别

get请求发送数据的时候,数据会挂在URI的后面,以问号连接URI和数据,这样会导致发送的数据回显在浏览器的地址栏上。即get请求在“请求行”上发送数据。只能发送普通的字符串,有长度限制,不同的浏览器有不同的限制,即不能发送大数据串。get请求比较适合从服务器端获取数据,即发送获取数据请求。get请求是绝对安全的,因为get请求只是为了从服务器上获取数据而发送的请求,不会对服务器造成威胁。这里的安全是相对服务器来说的。

post请求发送数据的时候,数据会挂在请求体中,数据不会回显在浏览器的地址栏上。即post请求在“请求体”上发送数据。可以发送任何数据类型的数据,如图片、视频等等,理论上没有长度限制。post请求比较适合向服务器端传送数据,即发送传输数据请求。但是post请求是危险的,因为post请求是向服务器提交数据,如果这些数据通过后门的方式进入到服务器当中,服务器是很危险的。

二者的数据格式在传输过程中,都是 name=value&name=value…的形式,这是协议规定好的。name就是标签的属性名name的值,value就是标签的属性名value的值。

另外,get请求是支持缓存的,浏览器可以缓存get请求的数据(一个get请求路径会对应一个缓存资源,如果想要浏览器直接请求而不检查缓存中的数据,可以每次在请求路径后面加一个系统时间戳);而post请求则不支持缓存,获得的响应数据不会被浏览器缓存。

大部分的form表单提交,都是采用的post方式;如果form表单中有敏感信息,也是建议采用post方式;另外,如果是文件上传,也采用post。其他情况可以使用get。

7.2 HTTP响应协议

HTTP响应协议包括四部分:

  • 状态行(重点

    由三部分组成

    • 协议版本号(HTTP/1.1)
    • 状态码(200表示请求响应成功,正常结束;404表示资源不存在;405表示前端发送的请求方式和后端请求的处理方式不一致,如get和post;500表示服务器端的程序出现异常;以4开始的一般是浏览器端的错误,以5开始的一般是服务器端的错误)
    • 状态描述信息(ok表示成功,not found表示找不到)
  • 响应头

    • 响应的内容类型
    • 响应的内容长度
    • 响应的时间
  • 空白行

    • 用来分割响应头和响应体。
  • 响应体

    • 服务器响应的具体内容,即实际数据,可能是html源码。
1
2
3
4
5
6
7
8
HTTP/1.1 200 ok									(状态行)
Content-Type: text/html;charset=ISO-8859-1 (响应头)
Content-Length: 25
Date: Sun, 09 Jan 2022 07:28:25 GMT
Keep-Alive: timeout=20
Connection: keep-alive
(空白行)
this response if from get (响应体)

8. 设计模式(概述)

设计模式就是某个问题的固定的解决方案,也就是套路。常见的设计模式有:

  • GoF设计模式
    • Gang of Four(4人组)提出的23种设计模式。比如单例模式、适配器模式、模板方法模式等等。
  • JavaEE设计模式
    • DAO、DTO、VO等等。

模板方法模式概述如下:

先想象一个场景,学生和教师,两人一天的流程包括:起床、吃早饭、吃午饭、工作(上课/教课)、吃完饭、睡觉。既然两个人的流程一样,只有工作的方法实现不一样,所以可以将二人抽象出一个类,类中的一天流程方法是固定的,一些常用的方法(吃饭、睡觉)也是固定的,只有工作的方法具体实现不一样,可以将其作为抽象方法。

也就是说,上面的抽象类就是模板类,一天的流程(核心算法骨架,可以用final修饰,不被子类改写)是模板方法,具体的工作方法留给子类来具体实现。

也就是说,模板方法设计模式在模板类中定义核心的算法骨架,具体的实现步骤可以延迟到子类当中去实现。模板类通常是一个抽象类,定义的核心方法通常是final的。模板类中的抽象方法就是不确定怎么实现的方法,通常交给子类来做。

其实这种模式可以类似Tomcat服务器创建的Servlet实例,Tomcat固定了模板类,我们只需要实现具体的子类中的具体的service方法即可,整体的步骤:创建对象、init方法初始化、调用服务、destroy这些流程是固定的,具体的调用服务的实现方法则是延迟到子类中具体实现的。

9. HttpServlet抽象类

前面提到,我们在实际开发中,通常继承HttpServlet抽象类而不是GenericServlet抽象类,这个抽象类是专门为HTTP协议准备的,比GenericServlet更加适合HTTP协议下的开发。该抽象类在javax.servlet.http包下,该包下有很多的接口和类,主要下面几个类比较重要:

  • javax.servlet.http.HttpServlet(HTTP协议专用的Servlet类,抽象类)

  • javax.servlet.http.HttpServletRequest(HTTP协议专用的请求对象)

    该类的实例简称为request对象,封装了HTTP请求协议的全部内容,即Tomcat(WEB服务器)将“请求协议”中的数据全部解析出来,然后将这些数据全部封装到request对象中了。

  • javax.servlet.http.HttpServletResponse(HTTP协议专用的响应对象)

    该类的实例简称为response对象,该对象是专门用来响应HTTP协议到浏览器的,即通过该对象可以将数据封装到响应数据包中返回给浏览器。

9.1 HttpServlet源码简单分析

实际开发中就是要继承这个类,所以可以参考上面讲的Servlet对象的生命周期来分析。

  1. 创建对象

    调用HttpServlet提供的无参构造方法。

    1
    public HttpServlet() {}
  2. init初始化

    调用其父类GenericServlet的init方法。

    1
    2
    3
    4
    5
    6
    public void init(ServletConfig config) throws ServletException {
    this.config = config;
    this.init();
    }

    public void init() throws ServletException {}
  3. 调用service方法

    HttpServlet有两个service方法,一个是重写继承父类,一个是自己扩展的。那么Tomcat调用的时候,先调用继承父类的方法,因为形参确定了调用的方法,然后重写的方法又调用了自己扩展的方法。

    通过下面可以观察到,重写的方法就是为了数据类型转换,去调用自己扩展的方法。扩展service方法中,通过HttpServletRequest对象获取请求协议的方式,如GET、POST等等,然后针对不同的“请求方式”调用不同的方法进行处理。那么此时就会有疑问,我们继承的类该重写哪个方法呢?似乎JavaEE都已经实现了,其实不是的,仔细看源码就会发现,doGet()等方法其实就是简单的405响应,并没有真正的实质性的内容。实际上仍然需要我们重写实现方法,去避免这个错误。

    注意,此处就是模板方法设计模式,我们实现的子类只需要实现不同的请求方式对应的处理方法即可,也就是处理方法才是真正的服务。

    因为实际情况下,浏览器肯定会发送不同请求方式的请求协议包的,那么实际情况下,我们需要在服务器的电脑上,监听端口获取请求数据包解析数据包的内容针对内容作出响应。这几个简单的步骤,前三个都是通用的,所以Tomcat服务器以及JavaEE规范帮助我们实现了,我们只需要针对内容作出响应即可,这个作出响应就是重写doGet()等方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
    HttpServletRequest request;
    HttpServletResponse response;
    try {
    request = (HttpServletRequest)req;
    response = (HttpServletResponse)res;
    } catch (ClassCastException var6) {
    throw new ServletException(lStrings.getString("http.non_http"));
    }

    this.service(request, response);
    }
    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
    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;
    try {
    ifModifiedSince = req.getDateHeader("If-Modified-Since");
    } catch (IllegalArgumentException var9) {
    ifModifiedSince = -1L;
    }

    if (ifModifiedSince < lastModified / 1000L * 1000L) {
    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);
    }
    }
    1
    2
    3
    4
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String msg = lStrings.getString("http.method_get_not_supported");
    this.sendMethodNotAllowed(req, resp, msg);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    private void sendMethodNotAllowed(HttpServletRequest req, HttpServletResponse resp, String msg) throws IOException {
    String protocol = req.getProtocol();
    if (protocol.length() != 0 && !protocol.endsWith("0.9") && !protocol.endsWith("1.0")) {
    resp.sendError(405, msg);
    } else {
    resp.sendError(400, msg);
    }
    }

简单测试一下子类重写doGet()和doPost()方法,成功响应。

1
2
3
4
5
6
7
8
9
10
11
public class HttpServletSelf extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
PrintWriter writer = response.getWriter();
writer.print("doPost");
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
PrintWriter writer = response.getWriter();
writer.print("doGet");
}
}

10. 关于一个web站点的欢迎页面

对于一个webapp来说,我们是可以设置它的欢迎页面的。设置了欢迎页面之后,当你访问这个webapp的时候,或者访问这个web站点的时候,没有指定任何的“资源路径”,这个时候会默认访问你的欢迎页面。设置欢迎页面可以通过在web.xml文件中设置<welcome-file-list></welcome-file-list>标签来设置,设置该标签的内容为某静态资源的路径即可,该路径不需要加项目名,不需要以斜杠开始,默认从web目录下开始找。

另外,一个webapp是可以设置多个欢迎页面的,即多个<welcome-file>标签即可,优先级自上而下,如果都找不到就会返回404。注意,欢迎页面也可以不是html文件,可以是servlet实现类,只需要将映射路径填入即可。

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>

servlet_018.png (558×242) (gitee.io)

注意,上面写的配置是欢迎页面的局部配置,也就是只针对这个webapp项目有效。实际上Tomcat自己设置了一个全局配置,针对所有的webapp都有效,可以自己进行测试一下,就是当某个根目录下的资源文件名是index.html、index.htm以及index.jsp的时候,不需要局部配置,默认打开的就是该页面。但是局部配置的优先级比全局配置有效,全局配置的文件是在Tomcat的安装目录下的/conf/web.xml文件中,并且除了欢迎页面,还有其他一些全局的配置信息。

servlet_019.png (704×286) (gitee.io)

11. 关于WEB-INF目录

放在WEB-INF目录下的资源是受保护的,在浏览器上不能够通过路径直接访问,所以像HTML、CSS、JS以及image等静态资源一定要放到WEB-INF目录之外。

12. HttpServletRequest接口

通过上面的讲解,其实已经看到了Servlet接口及其相关的接口以及实现类,有ServletConfig、ServletContext、GenericServlet和HttpServlet,除了这些,还有Servlet中的service方法的两个形参涉及到的ServletRequest和ServletResponse接口没有讲到,这一节以及下一节讲解这两个接口。

HttpServlet继承了Servlet,并且service方法中也是将ServletRequest接口转换为HttpServletRequest接口。实际上HttpServletRequest接口是继承ServletRequest接口的,所以关于ServletRequest和ServletResponse接口的讲解就放到HttpServletRequest接口和HttpServletResponse接口中讲解了。

该接口的实现类是Tomcat服务器实现了:org.apache.catalina.connector.RequestFacade@3ad8abf,该类对象由Tomcat服务器创建,并作为实参传入该对象。实际上用户发送请求的时候,遵循了HTTP协议,Tomcat服务器将HTTP协议中的信息以及数据全部解析出来,然后将这些信息封装到HttpServletRequest对象中,传到了service()方法中供我们使用。而用户发送的请求信息是至关重要的,因为作为交互的一方,我们肯定是需要知道这些信息的,所以要想获得这些数据就要通过该对象来获得。因为每次请求的信息不同,所以这两个对象的生命周期很短,只在一次请求和响应中有效。

12.1 ServletRequest接口中的常用方法

常用的方法如下所示:

方法名 描述
String getParameter(String name) 获取指定name的value(适合于只有一个value的key,常用)
Map<String, String[]> getParameterMap() 获取整个参数Map集合
Enumeration<String> getParameterNames() 获取参数Map集合的的所有name
String[] getParameterValues(String name) 获取指定name的所有value,返回的是数组(适合复选框类似的数据,常用)

备注,因为用户传过来是以key=value[&key=value…]的形式传过来的,所以key是可以重复的,那么在服务器端怎么存储这些数据呢?肯定不能是简单的Map集合的,因为该集合key不能重复,所以为了存储重复的key,在value方面采用了数组来存储,即整体是依然是Map,但是value是一个数组,即Map<String, String[]>。简单测试如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class HttpServletRequestTest extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.print(request);

String username = request.getParameter("username");
String password = request.getParameter("password");
String[] interest = request.getParameterValues("interest");

out.print("<br>");
out.print(username + "<br>");
out.print(password + "<br>");
for (String s : interest) {
out.print(s + ",");
}

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试request对象</title>
</head>
<body>
<h1>get请求</h1>
<form action="/servlet06/request" method="get">
用户名:<input type="text" name="username"/><br>
密码:<input type="password" name="password"><br>
爱好:
抽烟<input type="checkbox" name="interest" value="c">
喝酒<input type="checkbox" name="interest" value="h">
烫头<input type="checkbox" name="interest" value="t"><br>
<input type="submit" value="login">
</form>

<br>

</body>
</html>

servlet_020.png (803×239) (gitee.io)

注意,前端表单提交数据的时候,都是以字符串形式在后端获得的。除了上面四个获取请求对象的数据参数之外,还有下面的三个方法比较常用。

12.2 请求域

request对象实际上又被称为“请求域”对象,和应用域对象类似,可以作为缓存存储一些数据。注意,请求域对象要比应用域对象范围小很多,生命周期短很多。请求域只在一次请求内有效,一个request对象对应一个请求域对象,一次请求结束之后,这个request对象就销毁了,请求域对象也就销毁了。

请求域和应用域的选用原则?

  • 尽量使用小的域对象,因为小的域对象占用的资源较少。

请求域对象的三个方法如下所示:

方法名 描述
void setAttribute(String name, Object object) 向域中存数据
void getAttribute(String name) 向域中取数据
void removeAttribute(String name) 删除域中的数据

简单测试案例如下所示,在01Servlet中存入并读取数据,请求之后生效;在02Servlet中读取不到,请求之后生效。

1
2
3
4
5
6
7
8
9
10
11
12
public class RequestServletTest01 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setAttribute("name", "zhangSan");
String name = (String) request.getAttribute("name");

response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.write(name);

}
}
1
2
3
4
5
6
7
8
9
10
public class RequestServletTest02 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Object name = request.getAttribute("name");
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.write(String.valueOf(name));

}
}

既然一个请求域对象对应一次请求(目前来看,一次请求对应一个Servlet),那么这个请求域对象的存在还有什么意义呢?其实是可以将两个Servlet放到一次请求中的(即一次请求对应多个Servlet),这样,就可以在一次请求中实现不同的Servlet实现类对象共享数据。而应用域则是所有的Servlet实现类对象以及所有的对其请求都共享,这样实际会造成一定的资源浪费。

注意,虽然可以在01Servlet类中自己创建02Servlet对象,并将请求域对象传入,但是这样自己new的对象并不受Tomcat管理。本质上来说,我们不能自己创建Servlet对象,只能由Tomcat自己创建和管理以及销毁对象。

12.3 请求转发机制

上面提到了可以在一个Servlet类中将请求对象转发到另一个Servlet中,这种机制称为请求转发机制。主要用到ServletRequest接口中的getRequestDispatcher(String path)方法,获得请求转发器对象,参数path指的是另一个Servlet在web.xml中的配置地址,返回的是请求转发器对象。然后再调用转发器对象中的forward方法,将本Servlet类当前的request和response对象传入到对应的Servlet类中。本质上就是为了保证两个Servlet中的request和response对象是一样的。

测试代码如下所示,在上面的基础上,将01中的输出删掉,增加请求转发代码,02保持不变,这样请求01会产生输出,实际这个输出是02产生的(注意,两个Servlet都返回内容,似乎后返回的会覆盖前面返回的内容,所以这里将01不返回内容)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RequestServletTest01 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setAttribute("name", "zhangSan");
// String name = (String) request.getAttribute("name");
//
// response.setContentType("text/html");
// PrintWriter out = response.getWriter();
// out.write(name);

// 获取请求转发器对象
RequestDispatcher requestDispatcher = request.getRequestDispatcher("/2");

// 调用转发器的forward方法完后完成转发
requestDispatcher.forward(request, response);
}
}

所以两个Servlet共享资源的方式如下:

  • 将数据放到ServletContext应用域当中。但是应用域范围太大,占用资源太多,不建议使用。
  • 将数据放到request请求域中,然后采用请求转发机制,将数据转发到另一个Servlet中,这样两个或多个Servlet共享一份数据。

注意,转发的下一个地址不一定是Servlet,也可以是其他WEB容器中的合法资源,如html等等(只不过这个页面会作为资源返回到浏览器当中)。

12.4 ServletRequest接口的其他方法

ServletRequest接口中的其他方法:

方法名 描述
String getRemoteAddr() 获取访问客户端的IP地址
String getRemoteHost() 同上
String getRemotePost() 端口号
void setCharacterEncoding(String env) 设置request对象请求体的字符编码集。主要是为了解决post请求乱码,因为post方式是在请求体中存放数据。注意,这仅仅是服务器端获取到的数据解决了乱码,服务器端数据返回到浏览器还需要解决,即response.setContentType(“text/html;charset=UTF-8”)。

注意,那么针对get请求的乱码怎么解决呢?会不会发生乱码呢?

Tomcat9及以后,不会发生乱码,在Tomcat安装目录的conf/server.xml配置文件的<connector>标签中可以设置URIEncoding属性编码为UTF-8,默认不写就是UTF-8。这个URI指的就是get的请求路径后面的数据。这个说明信息在安装目录的webapps/docs/config/http.html中有说明。

12.5 HttpServletRequest接口中的常用方法

该类相当于扩展了ServletRequest接口,常用方法如下所示:

方法名 描述
String getContextPath() 返回请求对象请求资源的所在项目的根路径。
String getMethod() 返回请求的方式(get、post…)
String getRequestURI() 返回请求的URI(带项目名)
StringBuffer getRequestURL() 返回请求的URL
String getServletPath() 返回请求的servlet的路径(不带项目名)

13. 案例:用Servlet进行数据库表的CRUD操作

CRUD操作即增(Create)查(Retrive)改(Update)删(Delete)。案例的实现采用:前端提交请求,后端Servlet进行处理。功能如下:

  • 显示信息
  • 新增数据
  • 删除数据
  • 修改数据

servlet_021.png (1055×457) (gitee.io)

servlet_022.png (1034×216) (gitee.io)

servlet_023.png (574×172) (gitee.io)

servlet_024.png (989×182) (gitee.io)

  1. 注意,因为CRUD操作频繁涉及到数据库操作,所以可以提供一个数据库的工具类,包含数据库连接以及释放资源
  2. 另外,在前端显示数据库的内容,而数据库中的数据是变化的,所以前端也就得是动态的。目前的技术做不到动态变化。所以采用后端Servlet向前端发送网页,即Servlet输出前端代码,交给浏览器去编译。
  3. 注意,表单的提交是post,而一些页面跳转则是get,注意Servlet的方法实现。
  4. 对于新增,在展示信息页面,点击新增链接(get请求)进入到新增页面,在新增页面输入信息之后,点击新增(这里相当于提交信息,到某个Servlet执行数据库操作,post请求,然后再跳转到展示信息页面,这里跳转采用的转发,注意,转发的是当前Servlet接收到的post请求,所以在展示信息页面需要实现post方法,理论上用跳转比较合适,因为二者不需要共享请求域。)进入到展示信息页面。
  5. 具体的代码这里不再展示,源码在servlet08这个module中。

14. 转发和重定向

在一个web应用中,资源的跳转有两种方式(这里的跳转指的是在后端Servlet中实现的跳转,因为浏览器或者前端只能是URL):

  • 转发

    1
    2
    3
    4
    5
    // 获取请求转发器对象
    RequestDispatcher requestDispatcher = request.getRequestDispatcher("/show");

    // 请求转发,转发就是将获得请求转发到另一个Servlet对象中,即多个Servlet对象共享请求域。
    requestDispatcher.forward(request, response);
  • 重定向

    重定向顾名思义,就是重新定方向,即重新修改整个URL。而且重定向不是request对象中的方法,而是response对象中的方法。因为是修改整个URL,所以重定向的参数是:项目名+资源名(而转发则只需要资源名即可)。本质上是通过response对象返回给浏览器一个URL,浏览器收到后再次发起这个URL请求(因为是重新请求,所以也就必须带上项目名了)。

    1
    response.sendRedirect(request.getContextPath()+ "/b");

二者的区别是什么?

  • 可以测试,转发只是将请求转到另一个资源,从浏览器端是察觉不到的,即浏览器的地址栏仍然是最开始的那个资源,转发相当于最开始的资源调用了另一个资源,相当于递归服务器帮助用户向其他服务器请求查询,而用户只是觉得结果是递归服务器给它的。而重定向则是命令浏览器再次请求一个新的URL,所以浏览器的地址栏是重定向后的URL。重定向相当于各级域名服务器返回给递归服务器各级权威域名服务器的IP。(在上面的案例中,可以看到,关于转发的Servlet,确实在浏览器中URL没有改变)
  • 转发是服务器内部的一个资源调用,浏览器端只发送了一次请求;而重定向则是服务器返回给浏览器一个结果,使得浏览器重新请求另一个资源,浏览器端发送了多次请求。

转发的流程图如下所示:

servlet_025.png (1215×627) (gitee.io)

cybersecurity.gitee.io/blog-image-bed/JavaWeb/servlet/servlet_026.png

转发和重定向应该如何选择?什么时候使用转发,什么时候选择重定向?

  • 如果在上一个Servlet当中向request域当中绑定了数据,希望从下一个Servlet当中把request域里面的数据取出来,使用转发机制。
  • 剩下所有的请求均使用重定向(重定向使用较多)。注意,因为是URL,所以只能是get请求。

注意:

  • 转发会存在浏览器的刷新问题,因为转发不会修改浏览器的URL,这时候操作完之后,如果在浏览器端刷新,那么就相当于再次发送了请求(发送的请求是重定向之前的请求)。此时如果该请求涉及到数据库操作,比如新增一条记录,那么这时候,每刷新一次就会新增一次记录,比如上述的新增操作,在新增成功后,再次刷新,服务器端就会返回主键重复异常。而重定向则不会,因为重定向已经将浏览器的URL修改为操作之后的URL了,即使再刷新,请求的仍然是重定向之后的URL,不涉及到数据库操作。

15. Servlet线程安全问题

注意,客户端的每一次Http请求,服务器都会分配一个线程处理该请求,而该请求在返回给客户端之前,这个请求【这个线程】时候不会结束的。后续在框架中,如果在其他类中不方便获取请求中的某个参数时,可以通过线程对象ThreadLocal来拿到。

Servlet是单实例多线程环境下运行的,什么时候存在线程安全问题呢?

  • 多线程并发
  • 有共享的数据
  • 共享数据有修改操作

在JVM中,哪些数据可能会存在线程安全问题?

所有线程共享一个堆内存,共享一个方法区内存。所以这两个内存区中的数据会发生线程安全问题(但是常量不涉及到修改,所以不会存在线程安全问题)。如

  • 堆内存中的Java对象,对象内部的实例变量
  • 方法区中的静态变量,如果涉及到修改操作,也会存在线程安全问题。

线程安全不只是体现在JVM中,还有可能发生在数据库中,例如多个线程共享同一张表,并且同时去修改表中的一些记录,这些记录就存在线程安全问题,解决方法有多种,(1)在Java程序使用Synchronized关键字,线程排队执行;(2)采用数据库中的行级锁(悲观锁);(3)事务机制(程序线程串行化);(4)乐观锁。

Servlet因为是单实例多线程的,所以Servlet对象中的成员变量只有一份,那么多个用户访问的时候,比如注册信息,(如果采用成员变量保存数据

  • A用户注册的信息保存在Servlet的实例变量中,但是还没有存入数据库,
  • 此时B也注册信息,填入了信息,还没有存入数据库。
  • 此时B填的信息就会覆盖A填的信息(多线程共享一个Servlet实例对象),那么本质上就是A注册了B注册的信息,B注册时因为信息已经存在,注册失败。

此时的问题该怎么解决呢?

  • 采用局部变量保存数据(多个用户请求,相当于多次调用service方法,而方法中的变量属于局部变量,每次调用都会创建不同的变量,所以数据不会覆盖。)
  • 采用Synchronized关键字,将多线程串行化(不优先考虑这个方法)

Cookie可以保存会话状态,只要Cookie清除,或者Cookie失效,这个会话状态就没有了。Cookie是保存在浏览器客户端上的:

  • 可以是浏览器缓存中,浏览器关闭Cookie消失;
  • 也可以保存在浏览器所在客户端的硬盘中,浏览器关闭Cookie还在,除非Cookie失效。

Cookie这种机制是HTTP协议规定的,和get、post请求类似,属于协议规定的。不止是在Javaweb中存在,只要是web开发,只要是B/S架构的系统,只要是基于HTTP协议,就有Cookie存在。Cookie实现的功能,有如下几种:

  • 保留购物车商品的状态在客户端上。(适用于前期免登陆加购物车的网站)
  • 十天内免登陆

在Java中Cookie被当做类来处理,使用new运算符可以创建Cookie对象,即Servlet中可以由自己来创建Cookie对象。Cookie由两部分组成:name和value,都是String类型。该类的全称是javax.servlet.http.Cookie。构造方法和常用方法如下:

1
2
3
// 构造方法
Cookie(String name, String value)
// Returns an array containing all of the Cookie objects the client sent with this request.

之后,由response对象用response.addCookie(Cookie对象)将Cookie发送给浏览器客户端。另外,JavaScript也可以创建Cookie,前面提到过,Cookie属于HTTP协议规定的,在Servlet中创建属于采用Java提供的工具类来创建;在前端中采用JS创建则是采用JS提供的工具类来创建。

注意,服务器可以一次向浏览器发送多个Cookie,默认情况下,服务器发送Cookie给浏览器之后,浏览器将Cookie保存在缓存当中,只要不关闭浏览器,Cookie永远存在,并且有效;当浏览器关闭之后,缓存中的Cookie被清除。在浏览器客户端,无论是硬盘文件还是缓存中保存的Cookie,什么时候会再次发送给服务器呢?浏览器会不会提交发送这些Cookie给服务器?浏览器在发送请求的时候,会先检查一遍是否有绑定的Cookie,如有,则附加cookie发送。其实Cookie和请求路径是紧密关联的,不同的请求路径会发送提交不同的Cookie,默认情况下Cookie是和创建该cookie的servlet路径相关联。

另外,绑定的路径是可以指定的,可以通过程序cookie.setPath(String uri)来设置,保证Cookie和某个特定的路径绑定在一起。这样,只要请求该绑定路径下的各个资源,浏览器都会自动发送存储的与该路径绑定下的cookie。

16.1 Cookie保存在硬盘

注意,默认情况下,Cookie是没有设置有效时长的,该Cookie被默认保存在浏览器的缓存当中,只要浏览器不关闭,Cookie就一直有效,而关闭浏览器则消失。我们可以设置Cookie的有效时长,以保证Cookie保存在硬盘文件当中。但是这个有效时长必须是>0的。换句话说,只要设置Cookie的有效时长大于0,该cookie就会保存在硬盘中,有效时长过去之后,则硬盘文件中的Cookie失效。调用Cookie的方法void setMaxAge(int expiry)来设置cookie的有效时长,单位为秒。

  • cookie有效时长=0 直接被删除
  • cookie有效时长<0 不会被存储
  • cookie有效时长>0 存储在硬盘文件
  • 没有有效时长 存储在浏览器缓存中

Servlet代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CookieTest extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

// 创建cookie对象
Cookie cookie_name = new Cookie("name", "zhangSan");
Cookie cookie_password = new Cookie("password", "zhangSan123");

// 设置cookie关联的路径,默认创建该Cookie的路径(即所在servlet)关联在一起。
cookie_name.setPath(request.getContextPath() + "/qwer");

// 将Cookie对象发送到浏览器客户端
response.addCookie(cookie_name);
response.addCookie(cookie_password);
}
}

cookie如下所示:

servlet_027.png (868×768) (gitee.io)

设定Cookie的有效时长后如下所示,注意,下面的创建时间并不是真正的本次请求创建的时间,忘记清除cookie:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CookieTest extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

// 创建cookie对象
Cookie cookie_name = new Cookie("name", "zhangSan");
Cookie cookie_password = new Cookie("password", "zhangSan123");

// 设置cookie关联的路径,默认创建该Cookie的路径(即所在servlet)关联在一起。
cookie_name.setPath(request.getContextPath() + "/qwer");

// 设置cookie时长
cookie_name.setMaxAge(60 * 10);

// 将Cookie对象发送到浏览器客户端
response.addCookie(cookie_name);
response.addCookie(cookie_password);
}
}

servlet_028.png (870×776) (gitee.io)

16.2 Servlet读取Cookie

上面讲述了由Servlet创建Cookie对象并返回给浏览器端,而浏览器端也会将根据请求路径将所绑定的Cookie跟随请求将其发送到服务器端,那么服务器端怎么读取Cookie呢?HttpServletRequest对象request中有一个Cookie getCookies()方法,返回请求对象中包含的Cookie,类型为数组,可能为null。

如果有Cookie对象的话,可以通过Cookie类的相关方法String getName()Strigng getValue()来获得name和value。案例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CookieTest extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

// 创建cookie对象
Cookie cookie_name = new Cookie("name", "zhangSan");
Cookie cookie_password = new Cookie("password", "zhangSan123");

// 设置cookie关联的路径,默认创建该Cookie的路径(即所在servlet)关联在一起。
cookie_name.setPath(request.getContextPath() + "/qwer");
cookie_name.setMaxAge(60 * 10);

// 将Cookie对象发送到浏览器客户端
response.addCookie(cookie_name);
response.addCookie(cookie_password);
}
}

16.3 案例:10天内免登陆

十天内免登陆,本质上是根据服务器端利用首次成功登陆信息创建Cookie对象(和用户名密码有关,并且和登陆页面路径绑定)返回给浏览器端,然后浏览器端将Cookie信息保存,下次再次访问该登陆页面时,自动将Cookie信息发送到服务器端,完成免登陆。

  • 这个功能首先是在登录的基础上完成的,所以要先完成登录功能。然后附加一个复选框,用于选择10天免登陆功能。之后账号密码验证成功后,服务器端再判断该功能是否选择,选择后就根据登录信息创建Cookie对象返回给浏览器端。
  • 之后,浏览器再次访问该路径时,只需要判断cookie的内容是否匹配即可,匹配成功即直接跳转到欢迎页面,匹配失败则需要在该页面正常输入账户密码。
  • 注意,一般情况下,浏览器会直接访问网站主页,所以上面的Cookie应该绑定网站主页,那么浏览器访问的时候,服务器首先判断是否包含Cookie,如果包含则继续验证账号密码是否匹配,匹配成功则跳转到登录成功页面(或者主页),匹配失败则跳转到登录页面。所以网站的主页应该设置一个servlet,用于判断是否免登陆。

17. 路径的编写方式

17.1 常见的路径编写

目前为止,需要编写路径的地方有:超链接、form表单、重定向、转发、web配置文件(欢迎页面、Servlet路径映射)等等。

  • <a href=”/项目名/资源路径”></a>

  • <form action=”/项目名/资源路径”></form>

  • 重定向:response.sendRedirect(“/项目名/资源路径”);

  • 转发:request.getRequestDispatcher(“/资源路径”).forward(request, response);

  • 欢迎页面:

    1
    2
    3
    <welcome-file-list>
    <welcome-file>资源路径</welcome-file>
    </welcome-file-list>
  • Servlet路径

    1
    2
    3
    4
    5
    6
    7
    8
    <servlet>
    <servlet-name>hello</servlet-name>
    <servlet-class>servlet.HelloServlet</servlet-class>
    </servlet>
    <servlet-mapping>
    <servlet-name>hello</servlet-name>
    <url-pattern>/资源路径</url-pattern>
    </servlet-mapping>
  • Cookie:cookie.setPath(“项目名/资源路径”);

  • ServletContext

    1
    2
    3
    ServletContext application = config.getServletContext();
    application.getRealPath("/WEB-INF/classesdb.properties");
    application.getRealPath("/资源路径");

17.2 url-pattern的编写方式

17.2.1 精确匹配

精确匹配即路径是指定的一条路径,上面写的都是精确匹配。另外和欢迎页面类似,<servlet-mapping>中的<url-pattern>也可以编写多个,即多条路径都对应一个servlet。

1
2
3
4
5
6
7
8
9
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>servlet.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/Hello</url-pattern>
<url-pattern>/system/Hello</url-pattern>
</servlet-mapping>

即外部链接的路径可以是/Hello,也可以是system/Hello,两条路径均能找到HelloServlet实现类。

17.2.1 扩展匹配

扩展匹配类似正则表达式,星号代表任意路径。即system下的任意子路径均能映射到该Servlet实现类(前缀匹配)。除了前缀匹配,还有后缀匹配,即父路径任意。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>servlet.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>

<!-- 前缀匹配 -->
<url-pattern>/system/*</url-pattern>

<!-- 后缀匹配 -->
<url-pattern>*.action</url-pattern>
<url-pattern>*.qwerasdfzxvc</url-pattern>
</servlet-mapping>

18. Session

前面的Cookie是将“连接信息”保存到浏览器端,那么可不可以将“连接信息”保存到服务器端呢?可以用Session做到。同样,Session机制不仅仅是Java中有的,是HTTP协议规定的,在任何B/S架构中均存在。Session表示会话,在Java中的完整类名是:javax.servlet.http.HttpSession,简称session。上面的“连接信息”表示会话状态,即Cookie可以将会话状态保存在客户端,HttpSession可以将会话状态保存在服务器端。

HttpSession对象是一个会话级别的对象,一次会话对应一个HttpSession对象。什么是会话呢?简单地说,用户打开浏览器,在浏览器上发送多次请求,直到最终关闭浏览器,表示一次完整的会话(准确地说,一次会话就是一次连接,服务器为该连接创建的一个对象,只要连接不断开,该Session对象就一直存在)。可以自己测试一下,用request对象的getSession方法获得该连接的session对象,连接不断开,session对象就一直是那个;关闭浏览器断开连接,再次请求,会发现session发生改变。

1
2
3
4
5
6
7
8
9
public class HttpSessionTest01 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
String ip = request.getRemoteAddr();

System.out.println(ip + "'s session = " + session);
}
}

servlet_030.png (1000×366) (gitee.io)

1
2
3
4
5
6
7
HttpSession getSession();
// Returns the current session associated with this request, or if the request does not have a session, creates one.
// 注意,该方法是如果在服务端,没有该请求对应的session对象,就会创建一个,返回该创建的对象,同时创建对应的cookie,将cookie对象发送到客户端;如果有,就从该请求中获取该cookie,然后获取对应的session。

HttpSession getSession(boolean create);
// Returns the current HttpSession associated with this request or, if there is no current session and create is true, returns a new session.
// 和上面的方法类似,只不过如果是false,则不会创建session对象,返回null。

在WEB容器中,WEB容器维护了大量的HttpSession对象,换句话说,在WEB容器中应该有一个session列表。那么是如何做到在会话连接不断开的情况下,获取到的session对象不变呢?是怎么映射的呢?

  1. 打开浏览器,在浏览器上发送首次请求;
  2. (如果调用了getSession方法)服务器会创建一个HttpSession对象,该对象代表一次会话;
  3. 同时生成HttpSession对象对应的Cookie对象,并且Cookie对象的name是JSESSIONID,value是32位长度的字符串;
  4. 即服务器将Cookie的value和HttpSession对象绑定到session列表中;
  5. 服务器将Cookie完整发送给浏览器客户端;
  6. 浏览器客户端将Cookie保存到缓存中;
  7. 只要浏览器不关闭,Cookie不会消失;
  8. 当再次发送请求的时候,会自动提交缓存中的Cookie;
  9. 服务器接收到Cookie,验证该Cookie的name确实是:JSESSIONID,知道该Cookie保存的信息是session相关的信息,然后获取该Cookie的value;
  10. 通过Cookie的value去session列表中检索对应的HttpSession对象。

cybersecurity.gitee.io/blog-image-bed/JavaWeb/servlet/servlet_031.png

所以,session机制是依据cookie机制来实现的。上述就是将一次的会话和客户端请求对应起来,那么这样服务端就会知道这是哪个会话,接下来就是利用session对象保存的相关信息提供一些服务。

servlet_029.png (879×312) (gitee.io)

**注意,和HttpSession对象关联的这个Cookie的name是比较特殊的,在Java中就叫做:JSESSIONID**;

浏览器禁用Cookie会出现什么问题?怎么解决?

浏览器禁用Cookie,则浏览器缓存中不再保存Cookie;导致在同一个会话中,无法获取到对应的会话对象,那么服务器就会生成多个session对象,即每次获取的会话对象都是新的。

了解一下,如果在禁用cookie之后,仍然想要拿到对应的cookie对象,必须使用URL重写机制,即在URL后加分号,加jsessionid=id值,以get的方式将sessionid提交到服务器。重写URL会给编程带来复杂度,所以一般的web站点是不建议禁用Cookie的。

浏览器关闭之后,服务器端对应的session对象会被销毁吗?为什么?

浏览器关闭之后,服务器不会销毁session对象。因为B/S架构的系统基于HTTP协议,而HTTP协议是一种无状态/无连接的协议。(无连接指的是,二者之间的通道是在请求的时候建立的,通信结束之后通道关闭,这样做的目的是降低服务器的压力。)

可以用上面的URL重写机制,在其他的浏览器上进行测试,服务端后台输出的session对象是同一个。

session对象在什么时候被销毁?

web系统引入了session超时的概念。当很长一段时间(这个时间可以配置)没有用户再访问该session对象,此session对象超时,web服务器自动回收session对象。可以在web.xml文件中用<session-config>标签来设置,默认为30分钟。

1
2
3
4
<session-config>
<session-timeout>120</session-timeout>
<!-- 单位是分钟 -->
</session-config>

什么是一次会话呢?

一般情况下是这样描述的:用户打开浏览器,在浏览器上进行一些操作,然后将浏览器关闭,表示一次会话结束。(实际上因为关闭了浏览器,cookie对象不再存在,那么session对象将不会被访问到,一段时间过后自动被销毁)。

本质上的描述:从session对象的创建,到最终session对象超时之后被销毁,这个才是真正意义上的一次完整会话。

18.1 HttpSession中的常用方法

HttpSession接口中常用的方法有如下几种:

方法名 描述
Object getAttribute(String name) 获取会话域中指定name的值
void setAttribute(String name, Object value) 向会话域中存取数据
void removeAttribute(String name) 向会话域中删除指定name的数据
void invalidate() Invalidates this session then unbinds any objects bound to it.即销毁session对象。

前三个方法在应用域和请求域中见过,用于数据共享。那么这里的这三个方法就意味着session对象也被称为会话域,一次会话可以对应多次请求,即多次请求之间进行数据共享。

测试案例如下所示:

主页面

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Session测试</title>
</head>
<body>
<a href="/servlet12/firstSession">测试session</a><br>
<a href="/servlet12/accessSession">获取session</a><br>
<a href="/servlet12/logoutSession">销毁session</a>
</body>
</html>

测试session对象

1
2
3
4
5
6
7
8
9
10
11
public class HttpSessionTest01 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
String ip = request.getRemoteAddr();

System.out.println(ip + "'s session = " + session);

session.setAttribute("name", "zhangsan");
}
}

获取session数据

1
2
3
4
5
6
7
8
public class AccessSessionServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
Object name = session.getAttribute("name");
System.out.println(session + ":" + name);
}
}

销毁session对象

1
2
3
4
5
6
7
8
9
10
11
12
public class LogoutServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

// 获取session对象,如果没有session对象,则返回null。有的话,则销毁它。
HttpSession session = request.getSession(false);
if(session != null){
System.out.println("销毁对象:" + session);
session.invalidate();
}
}
}

注意,一次会话只有一个session,这在getSession()方法中可以看出来,有的话,就获取该对象,没有的话,就创建一个session对象。所以下面第4行,获取对象没有取到,所以就创建了一个,在第5行销毁的时候,销毁的是第4行新创建的那个。

servlet_032.png (1023×184) (gitee.io)

18.2 ServletContext、HttpSession、HttpServletRequest接口的对比

以上三者都是范围对象,分别对应:应用范围、会话范围和请求范围。三个范围的排序:应用范围>会话范围>请求范围。

  • 应用范围完成跨会话共享数据。
  • 会话范围完成跨请求共享数据。(这些请求必须在同一个会话中)
  • 请求范围完成跨Servlet共享数据。(这些Servlet必须在同一个请求当中,如转发)

使用原则:由小到大尝试,优先使用小范围。因为数据共享必然存在访问和修改以及资源耗费情况,那么就会存在线程安全。

18.3 案例:会话对象保存登录状态

以前面的数据库CRUD操作案例为例,可能会发现,虽然有了登录功能,但是依然可以通过直接输入特定页面的URL来进行访问,即跳过了登录验证。那么这该怎么做呢?可以用会话session来保存登录状态,每次进入页面前都要检查一遍登录状态,如果没有登录或者会话失效,则跳转到登录页面。(即通过会话域来存储会话信息。

保存登录状态,简单地说,就是产生了session,并且session会话域中的key是name,value是其对应的名字,即保存了当前登录的用户名。(实际验证的时候,只需要考虑有name字段非空即可。)后续访问其他页面的时候,需要验证一下是否name字段为空即可,即只要有用户名就行。

账号密码登录成功后创建session对象

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
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

request.setCharacterEncoding("UTF-8");

String username = request.getParameter("username");
String password = request.getParameter("password");

Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
boolean result = false;

try {
conn = DBUtil.getConnection();
String sql = "select * from t_user where login=? and password=?";
ps = conn.prepareStatement(sql);
ps.setString(1, username);
ps.setString(2, password);

rs = ps.executeQuery();

if(rs.next()){
result = true;
}

} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(conn, ps, rs);
}

if(result){
// 查询成功,即账号密码匹配,创建session保存会话状态。
HttpSession session = request.getSession();
session.setAttribute("username", username);

response.sendRedirect(request.getContextPath() + "/index.html");
} else{
response.sendRedirect(request.getContextPath() + "/error.html");
}
}
}

在另一个Servlet中(有请求路径),首先查看是否有session,有则查看会话域中是否有相关信息(username),匹配则跳转到指定页面;没有或者不匹配则跳转到登录页面,该servlet页面没有权限访问。

1
2
3
4
5
6
7
8
9
10
11
12
public class IndexServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("username") != null) {
response.sendRedirect(request.getContextPath() + "/index.html");
// System.out.println(session.getAttribute("username"));
} else {
response.sendRedirect(request.getContextPath());
}
}
}

测试的时候,首先请求该Servlet路径,可以看到因为没有session对象,直接跳转到登录页面;之后再次请求该路径,因为session对象已经创建,并且在会话域中保存了会话信息,所以可以直接跳转到欢迎页面。

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
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<welcome-file-list>
<welcome-file>login.html</welcome-file>
</welcome-file-list>

<servlet>
<servlet-name>login</servlet-name>
<servlet-class>servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>login</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>

<servlet>
<servlet-name>indexValidation</servlet-name>
<servlet-class>servlet.IndexServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>indexValidation</servlet-name>
<url-pattern>/indexValidation</url-pattern>
</servlet-mapping>
</web-app>

19. 监听器

监听器(Listener)是Servlet规范的一种扩展,是一组来自Servlet规范下的接口,共有9个接口。监听器接口用于监控【作用域对象生命周期变化时刻】以及【作用域对象共享数据变化时刻】,比如session对象的创建和销毁以及所存储数据的变化情况。

这里的作用域对象就是指的上面的三个对象:ServletContext、HttpSession、HttpRequest。和Servlet一样,如果我们需要监听器功能,就要实现监听器接口,而什么时候调用则已经在WEB容器中设置好了,我们只需要实现具体的监控功能即可。开发实现类一般情况下分为三步:

  1. 根据监听的实际情况,选择对应的监听器接口进行实现(比如监听哪个作用域,监听对象的生命周期还是存储数据的变化情况)

  2. 选择好监听器接口后,重写接口的抽象方法(具体的操作)。

  3. 在web.xml文件中注册监听器接口实现类。

    在web.xml文件注册语法格式如下所示:

    1
    2
    3
    <listener>
    <listener-class>监听器实现类全路径</listener-class>
    </listener>

    通过在xml中注册实现类,Tomcat启动时会检测是否有监听器注册,有的话,则创建对象,并将其与作用域对象进行绑定,让满足监听器方法时就会被调用。

8个监听器接口如下所示:

接口名 描述
ServletContextListener 关于Servlet应用域对象的生命周期的监听器接口
ServletContextAttributeListener 关于Servlet应用域对象存储数据的变化的监听器接口
ServletRequestListener 关于Request请求域对象的生命周期的监听器接口
ServletRequestAttributeListener 关于Request请求域对象存储数据的变化的监听器接口
HttpSessionListener 关于Session会话域对象的声明周期的监听器接口
HttpSessionIdListener
HttpSessionActivationListener
HttpSessionAttributeListener 关于Session会话域对象存储数据的变化的监听器接口
HttpSessionBindingListener

19.1 ServletContextListener

该接口有两个抽象方法,分别负责监听应用域对象的初始化(创建)和销毁,即void contextDestroyed(ServletContextEvent sce)void contextInitialized(ServletContextEvent sce)。简单测试案例如下所示:

1
2
3
4
5
6
7
8
9
10
11
public class ListenerTest01 implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("Servlet应用域对象被创建");
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("Servlet应用域对象被销毁");
}
}
1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<listener>
<listener-class>listener.ListenerTest01</listener-class>
</listener>
</web-app>

可以看到在Tomcat服务器启动和关闭的时候,监听器中的两个方法确实被执行了。

19.2 ServletContextAttributeListener

该接口有三个抽象方法,分别负责存储数据的添加、删除和修改。即void attributeAdded(ServletContextAttributeEvent scae)void attributeRemoved(ServletContextAttributeEvent scae)void attributeReplaced(ServletContextAttributeEvent scae)。简单测试案例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// servlet
public class ListenerServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

ServletContext application = request.getServletContext();

// 新增存储数据
application.setAttribute("name", "zhangSan");

// 修改存储数据
application.setAttribute("name", "listener");

// 删除存储数据
application.removeAttribute("name");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Listener
public class ListenerTest02 implements ServletContextAttributeListener {
@Override
public void attributeAdded(ServletContextAttributeEvent scae) {
System.out.println("应用域对象添加数据");
}

@Override
public void attributeRemoved(ServletContextAttributeEvent scae) {
System.out.println("应用域对象删除数据");
}

@Override
public void attributeReplaced(ServletContextAttributeEvent scae) {
System.out.println("应用域对象修改数据");
}
}

servlet_033.png (282×122) (gitee.io)

19.3 监听器作用

前面提到过,实际上数据库操作中最耗时的就是连接对象的创建,即二者的I/O通道创建比较耗时,所以为了提升效率,可以在服务器启动的时候创建好一批连接对象(数据库连接池,用的时候取一个,用完之后再放回),并且直到服务器关闭的时候再销毁连接对象。那么怎么知道服务器启动和关闭呢?可以监听ServletContext应用域,因为服务器启动的时候会先创建应用域对象,关闭的时候会先销毁应用域对象,那么监听二者,在二者事件触发的时候,就创建数据库连接对象以及销毁对象即可。

案例如下所示(可以测试一下插入语句消耗的时间):

1
// TODO

20. 过滤器

和监听器(Listener)一样,过滤器(Filter)也是来自于Servlet下的接口。过滤器主要是在Http服务器调用资源文件之前,对Http服务器进行拦截,即不是无限制的对所有请求都应答,设置一个过滤器,即增加一个门槛。主要的作用如下所示:

  1. 拦截Http服务器,帮助Http服务器检测当前请求合法性;
  2. 拦截Http服务器,对当前请求进行增强操作;

开发过滤器实现类分为以下三步:

  1. 创建一个类实现Filter接口;
  2. 重写接口中的doFilter方法;
  3. 在web.xml配置文件中将实现类注册到WEB容器上。

Filter接口全称是javax.servlet.Filter,该接口有三个方法,如下所示:

1
2
3
4
5
6
7
8
9
public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {
}

void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;

default void destroy() {
}
}

其中初始化方法和销毁方法已经default默认实现了,所以实现doFilter方法即可,即真正的过滤操作是在doFilter中执行的。

其中doFilter方法中的三个参数分别是请求对象、响应对象以及过滤链对象,请求和响应对象即上面说的HTTP请求和响应。过滤链对象指的是这个链上有很多个过滤器(具体多少个由WEB容器自动创建并添加到该对象上),本过滤器过滤完之后,就调用方法将request和response传送到链的下一个过滤器中,如果是链的最后端,则相当于过滤结束(将请求和响应对象返回给WEB容器),让它调用请求的资源文件返回给客户端,该接口的全称是javax.servlet.FilterChain,该接口只有一个方法void doFilter()

1
2
3
void doFilter(ServletRequest request, ServletResponse response)

// Causes the next filter in the chain to be invoked, or if the calling filter is the last filter in the chain, causes the resource at the end of the chain to be invoked.

在web.xml中注册过滤器的格式如下所示(注意,和监听器不一样,因为监听器本身就指明了监听哪个动作,所以不需要在配置文件中指明监听哪些动作;而过滤器需要在配置文件中手动指明需要对哪些请求进行过滤操作,即请求哪些资源文件时需要对其进行过滤,所以下面的url-pattern可以写多个,和servlet一样。):

1
2
3
4
5
6
7
8
<filter>
<filter-name>myFilter</filter-name>
<filter-class>servlet.filter.myFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/mm.jpg</url-pattern>
</filter-mapping>

所以,WEB容器在读取web.xml文件的时候,就会知道哪些请求会有过滤器,所以在创建过滤器对象的时候,会创建FilterChain对象,将该请求上的所有过滤器对象都添加到FilterChain中。

20.1 检测请求的合法性

年龄未满18周岁禁止访问

1
2
3
4
5
6
7
8
9
10
11
public class FilterTest01 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String age = request.getParameter("age");
if(Integer.valueOf(age) > 18){
chain.doFilter(request, response);
}else{
((HttpServletResponse) response).sendRedirect(((HttpServletRequest) request).getContextPath() + "/no_access.jsp");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<filter>
<filter-name>myFilter</filter-name>
<filter-class>filter.FilterTest01</filter-class>
</filter>
<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/index.jsp</url-pattern>
</filter-mapping>
</web-app>

20.2 对请求进行增强操作

一个典型的案例就是:有很多个请求,针对不同的Servlet,因为请求中可能存在中文,所以就需要设置字符编码方式,但是如果在每个Servlet中都写一遍,比较复杂。所以可以设置过滤器,在过滤器中因为传入了request对象,所以可以在这里设置,设置完之后再交给服务器进行后续操作。同理,如果很多个Servlet的response需要进行一些操作,也可以在过滤器中提前设置好。

20.3 总结

一般情况下,过滤器是在好多个文件下都添加的,那么难道为这些文件都创建映射吗(比如登录验证,要对所有的资源文件无论是动态还是静态的)。这个太多了,很麻烦,而且静态资源文件也无法写Java代码;所以,一般是将这些文件放在一个文件夹下,在<url-pattern>中采用星号通配符来映射。

21. MVC开发规则

这种开发规则规定了开发过程中出现的角色、所担负的职责以及出场顺序等等,使得开发业务更加方便高效地进行。MVC分别指的是(Model、Controller、View):

  • Model:

    业务模型对象(指的是具体业务实现步骤模型,一般用service对象表示)。

    1. 处理业务任务;
    2. 根据分支任务的执行情况判断业务是否处理成功;
    3. 将处理结果返回给控制层对象Servlet。
  • View:

    视图层对象(指的是给用户展示的模型,一般用jsp或者response对象表示)。

    1. 禁止参与业务处理;
    2. 唯一的任务就是将处理结果写入到响应体中。
  • Controller:

    控制层对象(指的是核心控制,即针对用户的请求调用特定的业务模型,一般用servlet对象表示,实际上servlet就是相当于指路的,即表名哪种请求用哪种模型,然后将业务模型的执行结果传输到视图层模型展示)。

    1. 该对象可以调用请求对象,读取请求包的参数信息;
    2. 必须调用Service对象处理业务;
    3. 可以将处理结果写入到作用域对象中作为共享数据;
    4. 必须调用视图对象将写入到响应体中。

22. DAO封装

DAO的全称是:DataBase Access Object,即数据库访问对象;作用是在开发时提供针对某张表的操作细节【增删改查】。优点是:

  1. 在开发时,通过数据库访问对象可以避免反复的SQL命令书写;
  2. 在开发时,通过数据库访问对象可以避免反复的JDBC开发步骤书写;

DAO类指的是提供数据库访问对象的类,DAO类开发规则如下所示:

  1. 一个DAO类中封装的是一张表的操作细节。
  2. DAO类命名规则:表名+Dao。比如封装的是emp表的操作细节:EmpDao。
  3. DAO类所在包的命名规则:公司网站域名倒序.dao。

DAO封装就是提供一张表的增删改查,增删改查需要的参数直接作为实参传入即可。和前面的util类似,只不过这个DAO封装的功能更加全面。

因为操作返回的结果集要在DAO中销毁,但是我们又需要返回给调用者结果集,这个时候该怎么办呢?需要将数据封装一个实体类,一个类关联一张表,类名和表名一致,可以忽略大小写,另外类的属性和表的字段保持一致。这样一个类对象就是一条记录,返回结果可以封装成Map等集合,包含多个类对象返回给调用者。

23. 总结

可以看到,

  • Servlet是最顶层的接口,Tomcat通过创建该接口的实现类对象为浏览器提供服务。
  • 在创建Servlet实现类对象的时候,调用init方法需要初始化ServletConfig接口的实现类对象。
  • ServletConfig接口中有4个方法,3个方法返回的是该Servlet实现类的web.xml的初始化配置信息,1个返回的是ServletContext接口的实现类对象。
  • ServletContext接口是负责一个webapp的web.xml文件对象,可以获取所有的配置信息等等。也被称为应用域,可以在该应用域中存储一些共享信息(所有Servlet共享)
  • Servlet的核心方法是Service()方法,里面有两个参数ServletRequest和ServletResponse两个接口的实现类对象。
  • 为了方便HTTP协议的数据获取,JavaEE提供了HttpServlet这个类,该类实现了Servlet接口。以及ServletRequest和ServletResponse的实现类HttpServletRequest和HttpServletResponse。
  • 另外,JavaEE提供了HTTP协议中的Cookie和Session两个客户端和浏览器端的会话类,方便业务的实现。
  • 对于过滤器,其实就是将request和response对象提取出来,在web.xml中统一进行操作,之后再将两个对象返回给服务器。
  • 对于监听器,就是当某个事件发生时触发监听器函数进行一些操作。

24. 备注

参考B站《动力节点》。


文章作者: 浮云
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 浮云 !
  目录