本篇对Spring MVC 的执行流程做一个简单总结
MVC执行流程总结
当浏览器发送一个请求,例如http://localhost:8080/hello,请求到达服务器后,一般会进行如下操作:
1、首先会经过DispatcherServlet,默认映射路径为 /,即会匹配到所有请求 URL,可作为请求的统一入口,也被称之为前控制器(jsp 不会匹配到 DispatcherServlet )
非Spring Boot 程序,需要手动进行创建,此前的案例中已多次演示:
   /**
     * 创建DispatcherServlet
     * @return
     */
    @Bean
    public DispatcherServlet dispatcherServlet(){
        return new DispatcherServlet();
    }Spring Boot 程序,由 DispatcherServletAutoConfiguration 进行自动装配:

DispatcherServlet默认是在首次使用时,由tomcat容器初始化,也可以进行设置setLoadOnStartup() 为启动时初始化:
    /**
     * 注册DispatcherServlet springmvc入口
     * @param dispatcherServlet
     * @return
     */
    @Bean
    public DispatcherServletRegistrationBean dispatcherServletRegistrationBean
    (DispatcherServlet dispatcherServlet,WebMvcProperties webMvcProperties){
        DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
        //设置tomcat容器启动时即进行DispatcherServlet初始化
        registrationBean.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
        return registrationBean;
    }DispatcherServlet 初始化时会优先到容器里寻找各种组件,作为它的成员变量:
下面的init方法有一个共同点:首先会去父子容器中寻找有无相关组件,如果没有会使用默认的组件:

- HandlerMapping :初始化时记录映射关系。(初始化时,会收集所有映射信息,封装为 Map)
- HandlerAdapter :初始化时准备参数解析器、返回值处理器、消息转换器 。(分派请求)
- HandlerExceptionResolver :初始化时准备参数解析器、返回值处理器、消息转换器。(处理异常)
- ViewResolver :准备视图处理
容器初始化时,会收集所有 @RequestMapping 映射信息,封装为 Map:
//        RequestMappingHandlerMapping 初始化时,会收集所有 @RequestMapping 映射信息,封装为 Map
//        k:请求方式 路径{ /test4}  v 方法信息com.itbaima.a18.Controller1#test4()
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
        handlerMethods.forEach((k,v)->{
            System.out.println( k + "=" + v);
        });2、DispatcherServlet 会利用HandlerMapping 的实现去查找控制器方法,我们使用最常用的 RequestMappingHandlerMapping 举例:
- 
根据 /hello 路径找到 @RequestMapping("/hello") 对应的控制器方法 
- 
控制器方法会被封装为 HandlerMethod 对象,并结合匹配到的拦截器一起返回给 DispatcherServlet 
- 
HandlerMethod 和拦截器合在一起称为 HandlerExecutionChain(调用链)对象 //发送请求了,根据路径K 获取RequestMappingHandlerMapping 封装的 Map 对应的V HandlerMethod 
 //获取的结果会包装在拦截器链中
 //HandlerExecutionChain with [com.itbaima.a18.Controller1#test1()] and 0 interceptors
 HandlerExecutionChain chain = handlerMapping.getHandler(request);
3、DispatcherServlet 接下来会:
调用拦截器的 preHandle 方法:如果与preHandle方法中定义的拦截规则不匹配,就直接返回错误信息,不再向下执行。
RequestMappingHandlerAdapter 调用 handle 方法,准备数据绑定工厂、模型工厂、ModelAndViewContainer、将 HandlerMethod 完善为 ServletInvocableHandlerMethod:

