Spring源码14-DispatcherServlet初始化

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

SpringMVC 的实现原理是通过 servlet 拦截所有的URL来达到控制的目的。而所使用的Servlet 即是 DispatcherServlet

简单来说,就是所有的请求(这里说的比较绝对,仅为了表述用,具体拦截多少请求是通过 <url-pattern> 标签配置)都是首先请求到 DispatcherServlet,由 DispatcherServlet 来根据路径等信息转发到不同的处理器,再将结果返回。

我们来看一下 DispatcherServlet 的结构,如下:

image.png

可以看到,DispatcherServlet本质上是一个Servlet。在上面讲述 Servlet 生命周期的时候我们说过,Servlet 仅初始化一次,并且调用init 方法进行初始化。DispatcherServlet 调用的是 HttpServletBean#init 方法,下面我们来具体看看。

1. DispatcherServlet 的初始化

1.1 HttpServletBean#init

DispatcherServlet 的初始化过程主要是通过将当前的Servlet 类型实例转换为 BeanWrapper 类型实例,以便于使用Spring中提供注入功能进行对应属性的注入。

同时上面也提到了,在这里面会完成 WebApplicationContext 的具体初始化。

@Override
public final void init() throws ServletException {

        // Set bean properties from init parameters.
        // 1. 封装及验证初始化参数
        PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
        if (!pvs.isEmpty()) {
                try {
                        // 2. 将当前 Servlet 实例转化为 BeanWrapper 实例
                        BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                        // 3. 注册于相对于 Resource 的属性编辑器
                        ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
                        bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
                        // 4. 属性注入
                        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.
        // 5. servletBean的初始化
        initServletBean();
}
复制代码

上面的流程大致梳理如下:

  1. 封装及验证初始化参数: ServletConfigPropertyValues 除了封装属性外还有对属性验证的功能。

        public ServletConfigPropertyValues(ServletConfig config, Set<String> requiredProperties)
                        throws ServletException {
    
                Set<String> missingProps = (!CollectionUtils.isEmpty(requiredProperties) ?
                                new HashSet<>(requiredProperties) : null);
                // 获取 web.xml 中 DispatcherServlet 配置的 init-params 属性内容
                Enumeration<String> paramNames = config.getInitParameterNames();
                while (paramNames.hasMoreElements()) {
                        String property = paramNames.nextElement();
                        Object value = config.getInitParameter(property);
                        // 保存 init-params 属性 
                        addPropertyValue(new PropertyValue(property, value));
                        if (missingProps != null) {
                                missingProps.remove(property);
                        }
                }
    
                // Fail if we are still missing properties.
                if (!CollectionUtils.isEmpty(missingProps)) {
                        throw new ServletException(
                                        "Initialization from ServletConfig for servlet '" + config.getServletName() +
                                        "' failed; the following required properties were missing: " +
                                        StringUtils.collectionToDelimitedString(missingProps, ", "));
                }
        }
    复制代码
  2. 将当前 Servlet 实例转化为 BeanWrapper 实例: PropertyAccessorFactory.forBeanPropertyAccess 是Spring提供的工具方法,主要用于将指定实例转化为 Spring 中可以处理的 BeanWrapper 类型的实例

  3. 注册于相对于 Resource 的属性编辑器:在 对当前实例属性注入过程中一旦遇到 Resource 类型的属性就会使用 ResourceEditor 去解析

  4. 属性注入 BeanWrapper 为Spring中的方法,支持Spring的自动注入

  5. servletBean的初始化

    实际上,在ContextLoaderListener 加载的时候就已经创建了 WebApplicationContext实例(父容器),在这里则是对 WebApplicationContext 的进一步的初始化补充。

@Override
protected final void initServletBean() throws ServletException {
        ....
        long startTime = System.currentTimeMillis();

        try {
                // 委托给 initWebApplicationContext 方法来完成进一步的初始化
                this.webApplicationContext = initWebApplicationContext();
                // 留给子类覆盖操作
                initFrameworkServlet();
        }
        catch (ServletException | RuntimeException ex) {
                logger.error("Context initialization failed", ex);
                throw ex;
        }
        ...
}
复制代码

关于 initWebApplicationContext 的实现是在 FrameworkServlet#initWebApplicationContext 中完成,下面我们就来看看其实现过程。

1.2 FrameworkServlet#initWebApplicationContext

下面我们看看 FrameworkServlet#initWebApplicationContext 的代码如下:

protected WebApplicationContext initWebApplicationContext() {
        WebApplicationContext rootContext =
                        WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        WebApplicationContext wac = null;

        // 1. 如果 webApplicationContext 在构造函数的时候被注入,则 wac != null, 则可以直接使用
        if (this.webApplicationContext != null) {
                wac = this.webApplicationContext;
                if (wac instanceof ConfigurableWebApplicationContext) {
                        ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
                        if (!cwac.isActive()) {
                                if (cwac.getParent() == null) {
                                        cwac.setParent(rootContext);
                                }
                                // 2.刷新上下文环境
                                configureAndRefreshWebApplicationContext(cwac);
                        }
                }
        }
        // 3. 如果构造函数并没有注入,则wac为null,根据 contextAttribute 属性加载 WebApplicationContext
        if (wac == null) {
                // 根据 contextAttribute 属性加载 WebApplicationContext
                wac = findWebApplicationContext();
        }
        // 4. 如果上面两个方式都没有加载到 WebApplicationContext,则尝试自己加载
        if (wac == null) {
                // 自己尝试创建WebApplicationContext
                wac = createWebApplicationContext(rootContext);
        }

        if (!this.refreshEventReceived) {
                synchronized (this.onRefreshMonitor) {
                        // 5.刷新
                        onRefresh(wac);
                }
        }

        if (this.publishContext) {
                // Publish the context as a servlet context attribute.
                String attrName = getServletContextAttributeName();
                getServletContext().setAttribute(attrName, wac);
        }

        return wac;
}
复制代码

其实我们可以看到,第1,3,4步都是在找 WebApplicationContext的实例。第2,5步才是真正的初始化。

1.2.1 WebApplicationContext 的获取

结合上面的代码我们可以看到 寻找 WebApplicationContext的实例可以分为三步:

  • 判断是否通过构造函数注入了WebApplicationContext。若注入直接使用
  • 尝试根据 contextAttribute 属性加载 WebApplicationContext
  • 如果第二步仍未加载成功,则尝试自己创建 WebApplicationContext。 下面我们来一步一步分析

1.2.1.1 构造注入的WebApplicationContext

在调用 initWebApplicationContext 方法时 第一步的判断条件就是 this.webApplicationContext != null。如果this.webApplicationContext != null。则说明 WebApplicationContext 是通过构造注入的方式注入进来,可以直接使用。

这里需要注意,因为 Servlet 只会初始化一次,所以这里的 this.webApplicationContext 不会是之前初始化留下的值。

1.2.1.2 通过 contextAttribute 属性获取 WebApplicationContext

即通过 web.xml 文件中配置的servlet 参数 contextAttribute 来查找 ServerContext 中对应的属性。这里可以回忆一下,在ContextLoaderListener 中,已经将创建好的WebApplicationContext 实例保存到了ServletContext 中,其key值默认为 WebApplicationContext.class.getName() + ".ROOT"。不过这个key值是可以更改的。这里即是通过 contextAttribute 作为 key 来尝试去ServletContext中获取WebApplicationContext。contextAttribute 在初始化 DispatcherServlet 的时候可以通过 setContextAttribute 进行设置。默认的contextAttribute 为null。

protected WebApplicationContext findWebApplicationContext() {
        String attrName = getContextAttribute();
        if (attrName == null) {
                return null;
        }
        WebApplicationContext wac =
                        WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
        if (wac == null) {
                throw new IllegalStateException("No WebApplicationContext found: initializer not registered?");
        }
        return wac;
}

...

public String getContextAttribute() {
        return this.contextAttribute;
}
复制代码

1.2.1.3 尝试自己创建WebApplicationContext

上面两种方式都没有找到 WebApplicationContext,则就只能重新创建 WebApplicationContext 实例了。

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
        // 获取 Servlet 初始化参数 contextClass,即获取WebApplicationContext 的具体类型,如果没有配置默认是 XmlWebApplicationContext 类型。
        Class<?> contextClass = getContextClass();
        if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
                throw new ApplicationContextException(
                                "Fatal initialization error in servlet with name '" + getServletName() +
                                "': custom WebApplicationContext class [" + contextClass.getName() +
                                "] is not of type ConfigurableWebApplicationContext");
        }
        // 反射创建,并设置属性
        ConfigurableWebApplicationContext wac =
                        (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

        wac.setEnvironment(getEnvironment());
        wac.setParent(parent);
        // 获取web.xml 配置的 DispatcherServlet init-params contextConfigLocation 属性
        String configLocation = getContextConfigLocation();
        if (configLocation != null) {
                wac.setConfigLocation(configLocation);
        }
        // 配置 WebApplicationContext 实例并刷新
        configureAndRefreshWebApplicationContext(wac);

        return wac;
}
复制代码

1.2.1.4 configureAndRefreshWebApplicationContext

我们这里额外注意一个方法:configureAndRefreshWebApplicationContext

无论是通过构造注入还是单独创建,都会调用 configureAndRefreshWebApplicationContext 方法来对已经创建的 WebApplicationContext 实例进行配置及刷新。其实就是刷新Spring容器

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
        if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
                // The application context id is still set to its original default value
                // -> assign a more useful id based on available information
                if (this.contextId != null) {
                        wac.setId(this.contextId);
                }
                else {
                        // Generate default id...
                        wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                                        ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
                }
        }
        // 设置一些属性,ServletContext、ServletConfig等
        wac.setServletContext(getServletContext());
        wac.setServletConfig(getServletConfig());
        wac.setNamespace(getNamespace());
        wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

        // The wac environment's #initPropertySources will be called in any case when the context
        // is refreshed; do it eagerly here to ensure servlet property sources are in place for
        // use in any post-processing or initialization that occurs below prior to #refresh
        ConfigurableEnvironment env = wac.getEnvironment();
        if (env instanceof ConfigurableWebEnvironment) {
                ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
        }

        postProcessWebApplicationContext(wac);
        applyInitializers(wac);
        // 这里调用了 AbstractApplicationContext#refresh
        wac.refresh();
}
复制代码

