SpringMVC流程分析(五):SpringMVC内部如何优雅的对一个请求进行前置、后置处理

本系列文章皆在分析SpringMVC的核心组件和工作原理,让你从SpringMVC浩如烟海的代码中跳出来,以一种全局的视角来重新审视SpringMVC的工作原理.

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜

前言

在上一章SpringMVC流程分析(四):SpringMVC中如何为一个请求选择合适的处理器 中,我们通过对doDispatch方法中getHandler方法分析由浅入深的讨论了HandlerMapping组件的相关内容。通过分析我们知道了HanlderMapping的主要工作就是根据前端传来的请求,然后找到合适的处理器

此外,在分析HanlderMapping的过程中我们注意到在getHandler方法的内部会遍历全部的HanlderMapping信息,然后逐一进行匹配。如果找到合适的处理器组件,则返回一个HandlerExecutionChain对象。上一节中我们指出HandlerExecutionChain其实相当于对处理器的一种封装,其内部会包含处理器handler和拦截器HandlerInterceptor。其中处理器很好理解,无非就是在程序中被我们使用@GetMapping,@PostMapping注解所标记的方法,那HandlerInterceptor又是什么呢?别急,本章我们便来揭开HandlerInterceptor神秘面纱。

doDispatch的调用链中,有关HandlerInterceptor的调用位于方法mappedHandler.applyPreHandle中。所以要了解HandlerInterceptor在处理Http请求中的作用,我们首先应该关注mappedHandler.applyPreHandle的逻辑。(mapperHandler.applyPostHandler与之类似)

(注:为了行文整体的连贯性,本文将首先介绍HandlerInterceptor的相关调用信息,即方法mappedHandler.applyPreHandle背后的调用关系链;方法getHandlerAdapter的背后的逻辑将在后续介绍。)

下图展示了本系列文章重点分析的组件信息,其中 HandlerInterceptor为本文分析的重点。

applyPostHandle背后的逻辑

HandlerExecutionChain # applyPreHandle和applyPostHandle的方法信息

java 复制代码
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // <1> 获得拦截器数组
    HandlerInterceptor[] interceptors = getInterceptors();
    if (!ObjectUtils.isEmpty(interceptors)) {
        // <2> 遍历拦截器数组
        for (int i = 0; i < interceptors.length; i++) {
            HandlerInterceptor interceptor = interceptors[i];
            // <3> 前置处理
            if (!interceptor.preHandle(request, response, this.handler)) {
                // <3.1> 已完成处理 拦截器
                triggerAfterCompletion(request, response, null);
                // 返回 false ,前置处理失败
                return false;
            }
            // <3.2> 标记 interceptorIndex 位置
            this.interceptorIndex = i;
        }
    }
    // <4> 返回 true ,前置处理成功
    return true;
}


void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
      throws Exception {

   HandlerInterceptor[] interceptors = getInterceptors();
   // 如果配置有拦截器
   if (!ObjectUtils.isEmpty(interceptors)) {
   // 逆序遍历拦截器信息
      for (int i = interceptors.length - 1; i >= 0; i--) {
         HandlerInterceptor interceptor = interceptors[i];
         // 执行后置处理器
         interceptor.postHandle(request, response, this.handler, mv);
      }
   }
}

调用链中applyPreHandleapplyPostHandle方法的逻辑如上所示。此时,我们以为applyPreHandle例来进行分析有关HandlerInterceptor的相关处理逻辑,applyPreHandle的大致逻辑如下:

  1. 获得拦截器数组,通过上面的 getInterceptors() 方法,获得 interceptors 数组
  2. 遍历 interceptors 拦截器数组
  3. 依次执行拦截器的前置处理
  4. 返回 true,拦截器们的前置处理都成功

(注:拦截器在执行过程中,如如果有某个拦截器的前置处理失败,则调用 triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, Exception ex) 方法,触发拦截器们的已完成处理,最后返回 false

可以看到,applyPreHandleapplyPostHandle在逻辑上大致相似,基本都是遍历持有的HanderInterceptor然后执行其中所定义的applyPreHandleapplyPostHandle方法。

