Spring MVC学习(3)—Spring MVC中的核心组件以及请求的执行流程

「这是我参与2022首次更文挑战的第15天,活动详情查看:2022首次更文挑战」。

基于最新Spring 5.x,详细介绍了Spring MVC中的核心组件,包括Handler、HandlerMapping、HandlerAdapter、HandlerExceptionResolver、ViewResolver等组件,最后还大概介绍了Spring MVC的请求执行流程,以及默认的Spring MVC组件。

此前,我们已经学习过了Spring MVC的基本概念,以及父子容器的概念,Spring MVC是组件式架构,现在我们来学习Spring MVC的核心组件,以及请求执行流程,以及默认的Spring MVC组件。

Spring MVC学习 系列文章

Spring MVC学习(1)—MVC的介绍以及Spring MVC的入门案例

Spring MVC学习(2)—Spring MVC中容器的层次结构以及父子容器的概念

Spring MVC学习(3)—Spring MVC中的核心组件以及请求的执行流程

Spring MVC学习(4)—ViewSolvsolver视图解析器的详细介绍与使用案例

Spring MVC学习(5)—基于注解的Controller控制器的配置全解【一万字】

Spring MVC学习(6)—Spring数据类型转换机制全解【一万字】

Spring MVC学习(7)—Validation基于注解的声明式数据校验机制全解【一万字】

Spring MVC学习(8)—HandlerInterceptor处理器拦截器机制全解

Spring MVC学习(9)—项目统一异常处理机制详解与使用案例

Spring MVC学习(10)—文件上传配置、DispatcherServlet的路径配置、请求和响应内容编码

Spring MVC学习(11)—跨域的介绍以及使用CORS解决跨域问题

@[toc]

1 Spring MVC的核心组件

Spring MVC是基于组件式的Web框架,一个不同的功能点都由一个不同的组件负责,这样做的好处之一就是:各个组件分工明确、相互配合完成请求的处理和响应工作。而且每一个组件都是独立的扩展点,我们可以很容易对其进行扩展而不必特别关心其他组件,相当灵活(在自定义组件之前我们还是有必要了解这些组件的关系的)。

与其他许多Web框架一样,Spring MVC围绕分派控制器模式进行设计,在该模式下, DispatcherServlet提供了用于请求处理和分发的整体逻辑,而实际上的请求处理的工作是由各个可配置的组件来完成的。该模型非常灵活,并支持多种工作流程。DispatcherServlet也被称作前端控制器。

与任何Servlet一样,都需要根据Servlet规范使用JavConfig或在web.xml中声明和映射DispatcherServlet(我们在前面就学习过了)。反过来,DispatcherServlet将会使用Spring的配置发现、调用请求映射,视图解析,异常处理等委托组件。简单的说,用户请求到达DispatcherServlet,然后由它调用其它组件处理用户的请求,DispatcherServlet是整个请求处理的流程控制中心,DispatcherServlet的存在降低了组件之间的耦合性。

这些不同功能的组件,在框架中对应着不同的顶级接口,它们的实现类都可以看作是由Spring 管理一些特殊的bean,DispatcherServlet委托给不同的特殊 bean 以处理请求并呈现适当的响应。通常这些组件(接口)都有默认的实现,也就是说,这些特殊bean不需要我们去配置,但我们也可以自定义其属性并扩展或替换它们。

DispatcherServlet将会委托的特殊bean(接口、组件)有下面几种(来自Spring官网):

