抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

摘要:本文学习了Servlet的基本概念,以及如何使用Servlet技术开发Web应用。

环境

Windows 10 企业版 LTSC 21H2
Java 1.8
Tomcat 8.5.50

1 简介

1.1 背景

早期的Web应用主要用于浏览新闻等静态页面,用户通过HTTP协议请求服务器上的静态页面,服务器上的应用接收到请求后,读取URI标识的资源,再加上消息报头发送给客户端浏览器,浏览器负责解析响应,将结果呈现出来。

随着时间发展,用户已经不满足于仅浏览静态页面。用户需要一些交互操作,获取一些动态结果。基于HTTP协议开发服务端应用太过复杂,所以需要一些扩展机制来实现用户想要的功能。早期使用的Web服务器扩展机制是CGI(Common Gateway Interface,公共网关接口)。用户点击链接或输入网址来访问CGI程序,Web服务器收到请求后,运行该CGI程序,对用户请求进行处理,然后将处理结果响应给Web服务器,Web服务器对响应进行包装,以HTTP响应的方式返回给浏览器。

CGI程序在一定程度上解决了用户需求,不过还存在一些不足之处,比如CGI程序编写困难,响应时间较长,以进程方式运行导致性能受限。于是1997年,SUN公司推出了Servlet技术,作为Java阵营的CGI解决方案。

1.2 定义

Servlet(Server Applet,服务端应用程序)是运行在Web服务器或应用服务器上的Java程序,作为Web客户端(比如浏览器)请求与服务器上的数据库或应用程序之间的中间层。

Servlet程序是JavaWeb三大组件之一,其他两个是Listener监听器和Filter过滤器。

1.3 对比

Servlet与CGI的区别:

  • 运行方式:Servlet是运行在服务器上的Java程序,以线程方式运行,而CGI是运行在客户端的程序,以进程方式运行。
  • 数据处理:Servlet可以直接处理HTTP请求,而CGI需要通过解析HTTP请求才能获取用户数据。
  • 状态保持:Servlet可以保持状态,而CGI难以保持请求间的状态。
  • 可移植性:Servlet可以运行在任何支持Java的服务器上,而CGI只能运行在特定的服务器上。

2 生命周期

2.1 创建

当Servlet首次被请求时,会调用init()方法创建Servlet实例。该方法只执行一次,用于加载资源和建立数据库连接等一次性设置。

在服务器启动时创建Servlet实例的两种方式:

  • web.xml文件中配置<load-on-startup>标签。
  • @WebServlet注解中配置loadOnStartup属性,只有Servlet版本在3.0以上才支持。

2.2 服务

当Servlet被请求时,会调用service()方法处理请求。该方法会被多次调用,用于处理用户请求。

根据请求类型不同,Servlet会自动调用相应的处理方法。

2.3 销毁

当Servlet不再被请求时,会调用destroy()方法销毁Servlet实例。该方法只执行一次,用于释放资源和关闭数据库连接等一次性清理。

3 技术体系

3.1 实现方式

3.1.1 Servlet接口

Servlet接口是最底层的接口,所有Servlet都必须实现该接口。

实现javax.servlet.Servlet接口是最基础的方式。

需要实现全部抽象方法:

TestServlet.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TestServlet implements Servlet {
@Override
public void init(ServletConfig config) {
System.out.println("init ...");
}

@Override
public void service(ServletRequest req, ServletResponse res) {
System.out.println("service ...");
}

@Override
public void destroy() {
System.out.println("destroy ");
}

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

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

3.1.2 GenericServlet类

GenericServlet类实现了Servlet类,并且实现大部分业务无关的方法,GenericServlet类的子类只需要实现service()方法。

继承javax.servlet.GenericServlet类是最通用的方式,适用于非HTTP协议的场景。

简化接口实现,只需要实现service()方法:

TestServlet.java
1
2
3
4
5
6
public class TestServlet extends GenericServlet {
@Override
public void service(ServletRequest req, ServletResponse res) {
System.out.println("service ...");
}
}

3.1.3 HttpServlet类

HttpServlet类继承了GenericServlet类,并且进一步实现了service()方法,根据请求方法调用HTTP专属的请求处理方法,子类只需要重写处理方法。

继承javax.servlet.http.HttpServlet类是最常用的方式,适用于HTTP协议的场景。

只需要重写与请求方法对应的处理方法,最常用的是doGet()方法和doPost()方法:

TestServlet.java
1
2
3
4
5
6
7
8
9
10
11
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) {
System.out.println("doGet ...");
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res) {
System.out.println("doPost ...");
}
}

