本节是智客工坊-《Spring Boot 实战纪实》的第16篇,感谢您的阅读,预计阅读时长5min。
通俗的讲,过滤器可以简单理解为“取你所想取”,忽视掉那些你不想要的东西。

生活中有很多"过滤器"的例子。
一台带有过滤功能的饮水机,里面就会有个过滤器,可以将污水中的杂质过滤,从而使进入的污水变成净水。
渔民捕鱼使用的渔网,也有讲究,网眼尺寸要恰到好处,既要捕捞的捕猎到成鱼,又要给给小鱼苗一个逃生的机会。
建筑工地上常见的筛网,通常用于将沙子中细沙过滤...
Spring的过滤器也是一样,我们可以自己在一个没有过滤器的程序里面加上过滤器,同时可以选择拦截下什么资源,放行什么资源。
一个驻留在服务端的web组件,可以截取用户端和资源之间的请求与响应信息,并对这些信息过滤。

当Web容器接收到一个对的资源的请求时,它将判断是否有过滤器(可以有多个过滤器)与这个资源有关联
如果有,容器把请求交给过滤器处理。在过滤器中,可以改变请求内容,或者重新设置请求的信息,然后再将请求发送给目标资源。
当目标资源对请求作出响应后,容器同样将响应先转发给过滤器,过滤器可以对响应的内容进行转换,然后再将响应发送到客户端。
在一个Web应用中,可以定义多个过滤器,组成一个过滤器链(FilterChain)。过滤器链中的每个过滤器负责特定的
操作和任务,客户端的请求在这些过滤器之间传递,直到目标资源。

Notes: Filter 不是一个标准的Servlet,不能处理用户请求,也不能对客户端生成响应。主要用于对HttpServletRequest进行预处理,
也可以对HttpServletResponse进行处理,是一个典型的处理链。
Servlet API中提供了一个Filter接口,开发web应用时,如果编写的Java类实现了这个接口,则把这个java类称之为过滤器Filter。
过滤器的英文名称为 Filter, 是 Servlet 技术中最实用的技术。如同它的名字一样,过滤器是处于客户端和服务器资源文件之间的一道过滤网,帮助我们过滤掉一些不符合要求的请求,通常用作 Session 校验,判断用户权限,如果不符合设定条件,则会被拦截到特殊的地址或者基于特殊的响应。
过滤器放在web资源之前,通过Filter技术,可以实现在请求抵达它所应用的web资源(可以是一个Servlet、一个Jsp页面,甚至是一个HTML页面)之前截获进入的请求,并且在它返回到客户之前截获输出请求。
web服务器管理的所有web资源:例如Jsp, Servlet, 静态图片文件或静态 html 文件。
自定义过滤器需要实现Filter接口,然后重写它的三个方法
Notes: doFilter(...)实现过滤器的功能。在特定的操作完成之后,可以调用chain.doFilter()方法,
将亲贵传给下一个过滤器(或目标资源),可以直接向客户端返回响应信息,或者利用转发、重定向将请求转发到其他资源。
当服务器启动时,web应用加载后,立即创建这个web应用中的所有的过滤器,过滤器创建出来后立即调用init方法执行初始化的操作。
创建出来后一直驻留在内存中为后续的拦截进行服务.每次拦截到请求后都会导致doFilter方法执行。
在服务器关闭或web应用被移除出容器时,随着web应用的销毁过滤器对象销毁.销毁之前调用destory方法执行善后工作。
Filter的创建和销毁由WEB服务器负责。 web 应用程序启动时,web 服务器将创建Filter 的实例对象,并调用其init方法,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作,filter对象只会创建一次,init方法也只会执行一次。通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象。
Web容器调用destroy方法销毁Filter。destroy方法在Filter的生命周期中仅执行一次。在destroy方法中,可以释放过滤器使用的资源。
用户在配置filter时,可以使用为filter配置一些初始化参数,当web容器实例化Filter对象,调用其init方法时,会把封装了filter初始化参数的filterConfig对象传递进来。因此开发人员在编写filter时,通过filterConfig对象的方法,就可获得:
A FilterChain is an object provided by the servlet container to the developer giving a view into the invocation chain of a filtered request for a resource. Filters use the FilterChain to invoke the next filter in the chain, or if the calling filter is the last filter in the chain, to invoke the resource at the end of the chain.——《tomcat-5.5-doc》
Web应用允许多个过滤器来过滤页面请求——联想现实生活中的例子是最好理解的啦!比如:为了获得更加干净的水,可能需要多个过滤器来进行过滤。