如何实现对一个请求的前置、后置处理

在分析HanderInterceptor之前,不妨先思考一个问题,对于一个请求,哪些方式可以对其进行一些前置和后置处理操作?可能你会想到如下几种实现方式:

  1. 切面编程:Aop切面编程
  2. 过滤器:Filter

接下来,我们便逐一分析上述方案的优劣。对于AOP切面编程技术而言,其可以实现对一个请求进行前置操作,以及后置操作和环绕操作等。 但同时会引入切面和动态代理等额外的概念,频繁的使用Aop相关技术,会使得业务代码的逻辑变得复杂难以理解。

对于过滤器Filter来说,其同ServletListener并称为java Web的三大核心组件,其可在doFilter方法中定义预处理和后处理逻辑,并在方法中通过filterChain.doFilter继续向下传递,从而形成拦截器链。

Filter是在请求进入Servlet容器之前进行预处理,以及响应离开容器之前进行后处理。 所以某种意义上来讲Filter是工作在Servlet容器级别的,它对所有进入容器的请求和离开容器的响应进行处理,所以其在处理请求上具有更粗的粒度。

SpringMVC中的HanderInterceptor(拦截器)的出现恰好可以解决上述方案中存在的问题。接下来,我们便看看HandlerInterceptor是如何来对一个请求进行拦截的。

深入理解HandlerInterceptor

SpringMVC中,HandlerInterceptor )是一种拦截器组件,用于在请求的处理器方法执行前后进行拦截和处理HandlerInterceptor可以用于实现一些公共的处理逻辑,例如身份验证、日志记录、性能监控等。

HandlerInterceptor的方法信息

java 复制代码
  public interface HandlerInterceptor {
    /**
    * 前置处理,在 {@link HandlerAdapter#handle(HttpServletRequest, HttpServletResponse, Object)} 执行之前
    */
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception {
        
        return true;
    }
    
    /**
    * 后置处理,在 {@link HandlerAdapter#handle(HttpServletRequest, HttpServletResponse, Object)} 执行成功之后
    */
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                            @Nullable ModelAndView modelAndView) throws Exception {
    }
    
    /**
    * 完成处理,在 {@link HandlerAdapter#handle(HttpServletRequest, HttpServletResponse, Object)} 执行之后(无论成功还是失败)
    * 条件:执行 {@link #preHandle(HttpServletRequest, HttpServletResponse, Object)} 成功的拦截器才会执行该方法
    */
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
                                 @Nullable Exception ex) throws Exception {
    }
}

HandlerInterceptor组件在方法定义层面非常简洁,主要定义了前置和后置处理两个方法。其中前置处理方法preHandle多用于判断该拦截器所能拦截的信息;而后置方法postHandle则主要用于对请求完成后的处理操作。

(注:有关afterCompletion的调用可查看doDispatchertriggerAfterCompletion方法。)

拦截器的初始化

正如上一章SpringMVC流程分析(四):SpringMVC中如何为一个请求选择合适的处理器所述,在AbstractHandlerMapping会持有一个 initInterceptors的数组信息,从而用来记录容器中全部的拦截器信息,而其又实现 ApplicationContextAware接口,所以interceptor的初始化有可能是在AbstractHandlerMapping中完成的。为了验证我们这一猜测,我们不妨点开AbstractHandlerMapping中的initApplicationContext方法进行查看,相关代码如下所示:

AbstractHandlerMapping

