Servlet 是什么

Table of Contents

Web 开发1

JavaEE 是什么?它并不是一个软件产品,更多的是一种软件架构和设计思想,它是在 JavaSE 的基础上,开发的一系列基于服务的组件、API 标准和通用架构。

JavaEE 最核心的组件就是基于 Servlet 标准的 Web 服务器,开发者编写的应用程序是基于 Servlet API 并运行在 Web 服务器内部的:

此外,JavaEE 还有一系列的技术标准,目前最流行的基于 Spring 的轻量级 JavaEE 开发架构,使用最广泛的是 Servlet 和 JMS(Java Message Service,消息服务),以及一系列开源组件。

我们访问网站,使用 App 时,都是基于 Web 这种 Browser/Server 模式,简称 BS 架构,它的特点是,客户端只需要浏览器,应用程序的逻辑和数据都存储在服务器端。浏览器只需要请求服务器,获取 Web 页面,并把页面展示给用户即可。

在 Web 应用中,浏览器请求一个 URL ,服务器就把生成的 HTML 网页发送给浏览器,而浏览器和服务器之间的传输协议是 HTTP ,HTTP 协议是一个基于 TCP 协议上的请求-响应协议。

所为 HTTP 编程 是以客户端的身份去请求服务器资源,现在,我们需要以服务器的身份响应客户端请求,编写服务器程序来处理客户端请求通常就称之为 Web 开发

那么,如何编写一个 HTTP Server 呢?

一个 HTTP Server 本质上是一个 TCP 服务器,我们先用 TCP 编程的多线程实现一个服务器框架:

 1: public class Server {
 2:     public static void main(String[] args) throws IOException {
 3:         ServerSocket ss = new ServerSocket(8080); // 监听指定端口
 4:         System.out.println("Server is running...");
 5:         for (;;) {
 6:             Socket sock = ss.accept();
 7:             System.out.println("Connected from " + sock.getRemoteSocketAddress());
 8:             Thread t = new Handler(sock);
 9:             t.start();
10:         }
11:     }
12: }
13: 
14: class Handler extends Thread {
15:     Socket sock;
16: 
17:     public Handler(Socket sock) {
18:         this.sock = sock;
19:     }
20: 
21:     public void run() {
22:         try (InputStream input = this.sock.getInputStream()) {
23:             try (OutputStream output = this.sock.getOutputStream()) {
24:                 handle(input, output);
25:             }
26:         } catch (Exception e) {
27:             try {
28:                 this.sock.close();
29:             } catch (IOException ioe) {
30: 
31:             }
32:             System.out.println("Client disconnected.");
33:         }
34:     }
35: 
36:     private void handle(InputStream input, OutputStream output) throws IOException {
37:         var reader = new BufferedReader(new InputStreamReader(input, StrandardCharsets.UTF_8));
38:         var writer = new BufferedWriter(new OutputStreamWriter(output, StrandardCharsets.UTF_8));
39:         // TODO: 处理 HTTP 请求
40:         // ...
41:     }
42: }

只需要在 handle() 方法中,用 Reader 读取 HTTP 请求,用 Writer 发送 HTTP 响应,即可实现一个最简单的 HTTP 服务器。编写代码如下:

 1: private void handle(InputStream input, OutputStream output) throws IOException {
 2:     System.out.println("Process new http request...");
 3:     var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
 4:     var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
 5: 
 6:     // 读取 HTTP 请求
 7:     boolean requestOk = false;
 8:     String first = reader.readLine();
 9:     if (first.startsWith("GET / HTTP/1.")) {
10:         requestOk = true;
11:     }
12:     for (;;) {
13:         String header = reader.readLine();
14:         if (header.isEmpty()) { // 读取到空行时,HTTP Header 读取完毕
15:             break;
16:         }
17:         System.out.println(header);
18:     }
19:     System.out.println(requestOk ? "Response OK" : "Response Error");
20: 
21:     if (!requestOk) {
22:         // 发送错误响应
23:         writer.write("HTTP/1.0 404 Not Found\r\n");
24:         writer.write("Content-Length: 0\r\n");
25:         writer.write("\r\n");
26:         writer.flush();
27:     } else {
28:         // 发送成功响应
29:         String data = "<html><body><h1>Hello, world!</h1></body></html>";
30:         int length = data.getBytes(StandardCharsets.UTF_8).length;
31:         writer.write("HTTP/1.0 200 OK\r\n");
32:         writer.write("Connection: close\r\n");
33:         writer.write("Content-Type: text/html\r\n");
34:         writer.write("Content-Length: " + length + "\r\n");
35:         writer.write("\r\n");   // 空行标识 Header 和 Body 的分隔
36:         writer.write(data);
37:         writer.flush();
38:     }
39: }

这里的核心代码是,先读取 HTTP 请求,这里我们只处理 GET / 的请求。当读取到空行时,表示已读到连续两个 \r\n ,说明请求结束,可以发送响应。发送响应的时候,首先发送响应代码 HTTP/1.0 200 OK 表示一个成功的 200 响应,使用 HTTP/1.0 协议,然后,依次发送 Header ,发送完 Header 后,再发送一个空行标识 Header 结束,紧接着发送 HTTP Body,在浏览器输入 http://local.liaoxuefent.com/8080/ 就可以看到响应页面:

