什么是 Servlet?
Servlet(Server Applet),全称 Java Servlet,未有中文译文。是用 Java 编写的服务器端程序。其主要功能在于交互式地浏览和修改数据,生成动态Web内容。狭义的 Servlet 是指 Java 语言实现的一个接口,广义的 Servlet 是指任何实现了这个 Servlet 接口的类),一般情况下,人们将 Servlet 理解为后者
简单来讲,Servlet
就是运行在服务端的小程序,用来将客户端发送过来的 HTTP 请求解析封装进相关的Servlet
对象中,当开发者对该对象的数据进行处理后又封装到另一个Servlet
对象中,转换成 HTTP 响应返回给客户端。
认识 Servlet
Servlet 继承树
如图所示:
对各个接口/类做个简单说明:
Servlet
:接口,定义了一些规则;Serializable
:序列化接口,没啥好说的;ServletConfig
:接口,定义了在Servlet
初始化的过程中由Servlet
容器传递给Servlet
的配置信息对象GenericServlet
:抽象类,定义一个通用的、独立于底层协议的Servlet
。它给出了设计Servlet
的一些骨架,如定义了Servlet
的生命周期,还有一些得到名字、配置、初始化参数的方法,其设计的是和应用层协议无关的;HttpServlet
:抽象类子类,具有GenericServlet
的一切特性,还添加了doGet
,doPost
,doDelete
,doPut
,doTrace
等方法对应处理Http协议里的命令的请求响应过程。
一般没有特殊需要,自己写的Servlet
都扩展HttpServlet
。大多数Servlet
都会通过从GenericServlet
或HttpServlet
类进行扩展来实现。
Servlet 接口
从前面的继承树可以看出,Servlet
接口是顶层父类,它定义了一些规则,什么规则呢?
具体来说,Servlet
接口定义了其实现类在客户端访问服务器时触发的相关规则,这些规则在其源码中得到了体现:
1 | public interface Servlet { |
快速入门 Demo
下面通过一个简单的demo来认识Servlet
的执行原理。
① 创建接口实现类
要想定义一个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
31package cn.wk.web.servlet;
import javax.servlet.*;
import java.io.IOException;
public class ServletDemo implements Servlet {
public void init(ServletConfig config) throws ServletException {
System.out.println("init ...");
}
public ServletConfig getServletConfig() {
return null;
}
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
System.out.println("service ...");
}
public String getServletInfo() {
return null;
}
public void destroy() {
System.out.println("destroy ...");
}
}
② 配置 web.xml 文件
之后在web.xml
配置接口实现类的全限定包名和映射路径:1
2
3
4
5
6
7
8<servlet>
<servlet-name>ServletDemo</servlet-name>
<servlet-class>cn.wk.basicjava.web.servlet.ServletDemo</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ServletDemo</servlet-name>
<url-pattern>/servletDemo</url-pattern>
</servlet-mapping>
当 Tomcat 服务器启动后,会将全限定类名对应的字节码文件通过反射Class.forName()
方式加载进内存,之后通过class.newInstance()
方法会创建对象并调用service
方法。
③ 测试
当我们启动服务器,并通过 8080 端口访问ServletDemo
的映射路径/servletDemo
,控制台将输出:1
2init ...
service ...
当关闭服务器时,控制台输出:1
destroy ...
Servlet 执行原理
上面的 demo 间接说明了Servlet
的执行原理:
- 当服务器接收到了客户端的请求后,会解析请求的 URL 路径,获取访问的
Servlet
的资源路径; - 在
web.xml
文件查找对应的<url-pattern>
标签内容; - 查找到则去找对应的
<servlet-class>
全限定类名; - 服务器将全限定类名的字节码文件加载进内存并创建对象
- 调用相关方法:
init
(第一次访问时执行)service
(每次访问时都执行)destroy
(销毁时执行)
Servlet 生命周期
每个Servlet
都有自己的生命周期,生命周期由 Web 服务器来维护。
Servlet
的生命周期可以分为三个阶段:
- 初始化阶段:加载并创建
Servlet
对象和ServletConfig
对象,之后执行init
方法初始化Servlet
资源,该方法只执行一次; - 运行阶段:每一次
Servlet
被访问时会执行service
方法,根据请求决定调用doGet
或doPost
方法,构造ServletRequest
和ServeltResponse
对象; - 销毁阶段:销毁
Servlet
实例时,自动执行destroy
方法回收资源。
客户端每一次请求来到容器时,都会产生HttpServletRequest
与HttpServletResponse对象
,它们在调用Servlet
的service
方法时被作为参数传入进去。
当 Web 容器启动后,会读取Servlet
设置的信息,将Servlet
类加载并实例化,并为每个Servlet
设置信息产生一个ServletConfig
对象,而后调用Servlet
接口的init
方法,并将产生的ServletConfig
对象作为参数传入。
问与答
问
:Servlet
什么时候被创建?答
:默认情况下,Servlet
第一次被访问时创建,可以在web.xml
自定义配置Servlet
的创建时间:在<servlet>
标签中配置其<load-on-startup>
元素:
- 标记容器是否在启动的时候就加载这个
Servlet
(实例化并调用其init
方法); - 当值为
0
或者大于0
时,表示容器在应用启动时就加载并初始化这个Servlet
; - 当值小于
0
或者没有指定时(默认),则表示容器在该Servlet
被选择时才会去加载; - 正数的值越小,该
Servlet
的优先级越高,应用启动时就越先加载。
问
:Servlet
的init
方法只执行一次,说明了什么?答
:说明了Servlet
在内存中只存在一个对象,所以它应该是单例的。多个用户同时访问时可能有线程安全问题,所以尽量不要在Servlet
中定义成员变量,若定义了则不要修改它。
HTTP 协议相关
本文不介绍 HTTP 协议相关内容,具体可以参考《图解 HTTP》
请求与响应
请求与响应一般都是通过HttpServletRequest
和HttpServletResponse
接口来实现的。这些接口的实现类一般由服务器(如 Tomcat)来实现。
比如浏览器访问服务器时, Tomcat 会创建request
和response
对象分别用于封装请求数据和响应数据,具体过程如下图:
Request 请求对象
Request 请求对象封装了客户端的请求消息,通过其相关 API 可以到获取其中的数据:
- 获取请求行数据:
String getMethod()
:获取请求方式,如 GET、POST、PUT 等String getContextPath()
:获取虚拟目录(Tomcat 发布路径)(常用)ServletContext getServletContext();
:获取当前 Web 应用String getServletPath()
:获取Servlet
路径;String getQueryString()
:获取 GET 方式请求参数;String getRequestURI()
:获取请求 URI(常用)String getProtocol()
:获取协议及版本;String getRemoteAddr()
:获取客户机的 IP 地址
- 获取请求头数据:
String getHeader(String name)
:通过请求头的名称获取请求头的值Enumeration<String> getHeaderNames()
:获取所有的请求头名称;
- 获取请求体数据(请求体中封装了 POST 请求的请求参数):
BufferedReader getHeader()
:获取字符输入流,只能操作字符数据;ServletInputStream getInputStream()
:获取字节输入流,可以操作所有类型数据,一般用于文件上传。
- 获取参数:
String getParameter(String name)
:根据参数名称获取参数值String[] getParameterValues(String name)
:根据参数名称获取参数值的数组Enumeration<String> getParameterNames()
:获取所有请求的参数名称Map<String,String[]> getParameterMap()
:获取所有参数的Map集合
该对象还有其他一些重要的功能:
- 请求转发
- 域对象,能共享数据
- 能获取到代表整个 Web 应用的
ServletContext
对象
请求转发:Redirect
请求转发是通过RequestDispatcher
对象的forward(req,res)
方法来实现的,该对象可以通过HttpServletRequest
对象的getRequestDispatcher(String path)
方法得到,下面是一个请求转发的例子:1
2
3
4
5
6
7
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getRequestDispatcher("/anotherServlet").forward(req, resp);
// 相当于下面 2 句
// RequestDispatcher requestDispatcher = req.getRequestDispatcher("/anotherServlet");
// requestDispatcher.forward(req, resp);
}
特点
请求转发的特点如下:
- 请求转发是一次
request
请求,可以使用request
对象来共享数据, - URL 不会改变,由服务器内部实现来跳转,客户端不知道
- 只能访问当前服务器下的资源
- 可以使用相对路径(推荐)或绝对路径
Response 响应对象
Response 响应对象同样也封装了程序员设置的响应消息,其相关 API:
- 设置响应行状态码:
setStatus(int sc)
; - 设置响应头:
setHeader(String name,String value)
; - 设置响应体:需要获取输入流再使用,这里的输入流包括:
PrintWriter getWriter()
:字符输出流ServletOutputStream getOutputStream()
:字节输出流
该还有重定向的功能:
sendRedirect(String location)
:设置重定向的路径
重定向:Forward
重定向是通过服务器返回的状态码来实现的。
客户端浏览器请求服务器的时候,服务器会返回一个状态码。服务器通过HttpServletResponse
的setStatus(int sc)
方法设置状态码。如果服务器返回301
或302
,则浏览器会到新的网站重新请求该资源。
下面为一个重定向例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 设置状态码
resp.setStatus(302);
// 设置响应头
resp.setHeader("location","/DemoProject/anotherServlet");
// 简写形式,相当于上面两句
resp.sendRedirect("/DemoProject/anotherServlet");
// 但建议这么写
resp.sendRedirect(req.getContextPath() + "/anotherServlet");
// 上面一句相当于下面两句
String contextPath = req.getContextPath();// 获取虚拟路径
resp.sendRedirect(contextPath + "/anotherServlet");
}
特点
重定向的特点如下:
- 重定向是两次请求,不能使用
request
对象共享数据 - URL 发生变化,客户端知道服务器发发生了跳转
- 重定向可以访问其他服务器的资源
- 必须写绝对路径(推荐动态获取虚拟目录(项目的访问路径))
请求转发与重定向理解
转发相当于以前坐汽车,由于司机开车到目的地,到中转站换司机,但你却知道你是直达到家。中转时票不作废,就算有人在你票上涂涂画画也没事,票还是那张票(就比如你删除 的session 数据,你知道你删除了什么 )。
重定向相当于坐高铁,到中转站了需要你自己去换乘。由于车次不同,旧票作废(这里假设扔掉),你现在只有新票的数据。
自动刷新:Refresh
自动刷新不仅可以在一段时间之后从本页面自动跳转到另一个页面,还可以实现一段时间之后自动刷新本页面的效果。
Servlet
中通过HttpServletResponse
对象的setHeader
方法实现自动刷新效果,比如:1
2
3
4
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setHeader("Refresh","2; URL=https://www.google.com/");
}
其中,2
为时间,单位秒,URL 参数指定的网站就是2
秒钟之后跳转的页面,当URL 设置的路径为Servlet
自己的路径时,就会每隔2
秒钟自动刷新本页面一次。
注意哦
:自动刷新与重定向原理是差不多的,如果把时间设置为0
,把 URL 设为另一个网址,效果就是重定向。
ServletContext对象
ServletContext
对象代表整个 Web 应用,能和程序的容器(服务器)进行通信。
我们可以通过request
对象或HttpServlet
的getServletContext
方法获取到它,ServletContext
对象的功能包括:
- 获取MIME数据类型
- 域对象,能共享数据
- 获取文件的真实路径
文件上传/下载
文件上传
一般情况下,多数文件上传都是通过表单形式提交给后台服务器的,因此,要实现文件上传功能,就需要提供一个文件上传的表单,而该表单必须满足下面 3 个条件:
form
表单的method
属性设置为post
;form
表单的enctype
属性设置为multipart/form-data
;- 提供
<input type="file" name="filename"
的文件上传输入框。
① 编写文件上传表单
既然需要表单,那么我们就编写一个文件上传的表单即可: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<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>图片上传界面</title>
</head>
<body>
<form method="post" enctype="multipart/form-data" action="${pageContext.request.contextPath}/upload">
<input type="text" name="username" value="请输入你的用户名"></br>
<input type="file" name="files" multiple="multiple"></br>
<input type="submit" value="上传图片">
</form>
</body>
</html>
//上传结果页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>上传结果</title>
</head>
<body>
<div style="align-content: center">
${message}
</div>
</body>
</html>
② 加入文件上传工具类依赖
由于上传文件时浏览器是以二进制的方式发送数据,因此Servlet
里不能简单的通过HttpServletRequest
的getParameter
方法来获取文件域及文本域的内容。要想获取其中的内容,必须根据 Http 协议规定的格式解析浏览器提交的请求。
解析二进制数据流比较麻烦,因此我们直接通过别人写好的工具类完成这项工作就好,Apache的Commons Fileupload
就是一个不错的文件上传工具类,在pom.xml
加入下面 2 个依赖即可:1
2
3
4
5
6
7
8
9
10<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
③ 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
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
90package cn.wk.web;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.List;
"/upload") (
public class uploadServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
// 上传文件存储目录
private static final String UPLOAD_DIRECTORY = "upload";
// 上传配置
private static final int MEMORY_THRESHOLD = 1024 * 1024 * 3; // 3MB
private static final int MAX_FILE_SIZE = 1024 * 1024 * 40; // 40MB
private static final int MAX_REQUEST_SIZE = 1024 * 1024 * 50; // 50MB
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 配置上传参数
DiskFileItemFactory factory = new DiskFileItemFactory();
// 设置内存临界值 - 超过后将产生临时文件并存储于临时目录中
factory.setSizeThreshold(MEMORY_THRESHOLD);
// 设置临时存储目录
factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
ServletFileUpload upload = new ServletFileUpload(factory);
// 设置最大文件上传值
upload.setFileSizeMax(MAX_FILE_SIZE);
// 设置最大请求值 (包含文件和表单数据)
upload.setSizeMax(MAX_REQUEST_SIZE);
// 中文处理
upload.setHeaderEncoding("UTF-8");
// 构造临时路径来存储上传的文件
// 这个路径相对当前应用的目录
String uploadPath = req.getServletContext().getRealPath("./") + File.separator + UPLOAD_DIRECTORY;
// 如果目录不存在则创建
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdir();
}
try {
// 解析请求的内容提取文件数据
List<FileItem> formItems = upload.parseRequest(req);
if (formItems != null && formItems.size() > 0) {
// 迭代表单数据
for (FileItem item : formItems) {
// 处理不在表单中的字段
if (!item.isFormField()) {
String fileName = new File(item.getName()).getName();
String filePath = uploadPath + File.separator + fileName;
File storeFile = new File(filePath);
// 在控制台输出文件的上传路径
System.out.println(filePath);
// 保存文件到硬盘
item.write(storeFile);
req.setAttribute("message", "文件上传成功!");
}
}
}
} catch (Exception ex) {
req.setAttribute("message", "文件上传失败");
}
// 跳转到 uploadResult.jsp
req.getServletContext().getRequestDispatcher("/uploadResult.jsp").forward(req, resp);
}
}
文件下载
文件下载较为简单,使用流即可,略
会话跟踪
HTTP 是无状态协议,它不对之前发生过的请求和响应的状态进行管理。也就是说,无法根据之前的状态进行本次的请求管理,服务器不知道用户上一次做了什么,这严重阻碍了交互式 Web 应用程序的实现。
在典型的网上购物场景中,用户浏览了几个页面买了件衣服。最后结帐时,由于HTTP的无状态性,不通过额外的手段,服务器并不知道用户到底买了什么,
不可否认,无状态协议有它的优点,由于不必保存状态,自然可以减少服务器的压力。那我们如何解决用户的购物问题呢?
为此,引入了会话跟踪技术以跟踪用户的整个会话。
此文不详细展开,具体见另一篇文章会话跟踪技术
Servlet 高级特性
过滤器:Filter
什么是 Filter?
Filter
翻译为滤镜或过滤器。
简单来讲:当客户端访问服务器的资源时,过滤器可以将请求拦截下来,完成一些特殊的功能,反之也可以在服务器的响应返回客户端之前拦截响应消息。
复杂点说,Filter
用于在Servlet
之外对request
请求或response
响应进行过滤修改,且还可以使用多个Filter
形成过滤链(FilterChain
)来增强过滤的能力。
如下图,客户端的请求request
在抵达Servlet
之前会经过FilterChain
的每个Filter
,Servlet
处理完成后,服务器的响应response
在从Servlet
抵达客户端之前又会经过FilterChain
的每个Filter
:
快速入门 Demo
① 定义 Filter 接口实现类
首先我们定义一个Filter
接口的实现类并重写其doFilter
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17"/*")//拦截所有请求 (
public class FilterDemo implements Filter {
public void init(FilterConfig config) throws ServletException {
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
System.out.println("Filter is executed");
}
public void destroy() {
}
}
② 配置拦截路径
配置拦截路径可以使用注解的方式(前面使用了)或更改web.xml
文件:1
2
3
4
5
6
7
8<filter>
<filter-name>FilterDemo</filter-name>
<filter-class>cn.wk.basicjava.web.filter.FilterDemo</filter-class>
</filter>
<filter-mapping>
<filter-name>FilterDemo</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
我们通过/*
方式拦截了所有的请求,也就是说所有的请求都会经过指定的Filter
。
③ 测试
当我们启动 Tomcat 后,访问一个Servlet
或 JSP 页面控制台会输出:1
2Filter is executed
Filter is executed
注意哦
:打印结果输出多次的原因是在doFilter
方法中没有通过chain
的doFilter(req, resp);
方法放行。
Filter 接口
一个Filter
必须实现javax.servlet.Filter
接口,该接口包含 3 个方法,源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public interface Filter {
/**
* Web应用启动时调用该方法,用于初始化该Filter
* @param filterConfig 可以从该参数中获取初始化参数及ServletContext信息等
*/
public void init(FilterConfig filterConfig) throws ServletException;
/**
* 客户端请求服务器时会经过该方法
* @param request 客户端请求
* @param response 服务器响应
* @param chain 过滤链,通过chain.doFilter(request,response)将请求传给
* 下个Filter或Servlet
*/
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;
/**
* Web应用关闭时调用次方法,销毁一些资源
*/
public void destroy();
}
Filter 生命周期
Filter
接口的 3 个方法直接反映了其生命周期:
init()
:Web 应用启动时调用该方法,用于初始化该Filter
,只执行一次doFilter()
:每次有客户端请求时调用该方法destroy()
:Web 应用关闭时调用该方法,用于销毁该Filter
回收资源,只执行一次
Filter的配置
Filter
的配置可以在实现类上通过注解方式简写,或者也可以标准一点在web.xml
配置,下面为一个例子:1
2
3
4
5
6
7
8
9
10
11<filter>
<filter-name>FilterName</filter-name>
<filter-class>全限定类名路径</filter-class>
</filter>
<filter-mapping>
<filter-name>FilterName</filter-name>
<url-pattern>/jsp/*</url-pattern>
<url-pattern>*.html</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
参数介绍:
<filter?
:配置filter
名称(<filter-name>
),实现类路径(<filter-class>
)及初始化参数(若有)<filter-mapping>
:配置什么规则下使用该<filter>
,其<filter-name>
必须和<filter>
的<filter-name>
匹配;<url-pattern>
配置URL的规则,可以配置多个;<dispatcher>
配置到达Servlet
的方式:REQUEST
:默认值,表示仅当当前请求Serlvet
时才生效;FORWARD
:转发资源,表示仅当某Serlvet
通过FORWARD
到达该Serlvet
时才生效;INCLUDE
:包含访问资源,JSP中可以通过<jsp:include/>
请求某Serlvet
;ERROR
:错误跳转资源,JSP中可以通过<%@page errorPage="error.jsp" %>
指定错误处理页面;ASYNC
:异步访问资源
注意哦
:<url-pattern>
与<dispatcher>
是“且”的关系,两者同时成立时该Filter
生效。
应用场景
Filter
的应用场景很多,如:
- 防盗链
- 字符编码
- 敏感词
- 权限验证
- 图像水印
- 日志记录
- 异常捕获
- 内容替换
- GZIP 压缩
- 缓存
- 文件上传
下面只介绍几个常用的需要了解的。
字符编码
字符编码Filter
非常常用,能用来解决 Tomcat 等服务器的request
、response
乱码的问题。
字符编码Filter
可以在request
提交到Servlet
之前对request
进行指定编码。
一般使用别人写好的字符编码Filter
,在web.xml
配置下就好。
防盗链
防盗链Filter
实现了一种效果:如果其他网站引用本网站的图片资源,将会显示一个错误图片。只有本站内的网页引用时,图片才会正常显示。
即在图片显示之前对request
进行验证,看用户请求是否来自本站,其代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22"/images/*")//拦截images目录下所有图片 (
public class FilterDemo implements Filter {
....
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
//链接来源地址
String referer = request.getHeader("Referer");
//来自其他网站
if (referer == null || !referer.contains(request.getServerName())) {
//显示错误图片
request.getRequestDispatcher("/error.gif").forward(request, response);
} else {
//显示正常图片
chain.doFilter(request, response);
}
}
....
}
现在没啥子用,别人都直接通过爬虫爬取数据了。。。
权限验证
Shiro安全框架就用到了Filter
来完成权限验证。
监听器:Listener
什么是 Listener?
Listener
用于监听 Java Web 程序中的事件,如创建、修改、删除Session
、request
、context
等,并触发相应的事件。
利用Listener
能用很少的代码实现很华丽的效果。
快速入门 Demo
使用Listener
需要实现相应的Listener
接口,应该触发Listener
事件的时候,Tomcat 自动调用Listener
的方法。
① 定义 Listener 接口实现类
首先创建一个ServletContextListener
接口的实现类,可以监听到服务器ServletContext
对象的创建与销毁:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class ListenerDemo implements ServletContextListener {
/**
* 监听到ServletContext创建则执行
*
* @param sce
*/
public void contextInitialized(ServletContextEvent sce) {
System.out.println("init ServletContext.....");
}
/**
* 监听到ServletContext销毁则执行
*
* @param sce
*/
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("destroy ServletContext.....");
}
}
② 配置 web.xml
Listener
需要在web.xml
配置才能生效:1
2
3<listener>
<listener-class>cn.wk.web.listener.ListenerDemo</listener-class>
</listener>
③ 测试
启动服务器和关闭服务器时,控制台分别打印:1
2init ServletContext.....
destroy ServletContext.....
Listener 接口分类
Servlet
2.5 规范中共有 8 种Listener
,分别用于监听Session
、context
、request
等的创建与销毁、属性变化等:
种类 | 监听 |
---|---|
ServletContextListener | context对象的创建与销毁 |
HttpSessionListener | Session对象的创建与销毁 |
ServletRequestListener | request对象的创建与销毁 |
ServletContextAttributeListener | context对象的属性变化 |
HttpSessionAttributeListener | Session对象的属性变化 |
ServletRequestAttributeListener | request对象的属性变化 |
HttpSessionBindingListener | Session内的对象添加移除 |
HttpSessionActivationListener | Session内的对象的钝化活化 |
Listener 生命周期
根据具体情况具体分析
Listener 配置
Listener
可以直接在其实现类上加上@WebListener
注解简单配置,或者标准一点的话也可以在web.xml
配置:1
2
3<listener>
<listener-class>全限定类名路径</listener-class>
</listener>
注意哦
:一个web.xml
可以配置多个<listener>
,同一类型的也可以配置多个,触发的时候服务器会依次执行各个Listener
的相应方法。
应用场景
Listener
的特性使得它可以做许多事情,如统计在线人数、实现单点登录,而且不会与Servlet
等有任何的耦合。
单点登录
单点登录,即一个帐号只能在一台机器上登录,如果在其他机器上登录了,则原来的登录自动失效。
单点登录的目的是防止多个机器同时使用一个帐号。
下面通过代码来说明单点登录的流程。
① 编写登录的 JSP 页面
首先编写一个登录的JSP页面,可以输入用户名后点击登录(提交给后面的Servlet
):1
2
3
4
5
6
7
8
9
10
11
12
13
14<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>登录页面</title>
</head>
<body>
<div style="text-align: center;margin-top: 10px">
<form action="/login" method="post">
<input type="text" name="username" placeholder="请输入用户名"></br>
<input type="submit" value="点击登录">
</form>
</div>
</body>
</html>
② 编写登录跳转的 Servlet
1 | "/login") ( |
这个Servlet
可以将用户名保存到当前Session
中,之后重定向到index.jsp
页面,该页面代码见下一步。
③ 编写登录后的 JSP 页面
1 | <%@ page contentType="text/html;charset=UTF-8" %> |
这个页面使用到了 JSP 标签,根据当前用户是否存在显示不同的页面,且登录成功后有一个登出的Servlet
。
④ 编写登出的 Servlet
1 | "/logout") ( |
该Servlet
可以清除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
45
46
47
48
49
50
51 ()
public class LoginListener implements HttpSessionAttributeListener {
/**
* 保存不同用户的Session
*/
Map<String, HttpSession> map = new HashMap<>();
/**
* Session通过setAttribute方法添加属性时被调用
*
* @param event
*/
public void attributeAdded(HttpSessionBindingEvent event) {
//获取Session的setAttribute方法增加的属性名
String name = event.getName();
if ("username".equals(name)) {
//若属性名和username相等则获取对应的值
String user = (String) event.getValue();
//判断map中是否含有当前帐号
if (map.get(user) != null) {
//若map中有当前帐号,则说明在其他地方登录过,让以前的登录失效
HttpSession session = map.get(user);
session.removeAttribute("username");
}
//将帐号以用户名为索引添加到map中
map.put(user, event.getSession());
}
}
/**
* Session通过removeAttribute方法移除属性时被调用
*
* @param event
*/
public void attributeRemoved(HttpSessionBindingEvent event) {
//获取Session的setAttribute方法移除的属性名
String name = event.getName();
if ("username".equals(name)) {
//将移除的用户从map中移除
map.remove(name);
}
}
public void attributeReplaced(HttpSessionBindingEvent event) {
}
}
这里使用到了@WebListener()
注解,这样就不需要去web.xml
配置了。该Listener
实现了HttpSessionAttributeListener
接口,可以根据Session
的属性变化来编写一些逻辑操作,简单来讲就是将帐号和Session
相关联,保存成一个Map
,再进行一些操作决定是否将当前Session
的帐号踢下线,具体说明见注释部分。
注意哦
:此监听器和登录的代码时完全解耦的,没有此监听器不影响登录的功能。
⑤ 测试
如图所示,当我启动 Tomcat 服务器后,首先在浏览器登录bai
用户,之后在另一个浏览器(不属于同一次会话)再次登录该用户后,回到原浏览器刷新当前用户则发现已经被踢下线了,需要重新登录了。
参考
- 维基百科
- 刘京华. Java Web 整合开发王者归来 [M]. 清华大学出版社,2010