文章目录
HTTP请求拦截器链
需求定义
我们写一个包含三个HTTP请求拦截器的拦截器链,写一个controller控制器方法,最后在postman里面调用controller控制器里面的接口方法,看看这个请求的经过路径。
首先说结论,如下图:
写一个Controller方法接口
如下图:
写三个http请求拦截器
我们定义三个http请求拦截器如下图:
然后每个http请求拦截器都实现preHandle、postHandle、afterCompletion这三个方法,如下图:
preHandle、postHandle、afterCompletion方法的执行时机?
当一个http请求发过来的时候,如果没有http请求拦截器这个请求是会直接发送到Controller控制器里面的,但是如果有http请求拦截器的话,外部发来的http请求会先进入到拦截器中拦截。可能我们程序中有多个http请求拦截器,比如有三个http请求拦截器,那么这三个http请求拦截器会组成一个拦截器链,外部发来的http请求先进入第一个拦截器的preHandle方法,如果这个拦截器放行了,也就是preHandle方法返回了true,那么该请求就会被第一个拦截器放行,然后该请求会进入到拦截器链中的第一个拦截器中,同样是进入到preHandle方法当中,如果preHandle方法返回true,则同样放行;接着http请求进入到第三个拦截器的preHandle方法里面;最后该请求才会进入到controller控制器中执行。注意如果preHandle方法返回false,则该请求就不能传递到controller控制器中了。
执行完controller方法之后,该http请求就算是执行完毕了,接着会从拦截器链中倒着走出去,先走拦截器3的postHandle方法,再走拦截器2的postHandle方法,最后再走拦截器1的postHandle方法;
走完了postHandle方法之后,再走拦截器3的afterCompletion方法,再走拦截器2的afterCompletion方法,最后走拦截器1的afterCompletion方法;
这样就执行完了一个http请求的全部过程,流程图如下图:
把拦截器加入到配置中,并且配置拦截规则
我们需要写一个拦截器配置类,把需要用到的拦截器放到拦截器链中,并且配置每个拦截器拦截的http请求的规则,就是拦截什么样的http请求,比如只拦截/user的请求,或者只拦截/student的请求,具体是什么样的请求规则,我们可以自定义。拦截器配置类如下图:
可以看到我们这里把拦截器1、拦截器2、拦截器3全部都放到了拦截器链中,然后每个拦截器都是拦截所有的http请求。
在postman里面发送请求,看下测试结果是否正确
首先看下我们控制器里面接收请求的方法,如下图:
然后在postman里面发送一个/test请求,如下图:
可以看到我们的controller控制器里面的方法确实成功执行了,接着去看下在执行controller控制器方法之前,三个拦截器里面的preHandle方法是否执行了,以及在执行controller控制器方法之后,三个拦截器里面的postHandle方法和afterCompletion方法是否执行了,控制台输出信息如下图:
可以发现这里在执行Controller控制器之前确实执行了拦截器中的preHandle方法,以及在执行Controller控制器之后也确实执行了拦截器中的postHandle和afterCompletion方法,并且顺序也是正确的。
第三个参数的作用
如下图:
第三个参数的作用主要是:提供对当前请求所匹配的处理器的描述。
在SpringMVC中,handler参数通常是一个HandlerMethod对象实例,它包含了请求对应的Controller控制器以及调用的控制器里面的方法的信息。通过第三个参数handler,我们可以了解到具体是哪个Controller控制器,以及具体是控制器里面的哪个方法处理请求的。
比如有下面一个场景,当访问UserController控制器中的getUsers方法的时候,我们需要认证一下当前用户是不是“董事长”,只有是董事长,我们的认证拦截器才会通过,那我们该怎么利用第三个参数呢?代码如下:
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取当前请求的处理器对象
Object handlerObj = handler;
// 判断处理器对象是否为Controller方法
if (handlerObj instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handlerObj;
// 获取控制器类和方法信息
Class<?> controllerClass = handlerMethod.getBeanType();
String methodName = handlerMethod.getMethod().getName();
// 如果处理http请求的控制器是UserController,并且里面的处理方法是getUsers,这个时候我们需要在拦截器里面校验一下认证逻辑,看看当前用户是不是董事长,不是的话不放行
if (controllerClass == UserController.class && "getUsers".equals(methodName)) {
boolean result = authenticateUser(request);
if (!result) {
// 如果用户未通过身份验证,阻止请求继续执行
return false;
}
}
}
// 如果身份验证通过,则允许请求继续执行
return true;
}
private boolean authenticateUser(HttpServletRequest request) {
// 通过request传递的参数得到当前用户姓名
if (currentUserName.equals("董事长")) {
return true;
}
return false;
}
}
注意我们第三个参数handler也不全是Controller处理器对象,也可能是其他对象,但是如果是Controller处理器对象的话,它的类型就是HandlerMethod类型,因此我们这里才会有一个类型判断,如下图:
但是上面的拦截控制器然后获取对应的方法名字不够灵活?自定义注解配合拦截器进行灵活的拦截操作
什么意思呢?就是我们通过上面的方式需要先得到控制器Class对象controllerClass,然后再得到控制器里面的方法的名字,我们需要同时判断下是否为对应的控制器和执行的控制器方法的名字是否准确,如果都满足,拦截器就拦截。看似很好,但是不知道你有没有发现一个弊端,就是假如你的拦截器也需要拦截另外一个控制器的方法,那么你是不是就需要修改拦截器代码了?可能你会修改很多的if语句,这就违背了一个软件设计原则,就是需要对扩展开放对修改关闭,因此我们这种方式显然不行的,那么我们需要怎么修改呢?可以使用自定义注解的方式,凡是有自定义注解的控制器方法对应的接口都会被拦截,具体的设计过程如下。
首先写一个自定义的注解,如下图:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionLimit {
/**
* 登录拦截 (默认拦截)
*/
boolean limit() default true;
}
然后把我们想要拦截的控制器方法加上这个注解,如下图:
最后在Interceptor拦截器里面写拦截规则,如下图:
代码如下:
public class MyInterceptor1 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod method = (HandlerMethod)handler;
PermissionLimit permission = method.getMethodAnnotation(PermissionLimit.class);
if (permission == null) {
//controller控制器里面的正在执行的方法没有加@PermissionLimit,因此我们无需用拦截器拦截去进行校验规则,这个接口无需校验直接通过
return true;
}
System.out.println("接口对应的Controller控制器方法上加了@PermissionLimit注解,这里需要进行拦截器拦截处理,处理通过才放行");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
如何在拦截器方法中获得Spring的IOC容器
可以使用下面的代码,如下:
BeanFactory factory = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
redisUtil = (RedisUtil) factory.getBean("redisUtil");
如果RedisUtil通过@Resource注解引用注入的时候失败了,我们就可以通过获取IOC容器手动的从IOC容器里面再去,如上。
需要用到拦截器的参数Request,如下图:
在preHandle中取出来参数做缓存
不知道为什么,在postHandle中调用取参数的逻辑的时候,会出错,取不出来request中传递的参数,但是在preHandle方法中是可以取出来的。原因是什么呢?我暂且理解为,preHandle是先处理请求,处理完之后会把request中的数据流关闭,所以在postHandle中就取不到参数了。
如下图:
这篇文章后续研究下:https://blog.youkuaiyun.com/cabbagexiu/article/details/129261557
如下图:
在拦截器中请求体只能被获取一次
假如我们现在有两个拦截器,都想要取获取请求体中的参数,那么当我的第一个拦截器通过getReader获取请求体参数之后,第二个拦截器就获取不到了,如下图:
第一个拦截器是可以读出来snCode请求体中的这个参数的(使用get方式的请求可以直接通过request.getParameter去获取参数,但是使用post方法的请求必须通过上面的获取参数的方式),但是第二个拦截器是读取不到的,因为当第一个拦截器读取请求体之后,第二个拦截器会把请求体给清空掉。
那么我们怎么解决这个问题呢?我们怎么可以在不同的拦截器还可以重复读取请求体中的参数呢?可以看一下mybatisplus项目。
拦截器中读取http请求中的请求体的时候,如果第一个拦截器已经调用了getReader或者getInputStream读取了请求体,
那么拦截器链中的下一个拦截器就不能再读取请求体中的参数了,因为第一次读取请求体之后,请求体中的内容会自动清空;
而我们这里的HttpServletRequestWrapper其实就是原生的HttpServletRequest的子类,这个子类额外增加了一些功能
可以在我们访问http请求的请求体的时候把请求体内容缓存起来,这样我们下一个拦截器也是可以访问这个请求体的。
写一个HttpServletRequestWrapper,代码如下:
public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
private final String body;
public MultiReadHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
StringBuilder sb = new StringBuilder();
BufferedReader reader = request.getReader();
String readCount = "";
while((readCount = reader.readLine()) != null){
sb.append(readCount);
}
body = sb.toString();
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream byteArrayIns = new ByteArrayInputStream(body.getBytes());
ServletInputStream servletIns = new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(javax.servlet.ReadListener listener) {
}
@Override
public int read() {
return byteArrayIns.read();
}
};
return servletIns;
}
}
再写一个filter过滤器把普通request转换为requestWrapper,如下:
@Component
@WebFilter(urlPatterns = "/testI")
public class MultiReadHttpFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
MultiReadHttpServletRequest requestWrapper = new MultiReadHttpServletRequest(request);
filterChain.doFilter(requestWrapper, response);
}
}
然后在拦截器中获取参数值,如下图:
代码请参照mybatisplus项目。
注意:一定要写一个过滤器把普通的Request请求转换为RequestWrapper请求,不太清楚原因,后续有时间再研究,暂时理解成否则的话拦截器无法识别ReqeustWrapper对象。
拦截器的pre,post,after方法执行完毕之前客户端不会收到返回数据
我之前一直以为,只要拦截器的pre方法执行之后,就会执行controller控制器方法,然后当controller控制器方法执行完毕之后会去执行postHandle方法,这个时候前端已经可以收到控制器中的数据了,如下图:
我之前一直以为不管是在执行postHandle还是在执行afterCompletion的时候,在执行完毕之前,都是会给客户端返回数据的,但其实是不会的。
在执行完pre方法和controller控制器方法,确实已经有了返回给客户端的数据原型,但是这个时候并不会返回给客户端,而是会保存起来,因为我们在postHandle方法中是可以对返回数据进行修改的,因此执行完pre和controller控制器方法之后并不会把数据立马返回给前端,只有当pre,post和after方法都执行完毕之后,才会把数据返回给客户端。
过滤器链
过滤器和拦截器的区别
所属的包不相同
Filter过滤器是来源于javax.servlet.Filter包,所以无需特别引入依赖,java自带的jdk里面就已经有了Filter过滤器;
Interceptor拦截器是Spring MVC特定框架的一部分,因此如果我们要使用Interceptor拦截器的话,必须要引入spring mvc框架依赖;Interceptor拦截器是Spring MVC框架集成的,而不是Java EE标准的一部分。
二者所能处理的请求范围不同
Filter过滤器可以处理所有的请求,包括访问Controller控制器的请求和访问jsp,html等静态资源的请求;
Interceptro拦截器只可以处理访问Controller控制器的请求;
因此如果我们想要拦截一些处理静态资源的请求,那么我们必须要使用Filter过滤器而不能使用Interceptor过滤器。
过滤器和拦截器谁先执行谁后执行?
过滤器最先执行,然后是拦截器执行。假如现在有一个过滤器链有一个拦截器链,过滤器链中有两个过滤器,拦截器链中有两个拦截器,现在有个访问controller控制器的请求,那么这个请求流经过滤器和拦截器的顺序是怎样的呢?如下图:
过滤器和拦截器的方法类比
如下图:
对于过滤器doFilter方法中的chain.doFilter(request, response)前面的语句,就好比是Interceptor拦截器中的preHandle方法,会在请求到达Controller控制器之前执行;
而对于后面的语句,就好比是Interceptor拦截器中的postHandle方法,会在请求执行完Controller控制器方法后执行;
所以,在执行控制器方法之前,会先执行Filter过滤器的doFilter方法的chain.doFilter(request, response)语句之前的代码,然后再执行Interceptor拦截器的preHandle中的代码;执行控制器方法之后,会先执行Interceptor拦截器中的postHandle方法中的代码,然后再执行Filter过滤器中的doFilter方法的chain.doFilter(request, response)语句之后的代码。
过滤器的init初始化方法有什么作用
首先过滤器的init初始化方法是什么时候加载的呢?是当我们项目启动的时候,会把Filter过滤器假如到IOC容器中的时候,这个时候会初始化Filter过滤器类,初始化类的时候就会执行init方法。因此当我们的项目启动起来之后,其实我们的init初始化方法就已经执行过了,因为这个时候我们的Filter过滤器已经被加载到了IOC容器中了。
启动类上把过滤器Filter假如到IOC容器的注解千万别忘记写
我之前碰到一个特别难发现的问题,就是我的过滤器链中的三个过滤器也已经写好了,但是当我从postman里面往项目里面发送请求的时候,请求就是不能被过滤器拦截,仿佛是我写的过滤器没有生效,当时找了好久都没找到问题,这是为什么呢?原来是我在启动类上少加了一个@ServletComponentScan注解,如下图:
如果不加这个注解的话,我写的Filter过滤器就不会在项目启动的时候假如到IOC容器里面,因此过滤器肯定无效了,所以就不能拦截请求了。
怎样控制过滤器链中过滤器的执行顺序?
默认情况下执行顺序是根据字母进行排序的,什么意思呢?如下图:
因为在二十六个英文字母中,C在L的前面,L在S的前面,因此我们的过滤器会执行CharacterEncodingFilter过滤器,再执行LoggingFilter过滤器,最后执行SecurityFilter过滤器;
那么我们怎样才能打破这个顺序呢?比如让SecurityFilter过滤器先执行,然后再执行LoggingFilter过滤器,最后执行CharacterEncodingFilter过滤器?可以通过@Order注解来实现,如下图:
但我发现通过Order注解控制过滤器在过滤器链中的执行顺序并不可以,所以那要怎么办呢?可以通过@WebFilter注解中的filterName过滤器名字来实现,字母排序越靠前,执行的就越靠前,如下图:
我们这里就会限制性filterName名字为filter1的过滤器,然后再执行filter2的过滤器,最后执行filter3的过滤器;
但是我最后测试的还是不行,既便我把filterName名字改了也并无效果,最后请求来了执行过滤器的顺序仍然是按照过滤器的类名的字母的排列顺序,排列越靠前,越早执行。
因此,如果我们想要控制过滤器链中过滤器的执行顺序,我目前知道的方式是只能修改过滤器的类名,比如我们可以在每一个过滤器的真正的名字之前都加上一个前缀FilterNum,比如我们上面的三个过滤器CharacterEncodingFilter、LoggingFilter、SecurityFilter,现在根据字母排序是CharacterEncodingFilter先执行,然后是LoggingFilter执行,最后是SecurityFilter执行,那如果我们想要把顺序反过来,可以重新定义过滤器类名为Filter1SecurityFilter、Filter2LoggingFilter、Filter3CharacterEncodingFilter。如下图:
亲测有效,因此我们可以通过给过滤器类名加前缀FilterNum的方式来控制过滤器在过滤器链中的执行顺序。