java 复制代码
    @Override
    protected void initApplicationContext() throws BeansException {
        // <1> 空实现,交给子类实现,用于注册自定义的拦截器到 interceptors 中
        // 目前暂无子类实现
        extendInterceptors(this.interceptors);
        // <2> 扫描已注册的 MappedInterceptor 的 Bean 们,添加到 mappedInterceptors 中
        detectMappedInterceptors(this.adaptedInterceptors);
        // <3> 将 interceptors 初始化成 HandlerInterceptor 类型,添加到 mappedInterceptors 中
        // 此处主要处理(1) 处自定义的,全局配置的在第二步就应该全部加载
        initInterceptors();
    }

    protected void initInterceptors() {
        // 遍历  List<Object> interceptors
        if (!this.interceptors.isEmpty()) {
            for (int i = 0; i < this.interceptors.size(); i++) {
               Object interceptor = this.interceptors.get(i);
              
               // 将 interceptors 初始化成 HandlerInterceptor 类型
               // 添加到 mappedInterceptors 中
               // 注意,HandlerInterceptor 无需进行路径匹配,直接拦截全部
               // List<HandlerInterceptor> adaptedInterceptors类型,添加拦截器信息
               this.adaptedInterceptors.add(adaptInterceptor(interceptor));
            }
        }
    }
    
    
    protected HandlerInterceptor adaptInterceptor(Object interceptor) {
        if (interceptor instanceof HandlerInterceptor) {
            return (HandlerInterceptor) interceptor;
        }
      // .....省略其他无关代码
    }

通过上述代码我们知道,在HandlerMapping进行加载时,会加载容器中全部的拦截器,并将相关信息保存在AbstractHandlerMapping的成员变量adaptedInterceptors中。其在容器中初始化逻辑如下所示:

具体而言,在Spring扫描类信息的时候会加载并执行全部的BeanPostProcessor信息,而在众多的BeanPostProcessor中,有一个ApplyBeanPostProcessor专门用于处理Aware接口的相关扩展。由于AbstractHandlerMapping实现了ApplicationContextAware接口,所以其在加载过程中可以完成某些扩展操作。

接下来,我们便将继续分析如何将这些拦截器应用到请求的处理当中。但在分析之前,我们需要先对HandlerExecutionChain有所介绍,这样有助于我加深我们的对于处理流程的理解。

走进HandlerExecutionChain

在开始分析HandlerExecutionChain之前,我们不妨考虑这样一个问题。在SpringMVC中通常我们的处理器通常可以看做的被@GetMapping、@PostMapping等所标记的一个方法,而处理的执行本质就是方法执行。换言之,如果要对一个请求进行前置和后置处理,我们只需在方法执行前后进行可以拦截该方法拦截器信息即可。 具体逻辑如下所示:

此时的这个逻辑其实同AOP编程中的前置处理器和后置处理器的思想很类似。此时我们的问题就是能否通过一种非动态代理的方式来进行实现这样的增强逻辑呢?问题的答案就在HandlerExecutionChain中,

这其实就是HandlerE对其相关代码如下:

java 复制代码
public class HandlerExecutionChain {
	/**
	 * 处理器
	 */
	private final Object handler;

	/**
	 * 拦截器数组
	 */
	@Nullable
	private HandlerInterceptor[] interceptors;

	/**
	 * 拦截器数组。
	 *
	 * 在实际使用时,会调用 {@link #getInterceptors()} 方法,初始化到 {@link #interceptors} 中
	 */
	@Nullable
	private List<HandlerInterceptor> interceptorList;

	/**
	 * 已成功执行 {@link HandlerInterceptor#preHandle(HttpServletRequest, HttpServletResponse, Object)} 的位置
	 *
	 * 在 {@link #applyPostHandle} 和 {@link #triggerAfterCompletion} 方法中需要用到,用于倒序执行拦截器的方法
	 */
	private int interceptorIndex = -1;

	public HandlerExecutionChain(Object handler) {
		this(handler, (HandlerInterceptor[]) null);
	}

	
     //.....省略其他无关代码
     
  
}

可以看到HandlerExecutionChain是对处理器(handler)和拦截器(interceptors)的封装。其关键成员如下所示:

  • handler:请求对应的处理器对象,可以先理解为 HandlerMethod 对象(例如我们常用的 @RequestMapping 注解对应的方法会解析成该对象),也就是我们的某个 Method 的所有信息,可以被执行
  • interceptors:请求匹配的拦截器数组
  • interceptorIndex:记录已成功执行前置处理的拦截器位置,因为已完成处理只会执行前置处理成功的拦截器,且倒序执行

(注:HandlerExecutionChain中的applyPostHandleapplyPreHandle在前文已经进行了详细的描述)