HandlerMapping 处理器映射器,用于查找能够处理请求的Handler,将请求映射为HandlerExecutionChain 对象(包含一个Handler处理器对象、多个 HandlerInterceptor 拦截器)对象。
HandlerAdapter 处理器适配器,帮助 DispatcherServlet调用请求映射到的Handler,但是不管Handler实际如何调用。将会返回一个ModelAndView对象,其中model是一个Map结构,存放了我们返回的所有数据,view是逻辑视图名,即ViewName。
HandlerExceptionResolver 异常解析器,如果在前面执行handler的过程中抛出了某个异常,将会走异常解析器的方法!在异常解析器中可以将此错误映射到其他handler、HTML 错误视图(错误页面)或统一抛出自己的异常。
ViewResolver 视图解析器,ViewResolver根据handler执行之后返回的ModelAndView中的String类型的逻辑视图名解析成物理视图名,即具体的资源地址,再生成对应的View视图对象。但是具体的事务解析以及数据填充工作由View视图自己完成(View. render方法)。
LocaleResolver, LocaleContextResolver 区域解析器,用户的区域也称为Locale,Locale信息是可以由前端直接获取的,可以根据不同的用户区域展示不同的视图,比如为不同区域的用户可以设置不同的语言和时区,也就是提供国际化视图支持。
ThemeResolver 主题解析器,主题就是系统的整体样式或风格,可通过Spring MVC框架提供的主题(theme)设置应用不同的整体样式风格,提高用户体验。Spring MVC的主题就是一些静态资源的集合,即包括样式及图片,用来控制应用的视觉风格。主题也支持国际化,同一个主题不同区域也可以显示不同的风格。
MultipartResolver 多部件解析器,用于处理上传请求,文件上传时就可以使用MultipartResolver来解析上传请求中的文件数据,方便快捷的实现文件上传!
FlashMapManager 存储并检索FlashMap,这些FlashMap可用于将属性从一个请求传递到另一个请求,通常是用在重定向中。也就是说FlashMap主要用在redirect中传递参数,而FlashMapManager则用于管理这些FlashMap。

下面我们将简单介绍一些核心的组件,并且没有涉及到源码,适合Spring MVC的初学者!

2 HandlerMapping

2.1 Handler

Handler,即处理器,它对应着MVC中的C也就是Controller,在Spring MVC框架中,它的实现有很多种,比如:

  1. 实现Servlet接口或者继承HttpServlet类等方法,这是最传统的方式,在使用框架之后,此方式几乎不再使用了。
  2. 实现了Coltroller接口或者继承Coltroller的实现类
  3. 实现了HttpRequestHandler接口或者继承HttpRequestHandler的实现类
  4. 标注了@RequestMapping注解以及使用@RequestMapping 作为元注解的注解(比如@GetMapping、@PostMapping、@PutMapping、@DeleteMapping、@PatchMapping等)的方法

Handler中就包含我们的业务代码逻辑!

一个Controller的实现被封装为一个对应类型的Handler对象。其中,采用@RequestMapping注解以及使用@RequestMapping 作为元注解的注解实现的Controller将被封装为HandlerMethod,内部保存了Controller方法。这种方式也最为常用,只需要在方法前加上@RequestMapping注解或者以@RequestMapping注解为元注解的注解,该方法就被作为Controller,对于该方法所属的具体类没有任何要求,因此一个Controller类中可以包含多个Controller方法,可以处理不同的请求,这种方法级别的Controller实现节省了大量类文件的编写,让应用更加轻量级。

Handler有多种类型,但是没有统一的接口,因此在源码中使用Object类型来保存。

2.2 HandlerMapping

在Spring MVC中每个请求都需要一个对应的Handler来处理,那么当接收到一个请求之后到底使用哪个Handler进行处理呢?这需要通过HandlerMapping来查找了。

HandlerMapping即处理器映射器,它由DispatcherServlet调用,被用来根据请求(request)查找对应的Handler,并将请求映射为HandlerExecutionChain 对象(包含一个Handler处理器对象、多个用于预处理和后处理的HandlerInterceptor 拦截器)对象。

public interface HandlerMapping {

    /**
     * 返回包含匹配此请求的处理程序Handler对象和全部HandlerInterceptor拦截器链的HandlerExecutionChain。
     * 可根据请求 URL、会话状态或实现类选择的任何因素进行选择。
     * <p>
     * DispatcherServlet将查询所有已注册的HandlerMapping的bean 以查找匹配项,如果未找到,则返回null。这不是错误。
     *
     * @return 一个包含handler对象和全部interceptor拦截器的HandlerExecutionChain对象,如未找到映射,那么返回null
     */
    @Nullable
    HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;

}
复制代码

