什么是 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 接口分类
Servlet2.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