其实关键就再最后一步,调用了AbstractApplicationContext#refresh方法,而这个方法正是整个Spring初始化的开始。关于该方法,这里可以简单的理解是完成了Spring容器的功能。详见Spring5源码11-容器刷新refresh方法(注解版)

综上之后,WebApplicationContext 已经获取完毕,下面开始进行进一步的初始化。

2. onRefresh-初始化九大核心组件

onRefreshFrameworkServlet 提供的模板方法,供子类实现。我们来看看 DispatcherServlet#onRefresh 的实现内容,主要用于 刷新Spring 在Web功能实现中所必须使用的全局变量。

private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";
// 默认策略配置文件
private static final Properties defaultStrategies;

@Override
protected void onRefresh(ApplicationContext context) {
        // 初始化策略
        initStrategies(context);
}

protected void initStrategies(ApplicationContext context) {
        // 初始化多文件上传解析器
        initMultipartResolver(context);
        // 初始化国际化解析器
        initLocaleResolver(context);
        // 初始化主题解析器
        initThemeResolver(context);
        // 初始化 HandlerMappering
        initHandlerMappings(context);
        // 初始化 HandlerAdapter
        initHandlerAdapters(context);
        // 初始化 Handler异常解析器
        initHandlerExceptionResolvers(context);
        // 初始化RequestToViewNameTranslator
        initRequestToViewNameTranslator(context);
        // 初始化 视图解析器
        initViewResolvers(context);
        // 初始化 FlashMapManager 
        initFlashMapManager(context);
}
复制代码

