Spring MVC执行流程

发布时间 2024-01-01 09:20:32作者: 残城碎梦

Spring MVC 执行流程

Spring MVC 执行流程如图:

SpringMVC的执行流程如下(控制器--模型--视图):

  1. 用户点击某个请求路径,发起一个 HTTP request 请求,该请求会被提交到 DispatcherServlet(前端控制器);
  2. 由 DispatcherServlet 请求一个或多个 HandlerMapping(处理器映射器),并返回一个执行链(HandlerExecutionChain)。
  3. DispatcherServlet 将执行链返回的 Handler 信息发送给 HandlerAdapter(处理器适配器);
  4. HandlerAdapter 根据 Handler 信息找到并执行相应的 Handler(常称为 Controller);
  5. Handler 执行完毕后会返回给 HandlerAdapter 一个 ModelAndView 对象(Spring MVC的底层对象,包括 Model 数据模型和 View 视图信息);
  6. HandlerAdapter 接收到 ModelAndView 对象后,将其返回给 DispatcherServlet ;
  7. DispatcherServlet 接收到 ModelAndView 对象后,会请求 ViewResolver(视图解析器)对视图进行解析;
  8. ViewResolver 根据 View 信息匹配到相应的视图结果,并返回给 DispatcherServlet;
  9. DispatcherServlet 接收到具体的 View 视图后,进行视图渲染,将 Model 中的模型数据填充到 View 视图中的 request 域,生成最终的 View(视图);
  10. 视图负责将结果显示到浏览器(客户端)。

Spring MVC组件

Spring MVC 涉及到的组件有 DispatcherServlet(前端控制器)、HandlerMapping(处理器映射器)、HandlerAdapter(处理器适配器)、Handler(处理器)、ViewResolver(视图解析器)和 View(视图)。下面对各个组件的功能说明如下。

DispatcherServlet

DispatcherServlet 是前端控制器,Spring MVC 的所有请求都要经过 DispatcherServlet 来统一分发。DispatcherServlet 相当于一个转发器或中央处理器,控制整个流程的执行,对各个组件进行统一调度,以降低组件之间的耦合性,有利于组件之间的拓展。

HandlerMapping

HandlerMapping 是用来查找Handler 的,也就是处理器,具体的表现形式可以是类也可以是方法。比如,标注了@RequestMapping 的每个method 都可以看成是一个Handler,由Handler 来负责实际的请求处理。HandlerMapping 在请求到达之后,它的作用便是找到请求相应的处理器Handler 和Interceptor。

HandlerAdapter

从名字上看,这是一个适配器。因为Spring MVC 中Handler 可以是任意形式的,只要能够处理请求便行, 但是把请求交给Servlet 的时候,由于Servlet 的方法结构都是如doService(HttpServletRequest req, HttpServletResponse resp) 这样的形式,让固定的Servlet 处理方法调用Handler 来进行处理,这一步工作便是HandlerAdapter 要做的事。

HandlerExceptionResolver

从这个组件的名字上看,这个就是用来处理Handler 过程中产生的异常情况的组件。具体来说,此组件的作用是根据异常设置ModelAndView, 之后再交给render()方法进行渲染, 而render() 便将ModelAndView 渲染成页面。不过有一点,HandlerExceptionResolver 只是用于解析对请求做处理阶段产生的异常,而渲染阶段的异常则不归他管了,这也是Spring MVC 组件设计的一大原则分工明确互不干涉。

ViewResolver

视图解析器,相信大家对这个应该都很熟悉了。因为通常在SpringMVC 的配置文件中,都会配上一个该接口的实现类来进行视图的解析。这个组件的主要作用,便是将String类型的视图名和Locale 解析为View 类型的视图。这个接口只有一个resolveViewName()方法。从方法的定义就可以看出,Controller 层返回的String 类型的视图名viewName,最终会在这里被解析成为View。View 是用来渲染页面的,也就是说,它会将程序返回的参数和数据填入模板中,最终生成html 文件。ViewResolver 在这个过程中,主要做两件大事,即,ViewResolver 会找到渲染所用的模板(使用什么模板来渲染?)和所用的技术(其实也就是视图的类型,如JSP 还是其他什么的)填入参数。默认情况下,Spring MVC 会为我们自动配置一个InternalResourceViewResolver,这个是针对JSP 类型视图的。