3.2 核心接口

3.2.1 ServletConfig接口

ServletConfig接口用于获取Servlet的配置信息,包括名称、初始化参数、上下文对象等。

在创建Servlet时创建,在销毁Servlet时销毁。

常用方法:

java
1
2
3
4
5
6
7
8
// 获取名称
String getServletName();
// 获取上下文对象
ServletContext getServletContext();
// 获取初始化参数
String getInitParameter(String name);
// 获取所有初始化参数名
Enumeration<String> getInitParameterNames();

3.2.2 ServletContext接口

ServletContext接口代表整个应用,在所有客户端之间共享数据。

在应用启动时创建,在应用关闭时销毁。

常用方法:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 获取应用上下文名称
String getServletContextName();
// 获取容器名称和版本号
String getServerInfo();
// 获取应用的相对路径
String getContextPath();
// 获取指定路径的绝对路径,从盘符开始
String getRealPath(String path);
// 获取指定路径的文件夹名和文件,从应用程序根目录开始
Set<String> getResourcePaths(String path);
// 将指定资源转化为流,从应用程序根目录开始
InputStream getResourceAsStream(String path);
// 获取请求转发器,从应用程序根目录开始
RequestDispatcher getRequestDispatcher(String path);
// 根据属性名获取属性值
Object getAttribute(String name);
// 获取请求域中所有属性名的枚举集合
Enumeration<String> getAttributeNames();
// 设置属性名和属性值
void setAttribute(String name, Object object);
// 根据属性名移除属性
void removeAttribute(String name);

3.2.3 ServletRequest接口

ServletRequest接口用于获取请求信息,包括请求方法、请求路径、请求参数、请求头等。

每次请求时创建,请求结束时销毁。

常用方法:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 根据参数名获取单个参数值
String getParameter(String name);
// 根据参数名获取所有参数值
String[] getParameterValues(String name);
// 获取所有参数名的枚举集合
Enumeration<String> getParameterNames();
// 获取所有参数的键值对集合
Map<String, String[]> getParameterMap();
// 获取请求协议(比如HTTP/1.1、HTTPS/1.1)
String getProtocol();
// 获取请求方案(比如HTTP、HTTPS)
String getScheme();
// 获取服务器名称(比如localhost)
String getServerName();
// 获取服务器端口号(比如8080)
int getServerPort();
// 获取上下文对象
ServletContext getServletContext();
// 获取客户端IP地址
String getRemoteAddr();
// 获取客户端主机名
String getRemoteHost();
// 获取客户端请求的端口号
int getRemotePort();
// 获取服务器接收请求的IP地址
String getLocalAddr();
// 获取服务器接收请求的主机名
String getLocalName();
// 获取服务器接收请求的端口号
int getLocalPort();
// 根据属性名获取属性值
Object getAttribute(String name);
// 获取请求域中所有属性名的枚举集合
Enumeration<String> getAttributeNames();
// 设置属性名和属性值
void setAttribute(String name, Object value);
// 根据属性名移除属性
void removeAttribute(String name);
// 获取请求的字符编码
String getCharacterEncoding();
// 设置请求字符编码
void setCharacterEncoding(String charset) throws UnsupportedEncodingException;
// 获取请求的MIME类型
String getContentType();
// 获取请求输入流
ServletInputStream getInputStream() throws IOException;
// 获取字符输入流
BufferedReader getReader() throws IOException;
// 获取请求转发器
RequestDispatcher getRequestDispatcher(String path);

3.2.4 ServletResponse接口

ServletResponse接口用于设置响应信息,包括响应状态、响应头、响应体等。

每次请求时创建,请求结束时销毁。

常用方法:

java
1
2
3
4
5
6
7
8
9
10
11
12
// 获取响应的字符编码
String getCharacterEncoding();
// 设置响应字符编码
void setCharacterEncoding(String charset);
// 获取响应的MIME类型
String getContentType();
// 设置响应内容类型
void setContentType(String type);
// 获取字节输出流
ServletOutputStream getOutputStream() throws IOException;
// 获取字符输出流
PrintWriter getWriter() throws IOException;

3.2.5 HttpServletRequest接口

HttpServletRequest接口继承了ServletRequest接口,并进一步实现了HTTP专属的请求信息。