需要提一句,这里的 defaultStrategies 属性是加载了默认的配置文件 DispatcherServlet.properties。这个文件和 DispatcherServlet 同级,里面记录了各种默认的加载策略。上面初始化了一大堆的东西,下面我们来一句一句分析。

2.1 initMultipartResolver(context)

这一步顾名思义,就是配置多文件解析器。在 Spring 中,MultipartResolver 主要用来处理文件上传。默认情况下,Spring是没有 Multipart 处理的。如果想使用 Spring 的 Multipart ,则需要手动注入 Multipart 解析器,即MultipartResolver。这样Spring 会检查每个请求是否包含 Multipart,如果包含,那么 MultipartResolver 就会解析它。一般我们可以注入 CommonsMultipartResolver

其实现代码也很简单,从容器中获取beanNamemultipartResolver 解析器,并保存到 DispatcherServlet 中。

public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";
private void initMultipartResolver(ApplicationContext context) {
        try {
                this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
                if (logger.isTraceEnabled()) {
                        logger.trace("Detected " + this.multipartResolver);
                }
                else if (logger.isDebugEnabled()) {
                        logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName());
                }
        }
        catch (NoSuchBeanDefinitionException ex) {
                // Default is no multipart resolver.
                this.multipartResolver = null;
                if (logger.isTraceEnabled()) {
                        logger.trace("No MultipartResolver '" + MULTIPART_RESOLVER_BEAN_NAME + "' declared");
                }
        }
}
复制代码