Servlet定义了过滤器接口Filter和过滤器链接口FilterChain,如下:
//// Source code recreated from a .class file by IntelliJ IDEA// (powered by FernFlower decompiler)//package javax.servlet;import java.io.IOException;public interface Filter { default void init(FilterConfig filterConfig) throws ServletException { } void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException; default void destroy() { }}//// Source code recreated from a .class file by IntelliJ IDEA// (powered by FernFlower decompiler)//package javax.servlet;import java.io.IOException;public interface FilterChain { void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;}所有自定义过滤器都继承自FIlter接口,并实现其doFilter()方法
FiterChain和Filter的接口的doFilter()方法签名不同:
//FilterChain中:public void doFilter(Target target) { if (index == filterList.size()) { return; } //获得下一个过滤器 Filter f = getNextFilter(); //执行一个处理器/过滤器的doFilter()方法,然后在该过滤器的doFilter()方法内回调此方法 f.doFilter(target, this);}//Filter中:public void doFilter(Target target, FilterChain chain) { //操作 ... //回调FilterChain的doFilter方法 chain.doFilter(target);}在前面《Spring拦截器》章节中,有讲到一个对象被多个拦截器拦截处理时,我们称这样的设计模式为责任链模式。同样的道理,一个web请求也可以被多个过滤器来进行处理,这就是FilterChain。
关于责任链模式在servlet中代码上是如何实现的,有兴趣的同学可以进一步阅读《JAVA与模式》之责任链模式。
场景:
在早期的BBS论坛中有很多发帖和回帖的操作,为了保持论坛的和谐,管理员会人工审核和删除一些带有敏感词的帖子。
针对这个场景,如果使用Spring框架,我们可以使用filter进行敏感词的过滤。
如下自定义过滤器 ReqResFilter 必须实现 javax.servlet.Filter。
然后添加注解 @WebFilter(javax.servlet.annotation.WebFilter),urlPatterns 过滤器要过滤的URL规则配置,filterName 过滤器的名称。
我们新建一个SensitiveWordFilter并实现javax.servlet.Filter
SensitiveWordFilter.java
package com.zhike.filter;import org.springframework.cglib.proxy.InvocationHandler;import org.springframework.cglib.proxy.Proxy;import org.springframework.util.ResourceUtils;import javax.servlet.*;import javax.servlet.annotation.WebFilter;import java.io.*;import java.lang.reflect.Method;import java.util.ArrayList;@WebFilter(filterName = "SensitiveWordFilter",urlPatterns = {"/*"})public class SensitiveWordFilter implements Filter { ArrayList<String> list = new ArrayList<String>(); /** * @param * @method 初始化过滤词的配置 */ @Override public void init(FilterConfig filterConfig) throws ServletException { try { String line; //读取我们的过滤配置文件 File resource = ResourceUtils.getFile("classpath:static/sensitive.txt"); BufferedReader sensitivewords = new BufferedReader(new FileReader(resource)); //把读取到的信息,存放到list中 while ((line = sensitivewords.readLine()) != null) { list.add(line); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException { //创建代理对象,增强getParameter ServletRequest proxy_req = (ServletRequest) Proxy.newProxyInstance(req.getClass().getClassLoader(), req.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //判断是不是getParameter方法 if (method.getName().equals("getParameter")){ //获取返回值 String value = (String)method.invoke(req, args); if (value != null){ for (String s : list){ if (value.contains(s)){ value = value.replaceAll(s,"***"); } } } return value; } return method.invoke(req,args); } }); chain.doFilter(proxy_req, resp); } @Override public void destroy() { System.out.println("----过滤器销毁----"); }}@WebFilter注解@WebFilter 用于将一个类声明为过滤器,该注解将会在部署时被容器处理,容器将根据具体的属性配置将相应的类部署为过滤器。该注解具有下表给出的一些常用属性 ( 以下所有属性均为可选属性,但是 value、urlPatterns、servletNames 三者必需至少包含一个,且 value 和 urlPatterns 不能共存,如果同时指定,通常忽略 value 的取值 )
@WebFilter 的常用属性| 属性名 | 类型(Filter) | 描述(Interceptor) |
| fileterName | String | 指定过滤器的Name属性,等价于 |
| value | String[] | 该属性等价于urlPattems,但两者不应同时使用 |
| urlPattems | String[] | 指定一组过滤器的URL匹配模式。等价于 |
| servlerNames | String[] | 指定过滤器将应用于哪些servlet。取值是@WebServlet中的name属性的取值或者是web.xml中 |
| dispatchTypes | DispatchType | 指定过滤器的转发模式。具体取值包括:FORWARD,INCLUDE,REQUEST,ERROR,ASYNC |
| initParams | WebInitParam[] | 指定一组过滤器初始化参数 等价于 |
| asyncSupported | boolean | 声明过滤器是否支持异步操作模式,等价于 |
| description | String | 该过滤器的描述信息, 等价于 |
| displayName | String | 该过滤器的显示名,通常配合工具使用 等价于 |
在启动类上加一个注解 @ServletComponentScan 就可以了,然后启动springboot 访问你的接口就会看到打印过滤器里的内容了。
@SpringBootApplication(scanBasePackages="com.zhike")@ServletComponentScan(basePackages = "com.zhike")public class BlogWebappApplication { public static void main(String[] args) { SpringApplication.run(BlogWebappApplication.class, args); }}通过上面的几个步骤,我们就创建好了一个自定义过滤器。
那如何验证我们的过滤器起作用呢?
正好我们的首页有一个输入框,本来用户输入关键词,通过文章标题来筛选文章。
我们以此为例,假设不消息输入了"坏蛋","笨蛋"这样的敏感词(这里我设置的敏感词库为sensitie.txt)。
sensitie.txt中文本如下:
笨蛋坏蛋期望服务端接收到敏感词之后,被替换为“***”。
value = value.replaceAll(s,"***");我们在DefaultController中Index方法
@RequestMapping("/index") public ModelAndView Index( HttpServletRequest request, Article article, @RequestParam(value = "pageNum",defaultValue = "0") int pageNum, @RequestParam(value = "pageSize", defaultValue = "5") int pageSize) { String title = request.getParameter("title"); System.out.println(title); .....}然后,修改一下/resources/templates/index.html视图
<div > <div > <form method="get" action="/default/index"> <input type="search" placeholder="Search" aria-label="Search" name="title" th:value="${article.title}"> <button type="submit">搜索</button> <hr/> </form> </div> </div> 
最后,启动调试,在搜索框中输入"笨蛋",通过查看title变量的值,发现他被替换成了"***"。

由此可见,我们的过滤敏感词功能实现了。
本章节介绍了Spring中过滤器的原理和机制,以及在springboot中如何使用。
git地址: https://github.com/zhikecore/superblog