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: 采 ...
随机推荐
- 2021-12-24:划分字母区间。 字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。 力扣763。某大厂面试
2021-12-24:划分字母区间. 字符串 S 由小写字母组成.我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中.返回一个表示每个字符串片段的长度的列表. 力扣763.某大厂面试 ...
- 解决:Invalid HTTP_HOST header: '192.168.56.1:8001'. You may need to add '192.168.56.1' to ALLOWED_HOSTS.
在setting.py下的ALLOWED_HOSTS=['*']添加'*'就可以解决显示不出来的问题.
- MVC 三层架构案例详细讲解
MVC 三层架构案例详细讲解 @ 目录 MVC 三层架构案例详细讲解 每博一文案 1. MVC 概述 2. MVC设计思想 3. 三层架构 4. MVC 与 三层架构的关系: 5. 案例举例:用户账户 ...
- 计算机网络 ACL和ANT
目录 一.ACL概况 二.ACL工作过程 三.ACL实验 四.ANT概况 五.ANT工作过程 六.ANT实验 一.ACL概况 概念:主要是对报文进行区分,路由器会对报文进行检查,查看是否符合通过标准或 ...
- odoo开发教程四:onchange、唯一性约束
一:onchange机制[onchange=前端js函数!可以实现前端实时更新以及修改验证] onchange机制:不需要保存数据到数据库就可以实时更新用户界面上的显示. @api.onchange( ...
- 如何用ReadWriteLock实现一个通用的缓存中心?
摘要:在并发场景中,Java SDK中提供了ReadWriteLock来满足读多写少的场景. 本文分享自华为云社区<[高并发]基于ReadWriteLock开了个一款高性能缓存>,作者:冰 ...
- [TSG开发]法如扫描仪SDK探幽-1.旧版SDK采集流程、问题解析、常见参数
做什么 法如扫描仪是一个三维的激光扫描仪,可以通过特定的作业模式将空间以三维激光点云的形式保存下来,并且通过特定的算法得出一些想要的具体参数. 这个SDK探幽日志主要是对目前SDK开发中遇到的一些问题 ...
- Spring 中的 Bean
前言 欢迎来到本篇文章,鸽了好久了,今天继续写下 Spring 的内容:Spring 中 Bean 的基本概念.基本写法和 3 种实例化 Bean 的方式等. 什么是 Bean? 我们回顾下,什么是 ...
- 一文解开主流开源变更数据捕获技术之Flink CDC的入门使用
@ 目录 概述 定义 什么是CDC? CDC的分类 特性 应用场景 支持数据源 实战 Flink DataStream方式代码示例 FlinkSQL方式代码示例 概述 定义 flink-cdc-con ...
- 分布式多协议接入网关FluxMQ-2.0功能说明
FluxMQ-2.0版本更新内容 前言 FLuxMQ是一款基于java开发,支持无限设备连接的云原生分布式物联网接入平台.FluxMQ基于Netty开发,底层采用Reactor3反应堆模型,具备低延迟 ...