可以看到,HandlerMapping接口包含唯一的一个getHandler方法,这个方法就是通过request找到HandlerExecutionChain,而HandlerExecutionChain包装了一个Handler和一组Interceptor拦截器。

2.3 HandlerMapping的实现

请求映射的详细规则因HandlerMapping的不同实现而各不相同,web项目中多个映射器可以共存互不影响,并且可以排序。在查找映射的时候,DispatcherServlet将遍历所有在当前容器中已注册的HandlerMapping的bean以查找匹配项,找到第一个即停止查找。

如果在配置文件中指定HandlerMapping的实现,那么Spring5.2.8.RELEASE版本默认将加载BeanNameUrlHandlerMapping、RequestMappingHandlerMapping以及RouterFunctionMapping(Spring 5.2新增的用于支持RouterFunctions的映射器)这三个HandlerMapping。如果手动配置了HandlerMapping,那么不会加载默认的HandlerMapping。

一般我们不需要指定HandlerMapping,直接采用默认配置即可!下面我们讲解常见的HandlerMapping实现!

2.3.1 BeanNameUrlHandlerMapping

beanName方式。BeanNameUrlHandlerMapping,利用Controller的beanname来作为URL映射查找对应的Handler。BeanNameUrlHandlerMapping只会查找采用实现了Coltroller接口或者继承Coltroller的实现类,以及实现了HttpRequestHandler接口或者继承HttpRequestHandler的实现类这两种方式实现的Handler。

BeanNameUrlHandlerMapping将会默认配置(在没有手动配置其他HandlerMapping时),因此我们只需要配置Handler。

采用注解的方式如下:

/**
 * 基于实现Controller接口的Handler
 *
 * @author lx
 */
@org.springframework.stereotype.Controller("/beanNameUrl")
public class BeanNameUrlController implements Controller {

    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //创建ModelAndView对象
        ModelAndView mv = new ModelAndView();
        //跳转到哪个页面    mvc会走视图解析器
        mv.setViewName("index.jsp");
        return mv;
    }
}
复制代码

采用XML的方式如下:

<!-- 注册 Handler(实现Controller接口),name表示url路径,name前要加'/' -->
<bean name="/beanNameUrl" class="com.spring.mvc.controller.BeanNameUrlController"/>
复制代码

2.3.2 SimpleUrlHandlerMapping

url方式。SimpleUrlHandlerMapping,根据配置的URL路径和Controller映射来查找对应的Handler,该方式可以将多个URL集中配置。SimpleUrlHandlerMapping只会查找采用实现了Coltroller接口或者继承Coltroller的实现类,以及实现了HttpRequestHandler接口或者继承HttpRequestHandler的实现类这两种方式实现的Handler。

SimpleUrlHandlerMapping默认不会被Spring自动配置,因此需要手动配置。

<!-- 注册 HandlerMapping,基于SimpleUrlHandlerMapping的方式 -->
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
        <!--配置请求url和handler的beanName的映射关系-->
        <props>
            <prop key="/simpleUrl1">simpleUrlController1</prop>
            <prop key="/simpleUrl2">simpleUrlController2</prop>
            <!--通过*配置一个通用的Handler-->
            <!--那么 比如/simpleUrl3  /simpleUrl4等请求都会被转发到simpleUrlController3的Handler-->
            <prop key="/simpleUrl*">simpleUrlController3</prop>
        </props>
    </property>
</bean>
复制代码
/**
 * @author lx
 */
@org.springframework.stereotype.Controller
public class SimpleUrlController {

    @org.springframework.stereotype.Controller("simpleUrlController1")
    public class SimpleUrlController1 implements Controller {
        @Override
        public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
            System.out.println("simpleUrlController1");
            //创建ModelAndView对象
            ModelAndView mv = new ModelAndView();
            //跳转到哪个页面    mvc会走视图解析器
            mv.setViewName("index.jsp");
            return mv;
        }
    }

    @org.springframework.stereotype.Controller("simpleUrlController2")
    public class SimpleUrlController2 implements Controller {
        @Override
        public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
            System.out.println("simpleUrlController2");
            //创建ModelAndView对象
            ModelAndView mv = new ModelAndView();
            //跳转到哪个页面    mvc会走视图解析器
            mv.setViewName("index.jsp");
            return mv;
        }
    }


    /**
     * 基于实现HttpRequestHandler接口的Handler
     */
    @org.springframework.stereotype.Controller("simpleUrlController3")
    public class SimpleUrlController3 implements HttpRequestHandler {
        @Override
        public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
            System.out.println("simpleUrlController3");
            response.sendRedirect("index.jsp");
        }
    }

}
复制代码