2.2 initLocaleResolver(context)

这里是初始化国际化解析器。代码如下:

private void initLocaleResolver(ApplicationContext context) {
   try {
      // 从容器中获取
      this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);
      if (logger.isTraceEnabled()) {
         logger.trace("Detected " + this.localeResolver);
      }
      else if (logger.isDebugEnabled()) {
         logger.debug("Detected " + this.localeResolver.getClass().getSimpleName());
      }
   }
   catch (NoSuchBeanDefinitionException ex) {
      // We need to use the default.
      // todo 获取默认的
      this.localeResolver = getDefaultStrategy(context, LocaleResolver.class);
      if (logger.isTraceEnabled()) {
         logger.trace("No LocaleResolver '" + LOCALE_RESOLVER_BEAN_NAME +
               "': using default [" + this.localeResolver.getClass().getSimpleName() + "]");
      }
   }
}
复制代码

如果获取不到,默认的从getDefaultStrategy方法获取:

protected <T> T getDefaultStrategy(ApplicationContext context, Class<T> strategyInterface) {
   List<T> strategies = getDefaultStrategies(context, strategyInterface);
   if (strategies.size() != 1) {
      throw new BeanInitializationException(
            "DispatcherServlet needs exactly 1 strategy for interface [" + strategyInterface.getName() + "]");
   }
   return strategies.get(0);
}

protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
   if (defaultStrategies == null) {
      try {
         // Load default strategy implementations from properties file.
         // This is currently strictly internal and not meant to be customized
         // by application developers.

         // 去DispatcherServlet所在类路径下找一个  DispatcherServlet.properties 文件
         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());
      }
   }

   // 获取Name
   String key = strategyInterface.getName();
   // 从配置文件中获取value
   String value = defaultStrategies.getProperty(key);
   // 获取到value 之后就是对value的处理,添加返回。
   if (value != null) {
      String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
      List<T> strategies = new ArrayList<>(classNames.length);
      for (String className : classNames) {
         try {
            Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
            Object strategy = createDefaultStrategy(context, clazz);
            strategies.add((T) strategy);
         }
         catch (ClassNotFoundException ex) {
            throw new BeanInitializationException(
                  "Could not find DispatcherServlet's default strategy class [" + className +
                  "] for interface [" + key + "]", ex);
         }
         catch (LinkageError err) {
            throw new BeanInitializationException(
                  "Unresolvable class definition for DispatcherServlet's default strategy class [" +
                  className + "] for interface [" + key + "]", err);
         }
      }
      return strategies;
   }
   else {
      return Collections.emptyList();
   }
}
复制代码

首先从容器中获取 beanName 为 localeResolver,如果获取不到,就获取默认的,默认是去DispatcherServlet所在类路径下找一个 DispatcherServlet.properties 文件,获取对应的默认值: image.png 默认为AcceptHeaderLocaleResolver

