【Spring MVC】Spring MVC拦截器(Interceptor)
本文将带你详细了解Spring MVC拦截器的执行顺序、底层原理以及生产应用。
目录
2.2 Spring MVC中提供的一些HandlerInterceptor接口实现类
4、ConversionServiceExposingInterceptor
5.1 applyPreHandle():执行拦截器 preHandle 方法
5.2 applyPostHandle(): 执行拦截器 postHandle 方法
5.3 processDispatchResult(): 执行拦截器 afterCompletion 方法
一、拦截器介绍
Spring MVC中提供了处理器拦截器组件(Interceptor),拦截器在 Spring MVC 中的地位等同于 Servlet 规范中的过滤器 过滤器(Filter),用于对处理器进行预处理和后处理。
拦截器拦截的是处理器的执行,由于是全局行为,因此常用于做一些通用的功能,例如:
- 日志记录:记录请求信息的日志,以便进行信息监控、信息统计、计算PV(Page View)等。
- 权限检查:如登录检测,进入处理器检测检测是否登录,如果没有直接返回到登录页面;
- 性能监控:有时候系统在某段时间莫名其妙的慢,可以通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间(如果有反向代理,如apache可以自动记录);
- 通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取Locale、Theme信息等,只要是多个处理器都需要的即可使用拦截器实现。
- OpenSessionInView:如Hibernate,在进入处理器打开Session,在完成后关闭Session。
拦截器本质也是AOP(面向切面编程),也就是说符合横切关注点的所有功能都可以放入拦截器实现。
我们把 Spring MVC DispatcherServlet 请求处理流程这张图拿出来。
当浏览器发起的请求到达 Servlet 容器,DispatcherServlet 先根据处理器映射器 HandlerMapping 获取处理器,这时候获取到的是一个包含处理器和拦截器的处理器执行链,处理器执行之前将会先执行拦截器。
不包含拦截器的情况下,DispatcherServlet 处理请求的流程可以简化如下:
添加了拦截器做登录检查后,DispatcherServlet 请求处理的流程可以简化如下:
二、拦截器 Interceptor 定义
2.1 HandlerInterceptor接口
事实上拦截器的执行流程远比上述 DispatcherServelt 简化后的流程图复杂,它不仅可以在处理器之前执行,还可以在处理器之后执行。先看拦截器 Interceptor 在 Spring MVC 中的定义,Spring MVC拦截器的顶级接口为HandleInterceptor,定义了三个方法:1、preHandle(请求前) 2、postHandle(请求提交) 3、afterCompletion(请求完成后拦截)
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
我们可能注意到拦截器一共有3个回调方法,而一般的过滤器Filter才两个,这是怎么回事呢?马上分析。
- preHandle:预处理回调方法,实现处理器的预处理(如登录检查),第三个参数为响应的处理器(如Controller实现);
- 返回值:true表示继续流程(如调用下一个拦截器或处理器);false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应;
- postHandle:后处理回调方法,实现处理器的后处理(但在渲染视图之前),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。
- afterCompletion:整个请求处理完毕回调方法,即在视图渲染完毕时回调,如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中preHandle返回true的拦截器的afterCompletion。
通过源码我们还可以发现,这三个方法都有的 handler 参数表示处理器,通常情况下可以表示我们使用注解 @Controller 定义的控制器。
对上面的流程图继续细化:
三个方法具体的执行流程如下:
- preHandle:处理器执行之前执行,如果返回 false 将跳过处理器、拦截器 postHandle 方法、视图渲染等,直接执行拦截器 afterCompletion 方法。
- postHandle:处理器执行后,视图渲染前执行,如果处理器抛出异常,将跳过该方法直接执行拦截器 afterCompletion 方法。
- afterCompletion:视图渲染后执行,不管处理器是否抛出异常,该方法都将执行。
注意:自从前后端分离之后,Spring MVC 中的处理器方法执行后通常不会再返回视图,而是返回表示 json 或 xml 的对象,@Controller 方法返回值类型如果为 ResponseEntity 或标注了 @ResponseBody 注解,此时处理器方法一旦执行结束,Spring 将使用 HandlerMethodReturnValueHandler 对返回值进行处理,具体来说会将返回值转换为 json 或 xml,然后写入响应,后续也不会进行视图渲染,这时postHandle 将没有机会修改响应体内容。
如果需要更改响应内容,可以定义一个实现 ResponseBodyAdvice 接口的类,然后将这个类直接定义到 RequestMappingHandlerAdapter 中的 requestResponseBodyAdvice 或通过 @ControllerAdvice 注解添加到 RequestMappingHandlerAdapter。
2.2 Spring MVC中提供的一些HandlerInterceptor接口实现类
1、AsyncHandlerInterceptor
继承HandlerInterceptor的接口,额外提供了afterConcurrentHandlingStarted方法,该方法是用来处理异步请求。当Controller中有异步请求方法的时候会触发该方法。 经过测试,异步请求先支持preHandle、然后执行afterConcurrentHandlingStarted。异步线程完成之后执行postHandle、afterCompletion。 有兴趣的读者可自行研究。
2、WebRequestInterceptor
与HandlerInterceptor接口类似,区别是WebRequestInterceptor的preHandle没有返回值。还有WebRequestInterceptor是针对请求的,接口方法参数中没有response。
public interface WebRequestInterceptor {
void preHandle(WebRequest request) throws Exception;
void postHandle(WebRequest request, @Nullable ModelMap model) throws Exception;
void afterCompletion(WebRequest request, @Nullable Exception ex) throws Exception;
}
AbstractHandlerMapping内部的interceptors是个Object类型集合。处理的时候判断为MappedInterceptor[加入到mappedInterceptors集合中];HandlerInterceptor、WebRequestInterceptor(适配成WebRequestHandlerInterceptorAdapter)[加入到adaptedInterceptors中]
3、MappedInterceptor
一个包括includePatterns和excludePatterns字符串集合并带有HandlerInterceptor的类。 很明显,就是对于某些地址做特殊包括和排除的拦截器。
4、ConversionServiceExposingInterceptor
默认的<annotation-driven/>标签初始化的时候会初始化ConversionServiceExposingInterceptor这个拦截器,并被当做构造方法的参数来构造MappedInterceptor。之后会被加入到AbstractHandlerMapping的mappedInterceptors集合中。该拦截器会在每个请求之前往request中丢入ConversionService。主要用于spring:eval标签的使用。
三、拦截器 Interceptor 使用及配置
3.1 实现拦截器
使用拦截器需要实现 HandlerInterceptor 接口,为了避免实现该接口的所有方法,Spring 5 之前提供了一个抽象的实现 HandlerInterceptorAdapter,Java 8 接口默认方法新特性出现后,我们直接实现 HandlerInterceptor 接口即可。
我们实现一个拦截器,示例如下:
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("请求来了");
// ture表示放行
return true;
}
}
Spring 配置通常有三种方式,分别是传统的 xml 、最新的注解配置以及通过 API 配置,拦截器也不例外。
3.2 配置拦截器
3.2.1 xml 文件配置
springmvc.xml 配置方式如下:
<mvc:interceptors >
<!-- 将拦截器类添加到Spring容器 -->
<bean class="com.zzuhkp.mvc.interceptor.LogInterceptor"/>
<!-- 设置拦截器 -->
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/login"/>
<bean class="com.zzuhkp.mvc.interceptor.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
- bean:mvc:interceptors 标签下的拦截器 bean 将应用到所有的处理器。
- mvc:interceptor:这个标签下的子标签可以指定拦截器应用到哪些请求路径。
- mvc:mapping:指定要处理的请求路径。
- mvc:exclude-mapping:指定要排除的请求路径。
- bean:指定要应用到给定路径的拦截器 bean。
<mvc:interceptors>这个标签是被InterceptorsBeanDefinitionParser类解析。
这里配置的每个<mvc:interceptor>都会被解析成MappedInterceptor类型的Bean。
其中
- 子标签<mvc:mapping path="/**"/>会被解析成MappedInterceptor的includePatterns属性;
- <mvc:exclude-mapping path="/**"/>会被解析成MappedInterceptor的excludePatterns属性;
- <bean/>会被解析成MappedInterceptor的interceptor属性。
3.2.2 注解配置
对于注解配置来说,需要将 MappedInterceptor 配置为 Spring 的 bean,和上述 xml 配置等价的注解配置如下:
// Configuration配置类
@Configuration
public class MvcConfig {
// 将logInterceptor拦截器添加到Spring容器
@Bean
public MappedInterceptor logInterceptor() {
return new MappedInterceptor(null, new LoginInterceptor());
}
// 将loginInterceptor拦截器添加到Spring容器
@Bean
public MappedInterceptor loginInterceptor() {
// 在MappedInterceptor构造方法中可以传入拦截器的配置信息
return new MappedInterceptor(new String[]{"/**"}, new String[]{"/login"}, new LoginInterceptor());
}
}
MappedInterceptor实现了HandlerInterceptor接口。可用来设置拦截器的配置信息。
3.2.3 API 配置
拦截器与 Spring MVC 环境紧密结合,并且是作用范围通常是全局性的,因此大多数情况建议使用这种方式配置。
与 xml 配置对应的 API 配置如下:
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
// 重写添加拦截器的方法,来注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器
registry.addInterceptor(new LogInterceptor());
// 可传入拦截器配置信息
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**").excludePathPatterns("/login");
}
}
这里在配置类上添加了@EnableWebMvc注解开启了 Spring MVC 中的某些特性,然后就可以实现 WebMvcConfigurer 接口中的 addInterceptors 方法向 Spring MVC 中添加拦截器。如果你使用了 spring-boot-starter-web,不再需要手工添加 @EnableWebMvc 注解。
四、拦截器 Interceptor 的执行顺序
通常情况下,我们并不需要关心多个拦截器的执行顺序,然而,如果一个拦截器依赖于另一个拦截器的执行结果,那么就需要注意了。使用多个拦截器后的 DispatcherServlet 请求处理流程可以简化为如下的流程图。
多个拦截器方法执行顺序如下:
- preHandle 按照拦截器的顺序先后执行。如果任意一次调用返回 false 则直接跳到拦截器的 afterCompletion 执行。
- postHandle 按照拦截器的逆序先后执行,也就说后面的拦截器先执行 postHandle。
- afterCompletion 也按照拦截器的逆序先后执行,后面的拦截器先执行 afterCompletion。
中断情况实例:
图1 正常流程
图2 中断流程
中断流程中,比如是HandlerInterceptor2中断的流程(preHandle返回false),此处仅调用它之前拦截器的preHandle返回true的afterCompletion方法。 这个底层原理看后面的源码分析就明白了,主要是由this.interceptorIndex这个变量控制的。
那么拦截器的顺序是如何指定的呢?
- 对于 xml 配置来说,Spring 将记录 bean 声明的顺序,先声明的拦截器将排在前面。
- 对于注解配置来说,由于通过反射读取方法无法保证顺序,因此需要在方法上添加@Order注解指定 bean 的声明顺序。
- 对应API配置来说,拦截器的顺序并非和添加顺序完全保持一致,为了控制先后顺序,需要自定义的拦截器实现Ordered接口。
注解配置指定顺序示例如下:
@Configuration
public class MvcConfig {
@Order(2)
@Bean
public MappedInterceptor loginInterceptor() {
return new MappedInterceptor(new String[]{"/**"}, new String[]{"/login"}, new LoginInterceptor());
}
@Order(1)
@Bean
public MappedInterceptor logInterceptor() {
return new MappedInterceptor(null, new LoginInterceptor());
}
}
此时虽然登录拦截器写在前面,但因为 @Order 注解指定的值较大,因此将排在日志拦截器的后面。
API配置指定顺序示例如下:
public class LoginInterceptor implements HandlerInterceptor, Ordered {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("已登录");
return true;
}
@Override
public int getOrder() {
return 2;
}
}
public class LogInterceptor implements HandlerInterceptor, Ordered {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("请求来了");
return true;
}
@Override
public int getOrder() {
return 1;
}
}
LogInterceptor 指定的排序号较 LoginInterceptor 来说比较小,因此 LogInterceptor 将排在前面。
五、拦截器 Interceptor 原理分析
DispatcherServlet 处理请求的代码位于 DispatcherServlet#doDispatch 方法,关于处理器和拦截器简化后的代码如下:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
//...
HandlerExecutionChain mappedHandler = null;
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
// 1.获取处理器执行链(从 HandlerMapping 获取处理器链)
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
// 2.获取处理器适配器
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
//...
//【3】.执行前置拦截器(拦截器 preHandle 执行(正序))
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 4.执行业务handler
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
//【5】.执行后置拦截器(拦截器 postHandle 执行(逆序))
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
//【6】.渲染视图,处理页面响应,同时也会去执行最终拦截器(拦截器 afterCompletion 执行(逆序))
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
}
}finally {
//...
}
}
可以看到,整体流程和我们前面描述是保持一致的。【3】、【5】、【6】步骤是对拦截器的执行处理,现在分别来查看第【3】、【5】、【6】步骤执行的具体方法的源码
5.1 applyPreHandle():执行拦截器 preHandle 方法
以拦截器预执行 preHandle 为例,看一下处理器执行链是怎么调用拦截器方法的。
HandlerExecutionChain.java
// 3.执行前置拦截器中的详细代码
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 获得本次请求对应的所有拦截器
// getInterceptors()是HandlerExecutionChain中的方法,获取到的是当前处理器执行链中所有的拦截器,也就是和当前请求的处理器映射器绑定在一起的所有拦截器
// 说明获得的拦截器都是用来拦截本次请求的,不会有别的请求的拦截器
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
// 按照拦截器顺序依次执行每个拦截器的preHandle方法。
// 并且,interceptorIndex值会一次 + 1 (该值是给后面的最终拦截器使用的)
for(int i = 0; i < interceptors.length; this.interceptorIndex = i++) {
HandlerInterceptor interceptor = interceptors[i];
// 只要每个拦截器不返回false,则继续执行,否则执行最终拦截器
if (!interceptor.preHandle(request, response, this.handler)) {
// 返回 false 则直接执行 afterCompletion
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
}
// 最终返回true
return true;
}
处理器链拿到拦截器列表后按照顺序(拦截器1、拦截器2)调用了拦截器的 preHandle 方法,如果返回 false 则跳到 afterCompletion 执行。
那处理器链中的拦截器的列表从哪来的呢?继续跟踪获取处理器链的方法DispatcherServlet#getHandler,可以发现获取处理器链的核心代码如下:
public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
implements HandlerMapping, Ordered, BeanNameAware {
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, LOOKUP_PATH);
// 将与当前处理器映射器绑定在一起的拦截器添加到处理器执行链中。this.adaptedInterceptors是AbstractHandlerMapping中的拦截器列表
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;
}
}
上面的源码显示Spring 创建处理器执行链 HandlerExecutionChain 后将 AbstractHandlerMapping 中拦截器列表 adaptedInterceptors 中的拦截器添加到了处理器执行链,那 AbstractHandlerMapping 中的拦截器列表中的拦截器又从哪来呢?
public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
implements HandlerMapping, Ordered, BeanNameAware {
private final List<Object> interceptors = new ArrayList<>();
private final List<HandlerInterceptor> adaptedInterceptors = new ArrayList<>();
// 该方法在创建HandlerMapping时,Spring会自动回调
@Override
protected void initApplicationContext() throws BeansException {
extendInterceptors(this.interceptors);
// 从Spring容器中获取全部拦截器
detectMappedInterceptors(this.adaptedInterceptors);
// 对拦截器进行适配,并且将其添加到AbstractHandlerMapping的adaptedInterceptors列表中
initInterceptors();
}
// 从容器中获取拦截器
protected void detectMappedInterceptors(List<HandlerInterceptor> mappedInterceptors) {
mappedInterceptors.addAll(
BeanFactoryUtils.beansOfTypeIncludingAncestors(
obtainApplicationContext(), MappedInterceptor.class, true, false).values());
}
// 拦截器适配
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");
}
// 将拦截器添加到AbstractHandlerMapping的adaptedInterceptors列表中
this.adaptedInterceptors.add(adaptInterceptor(interceptor));
}
}
}
}
各种 HandlerMapping 的实现都继承了 AbstractHandlerMapping,HandlerMapping 被容器创建时将回调#initApplicationContext方法,这个方法回调时会从容器中查找类型为 MappedInterceptor 的拦截器,然后对拦截器进行适配,这个流程是针对使用注解来实现的拦截器(MappedInterceptor类型)。Spring MVC 中如果使用了 @EnableWebMvc ,HandlerMapping bean 被创建时会回调WebMvcConfigurer#addInterceptors方法直接将拦截器设置到 AbstractHandlerMapping 中的 interceptors成员属性中。
MappedInterceptor类型的拦截器会被加到mappedInterceptors集合中,HandlerInterceptor类型的会被加到adaptedInterceptors集合中,WebRequestInterceptor类型的会被适配成WebRequestHandlerInterceptorAdapter加到adaptedInterceptors集合中。
5.2 applyPostHandle(): 执行拦截器 postHandle 方法
HandlerExecutionChain.java
// 5.执行后置拦截器
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
// 获得本次请求对应的所有拦截器
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
// 按逆序执行每个拦截器的postHandle方法,所以我们看到先执行的拦截器2的postHandle,再执行拦截器1的postHandle
for(int i = interceptors.length - 1; i >= 0; --i) {
HandlerInterceptor interceptor = interceptors[i];
// 执行拦截器的postHandle方法
interceptor.postHandle(request, response, this.handler, mv);
}
}
}
后置处理是按照拦截器顺序逆序处理的。
5.3 processDispatchResult(): 执行拦截器 afterCompletion 方法
DispatcherServlet.java
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
//...
if (mv != null && !mv.wasCleared()) {
// 处理响应,进行视图渲染
this.render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
} else if (this.logger.isDebugEnabled()) {
this.logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + this.getServletName() + "': assuming HandlerAdapter completed request handling");
}
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
// 6、执行最终拦截器
mappedHandler.triggerAfterCompletion(request, response, (Exception)null);
}
}
}
其中,有一个render()方法,该方法会直接处理完response。然后则是触发triggerAfterCompletion方法去执行本次请求对应的所有拦截器的afterCompletion 方法:
HandlerExecutionChain.java
// 6、执行拦截器的最终方法
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) throws Exception {
// 获得本次请求对应的所有拦截器
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
// 逆序执行每个拦截器(interceptorIndex为前置拦截器动态计算)的afterCompletion方法
// 由于this.interceptorIndex当前标记着最后一个执行到的前置拦截器下表,然后这里又是逆序遍历,所以只有成功执行了preHandle方法的拦截器,才回去执行其对应的afterCompletion方法
for(int i = this.interceptorIndex; i >= 0; --i) {
HandlerInterceptor interceptor = interceptors[i];
try {
// 执行拦截器的afterCompletion方法
interceptor.afterCompletion(request, response, this.handler, ex);
} catch (Throwable var8) {
logger.error("HandlerInterceptor.afterCompletion threw exception", var8);
}
}
}
}
由此可以看到,拦截器的最终方法的执行也是按照倒叙来执行的,而且是在视图渲染之后。
六、拦截器的应用
这里我们讲几个拦截器最常见的应用。
6.1 性能监控
如记录一下请求的处理时间,得到一些慢请求(如处理时间超过500毫秒),从而进行性能改进,一般的反向代理服务器如apache都具有这个功能,但此处我们演示一下使用拦截器怎么实现。
实现分析:
- 在进入处理器之前记录开始时间,即在拦截器的preHandle记录开始时间;
- 在结束请求处理之后记录结束时间,即在拦截器的afterCompletion记录结束实现,并用结束时间-开始时间得到这次请求的处理时间。
问题:
我们的拦截器是单例的,因此不管用户请求多少次都只有一个拦截器实现,即线程不安全,那我们应该怎么记录时间呢?
解决方案是使用ThreadLocal,它是线程绑定的变量,提供线程局部变量(一个线程一个ThreadLocal,A线程的ThreadLocal只能看到A线程的ThreadLocal,不能看到B线程的ThreadLocal)。
代码实现:
public class StopWatchHandlerInterceptor extends HandlerInterceptorAdapter {
private NamedThreadLocal<Long> startTimeThreadLocal = new NamedThreadLocal<Long>("StopWatch-StartTime");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1、开始时间
long beginTime = System.currentTimeMillis();
// 线程绑定变量(该数据只有当前请求的线程可见)
startTimeThreadLocal.set(beginTime);
// 继续流程
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 2、结束时间
long endTime = System.currentTimeMillis();
// 得到线程绑定的局部变量(开始时间)
long beginTime = startTimeThreadLocal.get();
// 3、消耗的时间
long consumeTime = endTime - beginTime;
// 此处认为处理时间超过500毫秒的请求为慢请求
if(consumeTime > 500) {
// TODO 记录到日志文件
System.out.println(String.format("%s consume %d millis", request.getRequestURI(), consumeTime));
}
}
}
NamedThreadLocal:Spring提供的一个命名的ThreadLocal实现。
在测试时需要把stopWatchHandlerInterceptor拦截器的排序设置成1,也就是放在拦截器链的第一个,这样得到的时间才是比较准确的。
6.2 登陆检测
在访问某些资源时(如订单页面),需要用户登录后才能查看,因此需要进行登录检测。
流程:
- 访问需要登录的资源时,由拦截器重定向到登录页面;
- 如果访问的是登录页面,拦截器不应该拦截;
- 用户登录成功后,往cookie/session添加登录成功的标识(如用户编号);
- 下次请求时,拦截器通过判断cookie/session中是否有该标识来决定继续流程还是到登录页面;
- 在此拦截器还应该允许游客访问的资源。
拦截器代码如下所示:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1、请求到登录页面则放行
if(request.getServletPath().startsWith(loginUrl)) {
return true;
}
// 2、TODO 比如退出、首页等页面无需登录,即此处要放行 允许游客的请求
// 3、如果用户已经登录则放行
if(request.getSession().getAttribute("username") != null) {
// 更好的实现方式是使用cookie
return true;
}
// 4、非法请求,即这些请求需要登录后才能访问
// 重定向到登录页面
response.sendRedirect(request.getContextPath() + loginUrl);
return false;
}
提示:推荐能使用servlet规范中的过滤器Filter实现相关功能的话,最好用Filter实现,因为HandlerInteceptor只有在Spring Web MVC环境下才能使用,因此Filter是最通用的、最先应该使用的。如登录这种拦截器最好使用Filter来实现。
七、总结
拦截器常用于初始化资源,权限监控,会话设置,资源清理等的功能设置。我们通过源码可以看到,拦截器类似于对我们业务方法的环绕通知效果,并且是通过循环收集好的拦截器集合来控制每个拦截器方法的执行顺序。要熟练运用拦截器,就需要我们对它的执行顺序完全掌握,做到深入掌握拦截器的执行机制!
总结 Spring MVC 整个拦截器相关的流程如下:
- HandlerMapping 被容器实例化并初始化。
- 初始化时默认从容器中查找类型为 MappedInterceptor 的拦截器添加到 HandlerMapping 中的拦截器列表,这种默认行为支持了 xml 和注解配置拦截器。
- 使用 @EnableWebMvc 注解后,Spring 通过 @Bean 创建 HandlerMapping bean,实例化后回调 WebMvcConfigurer#addInterceptors 将拦截器提前设置到 HandlerMapping 中的拦截器列表,这种行为支持了 API 配置拦截器。
- 客户端发起请求,DispatcherServlet 使用 HandlerMapping 查找处理器执行链,将 HandlerMapping 中的拦截器添加到处理器执行链 HandlerExecutionChain 中的拦截器列表。
- DispatcherServlet 按照拦截器的顺序依次调用拦截器中的回调方法。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)