Servlet 复习笔记

什么是 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都会通过从GenericServletHttpServlet类进行扩展来实现。

Servlet 接口

  从前面的继承树可以看出,Servlet接口是顶层父类,它定义了一些规则,什么规则呢?
  具体来说,Servlet接口定义了其实现类在客户端访问服务器时触发的相关规则,这些规则在其源码中得到了体现:

1
2
3
4
5
6
7
8
9
10
11
12
public interface Servlet {
// 在 Servlet 第一次被访问时,创建 Servlet 并执行该方法;
public void init(ServletConfig config) throws ServletException;
// 获取 Servlet 的配置对象,与 Web 容器通信;
public ServletConfig getServletConfig();
// 每一次 Servlet 被访问时执行该方法
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
// 获取 Servlet 的相关信息,如版本、作者
public String getServletInfo();
// 服务器正常关闭时执行该方法销毁 Servlet
public void destroy();
}

快速入门 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
31
package cn.wk.web.servlet;

import javax.servlet.*;
import java.io.IOException;

public class ServletDemo implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("init ...");
}

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

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

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

@Override
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
2
init ...
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方法,根据请求决定调用doGetdoPost方法,构造ServletRequestServeltResponse对象;
  • 销毁阶段:销毁Servlet实例时,自动执行destroy方法回收资源。

  客户端每一次请求来到容器时,都会产生HttpServletRequestHttpServletResponse对象,它们在调用Servletservice方法时被作为参数传入进去。
  当 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的优先级越高,应用启动时就越先加载。

Servletinit方法只执行一次,说明了什么?
:说明了Servlet在内存中只存在一个对象,所以它应该是单例的。多个用户同时访问时可能有线程安全问题,所以尽量不要在Servlet中定义成员变量,若定义了则不要修改它。

HTTP 协议相关

  本文不介绍 HTTP 协议相关内容,具体可以参考《图解 HTTP》

请求与响应

  请求与响应一般都是通过HttpServletRequestHttpServletResponse接口来实现的。这些接口的实现类一般由服务器(如 Tomcat)来实现。
  比如浏览器访问服务器时, Tomcat 会创建requestresponse对象分别用于封装请求数据和响应数据,具体过程如下图:

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
@Override
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

  重定向是通过服务器返回的状态码来实现的。
  客户端浏览器请求服务器的时候,服务器会返回一个状态码。服务器通过HttpServletResponsesetStatus(int sc)方法设置状态码。如果服务器返回301302,则浏览器会到新的网站重新请求该资源。
  下面为一个重定向例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
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
@Override
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对象或HttpServletgetServletContext方法获取到它,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里不能简单的通过HttpServletRequestgetParameter方法来获取文件域及文本域的内容。要想获取其中的内容,必须根据 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
90
package 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;

@WebServlet("/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

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}

@Override
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的每个FilterServlet处理完成后,服务器的响应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
@WebFilter("/*")//拦截所有请求
public class FilterDemo implements Filter {

@Override
public void init(FilterConfig config) throws ServletException {

}

@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
System.out.println("Filter is executed");
}

@Override
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
2
Filter is executed
Filter is executed

注意哦:打印结果输出多次的原因是在doFilter方法中没有通过chaindoFilter(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
21
public 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 等服务器的requestresponse乱码的问题。
  字符编码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
@WebFilter("/images/*")//拦截images目录下所有图片
public class FilterDemo implements Filter {
....
@Override
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 程序中的事件,如创建、修改、删除Sessionrequestcontext等,并触发相应的事件。
  利用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
21
public class ListenerDemo implements ServletContextListener {
/**
* 监听到ServletContext创建则执行
*
* @param sce
*/
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("init ServletContext.....");
}

/**
* 监听到ServletContext销毁则执行
*
* @param sce
*/
@Override
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
2
init ServletContext.....
destroy ServletContext.....

Listener 接口分类

  Servlet2.5 规范中共有 8 种Listener,分别用于监听Sessioncontextrequest等的创建与销毁、属性变化等:

种类 监听
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/login")
public class Login extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession();
String user= req.getParameter("username");
//将用户存储到session中
session.setAttribute("username", user);
//重定向到index.jsp页面
resp.sendRedirect(req.getContextPath() + "/index.jsp");
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
}

  这个Servlet可以将用户名保存到当前Session中,之后重定向到index.jsp页面,该页面代码见下一步。

③ 编写登录后的 JSP 页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<%@ page contentType="text/html;charset=UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<head>
<title>登录</title>
</head>
<body>
<div style="text-align: center">
<c:choose>
<c:when test="${!empty username }">
<p>你好啊!${username}</p>
<a href="/logout">登出当前用户</a>
</c:when>
<c:otherwise>
<p style="color:red">当前帐号未登录或已在其他地点登录,请重新登录</p>
<a href="/login.jsp">前往登录页面</a>
</c:otherwise>
</c:choose>
</div>
</body>
</html>

  这个页面使用到了 JSP 标签,根据当前用户是否存在显示不同的页面,且登录成功后有一个登出的Servlet

④ 编写登出的 Servlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@WebServlet("/logout")
public class Logout extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession();
session.removeAttribute("username");
resp.sendRedirect(req.getContextPath() + "/login.jsp");
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
}

  该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
@WebListener()
public class LoginListener implements HttpSessionAttributeListener {

/**
* 保存不同用户的Session
*/
Map<String, HttpSession> map = new HashMap<>();

/**
* Session通过setAttribute方法添加属性时被调用
*
* @param event
*/
@Override
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
*/
@Override
public void attributeRemoved(HttpSessionBindingEvent event) {
//获取Session的setAttribute方法移除的属性名
String name = event.getName();
if ("username".equals(name)) {
//将移除的用户从map中移除
map.remove(name);
}
}

@Override
public void attributeReplaced(HttpSessionBindingEvent event) {

}
}

  这里使用到了@WebListener()注解,这样就不需要去web.xml配置了。该Listener实现了HttpSessionAttributeListener接口,可以根据Session的属性变化来编写一些逻辑操作,简单来讲就是将帐号和Session相关联,保存成一个Map,再进行一些操作决定是否将当前Session的帐号踢下线,具体说明见注释部分。

注意哦:此监听器和登录的代码时完全解耦的,没有此监听器不影响登录的功能。

⑤ 测试


  如图所示,当我启动 Tomcat 服务器后,首先在浏览器登录bai用户,之后在另一个浏览器(不属于同一次会话)再次登录该用户后,回到原浏览器刷新当前用户则发现已经被踢下线了,需要重新登录了。

参考

  • 维基百科
  • 刘京华. Java Web 整合开发王者归来 [M]. 清华大学出版社,2010
0%