过滤器与拦截器
一、前言
常用项目编写规范参考:Spring Boot后端接口规范
1、概述
前面讲到数据统一响应、全局异常等常用后端框架,那么随着项目的开发,需要对请求进行校验(参数校验、前面校验等),不符合的不进入后端业务逻辑,提前返回并抛出异常。一般实现方法有拦截器和过滤器,这两者都可以实现对应的功能,可以根据自己喜好进行编写。
过滤器一般完成通用的操作。如:登录验证、统⼀编码处理、敏感字符过滤,常见的过滤器用途主要包括:对用户请求进行统一认证、对用户的访问请求进行记录和审核、对用户发送的数据进行过滤或替换、转换图象格式、对响应内容进行压缩以减少传输量、对请求或响应进行加解密处理、触发资源访问事件等;拦截器采用AOP的设计思想, 它跟过滤器类似, 用来拦截处理方法在之前和之后执行一些 跟主业务没有关系的一些公共功能,比如权限控制、日志、异常记录、记录方法执行时间
下面来讲讲这两者的异同和代码demo。
2、过滤器与拦截器异同
2.1 简介
过滤器(filter)和拦截器(Inteceptor)的执行顺序概览
2.2 异同
- 过滤器和拦截器触发时机不一样,过滤器是在请求进入容器后,但请求进入servlet之前进行预处理的。请求结束返回也是,是在servlet处理完后,返回给前端之前
- 拦截器可以获取IOC容器中的各个bean,而过滤器就不行,因为拦截器是spring提供并管理的,spring的功能可以被拦截器使用,在拦截器里注入一个service,可以调用业务逻辑。而过滤器是JavaEE标准,只需依赖servlet api ,不需要依赖spring
- 过滤器的实现基于回调函数。而拦截器(代理模式)的实现基于反射
- Filter是依赖于Servlet容器,属于Servlet规范的一部分,而拦截器则是独立存在的,可以在任何情况下使用
- Filter的执行由Servlet容器回调完成,而拦截器通常通过动态代理(反射)的方式来执行
- Filter的生命周期由Servlet容器管理,而拦截器则可以通过IoC容器来管理,因此可以通过注入等方式来获取其他Bean的实例,因此使用会更方便
2.3 总结
- 过滤器可以修改request,而拦截器不能
- 过滤器需要在servlet容器中实现,拦截器可以适用于javaEE,javaSE等各种环境
- 拦截器可以调用IOC容器中的各种依赖,而过滤器不能
- 过滤器只能在请求的前后使用,而拦截器可以详细到每个方法
具体的执行调用流程如下
- 过滤器(Filter) :可以拿到原始的http请求,但是拿不到你请求的控制器和请求控制器中的方法的信息
- 拦截器(Interceptor):可以拿到你请求的控制器和方法,却拿不到请求方法的参数
- 切片(Aspect): 可以拿到方法的参数,但是却拿不到http请求和响应的对象
这里说一下为什么spring security使用过滤器而不是拦截器。因为作为一个通用的安全框架不应该耦合其他web框架的元素。很显然拦截器是spring mvc或struts等框架提供的,如果基于拦截器势必耦合这些框架,就做不到通用了
3、Filters vs HandlerInterceptors
- Filter 是 Servlet 规范中的,而 HandlerInterceptor 是 Spring 中的一个概念
- 拦截器位置相对于过滤器更靠后
- 精细的预处理任务适用于拦截器,如授权检查等
- 内容处理相关或通用的流程,非常适合用过滤器;如上传表单、zip 压缩、图像处理、日志记录请求、身份验证等
- HandlerInterceptor 的
postHandle
方法允许我们向视图添加更多模型对象,但不能更改 HttpServletResponse,因为它已经被提交了 - 过滤器的
doFilter
方法比拦截器的postHandle
更通用。我们可以在过滤器中改变请求或响应,并将其传递给链,甚至阻止请求的处理 - HandlerInterceptor 提供了比过滤器更精细的控制,因为我们可以访问实际的目标 handler,甚至可以检查 handler 方法是否有某个特定的注解
二、过滤器
1、概述
过滤器(Filter)是处于客户端与服务器目标资源之间的⼀道过滤技术,当访问服务器的资源时,过滤器可以将请求拦截下来,完成⼀些特殊的功能
执行是在Servlet之前,客户端发送请求时,会先经过Filter,再到达目标Servlet中;响应时, 会根据执行流程再次反向执行Filter,⼀般用于完成通用的操作。如:登录验证、统⼀编码处理、敏感字符过滤。常见的过滤器用途主要包括:对用户请求进行统一认证、对用户的访问请求进行记录和审核、对用户发送的数据进行过滤或替换、转换图象格式、对响应内容进行压缩以减少传输量、对请求或响应进行加解密处理、触发资源访问事件等
2、生命周期
2.1 生命周期概述
过滤器的配置比较简单,直接实现Filter 接口即可,也可以通过@WebFilter
注解实现对特定URL拦截,看到Filter 接口中定义了三个方法。
init()
:该方法在容器启动初始化过滤器时被调用,它在 Filter 的整个生命周期只会被调用一次。注意:这个方法必须执行成功,否则过滤器会不起作用doFilter()
:容器中的每一次请求都会调用该方法,比如定义一个 Filter 拦截/path/*
,那么每一个匹配/path/*
访问资源的请求进来时,都会执行此方法, FilterChain 用来调用下一个过滤器 Filter。不同的过滤器通过@Order()
排序注解执行顺序destroy()
: 当容器销毁 过滤器实例时调用该方法,一般在方法中销毁或关闭资源,在过滤器 Filter 的整个生命周期也只会被调用一次
2.2 基于函数回调实现原理
在我们自定义的过滤器中都会实现一个 doFilter()
方法,这个方法有一个FilterChain
参数,而实际上它是一个回调接口。ApplicationFilterChain
是它的实现类, 这个实现类内部也有一个 doFilter()
方法就是回调方法。
1 | public interface FilterChain { |
ApplicationFilterChain
里面能拿到我们自定义的xxxFilter
类,在其内部回调方法doFilter()
里调用各个自定义xxxFilter
过滤器,并执行 doFilter()
方法
1 | public final class ApplicationFilterChain implements FilterChain { |
而每个xxxFilter
会先执行自身的 doFilter()
过滤逻辑,最后在执行结束前会执行filterChain.doFilter(servletRequest, servletResponse)
,也就是回调ApplicationFilterChain
的doFilter()
方法,以此循环执行实现函数回调
3、自定义过滤器两种实现方式
不论是注解配置还是Java配置,都需要在启动类上加上@ServletComponentScan("过滤器路径")
注解,过滤路径可以不写(或者直接注入容器交给spring管理)。注解注册和Java配置类注册,它们的自定义过滤器类都是一样的,只不过注册过程一个是通过@WebFilter
注解,一个是通过Java配置类注册Bean。
3.1 @WebFilter注解注册
1 | /** |
再举一个例子
1 | /** |
3.2 过滤器(配置类注册过滤器)
1 | public class BaseFilter implements Filter { |
然后设置配置类
1 | /** |
4、实战OncePerRequestFilter
自定义配置类
1 |
|
下面继承了OncePerRequestFilter 来实现我们自己的自定义过滤器,OncePerRequestFilter 特点是请求进入后只会过滤一次,不会重复过滤(有些情况请求可能会两次进入相同的过滤器),同时在不符合要求的请求需要即使抛出异常返回,或者重定向到其他接口
1 |
|
这里要注意的是我们用了HandlerExceptionResolver
,因为在Spring Boot由于全局异常处理@RestControllerAdvice
只会去捕获所有Controller层抛出的异常,所以在filter当中抛出的异常GlobalExceptionHandler类是没有感知的,所以在filter当中抛出的异常最终会被Spring框架自带的全局异常处理类BasicErrorController捕获,会返回基础格式的Json响应
一种方法是继承上面所说的BasicErrorController类
,并重写error()方法;另一种就是在filter当中引入HandlerExceptionResolver
类,通过该类的resolveException
方法抛出自定义异常,通过resolveException方法抛出的自定义异常可以被RestControllerAdvice
捕获,从而满足我们的需求,最终得到的响应格式
三、拦截器
1、概述
拦截器采用AOP的设计思想, 它跟过滤器类似, 用来拦截处理方法在之前和之后执行一些 跟主业务没有关系的一些公共功能,比如可以实现权限控制、日志、异常记录、记录方法执行时间等等
2、自定义拦截器
2.1 生命周期
SpringMVC提供了拦截器机制,允许运行目标方法之前进行一些拦截工作或者目标方法运行之后进行一下其他相关的处理。自定义的拦截器必须实现 HandlerInterceptor接口。
HandlerInterceptor 接口中定义了三个方法
preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
:这个方法将在请求处理之前进行调用。注意:如果该方法的返回值为false ,将视为当前请求结束,不仅自身的拦截器会失效,还会导致其他的拦截器也不再执行。postHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
:只有在 preHandle() 方法返回值为true 时才会执行。会在Controller 中的方法调用之后,DispatcherServlet 返回渲染视图之前被调用。 此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为nullafterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler)
:只有在 preHandle() 方法返回值为true 时才会执行。在整个请求结束之后, DispatcherServlet 渲染了对应的视图之后执行。如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try catch finally
中的finally,但仅调用处理器执行链中preHandle返回true的拦截器才会执行
2.2 代码示例
首先创建自定义拦截器
1 | public class MyFirstInterceptor implements HandlerInterceptor { |
然后在spring boot 项目中配置,实现 WebMvcConfigurer
接口 并重写 addInterceptors
方法
1 |
|
2.3 多拦截器示例
拦截顺序取决于配置的顺序
1 |
|
看到输出结果发现,先声明的拦截器 preHandle() 方法先执行,而postHandle()方法反而会后执行。但注意postHandle() 方法被调用的顺序跟 preHandle() 是相反的。我们要知道controller 中所有的请求都要经过核心组件DispatcherServlet路由,都会执行它的 doDispatch()
方法,而拦截器postHandle()、preHandle()方法便是在其中调用的。查看源码可知,发现两个方法中在调用拦截器数组 HandlerInterceptor[] 时,循环的顺序竟然是相反的
3.4 静态资源被拦截问题
配置拦截器会导致静态资源被拦截,比如在 resources/static/ 目录下放置一个图片资源或者 html 文件,然后启动项目直接访问,即可看到无法访问的现象。也就是说,虽然 Spring Boot 2.0 废弃了WebMvcConfigurerAdapter
,但是 WebMvcConfigurationSupport
又会导致默认的静态资源被拦截,这就需要我们手动将静态资源放开
除了在 MyInterceptorConfig 配置类中重写 addInterceptors
方法外,还需要再重写一个方法:addResourceHandlers
,将静态资源放开
1 | /** |
这样配置好之后,重启项目,静态资源也可以正常访问了。
另一种方法是不继承 WebMvcConfigurationSupport
类,直接实现 WebMvcConfigurer
接口,然后重写 addInterceptors
方法,将自定义的拦截器添加进去即可(上面讲到)
1 |
|
4、实战demo
判断用户有没有登录,一般用户登录功能我们可以这么做,要么往 session 中写一个 user,要么针对每个 user 生成一个 token,第二种要更好一点,那么针对第二种方式,如果用户登录成功了,每次请求的时候都会带上该用户的 token,如果未登录,则没有该 token,服务端可以检测这个 token 参数的有无来判断用户有没有登录,从而实现拦截功能。我们改造一下 preHandle
方法
1 |
|
最后还有一个监听器,可以参考:Spring事件监听
https://zhuanlan.zhihu.com/p/340397290
https://zhuanlan.zhihu.com/p/484289805
https://www.zhihu.com/question/443466900/answer/2509838187
https://blog.csdn.net/qq_45534061/article/details/106266747