深入理解SpringMvc 启动流程

简介: 深入理解SpringMvc 启动流程

彻底搞懂 HandlerMapping和HandlerAdapter#


知识点的回顾:


当Tomcat接收到请求后会回调Servlet的service方法,一开始入门Servlet时,我们会让自己的Servlet去实现HttpServlet接口,重写它的doGet()doPost()方法



在SpringMvc中,SpringMvc的核心组件DispatcherSerlvet的继承图如上,可以看到上图,其实这个DispatcherServlet终究还是一个Servlet


我们追踪一下他的生命周期创建过程, 首先是说Servlet的创建时机,其实是存在两种情况的, 这取决于.setLoadOnStartup(1);设置的启动级别,当然一般都会设置成正数,表示当容器启动时实例化Servlet


于是Tomcat实例化Servlet,Servlet被初始化时首先被回调的方法是init()这大家都知道的,但是SpringMvc提供的DispatcherServlet中存在一个静态块,源码如下: 这个静态块干了什么事呢? 读取的是class path下面的 DispatcherServlet.properties


static {
    // Load default strategy implementations from properties file.
    // This is currently strictly internal and not meant to be customized
    // by application developers.
    try {
      // todo 它读取的是class path 下面的 DispatcherServlet.properties 配置文件
      // todo resource/web/servlet/DispatcherServlet.properties
      // todo 将这些默认的实现信息,封装进了Properties  defaultStrategies
      ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
      defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
    }
    catch (IOException ex) {
      throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
    }
  }


那问题来了,这个配置文件到底存放的什么? 让DispatcherServlet如此迫切的去加载? 我们文件贴在下面,可以看到存放的是一些全类名,这些是DiapacherServlet针对不同策略接口提供的八个默认的实现,当在上下文中没有匹配到程序员添加的这些实现时,就会使用这些默认的实现


# 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.
# todo 这里存在 DiapacherServlet策略接口的八个默认的实现
# todo 当在上下文中没有匹配到程序员添加的这些实现时,就会使用这些默认的实现
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.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.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


好吧, 虽然这也不算跑题,但是我们还是回到DispatcherServletinit()方法,其实去这个DispatcherServlet中是找不到这个init()方法的, 那这个方法在哪里了呢? 其实就在他的祖父类HttpServletBean中,源码如下:


意图很明确,前面用来初始化环境参数,后者调用initServletBean();


@Override
public final void init() throws ServletException {
  if (logger.isDebugEnabled()) {
    logger.debug("Initializing servlet '" + getServletName() + "'");
  }
  // Set bean properties from init parameters.
  // todo 设置初始化参数
  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.
  // todo 初始化SerlvetBean
  initServletBean();
  if (logger.isDebugEnabled()) {
    logger.debug("Servlet '" + getServletName() + "' configured successfully");
  }
}


initServletBean(); 见名知意,初始化ServletBean,说白了就是想去初始化DispatcherServlet呗,跟进去查看,不出意料,他是个抽象方法,跟进它的实现类

他的实现类是FrameworkServlet,进去跟进,看他去创建应用的上下文,但是如果上下文已经被初始化了,他是不会重复创建上下文的


我们继续跟进它的onRefresh()方法,同样这个方法是一个抽象方法,而它是实现就是我们关注的DispatcherServlet,源码如下: 在下面做了很多事,我们还是仅仅关注两点,初始化了handlerMappinghandlerAdapater


protected void onRefresh(ApplicationContext context) {
    // todo 进行跟进,方法就在下面
    initStrategies(context);
  }
protected void initStrategies(ApplicationContext context) {
// todo 初始化和文件上传相关的解析器
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
/**
 *  todo 初始化处理器映射器,这是我们看到重点
 *  todo 什么是处理器映射器?
 *  todo Controller在Spring中有多种情况, 那当一个用户的请求到来时, 如何进一步找到哪一种Controller来处理用户的请求呢?
 *  todo 这一步就是通过处理器映射器完成的
 *  todo  说白了, 通过处理器映射器我们可以找到那个处理当前请求的特定的组件, 我们称它为handler
 *  todo 但是这之间存在一个问题,就是说,这个handler到底是方法级别的,还是类级别的我们是不知道的,只能说,这个handler就肯定能处理这次请求而已
 */
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}


到现在为止,本位关注的两个组件其实就完成了初始化了, 下一个问题就来了,什么时候使用他们呢?

那就得从Servlet的service()方法说起了,大家都知道这个方法会在Serlvet每一次收到请求时就会被回调一次,再回想我们原来是怎么变成来着? 但是后来我们都直接实现HttpServlet接口,然后重写他们的doGet()doPost()来实现我们自己的Servlet, 那SpringMvc是怎么做的呢?


源码如下:


@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
  processRequest(request, response);
}
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
  processRequest(request, response);
}


都是通过processRequest(request, response);来实现的,我们往下追踪这个方法,最终也是不出所料,我们来到了DispacherServlet

我截取部分源码如下:


protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
  HttpServletRequest processedRequest = request;
  HandlerExecutionChain mappedHandler = null;
  boolean multipartRequestParsed = false;
  //异步编程
  WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
  ModelAndView mv = null;
  Exception dispatchException = null;
  try {
    //检查请求中是否存在文件上传的操作
    processedRequest = checkMultipart(request);
    multipartRequestParsed = (processedRequest != request);
    // Determine handler for the current request.
    //todo 确定当前请求的处理程序,跟进去
    //todo 换句话说就是 推断Controller的类型, Controller存在三种类型
    // todo 跟进去看看这个handlerMapping的获取方式
    mappedHandler = getHandler(processedRequest);
    if (mappedHandler == null) {
      noHandlerFound(processedRequest, response);
      return;
    }
    // Determine handler adapter for the current request.
    // todo 用到了适配器设计模式,
    // todo 如果 上面的HandlerExecutionChain 是bean类型, 经过这个方法后将被设置成bean
    // todo 如果 上面的HandlerExecutionChain 是 method 类型, 经过这个方法后将被设置成吗method
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    // Process last-modified header, if supported by the handler.
    String method = request.getMethod();
    boolean isGet = "GET".equals(method);
    if (isGet || "HEAD".equals(method)) {
      long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
      if (logger.isDebugEnabled()) {
        logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
      }
      if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
        return;
      }


同样的,我们只是关注上面的两点,看看SpringMvc是如何玩转HandlerMappingHandlerAdapter的, 分两步,先跟进mappedHandler = getHandler(processedRequest);方法

看看他干啥了,遍历所有的HandlerMapping


第一个问题: 还记不记得哪里来的RequestMappingHandlerMapping,没错就是文章一开始我们去看初始化DispatcherServlet时在静态块里面完成的加载已经后续的初始化,所以按理说,下面的数组中就存在两个handlerMapping分别是BeanNameUrlHandlerMappingRequestMappingHandlerMapping


第二个问题: 什么是HandlerMapping? 直接看它的中文翻译就是处理器映射器是不好理解的,其实也没有特别难懂, 就是一个映射器嘛,映射什么呢? 就是映射用户的请求与后端程序员编写的Controller之间的关系, 再直白一点, 就是一个用户的请求经过了这个HandlerMapping就可以百分百确定出哪一个控制器是用来处理它的


第三个问题: 下面的HandlerMapping是个数组,意味着这个映射的规则是多种多样的,所以来个循环,如果没有任何一个映射器满足条件怎么办呢? 404呗


protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
      for (HandlerMapping hm : this.handlerMappings) {
        if (logger.isTraceEnabled()) {
          logger.trace(
              "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
        }
        HandlerExecutionChain handler = hm.getHandler(request);
        if (handler != null) {
          return handler;
        }
      }
    }
    return null;
  }


于是经过了上面HandlerMapping的处理我们获取出来了一个 HandlerExecutionChain ,并且我们百分百确定这个 HandlerExecutionChain 就是用来处理当前的请求的,但是!!! 我们却不能直接使用,因为我们是不清楚前面的获取到的这个执行器链是个方法,还是个类,于是适配器就用上了


源码如下:


这个适配器就是HandlerAdapter,使用设配器设计模式,不管得到的handler到底是什么类型的,都可以找到正确的方法区执行它


HandlerAdapter同样是DisapcherServlet的一大组件,它和上面的处理器映射器是一样的,同样是从配置文件中被读取出来


protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
    if (this.handlerAdapters != null) {
      for (HandlerAdapter ha : this.handlerAdapters) {
        if (logger.isTraceEnabled()) {
          logger.trace("Testing handler adapter [" + ha + "]");
        }
        if (ha.supports(handler)) {
          return ha;
        }
      }
    }
    throw new ServletException("No adapter for handler [" + handler +
        "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
  }


咋样? 其实本文倒是也没什么难度,就是觉得确实挺好玩的...

相关文章
|
8月前
|
缓存 Java 程序员
springboot的启动流程总结
springboot的启动流程总结
|
Java 应用服务中间件 容器
springboot启动流程
springboot的启动流程;
|
Java 开发者 微服务
SpringBoot启动流程大揭秘
通俗易懂读源码--SpringBoot启动流程详解
SpringBoot启动流程大揭秘
|
Java Spring 容器
springboot的启动流程
springboot的启动流程
|
Java 应用服务中间件 Spring
源码解析Spring Boot 的启动流程
最近有位开发同学说面试被问到Spring Boot 的启动流程,以及被问到Spring Boot 的嵌入式Web容器是什么时候加载的。如何加载的。是怎么无缝切换的。这些问题,其实回答起来也是比较复杂的。我们今天就从 `SpringApplication.run(Application.class, args);`入口,逐渐向下看下执行流程。来试着回答一下前面这两个问题。后面关于SpringBoot 的web容器可以无缝随意切换为`jetty`,`undertow.`.这个问题的回答涉及到Spring Boot是如何设计WebServer的。我们后续专门讲解一下。
11183 1
源码解析Spring Boot 的启动流程
|
Java 应用服务中间件 数据库连接
SpringBoot启动流程是什么?
Spring Boot 启动流程可以概括为以下几个步骤
786 0
|
Java
SpringBoot启动流程
SpringBoot启动流程
78 0
|
开发框架 Java Spring
SpringBoot 之启动流程
SpringBoot 是一个基于 Spring 框架的快速开发框架,旨在简化 Spring 应用程序的开发和部署。在本文中,我们将深入分析 SpringBoot 启动过程的源代码,并提供必要的解释和说明。
160 0
|
Java 程序员 Spring
Spring源码系列:Spring的启动过程
Spring源码系列:Spring的启动过程
157 0
|
XML 前端开发 Java
SpringMVC知识,spring的执行流程
SpringMVC知识,spring的执行流程
147 0

热门文章

最新文章

下一篇
开通oss服务