spring高级篇(八)

本篇对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 方法

相关推荐
程序员小崔日记4 小时前
20块钱注册一年 .COM 域名!Spaceship 最新优惠教程,支付宝即可付款(附优惠码)
服务器·腾讯云·域名
IKUN家族5 小时前
Spring MVC(一)
java·spring·mvc
实心儿儿5 小时前
Linux —— 进程间关系和守护进程
linux·运维·服务器
林浅不想努力6 小时前
LVS知识点总结
服务器·云原生·lvs
老马识途2.06 小时前
关于跨域问题的总结
java·前端
Dawn-bit7 小时前
Linux磁盘管理详解
linux·运维·服务器·计算机网络·云计算
都叫我大帅哥7 小时前
Java日期时间三十年战争:从Date考古到LocalDateTime革命,以及数据库与前端的那点事儿
java
Muscleheng7 小时前
SpringBoot 集成 DeepSeek 实现 RAG 文档问答
java·spring boot·ai·springai
2501_936415698 小时前
可变参数&综合练习&斗地主游戏
java·windows·游戏
颜酱9 小时前
07 | 把字段与指标同步到 Qdrant(生成阶段)
前端·人工智能·后端