HTTP 目前有多个版本, 1.0 是早期版本,浏览器每次建立 TCP 连接后,只发送一个 HTTP 请求并接收一个 HTTP 响应,然后就关闭 TCP 连接。

由于创建 TCP 连接本身就需要消耗一定的时间,因此,HTTP 1.1 允许浏览器和服务器在同一个 TCP 连接上反复发送、接收多个 HTTP 请求和响应,这样就大大提高了传输效率。

然而,HTTP 协议是一个请求-响应协议,它都是发送一个请求,然后接收一个响应。能不能一次性发送多个请求,然后再接收多个响应呢?

可以的!

HTTP 2.0 可以支持浏览器同时发出多个请求,但每个请求需要唯一标识,服务器可以不按请求的顺序返回多个响应,由浏览器自己把收到的响应和请求对应起来。可见,HTTP 2.0 进一步提高了效率,因为浏览器发出一个请求后,不必等待响应,就可以继续发下一个请求。

HTTP 3.0 为了进一步提高速度,将抛弃 TCP 协议,改为使用无需创建连接的 UDP 协议,目前仍然处于实验阶段。

技术的进步是无止境的……

Servlet 入门

在上一节中,可见,编写 HTTP 服务器其实是非常简单的,只需要先编写基于多线程的 TCP 服务,然后在一个 TCP 连接中读取 HTTP 请求,发送 HTTP 响应即可。

但是,要编写一个完善的 HTTP 服务器,以 HTTP/1.1 为例,需要考虑的包括:

  • 识别正确和错误的 HTTP 请求;
  • 识别正确和错误的 HTTP 头;
  • 利用 TCP 连接;
  • 利用线程;
  • IO 异常处理;

BUT 这些基础工作需要耗费大量的时间,并且经过长时间测试才能稳定运行, 太低效了,太难了

幸运的是,在 JavaEE 平台上,处理 TCP 连接,解析 HTTP 协议这些底层工作统统扔给现成的 Web 服务器去做,我们只需要把自己的应用程序跑在 Web 服务器上。

交给可靠的机制去运行,省力又省心。

JavaEE 是如何实现这一点的呢?

JavaEE 提供了 Servlet API,我们使用 Servlet API 编写自己的 Servlet 来处理 HTTP 请求,Web 服务器实现 Servlet API 接口,实现底层功能:

下面我们来实现一个最简单的 Servlet :

 1: // WebServlet 注解表示这是一个 Servlet ,并映射到地址 `/` :
 2: @WebServlet(urlPatterns = "/")
 3: public class HelloServlet extends HttpServlet {
 4:     protected void doGet(HttpServletRequest req, HttpServletResponse resp)
 5:         throws ServletException, IOException {
 6:         // 设置响应类型:
 7:         resp.setContentType("text/html");
 8:         // 获取输出流:
 9:         PrintWriter pw = resp.getWriter();
10:         // 写入响应:
11:         pw.write("<h1>Hello, world!</h1>");
12:         // 最后不要忘记 flush 强制输出:
13:         pw.flush();
14:     }
15: }
16: 

一个 Servlet 总是继承自 HttpServlet ,然后覆写 doGet()doPost() 方法

注意到 doGet() 方法传入了 HttpServletRequestHttpServletResponse 两个对象,分别代表 HTTP 请求和响应。

我们使用 Servlet API 时,并不直接与底层 TCP 交互,也不需要解析 HTTP 协议,因为 HttpServletRequestHttpServletResponse 就已经封装好了请求和响应。以发送响应为例,我们只需要设置正确的响应类型,然后获取 PrintWriter ,写入响应即可。

Servlet API 是什么?

Servlet API 是一个 jar 包,我们需要通过 Maven 来引入它,才能正常编译。通过 Maven 构建后会得到一个 .war 格式的文件,那么, 如何运行这个 war 文件呢

普通的 Java 程序是通过启动 JVM ,然后执行 main() 方法开始运行。但是 Web 应用程序有所不同,我们无法直接运行 war 文件,必须先启动 Web 服务器,再由 Web 服务器加载我们编写的 HelloServlet ,这样就可以让我们编写的 HelloServlet 处理浏览器发送的请求。

那么,Servlet API是谁提供的呢?

是由支持 Servlet API 的 Web 服务器提供的!常用的服务器有:

  • Tomcat :由 Apache 开发的开源免费服务器;
  • Jetty :由 Eclipse 开发的开源免费服务器;
  • GlassFish :一个开源的全功能 JavaEE 服务器;
  • WebLogic :Oracle 的商用服务器;
  • WebSphere :IBM 的调用服务器。

无论使用哪个服务器,只要它支持相同版本的 Servlet API ,我们在引版本上开发构建的 war 包都可以在上面运行。

我们通常选用最广泛的开源免费的 Tomcat 服务器。

实际上,类似 Tomcat 这样的服务器也是 Java 编写的,启动 Tomcat 服务器实际上是启动 Java 虚拟机,执行 Tomcat 的 main() 方法,然后由 Tomcat 负责加载我们的 .war 文件,并创建一个我们编写的 HelloServlet 实例,最后以多线程的模式来处理 HTTP 请求。例如,Tomcat 服务器收到的请求路径是 / 时,就转发到 HelloServlet 并传入 HttpServletRequestHttpServletResponse 现个对象。

