Spring HandlerInterceptor工作机制

发布时间 2023-08-17 13:54:33作者: Acelin_H

本文以一个通过正常注册拦截器流程注册拦截器失败的实际场景,来带领大家阅读源码,体会Spring的HandlerInterceptor拦截器整个工作流程

简单认识

org.springframework.web.servlet.HandlerInterceptor是Spring框架中的一个接口,用于拦截处理程序(Handler)的请求和响应。它允许开发人员在请求处理程序执行之前和之后执行自定义的预处理和后处理逻辑。 HandlerInterceptor接口定义了三个方法:

  1. preHandle:在请求处理程序执行之前调用。可以用于进行权限验证、日志记录等操作。如果该方法返回false,则请求将被中断,后续的拦截器和处理程序将不会被执行。
  2. postHandle:在请求处理程序执行之后、视图渲染之前调用。可以对请求的结果进行修改或添加额外的模型数据。
  3. 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路径下的所有.jsp

com/{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);

    }

}