2.3.3 RequestMappingHandlerMapping

注解方式。RequestMappingHandlerMapping是最常用的一个HandlerMapping,因为它支持查找通过@RequestMapping注解或者@RequestMapping作为元注解的方式实现的Handler。从spring3.1版本开始,废除了DefaultAnnotationHandlerMapping的使用。

RequestMappingHandlerMapping在构建HandlerExecutionChain对象时,会在内部将对象内的handler属性的类型设置成HandlerMethod类型。

RequestMappingHandlerMapping将会默认配置(在没有手动配置其他HandlerMapping时),因此我们只需要配置Handler。而Handler的案例就不用说了吧,这个太常见了,就我们最常用的在方法上标注@RequestMapping注解以及使用@RequestMapping 作为元注解的注解(比如@GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping)的方式!

3 HandlerAdapter

处理器适配器。帮助 DispatcherServlet调用找到的Handler,但是不管Handler实际如何调用。

在HandlerMapping找到对应的Handler之后,需要调用Handler,但是我们之前说过,Handler的实现形式有很多种,如果通过if else来判断类型并调用,那么可能随时涉及到修改此前的代码,这样就不符合开闭原则。通过HandlerAdapter对Handler进行执行,这是适配器模式的应用,通过扩展HandlerAdapter可以对更多类型的处理器进行执行而不修改修改源码。

当HandlerMapping获取到执行请求的Handler时,DispatcherServlte将会依次调用所有已注册的HandlerAdapter(可以排序),如果当前HandlerAdapter能够对当前的Handler进行执行,那么就通过当前HandlerAdapter来执行该Handler,这样就大大的减少了通过DispatcherServlet直接调用Handler的难度,同时这样可以使得程序的扩展性大大增强!

HandlerAdapter会通过反射调用具体Handler的方法,但是方法具体是怎么执行的,HandlerAdapter不会关心!执行完毕之后,HandlerAdapter将返回ModelAndView对象,其中model是一个Map结构,其实就是存放了我们返回给请求的所有结果值,view是逻辑视图名,即ViewName。如果不需要渲染视图,则可能返回null,比如application/json请求。

HandlerAdapter的接口方法如下,通过supports方法判断是否辅助执行该handler,通过handle方法对Handler进行执行。

public interface HandlerAdapter {

    /**
     * 给定一个handler实例,返回此 HandlerAdapter 是否可以支持它。
     * 通常一个类型的HandlerAdapter仅支持一个类型的handler。
     *
     * @param handler 给定的handler处理器
     * @return 此HandlerAdapter是否可以使用给定的handler
     */
    boolean supports(Object handler);


    /**
     * 使用给定的handler来处理此请求。
     *
     * @param request  当前 HTTP 请求
     * @param response 当前 HTTP 响应
     * @param handler  要使用的handler。此对象之前必须传递给此接口的supports方法,并且该方法必须返回true
     * @return 一个ModelAndView模型和视图对象,具有视图的名称和所需的模型数据,如果请求已直接处理,则返回null
     */
    ModelAndView handle(HttpServletRequest request, HttpServletResponse response,
                        Object handler) throws Exception;

    // 获取当前请求的最后更改时间,主要用于供给浏览器判断当前请求是否修改过,
    // 从而判断是否可以直接使用之前缓存的结果

    /**
     * Same contract as for HttpServlet's getLastModified method. Can simply return -1 if there's no support in the handler class.
     * 与HttpServlet的getLastModified方法相同的作用。如果handler中没有支持,只需返回 -1 即可。
     *
     * @param request 当前 HTTP 请求
     * @param handler 要使用的handler
     * @return 给定handler的最后修改值
     */
    long getLastModified(HttpServletRequest request, Object handler);

}
复制代码