RequestToViewNameTranslator

这个组件的作用,在于从Request 中获取viewName. 因为ViewResolver 是根据ViewName 查找View, 但有的Handler 处理完成之后,没有设置View 也没有设置ViewName, 便要通过这个组件来从Request 中查找viewName。

LocaleResolver

在上面我们有看到ViewResolver 的resolveViewName()方法,需要两个参数。那么第二个参数Locale 是从哪来的呢,这就是LocaleResolver 要做的事了。LocaleResolver用于从request 中解析出Locale, 在中国大陆地区,Locale 当然就会是zh-CN 之类,用来表示一个区域。这个类也是i18n 的基础。

ThemeResolver

从名字便可看出,这个类是用来解析主题的。主题,就是样式,图片以及它们所形成的显示效果的集合。Spring MVC 中一套主题对应一个properties 文件,里面存放着跟当前主题相关的所有资源,如图片,css 样式等。创建主题非常简单,只需准备好资源,然后新建一个"主题名.properties" 并将资源设置进去,放在classpath 下,便可以在页面中使用了。Spring MVC 中跟主题有关的类有ThemeResolver, ThemeSource 和
Theme。ThemeResolver 负责从request 中解析出主题名, ThemeSource 则根据主题名找到具体的主题, 其抽象也就是Theme, 通过Theme 来获取主题和具体的资源。

MultipartResolver

其实这是一个大家很熟悉的组件,MultipartResolver 用于处理上传请求,通过将普通的Request 包装成MultipartHttpServletRequest 来实现。MultipartHttpServletRequest可以通过getFile() 直接获得文件,如果是多个文件上传,还可以通过调用getFileMap得到Map<FileName, File> 这样的结构。MultipartResolver 的作用就是用来封装普通的request,使其拥有处理文件上传的功能。

FlashMapManager

说到FlashMapManager,就得先提一下FlashMap。FlashMap 用于重定向Redirect 时的参数数据传递,比如,在处理用户订单提交时,为了避免重复提交,可以处理完post 请求后redirect 到一个get 请求,这个get 请求可以用来显示订单详情之类的信息。这样做虽然可以规避用户刷新重新提交表单的问题,但是在这个页面上要显示订单的信息,那这些数据从哪里去获取呢,因为redirect 重定向是没有传递参数这一功能的,如果不想把参数写进url(其实也不推荐这么做,url 有长度限制不说,把参数都直接暴露,感觉也不安全), 那么就可以通过flashMap 来传递。只需要在redirect 之前, 将要传递的数据写入request ( 可以通过
ServletRequestAttributes.getRequest() 获得) 的属性OUTPUT_FLASH_MAP_ATTRIBUTE 中,这样在redirect 之后的handler 中Spring 就会自动将其设置到Model 中,在显示订单信息的页面上,就可以直接从Model 中取得数据了。而FlashMapManager 就是用来管理FlashMap 的。

Spring MVC 源码分析

初始化阶段

初始化九大组件

我们首先找到DispatcherServlet 这个类,必然是寻找init()方法。然后,我们发现它的init方法其实在父类HttpServletBean 中,其源码如下:

@Override
public final void init() throws ServletException {

    // Set bean properties from init parameters.
    PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
    if (!pvs.isEmpty()) {
        try {
            //定位资源
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
            //加载配置信息
            ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
            initBeanWrapper(bw);
            bw.setPropertyValues(pvs, true);
        }
        catch (BeansException ex) {
            if (logger.isErrorEnabled()) {
                logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
            }
            throw ex;
        }
    }

    // Let subclasses do whatever initialization they like.
    initServletBean();
}

我们看到在这段代码中, 又调用了一个重要的initServletBean() 方法。进入initServletBean()方法看到:

@Override
protected final void initServletBean() throws ServletException {
    getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
    if (logger.isInfoEnabled()) {
        logger.info("Initializing Servlet '" + getServletName() + "'");
    }
    long startTime = System.currentTimeMillis();

    try {
        this.webApplicationContext = initWebApplicationContext();
        initFrameworkServlet();
    }
    catch (ServletException | RuntimeException ex) {
        logger.error("Context initialization failed", ex);
        throw ex;
    }

    if (logger.isDebugEnabled()) {
        String value = this.enableLoggingRequestDetails ?
            "shown which may lead to unsafe logging of potentially sensitive data" :
        "masked to prevent unsafe logging of potentially sensitive data";
        logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
                     "': request parameters and headers will be " + value);
    }

    if (logger.isInfoEnabled()) {
        logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
    }
}

这段代码中最主要的逻辑就是初始化IOC 容器,最终会调用refresh()方法。

IOC 容器初始化之后,最后又调用了onRefresh()方法。这个方法最终是在DispatcherServlet中实现,来看源码:

/**
  * This implementation calls {@link #initStrategies}.
  */
@Override
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}

/**
  * Initialize the strategy objects that this servlet uses.
  * <p>May be overridden in subclasses in order to initialize further strategy objects.
  */
//初始化策略
protected void initStrategies(ApplicationContext context) {
    //多文件上传的组件
    initMultipartResolver(context);
    //初始化本地语言环境
    initLocaleResolver(context);
    //初始化模板处理器
    initThemeResolver(context);
    //handlerMapping
    initHandlerMappings(context);
    //初始化参数适配器
    initHandlerAdapters(context);
    //初始化异常拦截器
    initHandlerExceptionResolvers(context);
    //初始化视图预处理器
    initRequestToViewNameTranslator(context);
    //初始化视图转换器
    initViewResolvers(context);
    initFlashMapManager(context);
}

到这一步就完成了Spring MVC 的九大组件的初始化。

这里就会初始化我们后面需要用到的最终要的三个map集合:处理器映射器、处理器适配器、视图解析器

urlLookup集合

urlLookup作用:用来存放键值对,映射url(key)-RequestMappingInfo对象(value)。RequestMappingInfo可以理解为为我们添加的@RequestMapping注解的信息。

RequestMappingInfo对象内部维护的各种xxxCondition变量,对应为@RequestMapping注解上我们能够自己配置的参数。

AbstractHandlerMethodMapping类实现了InitializingBean接口,那么我们就可以联想到Spring中Bean的生命周期的初始化步骤,即执行afterPropertiesSet方法。

我们可以发现这个类是抽象类,在Spring中抽象类是无法实例化为Bean的,所以该抽象类有具体的实现,在将子类实例化为Bean的时候,调用对应的初始化步骤,完成数据的填充。

对应方法的调用逻辑,如下图:

请求的基本流程

以Get请求为例,当我们发起一次请求后

1)首先会根据请求的类型选择调用不同的方法,此处会调用HttpServlet#service,里面会调用doGet

2)doGet方法在FrameworkServlet抽象类中给出了实现,会调用当前类的processRequest方法

3)该类会处理一些基本的数据信息,然后又调用子类的doService方法(doService方法是一个模板方法)

4)此时就会调用到DispatcherServlet类中的doService方法,然后调用当前类的doDispatch方法

doDispatch

我将该方法中需要执行的逻辑分为三类:文件相关、拦截器相关、主体流程相关。

文件相关

1)开始选择合适的文件解析器资源,完成对应的解析操作。

2)根据返回的策略,调用具体的resolveMultipart方法,完成处理逻辑

我们平时是这样配置的:

3)清除生成的临时文件

拦截器

让我们先来观察对应的拦截器接口,是不是像极了Bean的生命周期的处理方法。

1)前置拦截

如果方法返回false,则直接结束该方法,后面的逻辑就不再执行。

方法内部调用逻辑:

  • 获取对应处理数组(数据在构建处理器映射器的时候添加)
  • 遍历执行
  • 只要有一个拦截器返回为false,就执行对应的处理完成后拦截,也就是afterCompletion方法。最终返回false,然后方法调用结束

2)后置拦截

getHandler(重点)

HandlerMapping

1)handlerMappings肯定不为空

2)遍历对应的mappings集合

3)根据当前的request请求,调用对应的getHandler方法

4)如果返回的结果不为null,则说明是与我们当前请求匹配的handlerMapping

所以对应的获取逻辑在最后面的hm.gethandler(request)方法中

