【Spring MVC】Spring MVC拦截器(Interceptor)

目录

一、拦截器介绍

二、拦截器 Interceptor 定义

2.1 HandlerInterceptor接口

2.2 Spring MVC中提供的一些HandlerInterceptor接口实现类

1、AsyncHandlerInterceptor

2、WebRequestInterceptor

3、MappedInterceptor

4、ConversionServiceExposingInterceptor

三、拦截器 Interceptor 使用及配置

3.1 实现拦截器

3.2 配置拦截器

3.2.1 xml 文件配置

3.2.2 注解配置

3.2.3 API 配置

四、拦截器 Interceptor 的执行顺序

五、拦截器 Interceptor 原理分析

5.1 applyPreHandle():执行拦截器 preHandle 方法

5.2 applyPostHandle(): 执行拦截器 postHandle 方法

5.3 processDispatchResult(): 执行拦截器 afterCompletion 方法

六、拦截器的应用

6.1 性能监控

6.2 登陆检测

七、总结


一、拦截器介绍

Spring MVC中提供了处理器拦截器组件(Interceptor),拦截器在 Spring MVC 中的地位等同于 Servlet 规范中的过滤器 过滤器(Filter),用于对处理器进行预处理和后处理。

拦截器拦截的是处理器的执行,由于是全局行为,因此常用于做一些通用的功能,例如:

  1. 日志记录:记录请求信息的日志,以便进行信息监控、信息统计、计算PV(Page View)等。
  2. 权限检查:如登录检测,进入处理器检测检测是否登录,如果没有直接返回到登录页面;
  3. 性能监控:有时候系统在某段时间莫名其妙的慢,可以通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间(如果有反向代理,如apache可以自动记录);
  4. 通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取Locale、Theme信息等,只要是多个处理器都需要的即可使用拦截器实现。
  5. 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 定义的控制器。

对上面的流程图继续细化:

三个方法具体的执行流程如下:

  1. preHandle:处理器执行之前执行,如果返回 false 将跳过处理器、拦截器 postHandle 方法、视图渲染等,直接执行拦截器 afterCompletion 方法。
  2. postHandle:处理器执行后,视图渲染前执行,如果处理器抛出异常,将跳过该方法直接执行拦截器 afterCompletion 方法。
  3. afterCompletion:视图渲染后执行,不管处理器是否抛出异常,该方法都将执行。

注意:自从前后端分离之后,Spring MVC 中的处理器方法执行后通常不会再返回视图,而是返回表示 json 或 xml 的对象,@Controller 方法返回值类型如果为 ResponseEntity 或标注了 @ResponseBody 注解,此时处理器方法一旦执行结束,Spring 将使用 HandlerMethodReturnValueHandler 对返回值进行处理,具体来说会将返回值转换为 json 或 xml,然后写入响应,后续也不会进行视图渲染,这时postHandle 将没有机会修改响应体内容

如果需要更改响应内容,可以定义一个实现 ResponseBodyAdvice 接口的类,然后将这个类直接定义到 RequestMappingHandlerAdapter 中的 requestResponseBodyAdvice 或通过 @ControllerAdvice 注解添加到 RequestMappingHandlerAdapter。

2.2 Spring MVC中提供的一些HandlerInterceptor接口实现类

1AsyncHandlerInterceptor

继承HandlerInterceptor的接口,额外提供了afterConcurrentHandlingStarted方法,该方法是用来处理异步请求。当Controller中有异步请求方法的时候会触发该方法。 经过测试,异步请求先支持preHandle、然后执行afterConcurrentHandlingStarted。异步线程完成之后执行postHandle、afterCompletion。 有兴趣的读者可自行研究。

2WebRequestInterceptor

与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中]

3MappedInterceptor

一个包括includePatterns和excludePatterns字符串集合并带有HandlerInterceptor的类。 很明显,就是对于某些地址做特殊包括和排除的拦截器。

4ConversionServiceExposingInterceptor