Handler的执行是一个复杂的过程,其中可能涉及到参数类型的转换和封装,JSON的返序列化和序列化等过程!我们后面再慢慢讲解!

3.1 HandlerAdapter的实现

通常一种类型的Handler需要一种对应的的HandlerAdapter。

  1. RequestMappingHandlerAdapter主要是适配注解处理器,注解处理器就是我们经常使用@RequestMapping注解及其作为元注解的方法处理器。
  2. HttpRequestHandlerAdapter主要是适配静态资源处理器,静态资源处理器就是实现了HttpRequestHandler接口或HttpRequestHandler接口子类的类处理器,这一类处理器的作用是处理通过Spring MVC来访问的静态资源的请求。
  3. SimpleControllerHandlerAdapter是Controller处理适配器,适配实现了Controller接口或Controller接口子类的处理器。
  4. SimpleServletHandlerAdapter是Servlet处理适配器,适配实现了Servlet接口或Servlet的子类的处理器,我们不仅可以在web.xml里面配置Servlet,其实也可以用SpringMVC来配置Servlet,不过这个适配器很少用到,而且Spring MVC默认的适配器没有它。

Spring5.2.8.RELEASE版本中,默认配置的HandlerAdapter为HttpRequestHandlerAdapter、SimpleControllerHandlerAdapter、RequestMappingHandlerAdapter,以及HandlerFunctionAdapter(Spring 5.2新增的用于支持RouterFunctions的处理器适配器)

同样,一般不会指定HandlerMapping,直接采用默认配置!

4 HandlerExceptionResolver

异常解析器,如果在前面执行handler的过程中抛出了某个异常,将会走异常解析器的方法!注意,HandlerExceptionResolver只能处理在handler执行完毕之前抛出的异常,如果是在handler执行完毕之后的步骤中抛出的异常,比如页面渲染过程中的异常,它是不能处理的。

异常解析器可以定义各种解决异常的策略,可能将请求映射到其他handler、HTML 错误视图(错误页面)或统一抛出自己的异常。自定义的异常解析器必须实现HandlerExceptionResolver接口,并实现resolveException方法,然后将该异常解析器配置到容器中!

一个大型且完善的项目通常都会有自己的异常解析器,我们后面会详细讲!

HandlerExceptionResolver只有一个resolveException方法,该方法返回一个ModelAndView,其中可以包含要返回的数据和视图名称,也就是说,比如在HandlerAdapter.handle()方法执行过程中抛出异常,那么转而会执行HandlerExceptionResolver.resolveException()方法,最终将返回并渲染该方法的返回值。

public interface HandlerExceptionResolver {

    /**
     * 尝试解决在处理程序执行期间抛出的给定异常,返回表示特定错误的ModelAndView视图(比如一个统一的异常页面)
     * <p>
     * 返回的ModelAndView可能是一个空的兑现(ModelAndView.isEmpty()返回true)
     * 表示异常已成功解决,但不会呈现任何视图,例如通过设置状态代码。
     *
     * @param request  当前 HTTP 请求
     * @param response 当前 HTTP 响应
     * @param handler  执行的handler
     * @param ex       在handler执行期间抛出的异常
     * @return 相应的要转发到的ModelAndView,或null
     */
    @Nullable
    ModelAndView resolveException(
            HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);

}
复制代码

5 ViewResolver

5.1 View

View,视图,Spring MVC框架提供了很多类型的View视图支持,包括:JstlView、FreemarkerView、PdfView等,我们最常用的视图就是jsp,它对应着InternalResourceView。

View对象中保存了经过逻辑视图名解析出来的物理视图资源URL路径,通过View.render方法可将返回的model数据(需要展示的数据)填充(渲染)到对应的物理视图中的对应位置,并将最终渲染完毕的页面通过response响应给客户端(比如返回拼接的HTML文本)。

但是,如果更加严格的话,View 更多的是用于在将模型数据移交给特定视图技术之前进行数据准备,而不是真正的进行视图渲染,视图渲染仍然依靠其该类型视图本身的视图渲染技术!