该代码片段来自AbstractHandlerMapping类,该类主要执行两件事情:

1)根据路径找到合适得handlerMethod

2)将找到得handlerMethod对象,封装成HandlerExecutionChain类对象,返回

getHandlerInternal

根据请求路径,找到对应的HandlerMethod类对象,根据已有的request请求,拿到我们的访问路径,底层就是各种getAttribute(xxx),然后辅助各种判断。

lookupHandlerMethod方法

在根据路径找HandlerMethod类对象时,会调用这个方法。在这个方法中,就会去根据我们的映射url路径,找到对应的HandlerMethod类对象。

该代码片段来自AbstractHandlerMethodMapping类:

getHandlerExecutionChain

前面我们在doDispatch方法中有提及,会调用对应的拦截器方法执行,可是拦截器的数据来自哪里?就是在这里

1)获取当前的映射路径

2)找到我们容器中配置的所有的拦截器

3)如果拦截器类继承自MappedInterceptor类,调用拦截器中的matches方法,如果路径匹配就添加到对应的拦截器链条中

4)其他常规的拦截器就都添加到拦截器链条中

5)最终返回封装好的带有拦截器链条和handlerMethod对象的HandlerExecutionChain

getHandlerAdapter(重点)

截至到目前,我们已经做好的工作为找到对应的handlerMethod对象,即处理的逻辑,封装好了对应的拦截器链

getHandlerAdapter方法

传入前面封装好的chain对象的handlerMethod属性。这里就会根据对应的handlerMethod类型,选择合适的处理器适配器(HandlerAdapter)

ha.handle()

前面流程是选择合适的handlerAdapter,这一步就是根据具体的adapter处理器对象,执行我们方法里面的具体逻辑。

我们以使用注解的方式为例,其使用的HandlerAdapter是RequestMappingHandlerAdapter。

注意:一定要是org.springframework.web.servlet.mvc.method.annotation路径下的HandlerAdapter。因为在org.springframework.web.reactive.result.method.annotation路径下也有一个HandlerAdapter。

点进handle方法,就会执行下面的方法逻辑。

该代码片段来自RequestMappingHandlerAdapter类:

invokeHandlerMethod

上面方法的核心方法就是invokeHandlerMethod,我们熟悉的@ModelAttribute注解,就是在该方法中解析的。

该代码片段来自RequestMappingHandlerAdapter类:

invokeAndHandle

该方法会调用我们的业务逻辑(invokeForRequest),并且完成返回内容的解析(handleReturnValue)。

该代码片段来自ServletInvocableHandlerMethod类:

invokeForRequest

我们最想看的还是在什么位置调用了我们controller层里面的方法,这个位置就是在invokeForRequest方法中。

该代码片段来自InvocableHandlerMethod类中:

handleReturnValue

我们都知道如果我们添加了@ResponseBody注解,那么返回值就是JSON字符串,这个处理逻辑,就是在这个方法中进行处理的。

1)根据返回的值和返回的类型,找到合适的返回值处理器

2)使用具体的返回值处理器处理我们的返回值

该代码片段来自HandlerMethodReturnValueHandlerComposite类:

这个selectHandler方法,会调用supportsReturnType方法,即调用具体返回值处理器支持的类型方法,如下图。是否有ResponseBody注解

该方法的调用逻辑过长(writeWithMessageConverters),仅展示关键部分。即在添加了@RequestBody注解的基础上,会根据具体的json实现类完成将结果转换成json字符串的业务逻辑

该代码片段来自AbstractMessageConverterMethodProcessor类:

processDispatchResult(重点)

该方法就是我们的视图解析器。根据方法内容很容易就能看出来,它会调用render方法。

render

该方法主要处理:

1)根据我们的视图名称 解析成为我们真正的物理视图,通过视图解析器对象(resolveViewName)

2)渲染模型视图(view.render)

resolveViewName

就是完成的映射路径的前缀和后缀的拼接

获取我们配置的视图解析器。

执行方法后,完成对应的url拼接

view.render

根据我们配置的视图解析器,执行对应的处理逻辑

在这一步,完成我们请求的转发,完成页面的逻辑处理

核心流程图