由上可知,我们编写的 Servlet 并不是直接运行,而是由 Web 服务器加载后创建实例运行,所以,类似于 Tomcat 这样的 Web 服务器也称为 Servlet 容器

在 Servlet 容器中运行的 Servlet 具有如下特点:

  • 无法在代码中直接通过 new 创建 Servlet 实例,必须由 Servlet 容器自动创建 Servlet 实例;
  • Servlet 容器只会给每个 Servlet 类创建唯一实例;
  • Servlet 容器会使用多线程执行 doGet()doPost() 方法。

*注:在 Servlet 中定义的实例变量会被多个线程同时访问,要注意线程安全。正确编写 Servlet ,要清晰理解 Java 的多线程模型,需要同步访问的必须同步。

Servlet 开发

通常,一个完整的 Web 应用程序的开发流程如下:

  1. 编写 Servlet ;
  2. 打包为 war 文件;
  3. 复制到 Tomcat 的 webapps 目录下;
  4. 启动 Tomcat 。

这个过程同样很繁琐(开发者都是“懒虫”啦),而且如果我们想在 IDE 中断点调试,还需要打开 Tomcat 的远程调试端口并且连接上去。

但是,许多初学者经常卡在如何在 IDE 中启动 Tomcat 并加载 webapp,更不要说断点调试了……

Tomcat 实际上也是一个 Java 程序,我们看看 Tomcat 的启动流程:

  1. 启动 JVM 并执行 Tomcat 的 main() 方法;
  2. 加载 .war 文件并初始化 Servlet ;
  3. 正常服务。

那么,启动 Tomcat 无非就是设置好 classpath 并执行 Tomcat 某个 jar 包的 main() 方法,我们完全可以把 Tomcat 的 jar 包全部引入进来,然后自己编写一个 main() 方法,先启动 Tomcat ,会后让它加载我们的 webapp 就可以了。

我们新建一个 web-servlet-embedded 工程,编写 pom.xml 如下:

 1: <project xmlns="http://maven.apache.org/POM/4.0.0"
 2:          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3:          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4:          <modelVersion>4.0.0</modelVersion>
 5: 
 6:          <groupId>com.itranswarp.leanjava</groupId>
 7:          <artifactId>web-servlet-embedded</artifactId>
 8:          <version>1.0-SNAPSHOT</version>
 9:          <packaging>war</packaging>
10: 
11:          <properties>
12:            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
13:            <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
14:            <maven.compiler.source>11</maven.compiler.source>
15:            <maven.compiler.target>11</maven.compiler.target>
16:            <java.version>11</java.version>
17:            <tomcat.version>9.0.26</tomcat.version>
18:          </properties>
19: 
20:          <dependencies>
21:            <dependency>
22:              <groupId>org.apache.tomcat.embed</groupId>
23:              <artifactId>tomcat-embed-core</artifactId>
24:              <version>${tomcat.version}</version>
25:              <scope>provided</scope>
26:            </dependency>
27:            <dependency>
28:              <groupId>org.apache.tomcat.embed</groupId>
29:              <artifactId>tomcat-embed-jasper</artifactId>
30:              <version>${tomcat.version}</version>
31:              <scope>provided</scope>
32:            </dependency>
33:          </dependencies>
34: </project>

其中, <packaging> 类型仍然为 war ,引入依赖 tomcat-embed-coretomcat-embed-jasper ,引入的 Tomcat 版本为 <tomcat.version>9.0.26

不必引入 Servlet API,因为引入 Tomcat 依赖后自动引入了 Servlet API 。因此,我们可以正常编写 Servlet 如下:

 1: @WebServlet(urlPatterns = "/")
 2: public class HelloServlet extends HttpServlet {
 3:     protected void doGet(HttpServletRequest req, HttpServletResponse resp)
 4:         throws ServletException, IOException {
 5:         resp.setContentType("text/html");
 6:         String name = req.getParameter("name");
 7:         if (name == null) {
 8:             name = "world";
 9:         }
10:         PrintWriter pw = resp.getWriter();
11:         pw.write("<h1>Hello, " + name + "</h1>");
12:         pw.flush();
13:     }
14: }

然后,我们编写一个 main() 方法,启动 Tomcat 服务器:

 1: public class Main {
 2:     public static void main(String[] args) throws Exception {
 3:         // 启动 Tomcat :
 4:         Tomcat tomcat = new Tomcat();
 5:         tomcat.setPort(Integer.getInteger("port", 8080));
 6:         tomcat.getConnectory();
 7:         // 创建 webapp :
 8:         Context ctx = tomcat.addWebApp("", new File("src/main/webapp"),getAbsolutePath());
 9:         WebResourceRoot resources = new StandardRoot(ctx);
10:         resources.addPreResources(new DirResourceSet(resources, "/WEB-INF/classes", new File("target/classes").getAbsolutePath(), "/"));
11:         ctx.setResources(resources);
12:         tomcat.start();
13:         tomcat.getServer().await();
14:     }
15: }

如此,我们直接运行 main() 方法,即可启动嵌入式 Tomcat 服务器,