比如对于一个简单的JSP的View来说,比如InternalResourceView,在渲染时首先就是将model数据存储到request域属性中,然后通过RequestDispatcher直接将请求include包含或者forward转发到物理视图路径URL对应的JSP资源,而include/forward方法则是我们在之前就见过的原始的Servlet API,最终还是通过response响应给用户的(对JSP来说就是将JSP渲染之后通过response输出为HTML文本)。

从上面JSP视图的渲染和输出可以看出来,View中仅仅是进行了数据的封装和准备(比如将model数据存储到request域属性中),并没有进行实际渲染视图的工作(仅仅是调用include/forward方法),对于JSP视图来说,真正的视图渲染和数据填充以及返回响应仍然是依赖最原始JSP技术,与Spring MVC无关,与View无关。

public interface View {

    /**
     * @return Content-Type 字符串,可能包含charset
     */
    @Nullable
    default String getContentType() {
        return null;
    }

    /**
     * 根据给定的model数据渲染当前的view
     *
     * @param model    一个Map,名称作为key,值作为value
     * @param request  当前请求
     * @param response 响应对象
     */
    void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
            throws Exception;
}
复制代码

5.2 ViewResolver

视图解析器,ViewResolver根据handler执行之后返回的ModelAndView中的String类型的逻辑视图名解析成物理视图名,即具体的资源URL地址,再生成对应的View视图对象,具体的视图渲染和数据填充的工作由View视图自己完成,实际上View中也仅仅是进行了数据准备工作,最终仍然是通过调用对应的视图的API来进行视图渲染和数据填充的,而这些方法都是各种视图模版或者技术规范自身的API,并非是View实现的。

ViewResolver接口只有一个resolveViewName方法,用于根据给定的逻辑视图名viewName和区域(用于国际化)locale查找对应的物理视图URL位置,并将其解析为一个View视图对象!

public interface ViewResolver {

    /**
     * 按给定的viewName解析为View视图。
     * <p>
     *
     * @param viewName 要解析的视图的名称
     * @param locale   解析视图的区域设置,用于支持国际化
     * @return View 对象,如果找不到对应的视图,那么返回null
     * @throws Exception 如果视图无法解析(通常在创建实际View视图对象时出现问题)
     */
    @Nullable
    View resolveViewName(String viewName, Locale locale) throws Exception;

}
复制代码

Spring为我们提供了非常多的视图解析器的默认实现,例如:

  1. BeanNameViewResolver:在当前应用程序上下文中将逻辑视图名称解析为 bean名称,随后查找对应的名称的bean实例并返回。也就是说对应名称的bean应该是一个View视图对象。
  2. FreeMarkerViewResolver:将逻辑视图名称解析为FreeMarkerView对象,说白了就是对FreeMarker模版的支持!
  3. InternalResourceViewResolver使用的最广泛的一个视图解析器。将逻辑视图名称解析为InternalResourceView对象,而InternalResourceView则会将返回的Model模型的属性都存放到对应的request属性中,然后通过RequestDispatcher在服务器端把请求forword到目标URL。我们知道/WEB-INF/下面的内容是不能直接通过请求的方式请求到的,为了安全性考虑,我们通常会把jsp等资源文件放在WEB-INF目录下,而InternalResourceViewResolver的自动转发机制就能即安全又方便(不需要写转发代码了)的访问这些资源了。

默认的ViewResolver就是InternalResourceViewResolver,指定的ViewResolver同样支持Order排序!

5.3 ViewResolver配置

Spring MVC的很多组件都不需要我们配置,但是ViewResolver则可能需要!通常我们这样配置InternalResourceViewResolver:

<!--配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <!--要访问文件所在的相对目录-->
    <property name="prefix" value="/WEB-INF/pages/"/>
    <!--文件的后缀名-->
    <property name="suffix" value=".jsp"/>
</bean>
复制代码

对于没有前后端分离的项目,通常各种视图比如JSP、CSS、JS文件是放在WEB-INF目录下的,因为/WEB-INF/下面的内容是不能通过直接请求的方式访问到,它需要服务器内部转发,因此我们通常配置InternalResourceViewResolver。