新增的常用方法:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 获取请求方法
String getMethod();
// 获取请求的Cookie数组
Cookie[] getCookies();
// 获取当前请求的HttpSession实例,没有则创建
HttpSession getSession();
// 获取应用上下文路径
String getContextPath();
// 获取应用上下文路径后面的映射路径,不包含查询字符串
String getServletPath();
// 获取映射路径后面的信息路径,不包含查询字符串
String getPathInfo();
// 获取请求的URI,不包含查询字符串
String getRequestURI();
// 获取请求的URL,包含协议、主机名、端口号,以及URI,不包含查询字符串
StringBuffer getRequestURL();
// 获取查询字符串
String getQueryString();
// 获取客户端认证方案
String getAuthType();
// 获取当前已认证用户的用户名
String getRemoteUser();
// 检查当前用户是否拥有指定角色
boolean isUserInRole(String role);
// 获取用户身份验证对象
Principal getUserPrincipal();
// 触发用户认证
void authenticate(HttpServletResponse response) throws IOException, ServletException;
// 触发用户登录
void login(String username, String password) throws ServletException;
// 触发用户注销
void logout() throws ServletException;
// 获取请求中指定名称的文件上传部件
Part getPart(String name) throws IOException, ServletException;
// 获取请求中所有文件上传部件
Collection<Part> getParts() throws IOException, ServletException;
// 根据请求头名获取值
String getHeader(String name);
// 获取所有请求头名的枚举集合
Enumeration<String> getHeaderNames();

3.2.6 HttpServletResponse接口

HttpServletResponse接口继承了ServletResponse接口,并进一步实现了HTTP专属的响应信息。

每次请求时创建,请求结束时销毁。

新增的常用方法:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 添加Cookie
void addCookie(Cookie cookie);
// 获取响应状态码
int getStatus();
// 设置响应状态码
void setStatus(int sc);
// 发送错误响应,包括状态码和原因短语
void sendError(int sc, String msg) throws IOException;
// 发送错误响应,使用默认原因短语
void sendError(int sc) throws IOException;
// 触发客户端重定向
void sendRedirect(String location) throws IOException;
// 设置响应头
void setHeader(String name, String value);
// 添加响应头
void addHeader(String name, String value);
// 根据响应头名获取值
String getHeader(String name);
// 根据响应头名获取所有值
Collection<String> getHeaders(String name);
// 获取所有响应头名的列表集合
Collection<String> getHeaderNames();

3.2.7 RequestDispatcher接口

RequestDispatcher接口用于将请求资源交由指定路径处理。

获取方式:

java
1
2
3
4
// 通过ServletContext获取,只能传入绝对路径
RequestDispatcher rd = context.getRequestDispatcher("/target");
// 通过ServletRequest获取,可以传入相对路径和绝对路径
RequestDispatcher rd = request.getRequestDispatcher("target");

常用方法:

java
1
2
3
4
// 请求转发,将当前请求转发指定转发器处理
void forward(ServletRequest, ServletResponse);
// 请求包含,在当前请求包含指定转发器处理的结果
void include(ServletRequest, ServletResponse);

4 配置文件

在Web应用根目录下创建WEB-INF目录,在目录中创建web.xml文件,用于声明Web应用的组件规则和全局配置,以及运行时行为。

4.1 全局参数

配置全局上下文参数,需要通过ServletContext对象获取:

web.xml
1
2
3
4
<context-param>
<param-name>contextParam</param-name>
<param-value>全局参数</param-value>
</context-param>

4.2 欢迎页面

配置欢迎页面,按照顺序查找:

web.xml
1
2
3
4
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>

4.3 错误页面

配置错误页面:

web.xml
1
2
3
4
5
6
7
8
<error-page>
<error-code>404</error-code>
<location>/error/404.html</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/error/500.html</location>
</error-page>

5 配置组件

5.1 基于文件

在配置文件中配置Servlet组件:

web.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<servlet>
<servlet-name>TestServlet</servlet-name>
<servlet-class>com.example.servlet.TestServlet</servlet-class>
<!-- 局部参数 -->
<init-param>
<param-name>initParam</param-name>
<param-value>局部参数</param-value>
</init-param>
<!-- 加载顺序 -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>TestServlet</servlet-name>
<url-pattern>/test</url-pattern>
</servlet-mapping>

5.2 基于注解

使用@WebServlet注解代替在文件中配置Servlet组件:

