Filter
Filter 过滤器它是 JavaWeb 的三大组件之一
三大组件分别是:Servlet 程序、Listener 监听器、Filter 过滤器
Filter 过滤器它是 JavaEE 的规范。也就是接口
Filter 过滤器它的作用是:拦截请求,过滤响应
拦截请求常见的应用场景有:
- 权限检查
- 日记操作
- 事务管理
等等………
Filter 初体验
首先创建一个 Java 类,实现 Filter 接口,可以看到实现的方法:
public class AdminFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
}
@Override
public void destroy() {
}
}
使用之前需要去 web.xml 文件配置访问路径:
<!-- filter 标签用于配置 Filter 过滤器 -->
<filter>
<!-- Filter 过滤器的别名 -->
<filter-name>AdminFilter</filter-name>
<!-- Filter 过滤器的全类名 -->
<filter-class>work.jkfx.filter.AdminFilter</filter-class>
</filter>
<!-- filter-mapping 用于配置 Filter 过滤器的拦截路径 -->
<filter-mapping>
<!-- 指定 Filter 过滤器的别名 -->
<filter-name>AdminFilter</filter-name>
<!-- 指定 Filter 过滤器的拦截路径
/ 斜杠表示:http://ip:port/工程路径/
/admin/* 表示:http://ip:port/工程路径/admin/所有文件
此资源路径会先经过 Filter 过滤器
-->
<url-pattern>/admin/*</url-pattern>
</filter-mapping>
每次访问 /admin 目录下的任何文件,都会先经过 AdminFilter 过滤器检查:
/*
doFilter 方法专门用于拦截请求,可做权限检查
*/
HttpServletRequest req = (HttpServletRequest) request;
Object user = req.getSession().getAttribute("user");
if(user == null) {
// 假设用户还没有登录 请求转发到登录界面
req.getRequestDispatcher("/login.jsp").forward(request, response);
} else {
// 假设用户已经登录 放行
chain.doFilter(request, response);
}
Filter 生命周期
- 构造器方法
- init 初始化方法
- doFilter 过滤方法
- destroy 销毁方法
第 1、2 步在 web 工程启动时执行
第 3 步在拦截到请求时执行
第 4 步在 web 工程停止时执行
public class FirstFilter implements Filter {
public FirstFilter() {
System.out.println("FirstFilter()");
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init()");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("doFilter()");
}
@Override
public void destroy() {
System.out.println("destroy()");
}
}
FilterConfig 类
FilterConfig 接口的定义:
public interface FilterConfig {
public String getFilterName();
public ServletContext getServletContext();
public String getInitParameter(String name);
public Enumeration<String> getInitParameterNames();
}
FilterConfig 类见名知义,它是 Filter 过滤器的配置文件类
Tomcat 每次创建 Filter 的时候,也会同时创建一个 FilterConfig 类,这里包含了 Filter 配置文件的配置信息
FilterConfig 类的作用是获取 filter 过滤器的配置内容:
- 获取 Filter 的名称 filter-name 的内容
- 获取在 Filter 中配置的 init-param 初始化参数
- 获取 ServletContext 对象
<filter>
<filter-name>FirstFilter</filter-name>
<filter-class>work.jkfx.filter.FirstFilter</filter-class>
<init-param>
<param-name>key1</param-name>
<param-value>value1</param-value>
</init-param>
<init-param>
<param-name>key2</param-name>
<param-value>value2</param-value>
</init-param>
</filter>
public void init(FilterConfig filterConfig) throws ServletException {
String filterName = filterConfig.getFilterName();
System.out.println("filter-name = " + filterName);
String key1 = filterConfig.getInitParameter("key1");
System.out.println("key1 = " + key1);
String key2 = filterConfig.getInitParameter("key2");
System.out.println("key2 = " + key2);
ServletContext servletContext = filterConfig.getServletContext();
System.out.println("servletContext = " + servletContext);
}
FilterChain 过滤器链
FilterChain 就是过滤器链(多个过滤器如何一起工作)
现有一个 jsp 页面:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>目标jsp文件</title>
</head>
<body>
<%
System.out.println("target.jsp 线程:" + Thread.currentThread().getName());
System.out.println("target.jsp 文件的代码");
%>
</body>
</html>
现有两个 Filter 类:Filter1、Filter2
/**
* Filter1 过滤器
* @author geekfx
* @create 2020-05-03 12:47
*/
public class Filter1 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Filter1 的前置代码");
chain.doFilter(request, response);
System.out.println("Filter1 的后置代码");
}
@Override
public void destroy() {
}
}
/**
* Filter2 过滤器
* @author geekfx
* @create 2020-05-03 12:47
*/
public class Filter2 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Filter2 的前置代码");
chain.doFilter(request, response);
System.out.println("Filter2 的后置代码");
}
@Override
public void destroy() {
}
}
这两个 Filter 过滤器在 web.xml 配置顺序:
<filter>
<filter-name>Filter1</filter-name>
<filter-class>work.jkfx.filter.Filter1</filter-class>
</filter>
<filter-mapping>
<filter-name>Filter1</filter-name>
<url-pattern>/target.jsp</url-pattern>
</filter-mapping>
<filter>
<filter-name>Filter2</filter-name>
<filter-class>work.jkfx.filter.Filter2</filter-class>
</filter>
<filter-mapping>
<filter-name>Filter2</filter-name>
<url-pattern>/target.jsp</url-pattern>
</filter-mapping>
启动服务器,在浏览器访问 /target.jsp 文件,看到控制台输出:
可以看到各个代码的执行顺序,如果将 Filter2 过滤器的 doFilter
方法注释掉,再次访问 target.jsp 文件,看到控制台输出:
将在 Filter2 的方法中断开不访问目标资源,直接原路返回
若将 Filter1 的 doFilter
方法注释掉,将直接原路返回,Filter2 过滤器也不会被执行:
Filter1 和 Filter2 的执行顺序是它们在 web.xml 配置文件中从上到下的配置顺序
Filter 过滤器的特点:
- 所有的 Filter 和目标资源默认都在同一个线程中
- 多个 Filter 过滤器共同执行时候,他们使用同一个 Request 对象
在两个 Filter 过滤器的 doFilter
方法中加入两行代码:
// Filter1 过滤器的 doFilter 方法
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Filter1 线程:" + Thread.currentThread().getName());
System.out.println("Filter1 Request 对象:" + request);
System.out.println("Filter1 的前置代码");
chain.doFilter(request, response);
System.out.println("Filter1 的后置代码");
}
// Filter2 过滤器的 doFilter 方法
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Filter2 线程:" + Thread.currentThread().getName());
System.out.println("Filter2 Request 对象:" + request);
System.out.println("Filter2 的前置代码");
chain.doFilter(request, response);
System.out.println("Filter2 的后置代码");
}
<%
System.out.println("target.jsp 线程:" + Thread.currentThread().getName());
System.out.println("target.jsp Request 对象:" + request);
System.out.println("target.jsp 文件的代码");
%>
启动服务器访问目标资源:
Filter 的拦截路径
- 精确匹配
<url-pattern>/target.jsp</url-pattern>
以上配置的路径,表示请求地址必须为:http://ip:port/工程路径/target.jsp
才会拦截 - 目录匹配
<url-pattern>/admin/*</url-pattern>
以上配置的路径,表示请求地址必须为:http://ip:port/工程路径/admin/
文件夹下的资源才会拦截 - 后缀名匹配
<url-pattern>*.jpg</url-pattern>
以上配置的路径,表示请求地址必须为.jpg
结尾的资源才会拦截
Filter 过滤器只在乎请求地址是否匹配,不在乎请求资源是否存在
ThreadLocal
ThreadLocal 的作用,它可以解决多线程的数据安全问题
ThreadLocal 它可以给当前线程关联一个数据(可以是普通变量,可以是对象,也可以是数组,集合)
ThreadLocal 的特点:
- ThreadLocal 可以为当前线程关联一个数据(它可以像 Map 一样存取数据,key 为当前线程)
- 每一个 ThreadLocal 对象,只能为当前线程关联一个数据,如果要为当前线程关联多个数据,就需要使用多个 ThreadLocal 对象实例
- 每个 ThreadLocal 对象实例定义的时候,一般都是 static 类型
- ThreadLocal 中保存数据,在线程销毁后。会由 JVM 虚拟自动释放
/**
* 模拟 ThreadLocal 的使用 使用线程安全的 Map 集合
* @author geekfx
* @create 2020-05-03 19:54
*/
public class ThreadLocalTest {
private static Map<String, Integer> data = new Hashtable<>();
private static Random random = new Random();
private static class Task implements Runnable {
@Override
public void run() {
// 在 run 方法中 随机生成一个随机数(线程要关联的数据) 以当前线程的 name 为 key 保存到 map 中
int i = random.nextInt(100);
// 获取当前线程名
String name = Thread.currentThread().getName();
System.out.println("线程[" + name + "]生成的随机数:" + i);
data.put(name, i);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 在 run 方法结束之前 以当前线程名为 key 取出 value 并打印
Object o = data.get(name);
System.out.println("线程[" + name + "]结束前获取的 value = " + o);
}
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(new Task()).start();
}
}
}
再来看一下 ThreadLocal 类的使用:
现有两个类:
/**
* @author geekfx
* @create 2020-05-03 21:14
*/
public class OrderTest {
public void test() {
String name = Thread.currentThread().getName();
Integer integer = ThreadLocalTest.threadLocal.get();
System.out.println("OrderTest 类下的线程[" + name + "]获取到的数据:" + integer);
}
}
/**
* @author geekfx
* @create 2020-05-03 21:17
*/
public class DaoTest {
public void test() {
String name = Thread.currentThread().getName();
Integer integer = ThreadLocalTest.threadLocal.get();
System.out.println("DaoTest 类的线程[" + name + "]获取到的数据:" + integer);
}
}
在 ThreadLocal 类下将刚刚的 Map 改成 ThreadLocal 对象:
/**
* @author geekfx
* @create 2020-05-03 19:54
*/
public class ThreadLocalTest {
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
private static Random random = new Random();
private static class Task implements Runnable {
@Override
public void run() {
// 在 run 方法中 随机生成一个随机数(线程要关联的数据)
int i = random.nextInt(100);
// 获取当前线程名
String name = Thread.currentThread().getName();
System.out.println("线程[" + name + "]生成的随机数:" + i);
threadLocal.set(i);
new OrderTest().test();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new DaoTest().test();
Integer o = threadLocal.get();
System.out.println("线程[" + name + "]结束前获取的 value = " + o);
}
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(new Task()).start();
}
}
}
执行 main 方法,查看运行结果:
线程[Thread-0]生成的随机数:96
线程[Thread-2]生成的随机数:27
线程[Thread-1]生成的随机数:54
OrderTest 类下的线程[Thread-0]获取到的数据:96
OrderTest 类下的线程[Thread-1]获取到的数据:54
OrderTest 类下的线程[Thread-2]获取到的数据:27
DaoTest 类的线程[Thread-0]获取到的数据:96
线程[Thread-0]结束前获取的 value = 96
DaoTest 类的线程[Thread-2]获取到的数据:27
线程[Thread-2]结束前获取的 value = 27
DaoTest 类的线程[Thread-1]获取到的数据:54
线程[Thread-1]结束前获取的 value = 54
可以发现使用 ThreadLocal 可以将一个线程和一个数据关联起来,不管是哪个类或哪个方法,只要是同一个线程,就可以获取到关联的数据
Filter 结合 ThreadLocal 实现事务管理
使用 ThreadLocal 重构 JdbcUtils 的 getConnection 和 close 的代码
如果想实现事务的管理,需要为每一个 Service 的每一个方法都加上 try-catch 捕获异常然后回滚事务
如果有 1 万个业务功能就会有 1 万条重复的代码
不要造重复的轮子
可以使用 Filter 实现将所有请求捕获 然后执行 doFilter 方法间接的执行了 Service 的方法
在 Filter 的 doFilter 中捕获异常处理提交事务或者回滚事务
相当于给所有的 Service 加上了事务的管理
/**
* 通过 Filter 结合 ThreadLocal 实现对事务的管理
* @author geekfx
* @create 2020-05-03 22:17
*/
public class TransactionFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
// 所有的请求都会被这个 Filter 拦截
// 直接放行 相当于间接的调用了 Service 层
chain.doFilter(request, response);
// 正常运行后提交下事务
JdbcUtils.commitAndClose();
} catch (Exception e) {
// 一旦出现了错误 就回滚事务
JdbcUtils.rollbackAndClose();
e.printStackTrace();
// 将错误抛给 Tomcat 服务器 由 Tomcat 统一处理
throw RuntimeException(e);
}
}
@Override
public void destroy() {
}
}
配置 TransactionFilter 的拦截地址:
<filter>
<filter-name>TransactionFilter</filter-name>
<filter-class>work.jkfx.filter.TransactionFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>TransactionFilter</filter-name>
<!-- 表示所有的请求都被这个 Filter 拦截 统一处理 -->
<url-pattern>/*</url-pattern>
</filter-mapping>
Tomcat 处理错误
将所有的错误交给 Tomcat 服务器并配置 web.xml 文件
说明出现了哪种错误跳转到哪个页面
<!-- error-page 标签用于配置出现错误时的处理 -->
<error-page>
<!-- error-code 表示出错的响应码 -->
<error-code>404</error-code>
<!-- location 标签表示出现了对应的响应码错误时跳转的页面 -->
<location>/pages/error/error404.jsp</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/pages/error/error500.jsp</location>
</error-page>