一般情况下, localeResolver 有三种注入实例:

  • AcceptHeaderLocaleResolver : 基于URL 参数的配置 。他会根据请求的URL后缀来判断国际化场景。比如 : “http://xxx?local=zh_CN”。local参数也可以是en_US。
  • CookieLocaleResolver : 基于Cookie的国际化配置。他是通过浏览器的 Cookies 设置取得Local对象。
  • SessionLocaleResolver :基于 Session 的配置,他通过验证用户会话中预置的属性来解析区域。常用的是根据用户本次会话过程中语言来决定语言种类。如果该会话属性不存在,则会根据 http的 accept-language 请求头确认国际化场景。

2.3 initThemeResolver(context)

在Web开发中经常会遇到通过主题Theme 来控制网页风格,这将进一步改善用户体验。简单的说,一个主题就是一组静态资源(比如样式表和图片),他们可以影响应用程序的视觉效果。Spring中的主题功能和国际化功能非常类似。代码跟 第1.3.2 小节差不多,简单说一下三个常用的主题解析器:

  • FixedThemeResolver :用于选择一个固定的主题
  • SessionThemeResolver :用于主题保存在用户的http session中
  • CookieThemeResolver :实现用户所选的主题,以cookie的形式存放在客户端的机器上

2.4 initHandlerMappings(context)

当客户端发出 Request 时, DispatcherServlet 会将 Request 提交给 HandlerMapping,然后HandlerMapping 根据WebApplicationContext的配置来回传给DispatcherServlet相应的Controller

在基于Spring mvc 的web应用程序中,我们可以为DispatcherServlet 提供多个 HandlerMapping 供其使用。DispatcherServlet 在选用 HandlerMapping 的过程中,将会根据我们所指定的一系列Handler 的优先级进行排序然后优先使用优先级在前的HandlerMapping。如果当前HandlerMapping能够返回可用的Handler,DispatcherServlet 则是使用当前返回的Handler 进行Web请求的处理,而不再询问其他HandlerMapping,否则DispatcherServlet将按照各个HandlerMapping 的优先级进行询问,知道获取到一个可用的Handler 为止。其代码如下:

private void initHandlerMappings(ApplicationContext context) {
   this.handlerMappings = null;

   // 如果启用所有的HandlerMapping。可以通过这个参数来控制是使用指定的HandlerMapping,还是检索所有的
   if (this.detectAllHandlerMappings) {
      // Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
      // 寻找所有的HandlerMapping类型的类
      Map<String, HandlerMapping> matchingBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
      if (!matchingBeans.isEmpty()) {
         this.handlerMappings = new ArrayList<>(matchingBeans.values());
         // We keep HandlerMappings in sorted order.
         // 按照优先级进行排序
         AnnotationAwareOrderComparator.sort(this.handlerMappings);
      }
   }
   else {
      // 如果使用指定的参数,从容器中获取beanName 为 handlerMapping 的HandlerMapping
      try {
         HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
         this.handlerMappings = Collections.singletonList(hm);
      }
      catch (NoSuchBeanDefinitionException ex) {
         // Ignore, we'll add a default HandlerMapping later.
      }
   }

   // Ensure we have at least one HandlerMapping, by registering
   // a default HandlerMapping if no other mappings are found.
   // 如果handlerMappings  为null。则使用默认策略指定的HandlerMapping
   if (this.handlerMappings == null) {
      // todo 默认的策略
      this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
      if (logger.isTraceEnabled()) {
         logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
               "': using default strategies from DispatcherServlet.properties");
      }
   }

   for (HandlerMapping mapping : this.handlerMappings) {
      if (mapping.usesPathPatterns()) {
         this.parseRequestPath = true;
         break;
      }
   }
}
复制代码

这里注意:

  • detectAllHandlerMappings 参数用来 判断是否启用所有的HandlerMapping。可以通过这个参数来控制是使用指定的HandlerMapping,还是检索所有的HandlerMapping。

  • 默认策略指定的 HandlerMapping 是从 DispatcherServlet.properties 配置文件中取出的值,以 org.springframework.web.servlet.HandlerMapping (HandlerMapping 的全路径类名) 作为key所取出的value。