TestServlet.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@WebServlet(
name = "TestServlet",
urlPatterns = "/test",
initParams = {
@WebInitParam(name = "initParam", value = "局部参数")
},
loadOnStartup = 1
)
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
// 获取全局参数
System.out.println("contextParam: " + getServletContext().getInitParameter("contextParam"));
// 获取局部参数
System.out.println("initParam: " + getServletConfig().getInitParameter("initParam"));
// 获取请求参数
System.out.println("username: " + req.getParameter("username"));
PrintWriter out = res.getWriter();
out.write(req.getParameter("username"));
out.flush();
out.close();
}
}

6 简单使用

目录结构:

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
项目根目录
├── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── servlet
│ │ └── TestServlet.java
│ ├── resources
│ │ └── db.properties
│ └── webapp
│ ├── error
│ │ ├── 404.html
│ │ └── 500.html
│ ├── WEB-INF
│ │ └── web.xml
│ └── index.html
└── pom.xml

导入依赖:

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

配置文件:

web.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?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_3_1.xsd"
version="3.1">

<!-- 全局参数 -->
<context-param>
<param-name>contextParam</param-name>
<param-value>全局参数</param-value>
</context-param>

<!-- 欢迎页 -->
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>

<!-- 错误页面 -->
<error-page>
<error-code>404</error-code>
<location>/error/404.html</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/error/500.html</location>
</error-page>

<!-- 配置Servlet组件 -->
<servlet>
<servlet-name>TestServlet</servlet-name>
<servlet-class>com.example.servlet.TestServlet</servlet-class>
<!-- 局部参数 -->
<init-param>
<param-name>initParam</param-name>
<param-value>局部参数</param-value>
</init-param>
<!-- 加载顺序 -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>TestServlet</servlet-name>
<url-pattern>/test</url-pattern>
</servlet-mapping>

</web-app>

后端代码:

TestServlet.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
// 获取全局参数
System.out.println("contextParam: " + getServletContext().getInitParameter("contextParam"));
// 获取局部参数
System.out.println("initParam: " + getServletConfig().getInitParameter("initParam"));
// 获取请求参数
System.out.println("username: " + req.getParameter("username"));
// 解决中文乱码
res.setContentType("text/html;charset=UTF-8");
// 输出到浏览器
PrintWriter out = res.getWriter();
out.write(req.getParameter("username"));
out.flush();
out.close();
}
}

前端代码:

index.html
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测试</title>
</head>
<body>
<a href="test?username=lisi">测试</a>
</body>
</html>

7 进阶技巧

7.1 请求转发

服务器收到请求后,将请求转发给另一个Servlet处理,然后将响应返回给客户端。

特点:

  • 地址栏没有变化。
  • 属于同一次请求,请求数据可以共享。
  • 可以转发到WEB-INF目录下的资源,不能转发到其他应用中的资源。
  • 路径以/开头,表示相对于当前应用的路径,不需要拼接当前应用。

转发请求:

TestServlet.java
1
2
3
4
5
6
7
8
9
10
@WebServlet("/test")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException {
// 获取请求参数
System.out.println("username: " + req.getParameter("username"));
// 转发请求
req.getRequestDispatcher("/testForward").forward(req, res);
}
}

处理转发:

TestForwardServlet.java
1
2
3
4
5
6
7
8
9
10
@WebServlet("/testForward")
public class TestForwardServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
// 获取请求参数
System.out.println("username: " + req.getParameter("username"));
// 处理转发
res.getWriter().println(req.getParameter("username"));
}
}

7.2 响应重定向

服务器收到请求后,将响应状态码设置为302,并将Location头设置为新的URL,客户端收到响应后会自动向新的URL发送请求。

特点:

  • 地址栏会发生变化。
  • 属于两次请求,请求数据不能共享。
  • 不能重定向到WEB-INF目录下的资源,可以重定向到其他应用中的资源。
  • 路径以/开头,表示相对于当前服务器的路径,需要拼接当前应用。

重定向响应:

TestServlet.java
1
2
3
4
5
6
7
8
9
10
@WebServlet("/test")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
// 获取请求参数
System.out.println("username: " + req.getParameter("username"));
// 重定向响应
res.sendRedirect("testRedirect");
}
}

处理重定向:

TestRedirectServlet.java
1
2
3
4
5
6
7
8
9
10
@WebServlet("/testRedirect")
public class TestRedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
// 获取请求参数
System.out.println("username: " + req.getParameter("username"));
// 处理重定向
res.getWriter().println(req.getParameter("username"));
}
}

评论