Servlet(服务器小程序) 是运行在服务端(如 Tomcat)的 Java 小程序。它由 Sun 公司定义,用于处理动态资源。简单来说,Servlet 是一套技术标准(接口),在 Java 中只有符合这些标准的类才能有效响应客户端的请求。通过使用 Servlet,开发者可以构建动态的 Web 应用程序。Servlet 具有以下特点:
- 处理请求:Servlet 是专门用于处理客户端请求并返回响应的一种技术
- 运行环境:Servlet 必须在 Web 项目中开发,并在像 Tomcat 这样的服务容器中运行
动态资源 vs 静态资源
在介绍 Servlet 之前,首先了解一下静态资源和动态资源的区别。
-
静态资源:在程序运行之前就已创建的文件,这些资源无需通过代码生成,常见的有 HTML、CSS、JavaScript、图片和视频等。
-
动态资源:在程序运行时通过代码生成的内容,这些资源在运行前无法确定,通常由服务器根据请求实时生成,例子包括 Servlet 和 Thymeleaf。需要注意的是,动态资源并不包括页面动画或简单交互效果。
可以用去蛋糕店的例子来说明:静态资源就像你直接购买柜台上已经做好的蛋糕,而动态资源则是你告诉柜员你的要求,让他们现场制作蛋糕。
(1)静态资源请求流程
在请求静态资源时,客户端浏览器向服务器发送 HTTP 请求。例如,假设请求的是 a.png。服务器接收到请求后,Tomcat 会将 a.png 转换为响应报文,并将其发送回客户端。这个响应报文包含了请求的资源数据:
- 响应行
- 响应头(浏览器通过 Content-Type 获取数据类型,并执行对应解析)
- 响应体(图片数据)
(2)动态资源请求流程
在请求动态资源时,客户端浏览器向服务器发送 HTTP 请求。服务器接收请求并提取参数,然后运行相应的 Java 代码生成响应数据。这些数据会通过 Tomcat 转换为响应报文返回给浏览器,响应报文的内容同样包括响应行、响应头、响应体信息。
每次动态资源请求都需要执行 Java 代码,而请求中的参数可能不同,因此生成的响应数据也会有所变化。在这个过程中,生成响应数据的 Java 代码必须遵循特定的技术标准和规范,这套标准其实就是 Servlet。
Servlet 运行流程
Servlet的运行流程是这样的:首先,客户端浏览器向服务器发送请求报文。Tomcat 接收到请求后,会将请求报文的信息转换为一个 HttpServletRequest 对象,该对象中包含了请求中的所有信息(请求行、请求头、请求体)。同时,Tomcat 会创建一个 HttpServletResponse 对象,该对象用于装载响应给客户端的信息,该对象将会被转换成响应的报文(响应行、响应头、响应体)。接着,Tomcat 根据请求中的资源路径找到对应的 Servlet(由后端定义并实现),将 Servlet 实例化,调用 service 方法,同时将 HttpServletRequest 和 HttpServletResponse 对象传入。待处理完成后,Tomcat 将 HttpServletResponse 对象转成响应报文。最后,服务器向客户端浏览器发送响应报文。
Servlet 开发流程
现在假设存在这样一个需求:校验用户注册时,用户名是否被占用。具体地,通过客户端向一个 Servlet 发送请求,携带 usename 信息,如果用户名存在(这里简化需求,假定存在用户名 IT
),则向客户端响应 NO
,否则响应 YES
。
(1)创建 Java Web 项目
创建一个名为 demo-servlet
项目,创建步骤在 《Tomcat 快速入门》这篇文章有提及。
(2)创建注册页面
在 web 文件夹下创建一个 index.html 文件(直接命名为 index,请求省略资源路径也能访问该页面),文件内容:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Register</title>
</head>
<body>
<!--
http://127.0.0.1:8080/demo/userServlet
请求体:username=it
-->
<form method="post" action="userServlet">
用户名:<input type="text" name="username"> <br>
<input type="submit" vlaue="校验">
</form>
</body>
</html>
(3)重写 service 方法,实现业务代码
在 src 文件夹下创建 com.it.servlet.UserServlet 类,实现 Servlet 接口中定义的方法:
java
package com.it.servlet;
import jakarta.servlet.*;
import java.io.IOException;
public class UserServlet implements Servlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {}
}
如果直接实现 Servlet 接口,需要实现上述多个方法,考虑到目前需求不需要实现这么多接口。因此,可以改成继承 HttpServlet 类,这个类间接实现了 Servlet 接口:
java
public class UserServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 从 request 对象中获取请求信息(user 参数)
String username = req.getParameter("username"); // 根据参数名获取参数
// 2. 处理业务代码
String res = "YES";
if ("IT".equals(username)) {
res = "NO";
}
// 3. 将响应数据存放入 response 对象中
PrintWriter writer = resp.getWriter(); // 返回一个响应体中打印字符串的打印流
writer.write(res);
}
}
(4)在 web.xml 中配置请求映射路径
在 web.xml 文件中为 Servlet 类起一个别名,用于关联请求的映射路径,并且通过 servlet-class 项告知 Tomcat 对应要实例化的 Servlet 类:
java
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0">
<!--
配置 Servlet 类,并起一个别名
servlet-name:用于关联请求的映射路径
servlet-class:告知 Tomcat 对应要实例化的 Servlet 类
-->
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>com.it.servlet.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/userServlet</url-pattern>
</servlet-mapping>
</web-app>
注意:servlet-mapping 中 url-pattern 项的内容需要有一个 /
作为开头。 /
后面的内容与 HTML 中 form 表单的 action 保持一致。
(5)启动项目,测试效果
运行前,点击运行键旁边的 Edit Configurations...
,将当前 Tomcat 的 Deployment 换成当前的项目:
配置完成后,启动项目。可以看到浏览器直接显示了注册的 HTML 页面,原因是当请求路径 http://localhost:8080/demo/
后没有接其他内容时,默认为 index.html
。此时,用户名输入为 IT
时,页面返回 NO
,输入其他值时,页面访问 YES
。
开发过程问题记录
(1)servlet-api.jar 导入问题
在开发过程中,我们自定义的 Servlet 需要继承 HttpServlet 类或实现 Servlet 接口。这些类和接口并不是 JDK 内置的,而是属于 Tomcat 中的 jar 包(servlet-api.jar)。因此,需要导入 servlet-api 这个 jar 包。正常导入 jar 包的流程是:在 WEB-INF 下创建一个 lib 目录,将 servlet-api.jar
复制到 lib 中。之后将当前 jar 包添加为项目依赖。右键 lib 目录,选择 Add as Library...
,将 lib 设置为 Module Library
:
然而,我们在开发 Servlet 项目的过程中,并没有按照这种正常流程导入 jar 包,原因是:我们在 Project Structure 中导入了 Tomcat 依赖,而 Tomcat 依赖中已经包含了 servlet-api.jar
。因此,不需要我们手动导入依赖。现在,我们还是解除一下手动导入的servlet-api.jar
。在 Project Structure 中删除 Dependencies 中手动导入的依赖,并且删除 lib 目录下的对应 jar 包即可。
(2)Content-Type 响应头的问题
若希望客户端将数据当作特定类型的数据来解析,比如当作 html 代码解析,服务器端就需要设置对应的 Content-Type。方法是通过 HttpServletResponse 对象调用 setHeader 方法或者直接调用 setContentType 设置:
java
public class UserServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 从 request 对象中获取请求信息(user 参数)
String username = req.getParameter("username"); // 根据参数名获取参数
// 2. 处理业务代码
String res = "<h1>YES</h1>";
if ("IT".equals(username)) {
res = "<h1>NO</h1>";
}
// 3. 将响应数据存放入 response 对象中
// 设置 Content-Type 响应头
resp.setHeader("Content-Type", "text/html");
// resp.setContentType("text/html");
PrintWriter writer = resp.getWriter(); // 返回一个响应体中打印字符串的打印流
writer.write(res);
}
}
其中,Content-Type 设置的值可以查看 Tomcat 目录下的 conf 中的 web.xml。比如,若希望客户端将数据当中 html 来处理,则可以在 web.xml 中查看 <mime-mapping>
中 html 对应的类型,结果为 text/html
。因此,将 text/html
作为 setHeader 方法的第二个参数值。设置完成后,启动服务,可以在开发者模式中查看设置结果:
(3)url-pattern 的特殊写法问题
客户端浏览器输入的 URL 到具体 Servlet 方法的调用过程如下:当浏览器发送请求,例如 http://localhost:8080/demo/s1
,它首先通过 URL 中的 IP 地址找到服务器,端口号则指向 Tomcat 服务。接着,demo
指定了运行的应用程序。最后,通过 WEB-INF/web.xml 中的路径映射,Tomcat 找到与 /s1
对应的 Servlet 实现类,并调用该类的 service 方法来处理请求。在配置路径映射的过程中,有几个具体细节需要注意:
首先,配置文件中的一个 servlet-name 是可以同时对应多个不同值的 url-pattern 的。比如下面的代码中,为 userServlet 配置了多个 url-pattern,通过 /userServlet
和 /aa
都可以找到 UserServlet 实现类。
xml
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>com.it.servlet.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/userServlet</url-pattern>
<url-pattern>/aa</url-pattern>
</servlet-mapping>
其次,配置文件中的一个 servlet 标签可以同时对应多个 servlet-mapping 标签,这种写法实际上和为 userServlet 配置了多个 url-pattern 类似。
xml
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>com.it.servlet.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/userServlet</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/aa</url-pattern>
</servlet-mapping>
最后,配置文件中,url-pattern 是支持模糊匹配的,使用 *
作为通配符,特别地:
/
表示匹配全部,不包含 jsp 文件/*
匹配全部,包含 jsp 文件/a/*
匹配前缀,后缀模糊*.html
匹配后缀,前缀模糊
Servlet 注解开发
在基于配置的 Servlet 开发中,之前需要在 web.xml 中写很多配置。现在,可以通过注解简化这一过程,只需在 Servlet 类上添加 @WebServlet 并指定路径即可。需要注意的是,如果使用注解,就不需要再使用 XML 配置;同时,两个方式不能同时使用,否则会导致错误。
java
@WebServlet({"/s1", "/s2", "/userServlet"})
// @WebServlet("/userServlet")
// @WebServlet(urlPatterns = {"/s1", "/s2", "/userServlet"})
public class UserServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 从 request 对象中获取请求信息(user 参数)
String username = req.getParameter("username"); // 根据参数名获取参数
// 2. 处理业务代码
String res = "<h1>YES</h1>";
if ("IT".equals(username)) {
res = "<h1>NO</h1>";
}
// 3. 将响应数据存放入 response 对象中
// 设置 Content-Type 响应头
resp.setHeader("Content-Type", "text/html");
// resp.setContentType("text/html");
PrintWriter writer = resp.getWriter(); // 返回一个响应体中打印字符串的打印流
writer.write(res);
}
}
Servlet 生命周期
每个 Servlet 类实例经历四个阶段:实例化、初始化、接收和处理请求,以及销毁。实例化通过构造器实现,初始化通过重写 init 方法,接收和处理请求通过重写 service 方法,销毁通过重写 destroy 方法。实例化、初始化和销毁只执行一次:实例化在第一次接收请求或服务器启动时触发,初始化在构造完成后触发,销毁在关闭服务前触发。接收和处理请求则在每次访问时执行。另外,Servlet 在 Tomcat 中采用单例模式,这意味着其成员变量在多个线程之间共享。因此,不建议在 service 方法中修改成员变量,以避免并发请求引发的线程安全问题。
java
/**
* 1. 实例化 构造器 第一次请求/服务器启动
* 2. 初始化 init 构造完毕
* 3. 接收和处理请求 service 每次请求
* 4. 销毁 destroy 关闭服务
*/
@WebServlet("/servletLifeCycle")
public class ServletLifeCycle extends HttpServlet {
public ServletLifeCycle() {
System.out.println("构造器");
}
@Override
public void init() throws ServletException {
System.out.println("初始化");
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("服务");
}
@Override
public void destroy() {
System.out.println("destroy");
}
}
前文提到,实例化在第一次接收请求或服务器启动时触发,这由 load-on-startup 参数决定。默认情况下,load-on-startup 的值为 -1
,表示 Tomcat 启动时不会实例化该 Servlet,而是在第一次接收请求时才进行实例化。如果将该值设置为正整数,则表示 Tomcat 启动时按指定顺序实例化 Servlet(适用于多个 Servlet 的情况)。如果出现序号冲突,Tomcat 会自动协调启动顺序。基于 XML 配置和基于注解开发的参数设置如下:
xml
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>com.it.servlet.UserServlet</servlet-class>
<load-on-startup>6</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/userServlet</url-pattern>
</servlet-mapping>
java
@WebServlet(urlPatterns = "/servletLifeCycle", loadOnStartup = 6)
另外,DefaultServlet 是 Tomcat 定义的一个 Servlet 实现类。当请求静态资源时,比如 /a.png
,该路径会与自定义的 Servlet 实现类实例进行匹配,若没有找到对应的 Servlet,Tomcat 会把请求交给 DefaultServlet 处理。