【小家Spring】Spring MVC执行流程 FrameworkServlet、DispatcherServlet源码分析(processRequest、doDispatch)(上)

简介: 【小家Spring】Spring MVC执行流程 FrameworkServlet、DispatcherServlet源码分析(processRequest、doDispatch)(上)

前言


本篇博文会更偏向于应用、更加宏观一些。

但是,本人还是建议,在了解Spring MVC的执行流程之前,先参阅这两篇博文:

【小家Spring】Spring容器(含父子容器)的启动过程源码级别分析(含web.xml启动以及全注解驱动,和ContextLoader源码分析)

【小家Spring】Spring MVC初始化(容器启动)时,Spring MVC九大组件初始化详解(Spring MVC的运行机制)

这样会有助于你更好的了解本文的一个流程,毕竟既会宏观的从步骤上去讲解,也会稍微微观(注意是稍微)的从源码角度去分析一下子


备注:如果你已经是高手,已经对Spring的容器管理比较了解了,请自行忽略~


Spring MVC的执行流程似乎是一个面必问的话题,面试官能通过一个问题,探底出你掌握此部分只是的深度甚至广度,此篇博文,不容忽视啊,哈哈~


问题剖细了,可以是:一个请求url是怎么样找到Handler进行处理的?拦截器为何preHandler顺序执行,postHandler就倒序执行呢?Spring MVC是怎么样去优雅的处理异常的?…、


请求处理流程


了解之前,我们先宏观看看,一个请求达到Spring MVC,它的一个处理流程。

这里我首先贴上一张非常权威的流程图,也是Spring in Action这本书里提供的,springmvc的核心组件和请求处理流程


image.png


描述:


1.DispatcherServlet是springmvc中的前端控制器(front controller),负责接收request并将request转发给对应的处理组件


2.HanlerMapping是springmvc中完成url到controller映射的组件.DispatcherServlet接收request,然后从HandlerMapping查找处理request的controller


3.Cntroller处理request,并返回ModelAndView对象,Controller是springmvc中负责处理request的组件(类似于struts2中的Action),ModelAndView是封装结果视图的组件


4.④ ⑤ ⑥:视图解析器解析ModelAndView对象并返回对应的视图给客户端


我个人认为,这本书里说的还是稍微抽象了点,有核心,但是缺失了一些较为细节的处理步骤。那么下面,我也贴出一副更加具象的图片,供以参考:


image.png

描述:


1.用户发送的所有请求(包括上传附件等任何请求),统一先交给DispatcherServlet


2.然后DispatcherServlet调用合适的HandlerMapping ,从而找到一个Handler(Controller中的方法以及拦截器),然后封装成HandlerExecutionChain返回给控制器DispatcherServlet


3.调用处理器适配器HandlerAdapter去执行handler(注意:执行之前需要先请求执行链中的拦截器的preHandle方法进行拦截,返回true就继续执行,返回false就不继续执行了)


4.处理器执行完后,返回给控制器DispatcherServlet一个ModelAndView(里面放有视图信息,模型数据信息)。 然后就执行postHandle方法


5.控制器调用视图解析器解析视图,根据逻辑名(xxxx/xxxx/xxxx.jsp)解析成真正的视图view(jsp,ftl等),然后返给控制器一个View


6.控制器开始渲染视图(视图渲染器可以是第三方或自己实现),然后将模型数据填充到request中。


7.DispatcherServlet响应用户请求,展示jsp等视图信息



备注:这里面还会设计到数据绑定、序列化、返序列化、异常处理等一些内容,那就是更加细节的东西了,那就以后专题再讲解。毕竟这一块对使用者来说还是非常的透明的。。。


DispatcherServlet执行流程的源码分析


image.png


我们从调用栈了可以很直接的看到调用关系。

我发的请求为:http://localhost:8080/demowar_war/controller/hello GET请求。

FrameworkServlet复写了service方法如下:


  /**
   * Override the parent class implementation in order to intercept PATCH requests.
   * 官方doc说得很清楚,复写是为了支持到PATCH请求(PATCH方法是新引入的,是对PUT方法的补充,用来对已知资源进行局部更新,目前使用得非常少,但SpringMVC也给与了支持)
   * 备注:源生的servlet并不支持PATCH请求
   */
  @Override
  protected void service(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
    if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
      processRequest(request, response);
    }
    else {
      super.service(request, response);
    }
  }