在上一章SpringMVC流程分析(四):SpringMVC中如何为一个请求选择合适的处理器的分析中,我们知道HandlerMappinggetHandler方法会为一个请求找到一个合适的处理器,并且其还会将可以拦截该请求的拦截器一并找出,进而将其全部封装到对象HandlerExecutionChain中。这便是HandlerExecutionChain中会将处理器和拦截器信息作为成员变量的原因。

事实上,通过HandlerMappinggetHandler返回的HandlerExecutionChain对象中会持有可以适配该处理器的全部拦截器,此时只要定义确保,拦截器的前置方法在处理处理逻辑之前执行,同时保证拦截器的后置方法在处理处理逻辑之后执行就能完成对一个请求的前置和后置处理。

(注:为处理器寻找可以处理该请求的拦截器的相关逻辑可参考AbstractHandlerMapping中的 getHandlerExecutionChain方法的处理逻辑,限于篇幅原因本文不在此进行过多赘述~)

此时再回头看 DispatcherServlet # doDispatch方法中有关处理器的执行逻辑是不是有种豁然开朗的感觉?

DispatcherServlet # doDispatch

java 复制代码
protected void doDispatch(HttpServletRequest request, 
                            HttpServletResponse response) throws Exception {
  
   HandlerExecutionChain mappedHandler = null;
 . // 根据请求获取HandlerExecutionChain
   mappedHandler = getHandler(processedRequest);
        
   // 对处理器进行适配(暂时忽略,下一章会讲适配器相关的内容)
   HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

   // 执行拦截器前置方法    
   if (!mappedHandler.applyPreHandle(processedRequest, response)) {
            return;
   }

  // 执行处理逻辑
   mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

  // 执行拦截器后置方法
  mappedHandler.applyPostHandle(processedRequest, response, mv);
}

通过上述代码不难发现,SpringMVC内部对一个请求进行前置和后置的处理并没有通过代理的方式来实现,而是通过一种类似静态代理的形式进行了实现。 此外,通过将处理器和增强信息封装到一个类中,接着在抽象层次更高的类中定义相关的执行顺序,从而实现逻辑的增强。这某种意义上来说其实又夹杂了模板方法的思想。

实际上,阅读源码的意义远不止于理解每句代码的含义,更重要的是通过阅读源码来体会代码的设计思想和类功能的拆分。为了理解这些内容,我们需要找到一条主线,这条主线可以是我们使用框架时的相关配置,也可以是某种既定的模板代码。以使用mybatis为例,通常我们会构建sqlSession工厂、获取sqlSession,构建Mapper代理对象等。沿着这些主线不断深入,我们才能对源码有更深入的认识和见解,才不至于在茫茫多的源码中迷失自我。

总结

本文以doDispahtch方法中调用的applyPreHandle为切入点,由浅入深的分析了HandlerInterceptor的组件的功能、类结构关系等内容。研究了SpringMVC拦截器的初始化时机,以及调用时机等。最后,我们对上一章提及的HandlerExecutionChain对象进行了详细的介绍。

相关推荐
程序员是干活的2 分钟前
私家车开车回家过节会发生什么事情
java·开发语言·软件构建·1024程序员节
煸橙干儿~~12 分钟前
分析JS Crash(进程崩溃)
java·前端·javascript
2401_8543910813 分钟前
Spring Boot大学生就业招聘系统的开发与部署
java·spring boot·后端
Amor风信子14 分钟前
华为OD机试真题---跳房子II
java·数据结构·算法
杨荧40 分钟前
【JAVA开源】基于Vue和SpringBoot的洗衣店订单管理系统
java·开发语言·vue.js·spring boot·spring cloud·开源
陈逸轩*^_^*1 小时前
Java 网络编程基础
java·网络·计算机网络
这孩子叫逆1 小时前
Spring Boot项目的创建与使用
java·spring boot·后端
星星法术嗲人1 小时前
【Java】—— 集合框架:Collections工具类的使用
java·开发语言
一丝晨光2 小时前
C++、Ruby和JavaScript
java·开发语言·javascript·c++·python·c·ruby
天上掉下来个程小白2 小时前
Stream流的中间方法
java·开发语言·windows