正如标题所言,本文主要介绍两部分。
- 如何SpringMVC中注册拦截器的方式?多个拦截器执行的顺序是什么样的?
- 结合SpringMVC源码,对拦截器进行分析
SpringMVC中注册拦截器的方式
首先创建一个拦截器,然后注册到SpringMVC中即可。
第一步,创建拦截器,SpringMVC中提供两种方式,
- 实现接口HandlerInterceptor
- 继承抽象类HandlerInterceptorAdapter
2其实是基于1的,而且已经弃用了
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI2EzX4xSZz91ZsAzNfRHLGZkRGZkRfJ3bs92YsAjMfVmepNHL20mN1AzQhVTQpxUNttUc142UhVTQClGVF5UMR9Fd4VGdsATNfd3bkFGazxycykFaKdkYzZUbapXNXlleSdVY2pESa9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLzIzYxcjM1IDOycjYwEjZhFGOhRDZxImNmJDMjhTO4Q2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
我们来基于第一种方法创建拦截器。
模拟一个需求,自定义Log注解,方法上使用了注解的就记录日志,没有的就不记录日志
需要定义一个注解@Log作用在方法。拦截器只能拦截方法上的注解
/**
* @author yujiaxing
* @date 2021/09/28
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default "";
}
定义拦截器
/**
* @author yujiaxing
* @date 2021/09/28
*/
@Slf4j
@Component
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, java.lang.Object handler) throws Exception {
log.info("handler type : {}", handler.getClass()); // handler type : class org.springframework.web.method.HandlerMethod
Log logAnnotation = getLogAnnotation(handler);
if (Objects.nonNull(logAnnotation)) {
log.info("【日志信息】:preHandle {}", logAnnotation.value());
log.info("【请求信息】,URL:{} ,Remote Host :{}", request.getRequestURI(), request.getRemoteHost());
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, java.lang.Object handler, ModelAndView modelAndView) throws Exception {
Log logAnnotation = getLogAnnotation(handler);
log.info("【日志信息】postHandle :{}", logAnnotation.value());
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, java.lang.Object handler, Exception ex) throws Exception {
Log logAnnotation = getLogAnnotation(handler);
log.info("【日志信息】afterCompletion :{}", logAnnotation.value());
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
private Log getLogAnnotation(Object handler) {
// HandlerMethod 提供对方法参数、方法返回值、方法注释等的便捷访问。
if (handler instanceof HandlerMethod) {
HandlerMethod methodHandle = (HandlerMethod) handler;
Method method = methodHandle.getMethod();
Log annotation = method.getAnnotation(Log.class);
if (Objects.nonNull(annotation)) {
return annotation;
}
return null;
}
return null;
}
}
将拦截器注册到springMVC中
这个也有两种方式:
- 实现接口WebMvcConfigurer
- 继承抽象类WebMvcConfigurerAdapter
2也是在1的基础上实现的。而且也被弃用了
这里基于接口WebMvcConfigurer实现注册拦截器
/**
* @author yujiaxing
* @date 2021/09/28
*/
@Configuration
public class InterceptorRegisterConfig implements WebMvcConfigurer {
@Autowired
LogInterceptor logInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 需要匹配的路径
List<String> includePatterns = new ArrayList<>();
includePatterns.add("/api/test");
includePatterns.add("/api/test/include");
// 需要排除的路径
List<String> excludePatterns = new ArrayList<>();
excludePatterns.add("/api/test/exclude");
// 注册拦截器
registry.addInterceptor(logInterceptor)
.addPathPatterns(includePatterns)
.excludePathPatterns(excludePatterns);
}
}
定义一个Controller,来检测一下拦截器。
/**
* @author yujiaxing
* @date 2021/09/28
*/
@RestController
public class TestController {
@Log("测试日志信息")
@GetMapping("/api/test")
public String test() {
return "this is /api/test";
}
@Log("测试日志信息---测试匹配路径")
@GetMapping("/api/test/include")
public String testInclude() {
return "this is /api/test/include";
}
@Log("测试日志信息---测试不匹配路径")
@GetMapping("/api/test/exclude")
public String testExclude() {
return "this is /api/test/exclude";
}
}
访问地址: http://localhost:8081/api/test
浏览器:
查看控制台:
访问:http://localhost:8081/api/test/include 与上面类似
但访问 http://localhost:8081/api/test/exclude,就不会打印出相关日志信息。
这是因为拦截器在注册到Spring MVC时配置了能够匹配的路径和排除匹配的路径。这样就达到我们的需求了,只有在特定URL下,添加了@Log注解的方法才会被日志拦截器拦截,进行日志记录。类似登陆,鉴权也可以用类似的方式实现。
那么多个拦截器怎么定义拦截器的执行顺序呢?
在添加一个拦截器LogInterceptorV2
/**
* @author yujiaxing
* @date 2021/09/28
*/
@Slf4j
@Component
public class LogInterceptorV2 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("handler type : {}", handler.getClass()); // handler type : class org.springframework.web.method.HandlerMethod
Log logAnnotation = getLogAnnotation(handler);
if (Objects.nonNull(logAnnotation)) {
log.info("【日志信息V2】:preHandle {}", logAnnotation.value());
log.info("【请求信息】,URL:{} ,Remote Host :{}", request.getRequestURI(), request.getRemoteHost());
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
Log logAnnotation = getLogAnnotation(handler);
log.info("【日志信息V2】postHandle :{}", logAnnotation.value());
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
Log logAnnotation = getLogAnnotation(handler);
log.info("【日志信息V2】afterCompletion :{}", logAnnotation.value());
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
private Log getLogAnnotation(Object handler) {
// HandlerMethod 提供对方法参数、方法返回值、方法注释等的便捷访问。
if (handler instanceof HandlerMethod) {
HandlerMethod methodHandle = (HandlerMethod) handler;
Method method = methodHandle.getMethod();
Log annotation = method.getAnnotation(Log.class);
if (Objects.nonNull(annotation)) {
return annotation;
}
return null;
}
return null;
}
}
注册到springMVC中,修改InterceptorRegisterConfig
@Configuration
public class InterceptorRegisterConfig implements WebMvcConfigurer {
@Autowired
LogInterceptor logInterceptor;
@Autowired
LogInterceptorV2 logInterceptorV2;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 需要匹配的路径
List<String> includePatterns = new ArrayList<>();
includePatterns.add("/api/test");
includePatterns.add("/api/test/include");
// 需要排除的路径
List<String> excludePatterns = new ArrayList<>();
excludePatterns.add("/api/test/exclude");
// 注册拦截器
registry.addInterceptor(logInterceptorV2)
.addPathPatterns(includePatterns)
.excludePathPatterns(excludePatterns);
registry.addInterceptor(logInterceptor)
.addPathPatterns(includePatterns)
.excludePathPatterns(excludePatterns);
}
}
这个与拦截器注册到springMVC中的顺序有关,因为注册器InterceptorRegistry是维护的是List,所以拦截器访问是有序的,先注册的在前,后注册的在后面。
一个拦截器时,执行的顺序是:preHandle --> postHandle -->afterCompletion
多个拦截器时:拦截器V1,拦截器V2
V1.preHandle ------> V2.preHandle ------> V2.postHandle------>V1.postHandle------>V2.afterCompletion------>V1.afterCompletion
结合SpringMVC源码,对拦截器进行分析
首先来看HandlerInterceptor接口
结合这张图:主要是在2,3这个步骤中,使用拦截器进行拦截。
三个方法:
- preHandle:执行处理程序之前的拦截点。 在 HandlerMapping 确定合适的处理程序对象之后,但在 HandlerAdapter 调用处理程序之前调用。
- postHandle:成功执行处理程序后的拦截点。 在 HandlerAdapter 实际调用处理程序之后调用,但在 DispatcherServlet 呈现视图之前调用
- afterCompletion:请求处理完成后的回调,即渲染视图后。注意:只有在此拦截器的preHandle方法成功完成并返回true才会调用!
来看下源码的调用流程:重启项目,第一次调用时,会出先下图
结合我们追前打印出来的结果:
如果没有debug日志信息,在application.properties配置文件中添加web包的日志级别
logging.level.web=debug
-
RequestMappingHandlerMapping中打印了映射到的处理程序对象。在执行preHandle方法之前,而preHandle在HandlerMapping 确定合适的处理程序对象之后。
来看下RequestMappingHandlerMapping这个类,实现了HandlerMapping,所以preHandle的确是在HandlerMapping 确定合适的处理程序对象之后。
如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析 - RequestResponseBodyMethodProcessor这个类打印出了参数和返回值,参考下图,参数解析,方法返回值处理,说明这个是调用了映射的处理程序对象了。也证明了preHandle方法在 HandlerAdapter 调用处理程序之前调用。
如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析 3.postHandle方法在调用了处理程序之后调用。
4.afterCompletion在postHandle方法之后调用。
来看下上图中HandlerMapping接收到一个请求,如何返回HandlerExecutionChain。
HandlerMapping#getHandler,该方法是在DispatcherServlet#getHandler中调用HandlerMapping#getHandler方法
DispatcherServlet#getHandler
HandlerMapping#getHandler
AbstractHandlerMapping 实现了HandlerMapping,重写了getHandler方法,而RequestMappingHandlerMapping继承了AbstractHandlerMapping。三者之间的关系如下
AbstractHandlerMapping#getHandler
RequestMappingHandlerMapping#getHandler
所以控制台可以看到
这就是一个HttpServletRequest请求到HandlerMapping中的过程,返回一个HandlerExecutionChain,包含的就是这个请求所匹配到的拦截器,没有匹配到的已经被过滤了
其中interceptorList就是handler所要执行的拦截器列表。下一步就是执行拦截器,遍历拦截器列表,执行拦截器。
在HandlerExecutionChain#applyPreHandle进行,下面的HandlerExecutionChain#applyPostHandle,HandlerExecutionChain#triggerAfterCompletion就是拦截器中postHandle,afterCompletion的执行。
这就是Spring MVC中 拦截器模式源代码的分析。
一个请求到达DispatcherServlet,调用DispatcherServlet#getHandler,在到HandlerMapping#getHandler中,在到RequestMappingHandlerMapping#getHandler得到一个HandlerExecutionChain,然后在执行HandlerExecutionChain#applyPreHandle,在到HandlerInterceptor#preHandle,在到HandlerExecutionChain#applyPostHandle在到HandlerInterceptor#postHandle,在到HandlerExecutionChain#triggerAfterCompletion
在到HandlerInterceptor#afterCompletion。