默认的<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 {@Overridepublic 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容器@Beanpublic MappedInterceptor logInterceptor() {return new MappedInterceptor(null, new LoginInterceptor());}// 将loginInterceptor拦截器添加到Spring容器@Beanpublic 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 {// 重写添加拦截器的方法,来注册拦截器@Overridepublic 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 请求处理流程可以简化为如下的流程图。

多个拦截器方法执行顺序如下:

  1. preHandle 按照拦截器的顺序先后执行。如果任意一次调用返回 false 则直接跳到拦截器的 afterCompletion 执行。
  2. postHandle 按照拦截器的逆序先后执行,也就说后面的拦截器先执行 postHandle。
  3. 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)@Beanpublic MappedInterceptor loginInterceptor() {return new MappedInterceptor(new String[]{"/**"}, new String[]{"/login"}, new LoginInterceptor());}@Order(1)@Beanpublic MappedInterceptor logInterceptor() {return new MappedInterceptor(null, new LoginInterceptor());}}

此时虽然登录拦截器写在前面,但因为 @Order 注解指定的值较大,因此将排在日志拦截器的后面。

API配置指定顺序示例如下:

public class LoginInterceptor implements HandlerInterceptor, Ordered {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("已登录");return true;}@Overridepublic int getOrder() {return 2;}
}public class LogInterceptor implements HandlerInterceptor, Ordered {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("请求来了");return true;}@Overridepublic 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.执行业务handlermv = 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 则直接执行 afterCompletionthis.triggerAfterCompletion(request, response, (Exception)null);return false;}}}// 最终返回truereturn true;
}

处理器链拿到拦截器列表后按照顺序(拦截器1、拦截器2)调用了拦截器的 preHandle 方法,如果返回 false 则跳到 afterCompletion 执行。

那处理器链中的拦截器的列表从哪来的呢?继续跟踪获取处理器链的方法DispatcherServlet#getHandler,可以发现获取处理器链的核心代码如下:

public abstract class AbstractHandlerMapping extends WebApplicationObjectSupportimplements 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 WebApplicationObjectSupportimplements HandlerMapping, Ordered, BeanNameAware {private final List<Object> interceptors = new ArrayList<>();private final List<HandlerInterceptor> adaptedInterceptors = new ArrayList<>();// 该方法在创建HandlerMapping时,Spring会自动回调@Overrideprotected 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的postHandlefor(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都具有这个功能,但此处我们演示一下使用拦截器怎么实现。 

实现分析:

  1. 在进入处理器之前记录开始时间,即在拦截器的preHandle记录开始时间;
  2. 在结束请求处理之后记录结束时间,即在拦截器的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 登陆检测

在访问某些资源时(如订单页面),需要用户登录后才能查看,因此需要进行登录检测。 

流程:

  1. 访问需要登录的资源时,由拦截器重定向到登录页面;
  2. 如果访问的是登录页面,拦截器不应该拦截;
  3. 用户登录成功后,往cookie/session添加登录成功的标识(如用户编号);
  4. 下次请求时,拦截器通过判断cookie/session中是否有该标识来决定继续流程还是到登录页面;
  5. 在此拦截器还应该允许游客访问的资源。 

拦截器代码如下所示:

@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 整个拦截器相关的流程如下:

  1. HandlerMapping 被容器实例化并初始化。
    1. 初始化时默认从容器中查找类型为 MappedInterceptor 的拦截器添加到 HandlerMapping 中的拦截器列表,这种默认行为支持了 xml 和注解配置拦截器。
    2. 使用 @EnableWebMvc 注解后,Spring 通过 @Bean 创建 HandlerMapping bean,实例化后回调 WebMvcConfigurer#addInterceptors 将拦截器提前设置到 HandlerMapping 中的拦截器列表,这种行为支持了 API 配置拦截器。
  2. 客户端发起请求,DispatcherServlet 使用 HandlerMapping 查找处理器执行链,将 HandlerMapping 中的拦截器添加到处理器执行链 HandlerExecutionChain 中的拦截器列表。
  3. DispatcherServlet 按照拦截器的顺序依次调用拦截器中的回调方法。

相关文章:【Spring MVC】Spring MVC的执行流程与源码分析