因为我们是get请求,所以我们重点只需要看看子类复写的doGet方法即可。但是猛的发现,FrameworkServlet复写所有的doGet/doPost等等都交给了processRequest(request, response);方法。


doOptions稍微有点特殊,它处理一些是否允许跨域的问题,TRACE请求:主要用于测试或诊断,可忽略

FrameworkServlet#processRequest方法解析


该方法作为FrameworkServlet的实现,其实它也是提供了一些模版实现,最终会开口给子类的 模版设计模式,在Spring源码中大量存在。此处我们关注点在于:FrameworkServlet为我们做了哪些事情(相对来说比较复杂点)~


阅读前博文参考:

【小家Spring】Spring MVC之RequestContextHolder和LocaleContextHolder的使用详解以及使用误区


  protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    long startTime = System.currentTimeMillis();
    // 记录抛出的异常~~~(若有的话)
    Throwable failureCause = null;
    //拿到之前的LocaleContext上下文(因为可能在Filter里已经设置过了)
    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
    // 以当前的request创建一个Local的上下文,后面会继续处理
    LocaleContext localeContext = buildLocaleContext(request);
    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    // 这里面build逻辑注意:previousAttributes若为null,或者就是ServletRequestAttributes类型,那就new ServletRequestAttributes(request, response);
    // 若不为null,就保持之前的绑定结果,不再做重复绑定了(尊重原创)
    ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
    // 拿到异步管理器。这里是首次获取,会new WebAsyncManager(),然后放到request的attr里面
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    //这里需要注意:给异步上下文恒定注册了RequestBindingInterceptor这个拦截器(作用:绑定当前的request、response、local等)
    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
    //这句话很明显,就是吧request和Local上下文、RequestContext绑定
    initContextHolders(request, localeContext, requestAttributes);
    try {
      //模版设计模式:由子类DispatcherServlet去实现实际逻辑
      doService(request, response);
    } catch (ServletException | IOException ex) {
      failureCause = ex;
      throw ex;
    } catch (Throwable ex) {
      failureCause = ex;
      throw new NestedServletException("Request processing failed", ex);
    } finally { //这个时候已经全部处理完成,视图已经渲染了
      //doService()方法完成后,重置上下文,也就是解绑
      resetContextHolders(request, previousLocaleContext, previousAttributes);
      if (requestAttributes != null) {
        requestAttributes.requestCompleted();
      }
      //关键:不管执行成功与否,都会发布一个事件,说我处理了这个请求(有需要监听的,就可以监听这个事件了,每次请求都会有)
      publishRequestHandledEvent(request, response, startTime, failureCause);
    }
  }

publishRequestHandledEvent()发布请求处理完后的事件源码

  private void publishRequestHandledEvent(HttpServletRequest request, HttpServletResponse response,
      long startTime, @Nullable Throwable failureCause) {
    //当publishEvents设置为true和 webApplicationContext 不为空就会处理这个事件的发布
    if (this.publishEvents && this.webApplicationContext != null) {
      // 计算出处理该请求花费的时间
      long processingTime = System.currentTimeMillis() - startTime;
      this.webApplicationContext.publishEvent(
          //ServletRequestHandledEvent这个事件:目前来说只有这里会发布
          new ServletRequestHandledEvent(this,
              request.getRequestURI(), request.getRemoteAddr(),
              request.getMethod(), getServletConfig().getServletName(),
              WebUtils.getSessionId(request), getUsernameForRequest(request),
              processingTime, failureCause, response.getStatus()));
    }
  }


下面我们来写个监听器,专门来监听这个事件:


/**
 * 专门监听ServletRequestHandledEvent时间的监听器
 *
 * @author fangshixiang
 * @description //
 * @date 2019/2/28 12:10
 */
@Slf4j
@Component
public class ServletReqestHandledEventListener implements ApplicationListener<ServletRequestHandledEvent> {
    @Override
    public void onApplicationEvent(ServletRequestHandledEvent event) {
        //url=[/demowar_war/controller/hello]; client=[127.0.0.1]; method=[GET]; servlet=[dispatcher]; session=[null]; user=[null]; time=[143ms]; status=[OK]
        log.info(event.getDescription()); 
        log.info("返回状态码为:" + event.getStatusCode()); //返回状态码为:200
        log.info("异常信息为:" + event.getFailureCause()); //异常信息为:null
        log.info("处理请求耗时为:" + event.getProcessingTimeMillis()); //处理请求耗时为:143
        log.info("事件源为:" + event.getSource()); //事件源为:org.springframework.web.servlet.DispatcherServlet@3e7fadbb
    }
}


