SpringBoot集成SpirngMVC之拦截器
SpringMVC中的拦截器
一、概念
1.1、什么是拦截器
SpringMVC的处理器拦截器,类似于Servlet开发中的过滤器Filter,用于对处理器进行预处理和后处理。
依赖于web框架,在实现上基于Java的反射机制,属于面向切面编程(AOP)的一种运用。由于拦截器是基于web框架的调用,因此可以使用Spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个 controller生命周期之内可以多次调用。
拦截器作用
SpringMVC 框架中的拦截器用于对处理器进行预处理和后处理的技术。可以定义拦截器链,连接器链就是将拦截器按着顺序结成一条链,在访问被拦截的方法时,拦截器链中的拦截器会按着定义的顺序执行。
拦截器和过滤器的功能比较类似,有以下区别:
过滤器是 Servlet 规范的一部分,任何框架都可以使用过滤器技术;拦截器是 SpringMVC 框架独有的。
过滤器配置了 /*,可以拦截任何资源;
拦截器只会对控制器中的方法进行拦截。
拦截器也是 AOP 思想的一种实现方式。
拦截器与过滤器的区别
1)过滤器:
依赖于servlet容器。在实现上基于函数回调,可以对几乎所有请求进行过滤,但是缺点是一个过滤器实例只能在容器初始化时调用一次。使用过滤器的目的是用来做一些过滤操作,比如:在过滤器中修改字符编码;在过滤器中修改HttpServletRequest的一些参数,包括:过滤低俗文字、危险字符等。
2)拦截器:
依赖于web框架,在实现上基于Java的反射机制,属于面向切面编程(AOP)的一种运用。由于拦截器是基于web框架的调用,因此可以使用Spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个controller生命周期之内可以多次调用。
过滤器(filter):
1) filter属于Servlet技术,只要是web工程都可以使用
2) filter主要对所有请求过滤
3) filter的执行时机早于Interceptor
拦截器(interceptor)
1) interceptor属于SpringMVC技术,必须要有SpringMVC环境才可以使用
2) interceptor通常对处理器Controller进行拦截
3) interceptor只能拦截dispatcherServlet处理的请求
应用场景
1)日志记录:记录请求信息的日志,以便进行信息监控、信息统计、计算PV(Page View)等。
2)权限检查:如登录检测,进入处理器检测是否登录,如果没有直接返回到登录页面;
3)性能监控:有时候系统在某段时间莫名其妙的慢,可以通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间(如果有反向代理,如apache可以自动记录);
4)通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取Locale、Theme信息等,只要是多个Controller中的处理方法都需要的,我们就可以使用拦截器实现。
二、HandlerInterceptor 定义
2.1、接口描述
直接看下SpringMVC中的接口:
public interface HandlerInterceptor {
/**
* 预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器,自定义Controller
* 返回值:true表示继续流程(如调用下一个拦截器或处理器);false表示流程中断
* 不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应
*/
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
/**
* 后处理回调方法,实现处理器的后处理(渲染视图之前)
* 此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理
* modelAndView也可能为null,如API接口返回JSON数据时
*/
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
/**
* 整个请求处理完毕回调方法,即在视图渲染完毕时回调
* 如性能监控中我们可以在此记录结束时间并输出消耗时间
* 还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中
*/
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
2.2、MappedInterceptor
MappedInterceptor类是HandlerInterceptor接口的包装类,也是其子类。可以说是装饰器模式的一种应用
根据类说明:
包含并委托对HandlerInterceptor的调用,以及拦截器应应用的包含(和可选排除)路径模式。还提供匹配逻辑来测试拦截器是否应用于给定的请求路径。
在MappedInterceptor中可以看到几个属性的属性:
// 包含进来的路径
private final String[] includePatterns;
// 需要排除的路径
private final String[] excludePatterns;
// 拦截器
private final HandlerInterceptor interceptor;
// 路径匹配器。有针对性的对路径来进行匹配
private PathMatcher pathMatcher;
2.3、初始化过程
在RequestMappingHandlerMapping进行创建的时候,可以看到有在其中进行设置:
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
mapping.setOrder(0);
mapping.setInterceptors(getInterceptors());
// ......
return mapping;
}
可以看到获取得到拦截器。直接来到org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#getInterceptors方法中来:
protected final Object[] getInterceptors() {
if (this.interceptors == null) {
// 创建拦截器集合
InterceptorRegistry registry = new InterceptorRegistry();
// 添加到DelegatingWebMvcConfiguration对象中的configurers集合中来
addInterceptors(registry);
// 默认添加了两个拦截器对象
registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService()));
registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider()));
this.interceptors = registry.getInterceptors();
}
return this.interceptors.toArray();
}
说明了RequestMappingHandlerMapping和Interceptor是存在着关系的。
其实不止这一处,因为RequestMappingHandlerMapping实现了ApplicationContextAware接口,那么必定重写setApplicationContext方法。
所以看下RequestMappingHandlerMapping类中的setApplicationContext方法:
public final void setApplicationContext(ApplicationContext context) throws BeansException {
// ...
this.initApplicationContext(context);
// ...
}
直接来到:org.springframework.web.servlet.handler.AbstractDetectingUrlHandlerMapping#initApplicationContext方法中
public void initApplicationContext() throws ApplicationContextException {
// 访问父类方法
super.initApplicationContext();
// 检测Handler
detectHandlers();
}
先来看下父类中的initApplicationContext()方法
protected void initApplicationContext() throws BeansException {
// 扩展方法!空实现
extendInterceptors(this.interceptors);
// 从IOC容器中获取得到所有MappedInterceptor类型的bean
detectMappedInterceptors(this.adaptedInterceptors);
// 初始化Interceptor
initInterceptors();
}
看一下如何初始化Interceptor的
protected void initInterceptors() {
if (!this.interceptors.isEmpty()) {
for (int i = 0; i < this.interceptors.size(); i++) {
Object interceptor = this.interceptors.get(i);
if (interceptor == null) {
throw new IllegalArgumentException("Entry number " + i + " in interceptors array is null");
}
// 添加到集合中来
this.adaptedInterceptors.add(adaptInterceptor(interceptor));
}
}
}
protected HandlerInterceptor adaptInterceptor(Object interceptor) {
// 通常都是这个类型
if (interceptor instanceof HandlerInterceptor) {
return (HandlerInterceptor) interceptor;
}else if (interceptor instanceof WebRequestInterceptor) {
return new WebRequestHandlerInterceptorAdapter((WebRequestInterceptor) interceptor);
}else {
throw new IllegalArgumentException("Interceptor type not supported" )
}
}
此时将所有的拦截器保存到AbstractHandlerMapping类中的属性adaptedInterceptors集合中来
2.4、MappedInterceptor和HandlerInterceptor比较
关系 | 作用 | 特点 | 如何添加拦截器 | |
---|---|---|---|---|
HandlerInterceptor | 一旦添加,表示拦截所有的HandlerMethod | 1、创建RequestMappingHandlerMapping对象时,添加拦截器; 2、重写extendInterceptors方法添加拦截器; 3、添加配置MappedInterceptor类型的bean到容器中; |
||
MappedInterceptor | MappedInterceptor是HandlerInterceptor的子类 | 可以针对指定URL的HandlerMethod进行拦截 | 类是用final关键字修饰的,无法继承 | 配置成为web容器中的一个bean即可。 |
其实99%的情况下,我们在web中配置的都是MappedInterceptor类型的拦截器,但是为什么我们在写的时候都是在直接实现HandlerInterceptor接口呢?
而且在上面分析的时候,可以看到MappedInterceptor使用final关键字修饰的,无法进行继承,那么也就无法自己创建MappedInterceptor类型的bean了。
所以这个时候得利用Web中的配置类来配置拦截器了。
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new XxxxIntecepter())
// 需要包含的路径
.addPathPatterns()
// 需要排除的路径
.excludePathPatterns()
// 添加拦截器顺序
.order(1);
}
}
2.5、拦截器使用
2.5.1、MappedInterceptor
因为MappedInterceptor无法继承,但是又因为在初始化的时候需要找到MappedInterceptor类型的bean,所以肯定是在某个过程中间是将HandlerInterceptor给封装成了MappedInterceptor。
2.5.2、HandlerInterceptor
虽然步骤是一样的,但是使用起来还是很爽的。
下面先来列出步骤,然后在HandlerExecutionChain讲解完成之后来完成对应的案例。
2.5.2.1、创建拦截器
public class XxxxIntecepter implements HandlerInterceptor {
// 有需要的重写三个方法
}
2.5.2.2、添加拦截器到容器中
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new XxxxIntecepter())
// 需要包含的路径
.addPathPatterns()
// 需要排除的路径
.excludePathPatterns()
// 添加拦截器顺序
.order(1);
}
}
然后在HandlerExecutionChain执行阶段的时候,会遍历所有的拦截器,将符合路径的拦截器筛选出来,执行其中的方法。
所以有必要先来看下HandlerExecutionChain中的拦截器。
三、HandlerExecutionChain
HandlerExecutionChain是由HandlerMethod和HandlerIntecepter组成的。
3.1、拦截器的筛选
在根据请求找到handler的时候,发现返回的是HandlerExecutionChain。那么这里为什么呢?
因为找到了handler的时候,想要在在handler方法执行前后添加一些逻辑!这也是一种常理,有点类似AOP原理。
那么下面就来看下如何根据请求找到HandlerExecutionChain的。
直接来到org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler方法中来,找到下面这行代码:
Object handler = getHandlerInternal(request);
// ......
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
然后看下如何获取得到HandlerExecutionChain的:
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
// 判断handler,当然不是!所以创建一个执行器链条
HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
// 获取得到URL,根据URL来进行筛选
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, LOOKUP_PATH);
// 获取得到拦截器集合,循环判断
for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
// 如果拦截器是MappedInterceptor类型,还需判断是否需要匹配对应路径
// 只有匹配了之后,才会将其放入到集合中去
if (interceptor instanceof MappedInterceptor) {
MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
chain.addInterceptor(mappedInterceptor.getInterceptor());
}
}
else {
// 没有路径匹配的,直接添加到链条集合中来
chain.addInterceptor(interceptor);
}
}
return chain;
}
也就是说找到了handler之后,还需要找到所有的方法拦截器进行筛选,符合条件的才会添加到当前的HandlerExecutionChain中来。
具体的筛选规则:
- 1、如果配置了路径过滤规则,那么就进行匹配;
- 2、如果没有配置路径,那么就是拦截所有的;
3.2、拦截器的执行
直接来到org.springframework.web.servlet.DispatcherServlet#doDispatch方法中来,找到下面这行代码:
// 执行拦截器中的preHandle方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
// 执行目标方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// ......
// 执行拦截器中的postHandle方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
也就是说,拦截器中的preHandle方法是在执行目标方法之前执行的,而拦截器中的postHandle方法是在执行目标方法之后执行的。
那么到此,还有一个方法是在哪里执行的呢?
对于controller中的HandlerMethod来说,是方法,只要是方法执行,那么就可能会出现问题。那么就有了两种情况:
- 1、HandlerMethod执行正常;
- 2、HandlerMethod执行异常;
所以springmvc分别对这两种情况做了处理:
HandlerMethod执行正常
这是在大的try...catch...中执行完成之后执行的。
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
HandlerMethod执行异常
这是在大的try...catch...finally中的catch和finally中的执行的。
通过这两种情况分析,也就是说,HandlerInterceptor类中的afterCompletion总是会执行的。
小结
1、如果拦截器的preHandle(前置方法)拦截(返回值为false),那么对于postHandle方法将不会执行;
2、如果拦截器的前置方法放行,那么后置和最终方法都将会执行;
四、拦截器执行顺序
如果存在多个拦截器,那么对应的执行顺序是怎样的呢?
preHandle方法执行顺序
直接看下源码:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for (int i = 0; i < interceptors.length; i++) {
HandlerInterceptor interceptor = interceptors[i];
if (!interceptor.preHandle(request, response, this.handler)) {
triggerAfterCompletion(request, response, null);
return false;
}
// 计数器!执行到了第几个拦截器记录下标
this.interceptorIndex = i;
}
}
return true;
}
获取得到拦截器集合,然后从第一个开始来进行执行。而一旦其中一个拦截器拦截了,那么将会直接执行对应的triggerAfterCompletion方法。
那么看一下对应的triggerAfterCompletion方法
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex)
throws Exception {
// 获取得到拦截器!
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
// 直接执行拦截器的最终方法!
// 看下这里的for循环是反转着执行的
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
try {
interceptor.afterCompletion(request, response, this.handler, ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}
}
postHandle方法执行顺序
倒序执行:
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
// 同样的倒序执行
for (int i = interceptors.length - 1; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
interceptor.postHandle(request, response, this.handler, mv);
}
}
}
afterCompletion方法执行顺序
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex)
throws Exception {
// 获取得到拦截器!
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
// 直接执行拦截器的最终方法!
// 看下这里的for循环是反转着执行的
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
try {
interceptor.afterCompletion(request, response, this.handler, ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}
}
执行图
拦截器全部放行
拦截器某个不放行
五、应用
5.1、记录controller中的接口耗时时间
- 需求:记录一下controller中添加了@Fuck注解的方法的执行时间
思路:编写一个拦截器识别方法上存在@Fuck注解的拦截器;然后再编写一个统计时间的方法拦截器;
注解
@Target(value = {ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Fuck {
}
找到HandlerMethod方法上添加@Fuck注解的拦截器
/**
* @author lg
* @Description 只找到controller层中的HandlerMethod方法上存在@Fuck注解的方法
* @date 2023/3/7 10:35
*/
public class CustomFuckIntecepter implements HandlerInterceptor {
/**
* 如果当前handlermehod方法上存在@Fuck就放行,否则不放行
*
* @param request 请求封装之后的对象;
* @param response 响应封装的对象;
* @param handler 如果是利用注解的话,那么就是HandlerMethod对象;
* @return true,表示放行,进行下一步处理;false表示的是拦截
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
Fuck annotation = method.getAnnotation(Fuck.class);
return annotation != null;
}
return false;
}
/**
* 在HandlerMethod方法执行完成之后执行的操作
*
* @param request 请求封装之后的对象;
* @param response 响应封装的对象;
* @param handler 如果是利用注解的话,那么就是HandlerMethod对象;
* @param modelAndView HandlerMethod对象执行完成之后的返回值
* @throws Exception 方法抛出的异常
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* @param request 请求封装之后的对象;
* @param response 响应封装的对象;
* @param handler 如果是利用注解的话,那么就是HandlerMethod对象;
* @param ex 抛出的异常信息
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
统计时间拦截器
public class CountTimeIntercepter implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(CountTimeIntercepter.class);
private static final ThreadLocal<Long> COUNT_TIME = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
long startTime = System.currentTimeMillis();
COUNT_TIME.set(startTime);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
long endTime = System.currentTimeMillis();
Long startTime = COUNT_TIME.get();
long methodExecuteCostedTime = endTime - startTime;
String requestURI = request.getRequestURI();
String method = request.getMethod();
logger.info("当前执行的接口路径是:{},请求方式是:{},耗时:{} ms",requestURI,method,methodExecuteCostedTime);
COUNT_TIME.remove();
}
}
添加到webconfig
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CustomFuckIntecepter())
// 需要包含的路径
.addPathPatterns()
// 需要排除的路径
.excludePathPatterns()
// 添加拦截器顺序
.order(1);
registry.addInterceptor(new CountTimeIntercepter())
.addPathPatterns("/**")
.order(2);
}
}