关于几种HandlerMapping,我们这里来简单看看。后面详细讲解。

  • BeanNameUrlHandlerMapping :以beanName 作为key值

  • RequestMappingHandlerMapping :完成@Controller@RequestMapping 的解析,并将解析保存。请求发送时与请求路径进行匹配对应找到合适的Handler。RequestMappingHandlerMapping 实现了 InitializingBean 接口,会在afterPropertiesSet 方法中

    • 调用时机:解析@Controller和@RequestMapping注解是在 afterPropertiesSet方法中进行的。匹配调用则是在 DispatcherServlet doDispatch方法中的getHandler中调用了HandlerMapper中的getHandler中的getHandlerInternal方法。
  • SimpleUrlHandlerMapping :基本逻辑是通过注入SimpleurlHandlerMapping 的mapping属性,mapping key为url, value为handler(beanName)。这里需要注意Controller必须要实现Controller接 口

2.5 initHandlerAdapters(context)

初始化处理器适配器。这里使用了适配器模式来设计。

private void initHandlerAdapters(ApplicationContext context) {
   this.handlerAdapters = null;

   // 如果启用所有的HandlerAdapter。可以通过这个参数来控制是使用指定的HandlerMapping,还是检索所有的
   if (this.detectAllHandlerAdapters) {
      // Find all HandlerAdapters in the ApplicationContext, including ancestor contexts.
      // 寻找所有的适配器并排序
      Map<String, HandlerAdapter> matchingBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
      if (!matchingBeans.isEmpty()) {
         this.handlerAdapters = new ArrayList<>(matchingBeans.values());
         // We keep HandlerAdapters in sorted order.
         AnnotationAwareOrderComparator.sort(this.handlerAdapters);
      }
   }
   else {
      // 没有启用则从Spring 容器中获取 beanName = handlerAdapter 并且类型是 HandlerAdapter 类型的bean。
      try {
         HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class);
         this.handlerAdapters = Collections.singletonList(ha);
      }
      catch (NoSuchBeanDefinitionException ex) {
         // Ignore, we'll add a default HandlerAdapter later.
      }
   }

   // Ensure we have at least some HandlerAdapters, by registering
   // default HandlerAdapters if no other adapters are found.

   // 如果还没有获取到适配器,则使用默认策略的适配器。
   // 从 DispatcherServlet.properties 中获取 org.springframework.web.servlet.HandlerAdapter 为key值的value加载到容器中。
   if (this.handlerAdapters == null) {
      this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class);
      if (logger.isTraceEnabled()) {
         logger.trace("No HandlerAdapters declared for servlet '" + getServletName() +
               "': using default strategies from DispatcherServlet.properties");
      }
   }
}
复制代码

可以看到逻辑和上面的HandlerMapping 基本相同。

这里我们简单介绍一下三个 HandlerAdapter:

  • HttpRequestHandlerAdapter : Http请求处理器适配器。
    • HTTP请求处理适配器仅仅支持 HTTP 请求处理器的适配。他简单的将HTTP请求对象和响应对象传递给HTTP请求处理器的实现,他并不需要返回值。主要应用在基于 HTTP的远程调用实现上。
  • SimpleControllerHandlerAdapter : 简单控制器处理器适配器
    • 这个实现类将HTTP请求适配到了一个控制器的实现进行处理。这里的控制器的实现是一个简单的控制器接口的 实现。简单控制器处理器适配器被设计成一个框架类的实现,不需要被改写,客户化的业务逻辑通常在控制器接口的实现类中实现的。
  • RequestMappingHandlerAdapter : 请求映射处理器适配器
    • 这个实现类需要通过注解方法映射和注解方法处理器协同工作。它通过解析声明在注解控制器的请求映射信息来解析相应的处理器方法来处理当前的http请求,在处理的过程中,他通过反射来发现探测处理器方法的参数,调用处理器方法,并映射返回值到模型和控制器对象。最后返回模型和控制器对象给作为主控制器的派遣器Servlet。

2.6 initHandlerExceptionResolvers(context)