                  【Spring MVC】处理器映射器:AbstractHandlerMethodMapping源码分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/278730.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

研究人员发现 OpenAI ChatGPT、Google Gemini 的漏洞

自 OpenAI 推出 ChatGPT 以来&#xff0c;生成式 AI 聊天机器人的数量及其在企业中的采用率在一年多时间里呈爆炸式增长&#xff0c;但网络安全专业人士的担忧也随之增加&#xff0c;他们不仅担心威胁组织对新兴技术的使用&#xff0c;还担心大型网络的安全性及模型&#xff08…

Chrome历史版本下载地址:Google Chrome Older Versions Download (Windows, Linux Mac)

最近升级到最新版本Chrome后发现页面居然显示错乱,是在无语, 打算退回原来的版本, 又发现官方只提供最新的版本下载, 为了解决这个问题所有收集了Chrome历史版本的下载地址分享给大家. Google Chrome Windows version 32-bit VersionSizeDate104.0.5112.10279.68 MB2022-05-30…

【小迪安全】学习cho1

介绍了一些名词&#xff1a; POC、EXP、Payload与Shellcode nc -lvvp 端口号 监听服务器端口 个人用机使用最多的是&#xff1a;windows10 服务器用机使用最多的是&#xff1a;Windows8&#xff0c;12&#xff0c;16 流量被防火墙拦截了&#xff0c;到这里进行给与权限 文件…

【CNN轻量化】RepViT: Revisiting Mobile CNN From ViT Perspective

RepViT: Revisiting Mobile CNN From ViT Perspective 论文链接&#xff1a;https://arxiv.org/abs/2307.09283 代码链接&#xff1a;https://github.com/THU-MIG/RepViT 一、摘要 探究了许多轻量级ViTs和轻量级CNNs之间的结构联系。文中从ViT的视角重新审视轻量级CNNs的高效…

Java 中的泛型(两万字超全详解)

文章目录 前言一、泛型概述1. 什么是泛型&#xff1f;为什么要使用泛型&#xff1f;2. 泛型使用场景3. 泛型概述小结 二、泛型类1. 泛型类的定义2. 泛型类的使用 三、泛型接口四、泛型方法1. 泛型方法的定义2. 泛型方法的使用3. 泛型方法中的类型推断 五、类型擦除1. 什么是类型…

本地虚拟机平台Proxmox VE结合Cpolar内网穿透实现公网远程访问

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《C》 《Linux》 《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&…

扇区架次数动态展示

打开前端Vue项目&#xff1a;kongguan_web&#xff0c;完成前端src/components/echart/SectorFlightChart.vue页面设计&#xff0c;使用ECharts插件实现柱状图和饼状图 在src/components目录下创建echart目录&#xff0c;完成src/components/echart/SectorFlightChart.vue 的页…

C++开发基础——类对象与构造析构

一、基础概念 类&#xff1a;用户自定义的数据类型。 对象&#xff1a;类类型的变量&#xff0c;类的实例。 类的成员&#xff1a;成员变量和成员函数。 成员变量&#xff1a;类中定义的变量。 成员函数&#xff1a;类中定义的函数。 定义类的代码样例&#xff1a; class…

嵌入式学习第二十九天!(数据结构的概念、单向链表)

数据结构&#xff1a; 1. 定义&#xff1a; 一组用来保存一种或者多种特定关系的数据的集合&#xff08;组织和存储数据&#xff09; 1. 程序设计&#xff1a; 将现实中大量而复杂的问题以特定的数据类型和特定的数据结构存储在内存中&#xff0c;并在此基础上实现某个特定的功…

【排序】快速排序

原理 对于一个数组x&#xff0c;快速排序流程如下&#xff1a; 确定分界点a&#xff0c;可以取x[l]、x[r]、x[l r / 2]、随机&#xff08;四种都可以&#xff09;调整区间&#xff0c;使得&#xff1a;区间被分成 < a 和 > a的两部分&#xff0c;左边 < a&#xff…

Jenkins-pipeline流水线构建完钉钉通知

添加钉钉机器人 在钉钉群设置里添加机器人拿出Webhook地址&#xff0c;设置关键词 Jenkins安装钉钉插件 Dashboard > 系统管理 > 插件管理&#xff0c;搜索构建通知&#xff0c;直接搜索Ding Talk也行 安装DingTalk插件&#xff0c;重启Jenkins 来到Dashboard > 系…

【物联网应用】基于云计算的智能化温室种植一体化平台

目录 第一章 作品概述 1.1. 作品名称 1.2. 应用领域 1.3.主要功能 1.4.创新性说明 第二章 需求分析 2.1 现实背景 2.2 用户群体及系统功能 2.3 竞品分析 第三章 技术方案 3.1. 硬件组成与来源 3.2. 硬件设计合理性 3.3. 硬件系统设计图 3.4. 接口的通用性与可扩展性 3.5. 代码规…

【ARM】DSTREAM上面的各个指示灯代表什么意思?

【更多软件使用问题请点击亿道电子官方网站查询】 1、 文档目标 对于DStream仿真器上面的指示灯亮灭代表的意义进行分析。 2、 问题场景 主要对于DStream仿真器的使用过程中&#xff0c;不同的情况下面仿真器的指示灯会进行相应的亮灭。了解一下不同指示灯的亮灭所提示的信息…

注册个人小程序

访问地址 https://mp.weixin.qq.com/ 立即注册 选择小程序 注册 填写信息 登录邮箱 访问邮箱的链接激活账号 选择个人&#xff0c;填写信息 注册完成&#xff0c;即可登录进入填写信息

idea 开发serlvet班级通讯录管理系统idea开发mysql数据库web结构计算机java编程layUI框架开发

一、源码特点 idea开发 java servlet 班级通讯录管理系统是一套完善的web设计系统mysql数据库 系统采用serlvetdaobean mvc 模式开发&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 servlet 班…

【iOS】——Blocks

文章目录 前言一、Blocks概要1.什么是Blocks 二、Block模式1.block语法2.block类型变量3.截获自动变量值4._Block修饰符5.截获的自动变量 三、Blocks的实现1.Block的实质2.截获自动变量值3._Block说明符4.Block存储域 前言 一、Blocks概要 1.什么是Blocks Blocks是C语言的扩…

使用jenkins-pipeline进行利用项目文件自动化部署到k8s上

Discard old builds:丢弃旧的构建,目的是管理存储空间、提升性能以及保持环境整洁 Do not allow concurrent builds: 禁止并发构建是指同一时间内只允许一个构建任务执行,避免多个构建同时运行可能带来的问题 Do not allow the pipeline to resume if the controller resta…

DockerHub搜索并拉取一个Redis镜像

1&#xff09;去DockerHub搜索Redis镜像 2&#xff09;查看Redis镜像的名称和版本 3&#xff09;利用docker pull命令拉取镜像 4&#xff09;利用docker save命令将 redis:latest打包为一个redis.tar包 5&#xff09;利用docker rmi 删除本地的redis:latest 6&#xff09;利用…

Flutter-自定义图片3D画廊

效果 需求 3D画廊效果 设计内容 StackGestureDetectorTransformPositioned数学三角函数 代码实现 具体代码大概300行 import dart:math;import package:flutter/material.dart; import package:flutter_xy/widgets/xy_app_bar.dart;import ../../r.dart;class ImageSwitc…

zookeeper集群安装部署和集群异常处理

准备jdk和zookeeper安装包【官网即可下载】 zookeeper-3.5.1-alpha.tar.gz jdk1.7.0_8020200612.tar 准备三台linux虚拟机【具体以项目实际需要为准】&#xff0c;并安装jdk和zookeeper 虚拟机地址如下&#xff1a;194.1.1.86&#xff08;server.1&#xff09;、194.1.1.74…