相关文章
|
16天前
|
前端开发 Java 微服务
《深入理解Spring》:Spring、Spring MVC与Spring Boot的深度解析
Spring Framework是Java生态的基石,提供IoC、AOP等核心功能;Spring MVC基于其构建,实现Web层MVC架构;Spring Boot则通过自动配置和内嵌服务器,极大简化了开发与部署。三者层层演进,Spring Boot并非替代,而是对前者的高效封装与增强,适用于微服务与快速开发,而深入理解Spring Framework有助于更好驾驭整体技术栈。
|
8月前
|
前端开发 Java 测试技术
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
本文介绍了 `@RequestParam` 注解的使用方法及其与 `@PathVariable` 的区别。`@RequestParam` 用于从请求中获取参数值(如 GET 请求的 URL 参数或 POST 请求的表单数据),而 `@PathVariable` 用于从 URL 模板中提取参数。文章通过示例代码详细说明了 `@RequestParam` 的常用属性,如 `required` 和 `defaultValue`,并展示了如何用实体类封装大量表单参数以简化处理流程。最后,结合 Postman 测试工具验证了接口的功能。
413 0
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
|
8月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestBody
`@RequestBody` 是 Spring 框架中的注解,用于将 HTTP 请求体中的 JSON 数据自动映射为 Java 对象。例如,前端通过 POST 请求发送包含 `username` 和 `password` 的 JSON 数据,后端可通过带有 `@RequestBody` 注解的方法参数接收并处理。此注解适用于传递复杂对象的场景,简化了数据解析过程。与表单提交不同,它主要用于接收 JSON 格式的实体数据。
643 0
|
8月前
|
前端开发 Java 微服务
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@PathVariable
`@PathVariable` 是 Spring Boot 中用于从 URL 中提取参数的注解,支持 RESTful 风格接口开发。例如,通过 `@GetMapping(&quot;/user/{id}&quot;)` 可以将 URL 中的 `{id}` 参数自动映射到方法参数中。若参数名不一致,可通过 `@PathVariable(&quot;自定义名&quot;)` 指定绑定关系。此外,还支持多参数占位符,如 `/user/{id}/{name}`,分别映射到方法中的多个参数。运行项目后,访问指定 URL 即可验证参数是否正确接收。
401 0
|
4月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
273 0
|
4月前
|
SQL Java 数据库连接
Spring、SpringMVC 与 MyBatis 核心知识点解析
我梳理的这些内容,涵盖了 Spring、SpringMVC 和 MyBatis 的核心知识点。 在 Spring 中,我了解到 IOC 是控制反转,把对象控制权交容器;DI 是依赖注入,有三种实现方式。Bean 有五种作用域,单例 bean 的线程安全问题及自动装配方式也清晰了。事务基于数据库和 AOP,有失效场景和七种传播行为。AOP 是面向切面编程,动态代理有 JDK 和 CGLIB 两种。 SpringMVC 的 11 步执行流程我烂熟于心,还有那些常用注解的用法。 MyBatis 里,#{} 和 ${} 的区别很关键,获取主键、处理字段与属性名不匹配的方法也掌握了。多表查询、动态
134 0
|
4月前
|
JSON 前端开发 Java
第05课:Spring Boot中的MVC支持
第05课:Spring Boot中的MVC支持
208 0
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
415 0
|
存储 开发框架 前端开发
[回馈]ASP.NET Core MVC开发实战之商城系统(五)
经过一段时间的准备,新的一期【ASP.NET Core MVC开发实战之商城系统】已经开始,在之前的文章中,讲解了商城系统的整体功能设计,页面布局设计,环境搭建,系统配置,及首页【商品类型,banner条,友情链接,降价促销,新品爆款】,商品列表页面,商品详情等功能的开发,今天继续讲解购物车功能开发,仅供学习分享使用,如有不足之处,还请指正。
292 0
|
开发框架 前端开发 .NET
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
187 0