基于 HandlerExceptionResolver接口的异常处理,使用这种方式只需要实现 org.springframework.web.servlet.HandlerExceptionResolver#resolveException 方法,该方法返回一个 ModelAndView 对象,在方法内部对异常的类型进行判断,然后尝试生成对应的 ModelAndView对象,如果该方法返回了null。则Spring会继续寻找其他的实现了HandlerExceptionResolver 接口的bean,直至找到一个可以返回ModelAndViewHandlerExceptionResolver

至于代码逻辑,和上面基本相同,这里不再赘述。

2.7 initRequestToViewNameTranslator(context)

当Controller处理器方法没有返回一个View对象或者逻辑视图名称,并且在该方法中没有直接放Response 的输出流中写数据的时候,Spring就会采用约定好的方式提供一个逻辑视图名称。这个逻辑视图名称是通过 Spring 定义的 org.springframework.web.servlet.RequestToViewNameTranslator 接口的 getViewName 方法实现的。Spring默认提供了一个实现 org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator 可以看一下其支持的属性

public class DefaultRequestToViewNameTranslator implements RequestToViewNameTranslator {
	// 分隔符
	private static final String SLASH = "/";

	// 视图前缀
	private String prefix = "";
	// 视图后缀
	private String suffix = "";
	// 默认的分隔符
	private String separator = SLASH;
	// 如果首字符是分隔符是否需要去除,默认true
	private boolean stripLeadingSlash = true;
	// 如果尾字符是分隔符是否需要去除,默认true
	private boolean stripTrailingSlash = true;
	// 如果请求路径包含扩展名是否需要去除,默认true
	private boolean stripExtension = true;
	// 通过这个属性可以设置多种属性
	private UrlPathHelper urlPathHelper = new UrlPathHelper();	
	
/**
	 * Shortcut to same property on underlying {@link #setUrlPathHelper UrlPathHelper}.
	 * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath
	 */
	 // URL查找是否应始终使用当前路径中的完整路径
	public void setAlwaysUseFullPath(boolean alwaysUseFullPath) {
		this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath);
	}

	/**
	 * Shortcut to same property on underlying {@link #setUrlPathHelper UrlPathHelper}.
	 * @see org.springframework.web.util.UrlPathHelper#setUrlDecode
	 */
	 // 是否要对 URL 解码,默认 true。它会采用request 指定的编码或者 ISO-8859-1 编码对 URL 进行解码
	public void setUrlDecode(boolean urlDecode) {
		this.urlPathHelper.setUrlDecode(urlDecode);
	}

	/**
	 * Set if ";" (semicolon) content should be stripped from the request URI.
	 * @see org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent(boolean)
	 */
	 // 是否删除 ; 内容
	public void setRemoveSemicolonContent(boolean removeSemicolonContent) {
		this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent);
	}

	....
}
复制代码

这里可以知道,我们日常使用的视图转换器就是 DefaultRequestToViewNameTranslator 。代码逻辑比较简单,和上面的类似,不赘述。

2.8 initViewResolvers(context)

在 Spring mvc 中。当 Controller 将请求处理结果放入到 ModelAndView 中以后,DispatcherServlet 会根据 ModelAndView 选择合适的视图进行渲染。在org.springframework.web.servlet.ViewResolver#resolveViewName 方法中,通过 viewName 创建合适类型的View。

代码逻辑相同,不再赘述。

2.9 initFlashMapManager(context)

Spring MVC Flash attributes 提供了一个请求存储属性,可供其他请求使用。在使用重定向的时候非常必要。 Flash attributes 在重定向之前暂存以便重定向之后还能使用,并立即删除。

Spring MVC 有两个主要的抽象类来支持 Flash attributes。FlashMap 用于保存 Flash attributes。而FlashMapManager 用于存储、检索、管理 FlashMap 实例。

Flash attributes 支持默认开启(“on”),并不需要显式启用,它永远不会导致Http Session 的创建,这两个方法都可以通过 静态方法 RequestContextUtils 从 Spring mvc 的任何位置访问。

初始化代码逻辑相同,不再赘述。

参考文章

Spring5源码注释github地址
Spring源码深度解析(第2版)
spring源码解析
Spring源码深度解析笔记
Spring注解与源码分析
Spring注解驱动开发B站教程

猜你喜欢

转载自juejin.im/post/7139330939489878029