后,通过预设的 tomcat.addWebapp("", new File("src/main/webapp") ,Tomcat 会自动加载当前工程作为根 webapp,可直接在浏览器访问 http://localhost:8080/:

通过 main() 方法启动 Tomcat 服务器并加载我们自己的 webapp 有如下好处:

  1. 启动简单,无需下载 Tomcat 或安装任何 IDE 插件;
  2. 高度方便,可在 IDE 中使用断点调试;
  3. 使用 Maven 创建 war 包后,也可以正常部署到独立的 Tomcat 服务器中。
看吧,你需要做一些基础的东西,才能在此基础上理方便地处理事物。

对 SpringBoot 有所了解的童鞋可能知道,SpringBoot 也支持在 main() 方法中一行代码直接启动 Tomcat,并且还能方便地更换成 Jetty 等其他服务器,它的启动方式和我们介绍的是基本一样的。

幸运地是,许多繁琐的事情都已经有了比较成熟的解决方式,但是为什么采取这些方式,以及它们的优缺点还是需要了然于胸的。

Servlet 是什么

好的,下面我们就来具体认识一下 Servlet 是什么。

Servlet 是 Server Applet 的简称,译为“服务器端小程序”。它是 Java 的一套技术标准,规定了如何使用 Java 来开发动态网站。换句话说,Java 可以用来开发网站后台,但是要提前定义好一套规范,并编写基础类库,这就是 Servlet 所做的事情。

Java Servlet 可以使用所有的 Java API ,Java 能做的事情,Servlet 都能做。

#+BEGINQUOTE Servlet 只是古老的 CGI 技术的替代品,然而直接使用 Servlet 开发还是很麻烦,所以 Java 后来又对 Servlet 进行了升级,推出了 JSP 技术。本质上,JSP 只是对 Servlet 加了一层壳,JSP 经过编译后还是 Servlet 。 #+ENDQUOTE

程序嘛,最终都是要变为二进制的 0 和 1 ,所以,一切不过都是抽象,一层一层的抽象罢了。

Servlet 是 Java Servlet 的简称,是使用 Java 语言编写的运行在服务器端的程序,具有独立平台和协议的特性,主要功能在于交互式地浏览和生成数据,生成动态 Web 内容。

通常来说,Servlet 是指所有实现了 Servlet 接口的类。

  • Servlet 主要用于处理客户端传来的 HTTP 请求,并返回一个响应,它能够处理的请求有 doGet()doPost() 等;
  • Servlet 由 Servlet 容器提供,所谓 Servlet 容器就是指提供了 Servlet 功能的服务器(如 Tomcat);
  • Servlet 容器会将 Servlet 动态加载到服务器上,然后通过 HTTP 请求和 HTTP 响应与客户端进行交互。

Servlet 应用程序的体系结构如下:

如上图中,Servlet 的请求首先会被 HTTP 服务器(如 Apache、Nginx)接收,HTTP 服务器只负责静态 HTML 页面的解析,而 Servlet 的请求会转交给 Servlet 容器,Servlet 容器会根据 web.xml 文件中的映射关系,调用相应的 Servlet ,Servlet 再将处理的结果返回给 Servlet 容器,并通过 HTTP 服务器将响应传输给客户端。

Servlet 相关的接口和类

SUN 公司提供了一系列的接口和类用于 Servlet 技术的开发(缅怀一下 SUN 公司吧),其中最重要的接口是 javax.servlet.Servlet 。在 Servlet 接口中定义了 5 个抽象方法,如下表:

Table 1: Servlet 接口的抽象方法
方法声明 功能描述
void init(ServletConfig config) 容器在创建好 Servlet 对象后,就会调用此方法。该方法接收一个 ServletConfig 类型的参数,Servlet 容器通过该参数向 Servlet 传递初始化配置信息
ServletConfig getServletConfig() 用于获取 Servlet 对象的配置信息,返回 Servlet 的 ServletConfig 对象
String getServletInfo() 返回一个字符串,其中包含关于 Servlet 的信息,如作者、版本和版权等信息
void service(ServletRequest request, ServletResponse response) 负责响应用户的请求,当容器接收到客户端访问 Servlet 对象的请求时,就会调用此方法。
  容器会构造一个表示客户端请求信息的 ServletRequest 对象和一个用于响应客户端的 ServletResponse 对象作为参数传递给 service() 方法。
  service() 方法中,可以通过 ServletRequest 对象得到客户端的相关信息和请求信息,在对请求进行处理后,调用 ServletResponse 对象的方法设置响应信息
void destroy() 负责释放 Servlet 对象占用的资源,当服务器关闭或者 Servlet 对象被移除时,Servlet 对象会被销毁,容器会调用此方法

在表中,列举了 Servlet 接口中的五个方法,其中 init()、service()destroy() 方法可以表现 Servlet 的生命周期,它们会在某个特定的时刻被调用。

针对 Servlet 的接口,SUN 公司提供了现个默认的接口实现类: GenericServletHttpServlet 其中:

  • GenericServlet 是一个抽象类,该类为 Servlet 接口提供了部分实现,它并没有实现 HTTP 请求处理;
  • HttpServletGenericServlet 的子类,它继承了 GenericServlet 的所有方法,并且为 HTTP 请求中的 GET 和 POST 等类型提供了具体的操作方法。

*注:通常情况下,编写的 Servlet 类都继承自 HttpServlet ,在开发中使用的也是 HttpServlet 对象。

Table 2: HttpServlet 类的常用方法
方法声明 功能描述
protected void doGet(HttpServletRequest req, HttpServletResponse resp) 用于处理 GET 类型的 HTTP 请求的方法
protected void doPost(HttpServletRequest req, HttpServletResponse resp) 用于处理 POST 类型的 HTTP 请求的方法

HttpServlet 主要有两大功能,具体如下:

  • 根据用户请求方式的不同,定义相应的 doXxx() 方法处理用户请求,例如,与 GET 请求方式对应的 doGet() 方法,与 POST 方式对应的 doPost() 方法;
  • 通过 service() 方法将 HTTP 请求和响应分别强转为 HttpServletRequestHttpServletResponse 类型的对象。

*注:需要注意的是,由于 HttpServlet 类在重写的 service() 方法,为每一种 HTTP 请求方式都定义了对应的 doXxx() 方法,因此,当定义的类的继承 HttpServlet 后,只需要根据请求方式重写对应的 doXxx() 方法即可,而不需要重写 service() 方法。

Servlet 生命周期

在 Java 中,任何对象都有生命周期,Servlet 也不例外,其生命周期如下:

按照功能的不同,大致可以将 Servlet 的生命周期分为三个阶段,分别是初始化阶段、运行阶段和销毁阶段。

1. 初始化阶段

当客户端向 Servlet 容器发出 HTTP 请求要求访问 Servlet 时,Servlet 容器首先会解析请求,检查内存中是否已经有了该 Servlet 对象,如果有,则直接使用该 Servlet 对象,如果没有,则创建 Servlet 实例对象,然后通过调用 init() 方法实现 Servlet 的初始化工作。

*注:在 Servlet 的整个生命周期内,它的 init() 方法只能被调用一次。

2. 运行阶段

这是 Servlet 生命周期中最重要的阶段,在这个阶段中,Servlet 容器会为这个请求创建代表 HTTP 请求的 ServletRequest 对象和代表 HTTP 响应的 ServletResponse 对象,然后将它们作为参数传递给 Servlet 的 service() 方法。

service() 方法从 ServletRequest 对象中获得请求信息并处理该请求,通过 ServletResponse 对象生成响应结果。

在 Servlet 的整个生命周期内,对于 Servlet 的每一次访问请求,Servlet 容器都会调用一次 Servlet 的 service() 方法,并且创建新的 ServletRequestServletResponse 对象。

*注:也就是说, service() 方法在 Servlet 的整个生命周期中会被调用多次。

3. 销毁阶段

当服务器关闭或 Web 应用被移除出容器时,Servlet 随着 Web 应用的关闭而销毁。在销毁 Servlet 之前,Servlet 容器会调用 Servlet 的 destroy() 方法,以便让 Servlet 对象释放它所占用的资源(Servlet 对象一旦创建就会驻留在内存中等待客户端的访问)。

*注:在 Servlet 的整个生命周期中, destroy() 方法也只能被调用一次。

第一个 Servlet 程序

1. 创建 Web 项目

2. 创建 Servlet 程序

3. 部署和访问 Servlet

Hmmm... 大致就是这么个过程,具体如何结合的,找个专题慢慢聊。

Servlet 虚拟路径映射的配置

web.xml 文件中,一个 <servlet-mapping> 元素用于映射一个 Servlet 的对外访问路径,该路径也称为虚拟路径。

1: <!-- web.xml -->
2: ...
3: <servlet-mapping>
4:   <servlet-name>TestServlet01</servlet-name>
5:   <url-pattern>/TestServlet01</url-pattern>
6: </servlet-mapping>
7: ...

如上, TestServlet01 所映射的虚拟路径为 /TestServlet01

*注:创建好的 Servlet 只有映射成虚拟路径,客户端才能对其进行访问。

在映射 Servlet 时,需要了解 Servlet 的多重映射、在映射路径中使用通配符、配置默认的 Servlet 等。

Servlet 的多重映射

Servlet 的多重映射指同一个 Servlet 可以被映射成多条虚拟路径。也就是说,客户端可以通过多条路径实现对同一个 Servlet 的访问。

那么,如何 Servlet 多重映射的实现方式有哪些呢?两种。

1. 配置多个 <servlet-mapping> 元素

 1: <!-- web.xml -->
 2: ...
 3: <servlet-mapping>
 4:   <servlet-name>TestServlet01</servlet-name>
 5:   <url-pattern>/TestServlet01</url-pattern>
 6: </servlet-mapping>
 7: <servlet-mapping>
 8:   <servlet-name>TestServlet01</servlet-name>
 9:   <url-pattern>/Test01</url-pattern>
10: </servlet-mapping>
11: ...

2. 配置多个 <url-pattern> 子元素

1: <!-- web.xml -->
2: ...
3: <servlet-mapping>
4:     <!-- 映射为TestServlet01和Test02 -->
5:     <servlet-name>TestServlet01</servlet-name>
6:     <url-pattern>/TestServlet01</url-pattern>
7:     <url-pattern>/Test02</url-pattern>
8: </servlet-mapping>
9: ...

Servlet 映射路径中使用通配符

在实际开发过程中,开发者有时会希望某个目录下的所有路径都可以访问同一个 Servlet ,这时,可以在 Servlet 映射的路径中使用通配符 *

通配符的格式有两种:

  • 格式为 *.扩展名 ,例如 *.do 匹配以 .do 结尾的所有 URL 地址;
  • 格式为 /* ,例如 /abc/* 匹配以 /abc 开始的所有 URL 地址。

但是,这两种通配符的格式 不能混合使用 ,例如, /abc/*.do 是不合法的映射路径。

当客户端访问一个 Servlet 时,如果请求的 URL 地址能够匹配多条虚拟路径,那么 Tomcat 将采取最具体匹配原则查找与请求 URL 最接近的 虚拟映射路径。

默认 Servlet

如果某个 Servlet 的映射路径仅仅是一个正斜线( / ),那么这个 Servlet 就是当前 Web 应用的默认 Servlet。Servlet 服务器在接收到访问请求时,如果在 web.xml 文件中找不到匹配的 <servlet-mapping> 元素的 URL ,则会将访问请求交给默认 Servlet 处理。

ServletConfig 和 ServletContext 接口

ServletConfig 接口

在运行 Servlet 程序时,可能需要一些辅助信息,例如,文件使用的编码、使用 Servlet 程序的共享信息等,这些信息可以在 web.xml 文件中使用一个或多个 <init-param> 元素进行配置。

当 Tomcat 初始化一个 Servlet 时,会将该 Servlet 的配置信息封装到 ServletConfig 对象中,此时可以通过调用 init(ServletConfig config) 方法将 ServletConfig 对象传递给 Servlet 。

Table 3: ServletConfig 接口的常用方法
方法说明 功能描述
String getInitParameter(String name) 根据初始化参数名返回对应的初始化参数值
Enumeration getInitParameterNames() 返回一个 Enumeration 对象,其中包含了所有的初始化参数名
ServletContext getServletContext() 返回一个代表当前 Web 应用的 ServletContext 对象
String getServletName() 返回 Servlet 的名字,即 web.xml<servlet-name> 元素的值

ServletContext 接口

当 Tomcat 启动时,Tomcat 会为每个 Web 应用创建一个唯一的 ServletContext 对象代表当前的 Web 应用,该对象封装了当前 Web 应用的所有信息。可以利用该对象获取 Web 应用程序的初始化信息、读取资源文件等。

1. 获取 Web 应用程序的初始化参数

在 web.xml 文件中,不仅可以配置 Servlet 的映射信息,还可以配置整个 Web 应用的初始化信息。

Web 应用初始化参数的配置方式具体如下:

 1: <!-- web.xml -->
 2: ...
 3: <context-param>
 4:     <param-name>XXX</param-name>
 5:     <param-value>xxx</param-value>
 6: </context-param>
 7: <context-param>
 8:     <param-name>AAA</param-name>
 9:     <param-value>aaa</param-value>
10: </context-param>
11: ...

在上面的示例中, <context-param> 元素位于根元素 <web-app> 中,它的子元素 <param-name><param-value> 分别用于指定参数的名字和参数值。

要想获取这些参数名和参数值的信息,可以使用 ServletContext 接口中定义的 getInitParameterNames()getInitParameter(String name) 方法分别获取。

 1: public class TestServlet extends HttpServlet {
 2:     public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
 3:         res.setContentType("text/html;charset=utf-8");
 4:         PrintWriter out = resp.getWriter();
 5:         // 得到 ServletContext 对象
 6:         ServletContext context = this.getServletContext();
 7:         // 得到包含所有初始化参数名的 Enumeration 对象
 8:         Enumeration<String> paramNames = context.getInitParameterNames();
 9:         // 遍历所有的初始化参数名,得到相应的参数值并打印
10:         while (paramNames.hasMoreElements()) {
11:             String name = paramNames.nextElement();
12:             String value = context.getInitparameter(name);
13:             out.println(name + ":" + value);
14:             out.println("<br/>");
15:         }
16:     }
17:     ...
18: }

2.TODO 读取 Web 应用下的资源文件

在实际开发中,有时会需要读取 Web 应用中的一些资源文件,如配置文件和日志文件等。为此,在 ServletContext 接口中定义了一些读取 Web 资源的方法,这些方法是依靠 Servlet 容器实现的。

Servlet 容器根据资源文件相对于 Web 应用的路径,返回关联资源文件的 I/O 流或资源文件在系统的绝对路径等。

Servlet 处理用户请求的完整流程

针对 Servlet 的每次请求,Web 服务器在调用 service() 方法之前,都会创建 HttpServletRequestHttpServletResponse 对象。其中, HttpServletRequest 对象用于封装 HTTP 请求消息,简称 request 对象。 HttpServletResponse 对象用于封装 HTTP 响应信息,简称 response 对象。

Figure: 浏览器访问 Servlet 过程

如图,首先浏览器向 Web 服务器发送了一个 HTTP 请求,Web 服务器根据收到的请求,会先创建一个 HttpServletRequestHttpServletResponse 对象,然后再调用相应的 Servlet 程序。

在 Servlet 程序运行时,它首先会从 HttpServletRequest 对象中读取数据信息,然后通过 service() 方法处理请求消息,并将处理后的响应数据写入到 HttpServletResponse 对象中。最后,Web 服务器会从 HttpServletResponse 对象中读取到响应数据,并发送给浏览器。

需要注意的是,在 Web 服务器运行阶段,每个 Servlet 都只会创建一个实例对象,针对每次 HTTP 请求,Web 服务器都会调用所请求 Servlet 实例的 service(HttpServletRequest request, HttpServletResponse response) 方法,并重新创建一个 request 对象和一个 response 对象。

HttpServletRequest

HttpServletRequest 接口继承处 ServletRequest 接口,其主要使用是封装 HTTP 请求信息。

由于 HTTP 请求消息分为请求行、请求消息头和请求消息体三部分。因此,在 HttpServletRequest 接口中定义了获取请求行、请求头和请求消息体的相关方法。

获取请求行信息的相关方法2

当访问 Servlet 时,所有请求消息将被封装到 HttpServletRequest 对象中,请求消息的请求行中包含请求方法、请求资源名、请求路径等信息,为了获取这些信息, HttpServletRequest 接口定义了一系列方法。

具体的属性表格,用的时候再查就好了。

获取请求消息头的相关方法

当浏览器发送 Servlet 请求时,需要通过请求消息头向服务器传递附加信息,例如,客户端可以接收的数据类型、压缩方式、语言等。为此,在 HttpServletRequest 接口中定义了一系列获取 HTTP 请求字段的方法。

TODO Servlet 获取 form 表单数据

Request 对象不仅可以获取一系列数据,还可以通过属性传递数据。

RequestDispatcher 实现请求转发3

当一个 Web 资源收到客户端的请求后,如果希望服务器通知另外一个资源处理请求,可以通过 RequestDispatcher 接口的实例对象实现。

老规矩喽,相关具体的属性列表有些了解就好,用的时候再查就行了,不经过反复的实践,你也记不住。

Figure:forward() 方法的工作原理

RequestDispatcher 接口中, forward() 方法可以实现请求转发, include() 方法可以实现请求包含。

如图:当客户端访问 Servlet1 时,可以通过 forward() 方法将请求转发给其他 Web 资源,其他 Web 资源处理完请求后,直接将响应结果返回到客户端。

HttpServletResponse

HttpServletResponse 接口继承自 ServletResponse 接口,主要用于封装 HTTP 响应消息。

由于 HTTP 响应消息分为状态行、响应消息头、消息体三部分。因此,在 HttpServletResponse 接口中定义了向客户端发送响应状态码、响应消息头、响应消息体的方法。

发送状态码相关的方法

当 Servlet 向客户端回送响应消息时,需要在响应消息中设置状态码。因此, HttpServletResponse 接口定义了两个发送状态码的方法。

1. setStatus(int status) 方法

该方法用于设置 HTTP 响应消息的状态码,并生成响应状态行。

由于响应状态行中的状态描述信息直接与状态码相关,而 HTTP 版本由服务器确定,因此,只要通过 setStatus(ini status) 方法设置了状态码,即可实现状态行的发送。

*注:在正常情况下,Web 服务器会默认产生一个状态码为 200 的状态行。

2. sendError(int sc) 方法

该方法用于发送表示错误信息的状态码。例如, 404 状态码表示找不到客户端请求的资源。

response 对象提供了现个重载的 sendError(int sc) 方法,具体如下:

public void sendError(int code) throws java.io.IOException
public void sendError(int code, String message) throws java.io.IOException

在上面重载的两个方法中,第一个方法只发送错误信息的状态码,而第二个方法除了发送状态以外,还可以增加一条用于提示说明的文本信息,该文本信息将出现在发送给客户端的正文内容中。

发送响应消息头相关的方法

Servlet 向客户端发送的响应消息中包含响应头字段,由于 HTTP 协议的响应头字段有很多种,因此, HttpServletResponse 接口定义了一系列设置 HTTP 响应头字段的方法。

发送响应消息体相关的方法

由于在 HTTP 响应消息中,大量的数据都是通过响应消息体传递的,因此, ServletResponse 遵循以 I/O 流传递大量数据的设计理念。在发送响应消息体时,定义了两个与输出流相关的方法。

1. getOutputStream() 方法

该方法所获取的字节输出流对象为 ServletOutputStream 类型。

由于 ServletOutputStreamOutputStream 的子类,它可以直接输出字节数组中的二进制数据。因此,要想输出二进制格式的响应正文,就需要使用 getOutputStream() 方法。

2. getWriter() 方法

该方法所获取的字符输出流对象为 PrintWriter 类型。

由于 PrintWriter 类型的对象可以直接输出字符文本内容,因此,要想输出内容全部为字符文本的网页文档,则需要使用 getWriter() 方法。

*注:虽然 response 对象的 getOutputStream()getWriter() 方法都可以发送响应消息体,但是,它们之间互相排斥,不可同时使用,否则会发生 IllegalStateException 异常。

sendRedirect() 实现重写向

在某些情况下,针对客户端的请求,一个 Servlet 类可能无法完成全部工作,这时,可以使用请求重定向完成这一工作。

请求重定向 指 Web 服务器接收到客户端的请求后,可能由于某些条件的限制,不能访问当前请求 URL 所指向的 Web 资源,而是指定了一个新的资源路径,让客户端重新发送请求。

为了实现请求重定向, HttpServletResponse 接口定义了一个 sendRedirect() 方法,该方法用于生成 302 响应码和 Location 响应头,从而通知客户端重新访问 Location 响应中指定的 URL , sendRedirect() 方法的完整语法如下所示:

public void sendRedirect(java.lang.String.location) throws java.io.IOException

在上述方法代码中,参数 location 可以使用相对 URL ,Web 服务器会自动将相对 URL 翻译成绝对 URL,再生成 Location 头字段。

Figure:sendRedirect() 方法的工作原理

如图:当客户端访问 Servlet1 时,由于在 Servlet1 中调用了 sendRedirect() 方法将请求重定向到 Servlet2 ,因此,浏览器收到 Servlet1 的响应消息后,立刻向 Servlet2 发送请求, Servlet2 对请求处理完毕后,再将响应消息回送给客户端浏览器并显示。

下面通过一个用户登录的案例分步骤讲解 sendRedirect() 方法的使用。

1. 创建页面文件

在servletDemo 项目的 WebContent 目录下创建一个用户登录的页面 login.html 和登录成功的页面 welcome.html ,如下:

 1: <!-- login.html -->
 2: <!doctype html>
 3: <html>
 4:   <head>
 5:     <meta charset="UTF-8"/>
 6:     <title>用户登录</title>
 7:   </head>
 8:   <body>
 9:     <!-- 把表单内容提交到 servletDemo 工程下的 LoginServlet -->
10:     <form action="/servletDemo/LoginServlet" method="POST">
11:       账号: <input name="username" type="text"  /><br/>
12:       密码: <input name="password" type="password" /><br/>
13:       <br/>
14:       <input type="submit" value="登录" />
15:     </form>
16:   </body>
17: </html>
 1: <!doctype html>
 2: <html>
 3:   <head>
 4:     <meta charset="UTF-8"/>
 5:     <title>欢迎页面</title>
 6:   </head>
 7:   <body>
 8:     欢迎你,登陆成功!
 9:   </body>
10: </html>

2. 创建 Servlet

在 servletDemo 项目的 com.mengma.response 包中创建一个名为 LoginServletServlet 类,用于处理用户登录请求,如下:

 1: package com.mengma.servlet;
 2: 
 3: import java.io.IOException;
 4: 
 5: import javax.servlet.ServletException;
 6: import javax.servlet.http.HttpServlet;
 7: import javax.servlet.http.HttpServletRequest;
 8: import javax.servlet.http.HttpServletResponse;
 9: 
10: public class LoginServlet extends HttpServlet {
11:     public void doGet(HttpServletRequest request, HttpServletResponse response)
12:         throws ServletException, IOException {
13:         response.setContentType("text/html;charset=utf-8");
14:         // 用 HttpServletRequest 对象的 getParameter() 方法获取用户名和密码
15:         String username = request.getParameter("username");
16:         String password = request.getParameter("password");
17:         // 假设用户名和密码分别为 admin 和 123456
18:         if ("admin".equals(username) && ("123456").equals(password)) {
19:             // 如果用户名和密码正确,重定向到 welcome.html
20:             response.sendRedirect("/servletDemo/welcome.html");
21:         } else {
22:             // 如果用户名和密码错误,重定向到 login.html
23:             response.sendRedirect("/servletDemo/login.html");
24:         }
25:     }
26: 
27:     public void doPost(HttpServletRequest request, HttpServletResponse response)
28:         throws ServletException, IOException {
29:         doGet(request, response);
30:     }
31: }

在上述代码中,首先通过 getParameter() 方法分别获取用户名和密码,然后判断表单中输入的用户名和密码是否为指定的 “admin”“123456” ,如果是,则将请求重定向到 welcome.html 页面,否则重定向到 login.html 页面。

3. 运行项目并查看结果

request/response 中文乱码问题

request 中文乱码问题以及解决方案

在填写表单数据时,难免需要输入中文,如用户名和公司名称,提交后控制台的显示乱码,如 é??é??...

HttpServletRequest 接口中提供了一个 setCharacterEncoding() 方法,该方法用于设置 request 对象的解码方式。

request.setCharacterEncoding("utf-8");    // 设置 request 对象的解码方式

重启 Tomcat 服务器后,再次输入中文即可正确解码。

*注:这种解决乱码的方式只对 POST 方式有效,而对 GET 方式无效。

那么,如何解决 GET 方式提交表单时出现的中文乱码问题,可以先使用错误码表 ISO-8859-1 将用户名重新编码,然后使用码表 UTF-8 进行解码。

name = new String(name.getBytes("iso8859-1"),"utf-8");

重启 Tomcat 服务器后,再次输入中文即可正确解码。

response中文乱码问题以及解决方案

由于计算机中的数据都是以二进制形式存储的,因此,当传输文本数据时,会发生字符和字节的转换。

字符和字节的转换是通过查码表完成的,将字符转换成字节的过程称为 编码 ,将字节转换成字符的过程称为 解码 ,如果编码和解码使用的码表不一致,则会导致乱码问题。

Figure:编码错误分析

为了解决上述编码错误,HttpServletResponse 对象提供了两种解决乱码的方式,具体如下:

response.setCharacterEncoding("utf-8");                        // 设置 HttpServletResponse 使用 utf-8 编码
response.setHeader("Content-Type", "text/html;charset=utf-8"); // 通知浏览器使用 utf-8 解码

response.setContentType("text/html;charset=utf-8");            // 包含第一种方式的两个功能(推荐)

重启 Tomcat 服务器服务器并使用浏览器访问,即可正确解码。

Footnotes:

Date: 2020-10-06 Tue 14:08

Author: Jack Liu

Created: 2020-10-07 Wed 18:48

Validate