Spring MVC中提供了处理器组件(Interceptor),在 Spring MVC 中的地位等同于 Servlet 规范中的过滤器 过滤器(Filter),用于对处理器进行预处理和后处理。
拦截的是处理器的执行,由于是全局行为,因此常用于做一些通用的功能,例如:
本质也是AOP(面向切面编程),也就是说符合横切关注点的所有功能都可以放入实现。
我们把 Spring MVC DispatcherServlet 请求处理流程这张图拿出来。
当浏览器发起的请求到达 Servlet 容器,DispatcherServlet 先根据处理器映射器 HandlerMapping 获取处理器,这时候获取到的是一个包含处理器和的处理器执行链,处理器执行之前将会先执行。
不包含的情况下,DispatcherServlet 处理请求的流程可以简化如下:
添加了做登录检查后,DispatcherServlet 请求处理的流程可以简化如下:
事实上的执行流程远比上述 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才两个,这是怎么回事呢?马上分析。
通过源码我们还可以发现,这三个方法都有的 handler 参数表示处理器,通常情况下可以表示我们使用注解 @Controller 定义的控制器。
对上面的流程图继续细化:
三个方法具体的执行流程如下:
注意:自从前后端分离之后,Spring MVC 中的处理器方法执行后通常不会再返回视图,而是返回表示 json 或 xml 的对象,@Controller 方法返回值类型如果为 ResponseEntity 或标注了 @ResponseBody 注解,此时处理器方法一旦执行结束,Spring 将使用 HandlerMethodReturnValueHandler 对返回值进行处理,具体来说会将返回值转换为 json 或 xml,然后写入响应,后续也不会进行视图渲染,这时postHandle 将没有机会修改响应体内容。
如果需要更改响应内容,可以定义一个实现 ResponseBodyAdvice 接口的类,然后将这个类直接定义到 RequestMappingHandlerAdapter 中的 requestResponseBodyAdvice 或通过 @ControllerAdvice 注解添加到 RequestMappingHandlerAdapter。
继承HandlerInterceptor的接口,额外提供了afterConcurrentHandlingStarted方法,该方法是用来处理异步请求。当Controller中有异步请求方法的时候会触发该方法。 经过测试,异步请求先支持preHandle、然后执行afterConcurrentHandlingStarted。异步线程完成之后执行postHandle、afterCompletion。 有兴趣的读者可自行研究。
与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中]
默认的<annotation-driven/>标签初始化的时候会初始化ConversionServiceExposingInterceptor这个,并被当做构造方法的参数来构造MappedInterceptor。之后会被加入到AbstractHandlerMapping的mappedInterceptors集合中。该会在每个请求之前往request中丢入ConversionService。主要用于spring:eval标签的使用。
使用需要实现 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 配置,也不例外。
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>
<mvc:interceptors>这个标签是被InterceptorsBeanDefinitionParser类解析。
这里配置的每个<mvc:interceptor>都会被解析成MappedInterceptor类型的Bean。
其中
对于注解配置来说,需要将 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接口。可用来设置的配置信息。
与 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 注解。
通常情况下,我们并不需要关心多个的执行顺序,然而,如果一个依赖于另一个的执行结果,那么就需要注意了。使用多个后的 DispatcherServlet 请求处理流程可以简化为如下的流程图。
多个方法执行顺序如下:
中断情况实例:
图1 正常流程
图2 中断流程
中断流程中,比如是HandlerInterceptor2中断的流程(preHandle返回false),此处仅调用它之前的preHandle返回true的afterCompletion方法。 这个底层原理看后面的源码分析就明白了,主要是由this.interceptorIndex这个变量控制的。
那么的顺序是如何指定的呢?
注解配置指定顺序示例如下:
@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 将排在前面。
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】步骤执行的具体方法的源码
以预执行 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集合中。
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);
}
}
}
后置处理是按照顺序逆序处理的。
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);
}
}
}
}
由此可以看到,的最终方法的执行也是按照倒叙来执行的,而且是在视图渲染之后。
这里我们讲几个最常见的应用。
如记录一下请求的处理时间,得到一些慢请求(如处理时间超过500毫秒),从而进行性能改进,一般的反向代理服务器如apache都具有这个功能,但此处我们演示一下使用怎么实现。
实现分析:
问题:
我们的是单例的,因此不管用户请求多少次都只有一个实现,即线程不安全,那我们应该怎么记录时间呢?
解决方案是使用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,也就是放在链的第一个,这样得到的时间才是比较准确的。
在访问某些资源时(如订单页面),需要用户登录后才能查看,因此需要进行登录检测。
流程:
代码如下所示:
@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 整个相关的流程如下:
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- cepb.cn 版权所有 湘ICP备2022005869号-7
违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务