- 
@ControllerAdvice 全局增强点1️:补充模型数据,通过解析@ModelAttribute 标注的方法补充模型数据到container中。 @ControllerAdvice static class MyControllerAdvice { @ModelAttribute("a") public String aa() { return "aa"; } } /** * ModelAttribute注解加在参数上,由参数解析器负责解析 * 加在方法上,由HandlerAdapt进行解析 */ @Controller static class Controller1 { @ResponseStatus(HttpStatus.OK) public ModelAndView foo(@ModelAttribute("u") User user) { System.out.println("foo"); return null; } }
- 
@ControllerAdvice 全局增强点2:补充自定义类型转换器,通过@InitBinder 注解标记一个用于初始化DataBinder对象,自定义数据绑定行为的方法,它会在控制器处理请求之前被调用。(如果@InitBinder注解加在被@ControllerAdvice 注解标记的控制器类的方法中时,其作用范围是全局的,并且是由RequestMappingHandlerAdapter 在初始化时解析并记录。而@InitBinder 注解加在被@Controller 标记的控制器中的方法上时,会在控制器方法首次执行时解析并记录。 @ControllerAdvice static class MyControllerAdvice { @InitBinder public void binder3(WebDataBinder webDataBinder) { webDataBinder.addCustomFormatter(new MyDateFormatter("binder3 转换器")); } } @Controller static class Controller1 { @InitBinder public void binder1(WebDataBinder webDataBinder) { webDataBinder.addCustomFormatter(new MyDateFormatter("binder1 转换器")); } public void foo() { } }
RequestMappingHandlerAdapter中有两个成员变量:
- private final Map<ControllerAdviceBean, Set<Method>> initBinderAdviceCache:用于存储被@ControllerAdvice标记的控制器中 @InitBinder 标注的方法。
- private final Map<Class<?>, Set<Method>> initBinderCache:用于存储@Controller 标记的控制器中@InitBinder标注的方法。


然后会使用 HandlerMethodArgumentResolver 准备参数
- @ControllerAdvice 全局增强点3:RequestBody 增强
调用 ServletInvocableHandlerMethod 、使用 HandlerMethodReturnValueHandler 处理返回值。
- @ControllerAdvice 全局增强点4:ResponseBody 增强
ResponseBody返回响应体前包装:
    @ControllerAdvice
    static class MyResponseAdvice implements ResponseBodyAdvice<Object> {
        /**
         * 支持的方法
         *
         * @param returnType    返回值类型
         * @param converterType 转换类型
         * @return
         */
        @Override
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
            //如果方法上加了ResponseBody注解,或者类上加了ResponseBody/RestController注解,才进行转换
            if (returnType.getMethodAnnotation(ResponseBody.class) != null
                    || AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null) {
                return true;
            }
            return false;
        }
        /**
         * 增强的逻辑
         *
         * @param body                  返回值
         * @param returnType            返回类型
         * @param selectedContentType   所选的响应内容类型。
         * @param selectedConverterType 所选的消息转换器类型。
         * @param request               当前的请求对象。
         * @param response              当前的响应对象。
         * @return
         */
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
            //如果返回值就是Result类型则直接返回
            if (body instanceof Result) {
                return body;
            }
            //否则包装成Result类型返回
            return Result.ok(body);
        }
    }最后会根据 ModelAndViewContainer 获取 ModelAndView:
- 返回值处理器调用了 HttpMessageConverter 来将结果转换为 JSON,这时 ModelAndView 就为 null
- 如果返回的 ModelAndView 为 null,不会进行后续的视图渲染与解析。
ModelAndView、String、未被@ResponseBody 注解控制的对象类型返回值(无论是否显式声明了@ModelAttribute),都要经历视图渲染的过程。
ModelAndView找视图,是根据ModelAndView构造中的viewName寻找同名的视图,还会使用.addObject() 方法中指定的数据对视图进行渲染 。
如果没有指定视图名,则会:
根据请求路径推断视图名: 如果在处理器方法中没有显式指定视图名,Spring MVC 会根据请求路径来推断视图名。
根据返回值类型推断视图名: 如果处理器方法的返回值类型是String类型,并且没有使用 @ResponseBody 注解,Spring MVC 会将返回的字符串作为视图名处理。
默认视图名: 如果以上两种方式都没有找到视图名,Spring MVC 会使用默认的视图名。默认的视图名通常是处理器方法所在的类名转换而来,再加上适当的前后缀。
String找视图,是根据返回值的名称去找同名的视图。
未被@ResponseBody 注解控制的对象类型返回值,找视图时,如果方法上使用 @RequestMapping("/") 及其派生注解声明了路径,则按照路径的值去匹配视图。如果没有,则需要手动指定路径。
HttpEntity、HttpHeaders、加上了@ResponseBody 注解的对象返回值类型,因为对应解析器的handleReturnValue 方法中标记了请求已经被处理,无需继续渲染视图,所以不走渲染视图流程。其区别在于返回的响应头和响应体的完整性。
4、调用拦截器的 postHandle 方法
5、处理异常或视图渲染
- @ControllerAdvice 全局增强点5️:@ExceptionHandler 异常处理
6、调用拦截器的 afterCompletion 方法