Spring HandlerInterceptor工作机制
本文以一个通过正常注册拦截器流程注册拦截器失败的实际场景,来带领大家阅读源码,体会Spring的HandlerInterceptor拦截器整个工作流程
简单认识
org.springframework.web.servlet.HandlerInterceptor是Spring框架中的一个接口,用于拦截处理程序(Handler)的请求和响应。它允许开发人员在请求处理程序执行之前和之后执行自定义的预处理和后处理逻辑。 HandlerInterceptor接口定义了三个方法:
- preHandle:在请求处理程序执行之前调用。可以用于进行权限验证、日志记录等操作。如果该方法返回false,则请求将被中断,后续的拦截器和处理程序将不会被执行。
- postHandle:在请求处理程序执行之后、视图渲染之前调用。可以对请求的结果进行修改或添加额外的模型数据。
- afterCompletion:在整个请求完成之后调用,包括视图渲染完毕。可用于进行资源清理等操作。 通过实现HandlerInterceptor接口,可以自定义拦截器,并将其注册到Spring MVC的配置中。拦截器可以拦截指定的URL或者所有请求,并在请求的不同阶段执行相应的逻辑。
正常的拦截器注册流程
定义一个拦截器,实现org.springframework.web.servlet.HandlerInterceptor接口
如:
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// ...
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
// ...
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
// ...
}
}
定义配置类,实现WebMvcConfigurer,实现addInterceptors进行拦截器注册
@Configuration(proxyBeanMethods = false)
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// ...
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns(HOTSWAP_ALL_SERVICE)
.addPathPatterns(hsAddPaths)
.excludePathPatterns(commonExcludePaths);
// ...
}
}
而有时候通过这种方式注册不成功。
我们先来说一下说一下HandlerInterceptor的工作机制。首先以上描述的是拦截器的注册过程。
然后再来看一下注册的拦截器存储在哪里,以及如何被使用的。
存储已注册拦截器的位置
查看接口方法org.springframework.web.servlet.config.annotation.WebMvcConfigurer#addInterceptors的调用之处,位于类org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration里面,其是WebMvcConfigurationSupport的子类
WebMvcConfigurationSupport的一个子类,它检测并委托给所有WebMvcConfigurer类型的bean,允许它们自定义WebMvcConfigurationSupport提供的配置。这是@EnableWebMvc实际导入的类。
org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration#addInterceptors方法:
@Override
protected void addInterceptors(InterceptorRegistry registry) {
this.configurers.addInterceptors(registry);
}
可以看出,这是从我们注册的WebMvcConfigurer实现类中,通过传入的InterceptorRegistry实例搜集拦截器,而调用该方法的地方位于
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#requestMappingHandlerMapping,实际是其默认子类DelegatingWebMvcConfiguration进行注册RequestMappingHandlerMapping bean 的行为,在其方法中调用了org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#getInterceptors方法,解析InterceptorRegistry里面添加的拦截器。
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#getInterceptors
* Provide access to the shared handler interceptors used to configure
* {@link HandlerMapping} instances with.
* <p>This method cannot be overridden; use {@link #addInterceptors} instead.
*/
protected final Object[] getInterceptors(
FormattingConversionService mvcConversionService,
ResourceUrlProvider mvcResourceUrlProvider) {
if (this.interceptors == null) {
InterceptorRegistry registry = new InterceptorRegistry();
addInterceptors(registry);
registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService));
registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider));
// 排序
this.interceptors = registry.getInterceptors();
}
return this.interceptors.toArray();
}
整个过程就是,WebMvcConfigurationSupport在注册RequestMappingHandlerMapping时候,会创建一个InterceptorRegistry实例, 被传入了org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration#addInterceptors方法,进而传入了org.springframework.web.servlet.config.annotation.WebMvcConfigurerComposite#addInterceptors方法,最后被每个WebMvcConfigurer的实现类的addInterceptors方法所接收,用于搜集用户自定义的拦截器注册。即帮助配置映射拦截器列表。
注册失败原因
我们来看一下下面这个注册失败的场景:
@Configuration
@ConditionalOnProperty(name = "spring.mvc.configuration.custom", havingValue = "false", matchIfMissing = true)
public class CustomSpringMvcConfiguration extends WebMvcConfigurationSupport {
// ...
}
原因是一个公共的jar包注册了一个的子类,导致默认的注册子类DelegatingWebMvcConfiguration没有被注册,而其对方法的重写,也没有复写DelegatingWebMvcConfiguration的默认实现。
@Override
protected void addInterceptors(InterceptorRegistry registry) {
if (auditTrailLogInterceptor != null) {
registry.addInterceptor(auditTrailLogInterceptor).addPathPatterns("/**");
log.info("已註冊 AuditTrailLog 攔截器");
}
if (openApiLogInterceptor != null) {
registry.addInterceptor(openApiLogInterceptor).addPathPatterns("/**");
log.info("已註冊 openApiLog 攔截器");
}
super.addInterceptors(registry);
}
其中
super.addInterceptors(registry);
是个无效的语句,因为父类是一个空实现。相当于文首的注册拦截器方法被堵死了,迫于无法直接修改jar包的情况,我们只能另寻方法。
初步尝试
由于所有注册的拦截器,最终都是放在org.springframework.web.servlet.handler.AbstractHandlerMapping#interceptors中,所以我们要想办法拿到AbstractHandlerMapping的注册bean,即上面提到的org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#requestMappingHandlerMapping方法注册的bean,然后将我们的拦截器注册并放置进去。
既然上述注册方法行不通,那么我们的配置类就无需实现WebMvcConfigurer接口了。
我们需要重写一个配置类,注入RequestMappingHandlerMapping对象,然后进行手动注册。直接在构造函数中注册。
@Configuration(proxyBeanMethods = false)
public class WebMvcConfiguration {
public WebMvcConfiguration(
@Qualifier("requestMappingHandlerMapping") RequestMappingHandlerMapping mapping) {
// 反射拿到已經註冊的攔截器集合
List<Object> interceptors = ReflectionUtil.getField(mapping, "interceptors");
InterceptorRegistry registry = new InterceptorRegistry();
registry.addInterceptor(new LoginInterceptor()).addPathPatterns(urlPatterns);
List<Object> list = ReflectionUtil.invoke(registry, "getInterceptors", null, null);
interceptors.addAll(list);
}
}
先通过反射拿到interceptors变量,然后new一个InterceptorRegistry,将其添加进去后反射调用其getInterceptors,并添加到RequestMappingHandlerMapping的变量interceptors当中去。
这似乎完成了注册,万事大吉了,但当我启动项目,发现还想没有注册成功。这就令我费解了。到底是哪里出了问题?
拦截器的使用
后面经过大神的指点,可以从以下思路寻找答案:
因为我这个拦截器拦截了部分路径而已,之所以说没注册成功,是因为拦截的这些路径没有被拦截到。那既然存放的地方已经解决了,那问题就应该从使用的地方寻找答案。
这些拦截器,都带着器拦截的路径信息,而sping的请求,都会进入一个Servlet,因此这些存储的拦截器,必定是在这个Servlet中使用,因为Servlet能拿到请求的HttpServletRequest信息,请求的路径正是从这里拿到。
这个Servlet,就是DispatcherServlet。
我们要查看DispatcherServlet是在哪里使用到这些拦截器的
在org.springframework.web.servlet.DispatcherServlet#doService中,会调用org.springframework.web.servlet.DispatcherServlet#doDispatch方法
/** 处理对处理程序的实际调度。 处理程序将通过按顺序应用servlet的HandlerMappings来获得。HandlerAdapter将通过查询servlet已安装的HandlerAdapter来获得,以找到第一个支持处理程序类的HandlerAdapter。 所有HTTP方法都由这个方法处理。由HandlerAdapters或处理程序本身来决定哪些方法是可接受的。
参数:
request -当前HTTP请求
response -当前HTTP响应
抛出:
异常——在任何处理失败的情况下 */
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
其中的
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
就是确定这次请求的所有的匹配的HandlerInterceptor,也就是说,这里会从RequestMappingHandlerMapping对象获取所有注册的HandlerInterceptor,然后根据request的路径匹配,觉得哪些拦截器是该拦截该请求的,并根据顺序形成拦截链。
这一关键操作的位置,位于org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandlerExecutionChain
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);
// 遍历所有拦截器,进行匹配,匹配上的假如拦截链
for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
if (interceptor instanceof MappedInterceptor) {
MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
chain.addInterceptor(mappedInterceptor.getInterceptor());
}
}
else {
chain.addInterceptor(interceptor);
}
}
return chain;
}
以上代码大概的流程就是渠道所有的拦截器,和取到的请求路径进行正则匹配,匹配上的话,就添加进入拦截链
org.springframework.web.servlet.handler.MappedInterceptor#matches
用到的工具类是org.springframework.util.AntPathMatcher,PathMatcher的默认实现类
ant风格路径模式的PathMatcher实现。
部分映射代码是从Apache Ant中借来的。
该映射使用以下规则匹配url:
? 匹配一个字符
*匹配零个或多个字符
** 匹配路径中的零个或多个目录
{spring:[a-z]+}匹配regexp [a-z]+作为名为"spring"的路径变量
例子
com/t?st .jsp -匹配com/test.jsp,也匹配com/ taste .jsp或com/txst.jsp .jsp com/t?st.jsp — matches com/test.jsp but also com/tast.jsp or com/txst.jsp
com/.jsp — 匹配所有在com文件夹中的 .jsp
com/ * * /test.jsp — 匹配com路径下的所有 test.jsp文件
org/springframework/ * * /*.jsp —匹配org/springframework路径下的所有.jspcom/{filename:\w+}.jsp 匹配com/test.jsp 并将值 test 赋值给 filename 变量
注意:模式和路径必须都是绝对的,或者都是相对的,以便两者匹配。因此,建议使用此实现的用户对模式进行消毒,以便在使用模式的上下文中使用“/”作为前缀。
重点来了,细心的朋友会发现, 其遍历的拦截器列表,是org.springframework.web.servlet.handler.AbstractHandlerMapping#adaptedInterceptors,不是我们上面提及的org.springframework.web.servlet.handler.AbstractHandlerMapping#interceptors,这也就解释了为啥我们的拦截器好像没注册成功的原因,原来还差一步没衔接上。
因此我们要找一下org.springframework.web.servlet.handler.AbstractHandlerMapping#interceptors是如何转换成org.springframework.web.servlet.handler.AbstractHandlerMapping#adaptedInterceptors的
通过查看的其被调用add的方法,可以找到下面转换方法
org.springframework.web.servlet.handler.AbstractHandlerMapping#initInterceptors
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));
}
}
}
其转换的时机是在应用上下文初始化成功后通知的时候
org.springframework.context.support.ApplicationObjectSupport#setApplicationContext
ApplicationObjectSupport实现了ApplicationContextAware接口
最终实现
至此拦截器的整个工作流程正式完成了闭环。由于initInterceptors是被保护的方法,我们同样需要解决反射工具来完成调用。因此最终的拦截器注册配置类实现如下
@Configuration(proxyBeanMethods = false)
public class WebMvcConfiguration {
public WebMvcConfiguration(
@Qualifier("requestMappingHandlerMapping") RequestMappingHandlerMapping mapping,
Environment environment) {
// 从配置文件中读取拦截路径的配置
String patternConfig = environment.getProperty("app.config.client-filter-url-patterns");
List<String> urlPatterns = new ArrayList<>();
if (StringUtils.hasText(patternConfig)) {
urlPatterns = Arrays.asList(patternConfig.split(SymbolConsts.COMMA));
}
// 反射拿到已经注册的拦截器存放点
List<Object> interceptors = ReflectionUtil.getField(mapping, "interceptors");
InterceptorRegistry registry = new InterceptorRegistry();
registry.addInterceptor(new LoginInterceptor()).addPathPatterns(urlPatterns);
List<Object> list = ReflectionUtil.invoke(registry, "getInterceptors", null, null);
// 添加进去
interceptors.addAll(list);
// 防止有已经转换完成的拦截器被覆盖或者冲突,先清空已经init的adaptedInterceptor
List<HandlerInterceptor> adaptedInterceptors = ReflectionUtil.getField(mapping, "adaptedInterceptors");
adaptedInterceptors.clear();
// 重新init
ReflectionUtil.invoke(mapping, "initInterceptors", null, null);
}
}
Spring HandlerInterceptor工作机制的更多相关文章
- Spring Ioc工作机制 初步
Spring IoC工作原理 Spring 启动时读取应用程序提供的Bean配置信息,并在Spring容器中生成一份相应的Bean配置注册表,然后根据这张注册表实例化Bean,装配好Bean之间的依赖 ...
- Spring是什么,Spring容器提供了那些功能,Spring的工作机制
spring是什么 spring 是一个轻型的容器,是J2EE规范的轻量级实现,可以一站式开发,其中提供了,bean工厂,用以构造我们需要的Model ,spring 是非侵入式的,spring应用中 ...
- spring 内部工作机制(二)
本章节讲Spring容器从加载配置文件到创建出一个完整Bean的作业流程及参与的角色. Spring 启动时读取应用程序提供的Bean配置信息,并在Spring容器中生成一份相应的Bean配置注册表, ...
- Spring学习记录2——简单了解Spring容器工作机制
简单的了解Spring容器内部工作机制 Spring的AbstractApplicationContext是ApplicationContext的抽象实现类,该抽象类的refresh()方法定义了Sp ...
- 浅析Spring MVC工作机制
1.如何使用Spring MVC? 在web.xml中配置一个DispatcherServlet DispatchServlet初始化的时候会去寻找一个在应用程序的WEB-INF目录下的配置文件,命名 ...
- spring 内部工作机制(一)
Spring内部机制的内容较多,所以打算多分几个阶段来写. 本章仅探索Spring容器启动做了哪些事: 前言: 都说Spring容器就像一台构造精妙的机器,此话一点不假,我们通过配置文件向机器传达控制 ...
- 手写Spring框架,加深对Spring工作机制的理解!
在我们的日常工作中,经常会用到Spring.Spring Boot.Spring Cloud.Struts.Mybatis.Hibernate等开源框架,有了这些框架的诞生,平时的开发工作量也是变得越 ...
- spring工作机制
Hibernate.struts,还差一个spring 就一起发出去.. spring工作机制及为什么要用? 1.springmvc请所有的请求都提交给DispatcherServlet,它会委托应用 ...
- Spring容器技术内幕之内部工作机制
引言 Spring容器就像一台构造精妙的机器,我们通过配置文件向机器传达控制信息,机器就能够按照设定的模式工作.如果将Spring容器比作一辆车,那么可以将BeanFactory看成汽车的发动机,而A ...
- Hibernate工作原理及为什么要用?. Struts工作机制?为什么要使用Struts? spring工作机制及为什么要用?
三大框架是用来开发web应用程序中使用的.Struts:基于MVC的充当了其中的试图层和控制器Hibernate:做持久化的,对JDBC轻量级的封装,使得我们能过面向对象的操作数据库Spring: 采 ...
随机推荐
- 2022-02-26:k8s安装swagger,yaml如何写?
2022-02-26:k8s安装swagger,yaml如何写? 答案2022-02-26: yaml如下: apiVersion: apps/v1 kind: Deployment metadata ...
- Java程序设计复习提纲(下:图形界面)
目录 上:Java程序设计复习提纲(上:入门语法) - 孤飞 - 博客园 (cnblogs.com) 基本语法与编译运行 数据类型和关键字 常用语法 数组与字符串 异常处理 中:Java程序设计复习提 ...
- ConcurrentHashMap是如何实现的?
众所周知 ConcurrentHashMap 是 HashMap 的多线程版本,HashMap 在并发操作时会有各种问题,比如死循环问题.数据覆盖等问题.而这些问题,只要使用 ConcurrentHa ...
- odoo总结---类继承和视图继承
类继承 自从有了类,就有继承,继承是类最大的特性,ODOO开发有不例外,先ODOO集采总结如下: 1)类继承:扩展类中没有_name属性,因为它继承了父类的_name.对现有模型的扩展, 添加新功能, ...
- 如何科学地利用MTTR优化软件交付流程?
谷歌提出的衡量 DevOps 质量的 DORA 指标让 MTTR(平均恢复时间) 名声大振.在本文中,你将了解到 MTTR 的作用.为什么它对行业研究很有用.你可能被它误导的原因以及如何避免 MTTR ...
- ENVI指定像元数量(行数与列数)裁剪栅格图像
本文介绍基于ENVI软件,实现栅格遥感影像按照像元行列号与个数进行指定矩形区域裁剪的方法. 一般的,如果我们需要裁剪某个具体的行政区域,按照对应区域的矢量图层裁剪即可:如果需要裁剪某个大致的区 ...
- ping不通能curl通
今天发现一个域名或ip居然在ping不通的情况下能curl通,以前的思维定式直接给整破防了啊!!! 涨见识了,具体原因和原理后续补充~
- 高精度减法(模板yxc)
#include <bits/stdc++.h> using namespace std; bool cmp(vector<int> &A, vector<int ...
- uniapp企业微信web-view父子通信问题
项目背景:开发工具为HBuilderX,框架为uniapp,开发移动端的Web应用,在企业微信中使用(自建应用),Web开发的应用,不是小程序. 需求:页面中用到<web-view>组件, ...
- mysql 日期和时间戳的转换
(18条消息) MySQL 日期和时间戳的转换 | 以及DATE_FORMAT()用法_慌途L的博客-CSDN博客_date_format能转换时间戳吗 一小时的时间戳是2*3600*1000,这是1 ...