ViewResolver默认解析的视图文件位置根路径为webapp,WEB-INF目录也在这个下面,因此如果我们需要找到一个/WEB-INF/pages/index.jsp文件,那么我们返回的逻辑视图名就必须是“/WEB-INF/pages/index.jsp”。

此时我们可以像上面的案例那样设置InternalResourceViewResolver的prefix和suffix属性,prefix表示视图文件的前缀路径,而suffix则表示视图文件的后缀。此时我们返回的逻辑视图名可以直接返回“index”,InternalResourceViewResolver在解析时会自动拼接为“/WEB-INF/pages/index.jsp”,这样代码也更加简洁!

6 Spring MVC的执行流程

在这里插入图片描述

  1. 请求到达DispatcherServlet。
  2. 确定当前请求的处理器——handler。通过遍历handlerMappings,依次调用每个HandlerMapping的getHandler方法获取HandlerExecutionChain对象,只要有一个HandlerMapping的getHandler方法返回值不为null,就返回该返回值,就不会继续向后查找,如果在所有的handlerMapping中都没找到,则返回null。对于RequestMappingHandlerMapping,他返回的handler就是HandlerMethod
    1. 找到的handler实际上是一个HandlerExecutionChain对象,它包含了真正的handler以及适用于当前handler的HandlerInterceptor拦截器链
    2. 如果没有找到任何handler(handler返回null),那么通常返回404响应码或者抛出NoHandlerFoundException异常。
  3. 根据handler确定当前请求的处理器适配器——HandlerAdapter。通过遍历handlerAdapters,依次调用每个HandlerAdapter的supports方法只要有一个HandlerAdapter的supports方法返回true,就返回该HandlerAdapter,就不会继续向后查找,如果在所有的HandlerAdapter中都没找到,则直接抛出ServletException。对于HandlerMethod,它的适配器就是RequestMappingHandlerAdapter
  4. 顺序应用拦截器链中所有拦截器的PreHandle预处理方法,只要有一个拦截器不通过,那么该请求就不会继续处理,而是直接从当前拦截器倒序执行afterCompletion方法,最后返回。如果都通过,那么继续后续处理
  5. 通过HandlerAdapter来执行给定的handler,将返回一个ModelAndView结果对象。
    1. 不同的handler的实际工作流程差别非常大,对于HandlerMethod来说,它是通过RequestMappingHandlerAdapter来调用的,最终目的就会尝试执行对应的@ReuqestMapping方法,也就是业务逻辑,其中还包括参数反序列化、数据绑定、检验、返回值处理、序列化等等步骤。
    2. ModelAndView对象的Model部分是业务返回的模型数据,它是一个map,View部分为视图的逻辑视图名,即ViewName
    3. 返回的ModelAndView用以渲染视图,如果不需要渲染视图,则返回null,此情况通常是已将返回的数据序列化为JSON字符串写入response中了,比如application/json请求(通常是前后端分离的项目)。
  6. handler正常处理完毕时,倒序应用拦截器链中的所有拦截器的postHandle后处理方法。
  7. 处理handler执行过程中抛出的异常(如果有),将会通过注册的HandlerExceptionResolvers确定处理错误的ModelAndView,借此我们可以对异常转发到统一配置的错误页面!
  8. 如果ModelAndView不为null,则根据ModelAndView(无论是正产返回的还是错误处理的)渲染视图。(对于前后端分离的项目,通常是基于JSON的application/json请求,因此是没有渲染视图这一步的)。
    1. 首先可能会调用ViewResolver尝试将给定的视图名称解析为一个View对象(待呈现,内部保存了经过逻辑视图名viewName解析出来的物理视图资源URL路径)。
    2. 随后调用View本身的render方法,真正的执行渲染的工作。但是,如果更加严格的话,View 更多的是用于在将模型数据移交给特定视图技术之前进行数据准备,而不是真正的进行视图渲染,视图渲染仍然依靠其该类型视图本身的视图渲染技术!
      1. 对于一个简单的转发请求或者包含请求,如果目标是JSP视图,则View就是InternalResourceView,在渲染时首先就是将model数据存储到request域属性中,然后通过RequestDispatcher直接将请求include包含或者forward转发到物理视图路径URL对应的JSP资源,而include/forward方法则是我们在之前就见过的原始的Servlet API,最终还是通过response响应给用户的(对JSP来说就是将JSP渲染之后通过response输出为HTML文本)。
      2. 从上面JSP视图的渲染和输出可以看出来,View中仅仅是进行了数据的封装和准备(比如将model数据存储到request域属性中),并没有进行实际渲染视图的工作(仅仅是调用include/forward方法),对于JSP视图来说,真正的视图渲染和数据填充以及返回响应仍然是依赖最原始JSP技术,与Spring MVC无关,与View无关。
  9. 最后,无论请求处理成功还是失败抛出异常,都会倒序应用拦截器链中的所有拦截器的afterCompletion处理方法,表示请求处理完毕。

如果不需要渲染给定的模型和视图,那么也就不需要ViewResolver和View的工作了,即第8步不需要了。比如,如果是返回的结果是JSON数据,那么在Handler执行handle方法的时候,也就是第5步时,执行返回的JSON结果就已经被写入response响应体中了,并且执行完毕之后的返回的ModelAndView为null。

所以说,如果是前后端分离的项目,统一使用JSON数据交互,那么表现层虽然仍然是MVC模式,但是通常Model和View并没有被用到,表现层也被称为Controller层!

上面的步骤仅仅是大概的步骤!还有些细节没有讲到,比如如果支持JSON数据交互,那么在Handler执行的时候会首先将JSON数据转换为对象,执行完毕之后还会将返回的对象转换为JSON数据。后面当我们学习了Spring MVC的源码之后,我们将会更加清晰!

另外,在处理器适配器HandlerAdapter执行handler的过程中(handle方法中),还会涉及到请求和响应相关的数据转换、格式化、检验的工作,比如JSON序列化和反序列化,以及数据类型转换、参数条件校验、方法参数的封装等,具体的逻辑还是很复杂的!

对于REST风格的请求与响应来说,核心类就是HttpMessageConverter,它用于完成从请求体到请求方法参数,以及从返回结果到响应体的转换。

7 默认MVC组件配置

我们可以在项目配置文件中配置上面的表格中列出来的各种基础bean的实现。DispatcherServlet将在当前WebApplicationContext容器中检查每一个特殊的bean,如果某个特殊bean我们并没有手动配置,那么DispatcherServlet将会加载位于org\springframework\web\servlet\DispatcherServlet.properties配置文件中该类型bean的默认配置,否则不会加载。

默认情况下,这些组件是在DispatcherServlet的onRefresh()方法中创建容器的时候被初始化的,并且会被配置到DispatcherServlet实例内部的对应属性中保存起来方便后续直接使用!另外,自动加载的bean仅存放于DispatcherServlet内部的集合属性中,并没有交给Spring 容器管理,这是要注意的地方!

Spring 5.2.8.RELEASE版本中,DispatcherServlet.properties的配置如下:

# Default implementation classes for DispatcherServlet's strategy 
interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
   org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
   org.springframework.web.servlet.function.support.RouterFunctionMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
   org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
   org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
   org.springframework.web.servlet.function.support.HandlerFunctionAdapter


org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
   org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
   org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

复制代码

需要注意的是,通过DispatcherServlet.properties默认加载的组件仅会创建对象实例,仅提供最基本的mvc功能而并没有激活其他默认配置,而如果通过@EnableWebMvc注解或者< mvc:annotation-driven/>标签开启mvc配置之后,除了会注册这些组件之外,还会加载一些默认配置,并且支持自定义配置。

比如,如果不开启MVC配置,那么mvc就不支持application/json请求和响应(即使有jackson的依赖也不会自动注册MappingJackson2HttpMessageConverter),也就无法提供REST风格的请求、响应和异常处理,没有默认的conversionService!

相关文章:

  1. spring.io/
  2. Spring Framework 5.x 学习
  3. Spring Framework 5.x 源码

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

Supongo que te gusta

Origin juejin.im/post/7062533951868370974
Recomendado
Clasificación