SpringMVC源码解析——DispatcherServlet初始化

在Spring中,ContextLoaderListener只是辅助功能,用于创建WebApplicationContext类型的实例,而真正的逻辑实现其实是在DispatcherServlet中进行的,DispatcherServlet是实现Servlet接口的实现类。Servlet是一个JAVA编写的程序,此程序是基于HTTP协议的,在服务端运行的(如Tomcat),是按照Servlet规范编写的一个JAVA类。主要是处理客户端的请求并将其结果发送到客户端。Servlet的生命周期是由Servlet的容器来控制的,它可以分为三个阶段:初始化、运行和销毁。

1、初始化阶段
  • Servlet容器加载Servlet类,把Servlet类的.class文件中的数据读入到内存中。
  • Servlet容器创建一个ServletConfig对象,ServletConfig对象包含了Servlet的初始化配置信息。
  • Servlet容器创建一个Servlet对象。
  • Servlet容器调用Servlet对象的init方法进行初始化。
2、运行阶段

当Servlet容器收到一个请求时,Servlet容器会针对这个请求创建ServletRequest和ServletResponse对象,然后调用service方法。并把这两个参数传递给service方法,service方法通过ServletRequest对象获取请求的信息,并处理该请求。再通过ServletResponse对象生成这个请求的响应结果。然后销毁ServletRequest和ServletResponse对象,不管这个请求时GET还是POST提交的,最终这个请求都会由service来处理。

3、销毁阶段

当web应用被终止时,Servlet容器会先调用Servlet对象的destroy方法,然后再销毁Servlet对象,同时也会销毁与Servlet对象相关联的ServletConfig对象。我们可以在destroy方法的实现中,释放Servlet所占用的资源,如关闭数据库连接,关闭文件输入输出流等。

Servlet的框架是由两个JAVA包组成:javax.servlet和javax.servlet.http。在javax.servlet包中定义了所有的servlet类都必须实现或扩展的通用接口和类,在javax.servlet.http包中定义了采用HTTP通信协议的HttpServlet类。

servlet被设计成请求驱动,servlet的请求可能包含多个数据项,当web容器接受到某个servlet请求时,servlet把请求封装成一个HttpServletRequest对象,然后把对象传给servlet的对应的服务方法。

HTTP的请求方式包括delete、get、options、post、put和trace,在HttpServlet类中分别提供了相应的服务方法,它们是doDelete、doGet、doOptions、doPost、doPut和doTrace。

DispatcherServlet的初始化

在servlet初始化阶段会调用其init方法,所以我们首先要查看DispatcherServlet中是否重写了init方法。DispatcherServlet类相关的结构图如下:

我们在HttpServletBean中找到了该方法:

/*** 将配置参数映射到这个servlet的bean属性,并调用子类的初始化方法。* @throws ServletException 如果bean属性无效(或缺少必需的属性),或者子类的初始化失败。*/
@Override
public final void init() throws ServletException {// 从初始化参数设置bean属性。PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);if (!pvs.isEmpty()) {try {// 将当前的这个Servlet类转换为一个BeanWrapper,从而能够以Spring的方式来对init-param的值进行注入。BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());// 注册自定义属性编辑器,一旦遇到Resource类型的属性将会使用ResourceEditor进行解析bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));// 空实现,留给子类覆盖initBeanWrapper(bw);bw.setPropertyValues(pvs, true);}catch (BeansException ex) {if (logger.isErrorEnabled()) {logger.error("无法设置 servlet '" + getServletName() + "' 的 bean 属性", ex);}throw ex;}}// 允许子类执行它们喜欢的任何初始化操作。initServletBean();
}

函数DispatcherServlet的初始化过程主要是通过将当前的Servlet类型实例转换为BeanWapper类型实例,以便使用Spring中提供的注入功能进行对应属性的注入。这些属性如contextAttribute、contextClass、nameSpace、contextConfigLocation等,都可以在web.xml文件中以初始化参数的方式配置在Servlet的声明中。DispatcherServlet继承自FrameworkServlet,FrameworkServlet类上包含对应的同名属性,Spring会保证这些参数被注入到对应的值中。属性注入主要是包含以下几个步骤。

1、封装及验证初始化参数

ServletConfigPropertyValues除了封装属性外还有对属性验证的功能。

		/*** 创建新的ServletConfigPropertyValues。* @param config 我们将使用它从ServletConfig中获取属性值* @param requiredProperties 我们需要的属性集合,对于这些我们不能使用默认值* @throws ServletException 如果缺少任何必需的属性*/public ServletConfigPropertyValues(ServletConfig config, Set<String> requiredProperties)throws ServletException {Set<String> missingProps = (!CollectionUtils.isEmpty(requiredProperties) ?new HashSet<>(requiredProperties) : null);Enumeration<String> paramNames = config.getInitParameterNames();while (paramNames.hasMoreElements()) {String property = paramNames.nextElement();Object value = config.getInitParameter(property);addPropertyValue(new PropertyValue(property, value));if (missingProps != null) {missingProps.remove(property);}}// 如果我们仍然缺少属性,则失败。if (!CollectionUtils.isEmpty(missingProps)) {throw new ServletException("从ServletConfig初始化Servlet '" + config.getServletName() +"'失败;缺少以下必需的属性:" +StringUtils.collectionToDelimitedString(missingProps, ", "));}}

这个函数创建一个ServletConfigPropertyValues对象,从给定的ServletConfig中获取属性值,并将其添加到PropertyValues中。如果requiredProperties中存在缺失的属性,则抛出ServletException异常。

2、将当前Servlet实例转化成BeanWapper实例

PropertyAccessorFactory.forBeanPropertyAccess是Spring中提供的工具方法,主要是用于将指定实例转化为Spring中可以处理的BeanWapper类型的实例。

3、注册相对于Resource的属性编辑器

这里使用属性编辑器的目的是在对当前实例(DispatcherServlet)属性注入过程中一旦遇到Resource类型的属性就会使用ResourceEditor去解析。

4、属性注入

BeanWapper为Spring中的方法,支持Spring的自动注入。其实我们最常用的属性注入无非是contextAttribute、contextClass、nameSpace、contextConfigLocation等。

5、ServletBean的初始化

在ContextLoaderListener加载的时候已经创建了WebApplicationContext实例,而在这个函数中最重要的就是对这个实例进行进一步的补充初始化。

继续查看initServletBean(),父类覆盖了HttpServletBean中的initServletBean函数,源码如下:

/*** 重写HttpServletBean类的方法,在设置完所有bean属性后调用。创建该Servlet的Web应用上下文。*/
@Override
protected final void initServletBean() throws ServletException {getServletContext().log("初始化Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");if (logger.isInfoEnabled()) {logger.info("Initializing Servlet '" + getServletName() + "'");}long startTime = System.currentTimeMillis();try {this.webApplicationContext = initWebApplicationContext();initFrameworkServlet();}catch (ServletException | RuntimeException ex) {logger.error("上下文初始化失败", ex);throw ex;}if (logger.isDebugEnabled()) {String value = this.enableLoggingRequestDetails ?"可能导致潜在敏感数据的不安全记录显示" :"已掩盖以防止对潜在敏感数据的不安全记录";logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +"': 请求参数和标头将被 " + value);}if (logger.isInfoEnabled()) {logger.info("完成初始化需要 " + (System.currentTimeMillis() - startTime) + " ms");}
}

这个函数会调用initWebApplicationContext用于创建并初始化WebApplicationContext实例,initFrameworkServlet()函数不做任何实现,可以在子类中进行扩展。

WebApplicationContext的初始化

initWebApplicationContext函数的主要工作就是创建并刷新WebApplicationContext实例并对Servlet功能所使用的变量进行初始化。initWebApplicationContext函数的源码如下:

	/*** 初始化并发布该servlet的WebApplicationContext。* <p>实际创建上下文的工作委托给{@link #createWebApplicationContext}方法。* 子类可以重写该方法。* @return WebApplicationContext实例* @see #FrameworkServlet(WebApplicationContext)* @see #setContextClass* @see #setContextConfigLocation*/protected WebApplicationContext initWebApplicationContext() {WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());WebApplicationContext wac = null;if (this.webApplicationContext != null) {// 在构造函数中注入了上下文实例 -> 使用它wac = this.webApplicationContext;if (wac instanceof ConfigurableWebApplicationContext cwac && !cwac.isActive()) {// 上下文尚未刷新 -> 提供服务,如设置父上下文、设置applicationContextId等if (cwac.getParent() == null) {// 注入父上下文时未指定明确的父上下文 -> 将根application上下文(如果有的话)设置为父上下文cwac.setParent(rootContext);}// 配置并刷新WebApplicationContext实例configureAndRefreshWebApplicationContext(cwac);}}if (wac == null) {// 在构造函数中没有注入上下文实例 -> 检查servlet context中是否存在一个注册的上下文。// 如果存在,则假定父上下文(如果有的话)已经设置,并且用户已经进行了任何初始化,例如设置上下文idwac = findWebApplicationContext();}if (wac == null) {// 为这个servlet没有定义上下文实例 -> 创建一个本地上下文wac = createWebApplicationContext(rootContext);}if (!this.refreshEventReceived) {// 该上下文不是支持刷新的ConfigurableApplicationContext或者在构造函数中注入的上下文已经刷新 ->// 在这里手动触发onRefresh方法。synchronized (this.onRefreshMonitor) {onRefresh(wac);}}if (this.publishContext) {// 将上下文发布为servlet context attribute。String attrName = getServletContextAttributeName();getServletContext().setAttribute(attrName, wac);}return wac;}

这个函数用于初始化和发布WebApplicationContext。它首先从servlet上下文中获取根应用程序上下文,然后根据需要创建和配置应用程序上下文实例。如果已经存在一个应用程序上下文实例,则直接使用它。如果没有,则根据需要创建一个本地应用程序上下文。最后,将应用程序上下文发布为servlet上下文属性,并返回该上下文实例。

对于initWebApplicationContext函数中的初始化工作主要包含几个部分。

1、通过构造函数的注入对WebApplicationContext进行初始化

当进入initWebApplicationContext函数后通过判断this.webApplicationContext !=null后,便可以确定this.webApplicationContext是否是通过构造函数来初始化的。

2、通过contextAttribute进行初始化

通过在web.xml文件中配置的servlet参数contextAttribute来查找ServletContext中对应的属性,默认为WebApplicationContext.class .getName()+".ROOT"。也就是在ContextLoaderListener加载时会创建WebApplicationContext实例,并将实例以WebApplicationContext.class.getName()+".ROOT"为key放入ServletContext中,当然我们也可以重写初始化逻辑使用自己创建的WebApplicationContext,并在servlet的配置中通过初始化参数contextAttribute指定key。

	/*** 从配置了名称的`ServletContext`属性中获取一个`WebApplicationContext`。* 在该 servlet 初始化(或调用)之前,`WebApplicationContext`必须已经加载并存储在 `ServletContext` 中。* <p>子类可以覆盖此方法以提供不同的`WebApplicationContext`检索策略。* @return 该 servlet 的`WebApplicationContext`,如果未找到则返回`null`* @see #getContextAttribute()*/@Nullableprotected WebApplicationContext findWebApplicationContext() {String attrName = getContextAttribute();if (attrName == null) {return null;}WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);if (wac == null) {throw new IllegalStateException("未找到WebApplicationContext:初始化器未注册?");}return wac;}
3、重写创建WebApplicationContext实例

如果以上两种方式都没有获取到WebApplicationContext实例,只能重写创建新的实例了。

	/*** 创建用于该servlet的WebApplicationContext,可以是默认的* {@link org.springframework.web.context.support.XmlWebApplicationContext}* 或者如果设置了的话,可以是一个自定义的上下文类(通过* {@link #setContextClass 设置})。* 代理到#createWebApplicationContext(ApplicationContext)方法。* @param parent 要使用的父WebApplicationContext,如果无,则传入{@code null}* @return 用于该servlet的WebApplicationContext* @see org.springframework.web.context.support.XmlWebApplicationContext* @see #createWebApplicationContext(ApplicationContext)*/protected WebApplicationContext createWebApplicationContext(@Nullable WebApplicationContext parent) {return createWebApplicationContext((ApplicationContext) parent);}/*** 创建本servlet的WebApplicationContext,可以是一个默认的XmlWebApplicationContext或者是一个自定义的上下文类(通过setContextClass方法设置)。* <p>此实现期望自定义上下文实现ConfigurableWebApplicationContext接口。可以在子类中重写。* <p>请不要忘记将此servlet实例作为创建的上下文的应用监听器注册(以便触发它的onRefresh回调),并在返回上下文实例之前调用ConfigurableApplicationContext的refresh方法。* @param parent 要使用的父级ApplicationContext,如果无父级则为null* @return 本servlet的WebApplicationContext* @see org.springframework.web.context.support.XmlWebApplicationContext*/
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {// 读取servlet的初始化参数contextClass,如果没有配置默认为XmlWebApplicationContext.classClass<?> contextClass = getContextClass();if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {throw new ApplicationContextException("在名称为'" + getServletName() +"'的servlet中发生致命的初始化错误:自定义WebApplicationContext类[" + contextClass.getName() +"]不是ConfigurableWebApplicationContext类型的");}// 通过反射方式实例化contextClassConfigurableWebApplicationContext wac =(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);wac.setEnvironment(getEnvironment());// parent为在ContextLoaderListener中创建的实例// 在ContextLoaderListener加载的时候初始化的WebApplicationContext类型的实例wac.setParent(parent);// 获取contextConfigLocation属性,配置在servlet初始化参数中String configLocation = getContextConfigLocation();if (configLocation != null) {wac.setConfigLocation(configLocation);}//初始化Spring环境,包括加载配置文件等configureAndRefreshWebApplicationContext(wac);return wac;
}
4、configureAndRefreshWebApplicationContext

无论是通过构造函数注入还是单独创建,都会调用configureAndRefreshWebApplicationContext方法来对已经创建的WebApplicationContext实例进行配置及刷新,源码如下:

	/*** 配置并刷新Web应用上下文* * @param wac 可配置的Web应用上下文*/protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {if (ObjectUtils.identityToString(wac).equals(wac.getId())) {// 应用上下文id仍然设置为其默认值// -> 基于可用信息分配一个更有用的idif (this.contextId != null) {wac.setId(this.contextId);} else {// 生成默认id...wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());}}wac.setServletContext(getServletContext());wac.setServletConfig(getServletConfig());wac.setNamespace(getNamespace());wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));// 当上下文刷新时,wac环境的#initPropertySources方法将被调用;这里提前调用以确保servlet属性源在以下下方的post-processing或初始化之前可用ConfigurableEnvironment env = wac.getEnvironment();if (env instanceof ConfigurableWebEnvironment cwe) {cwe.initPropertySources(getServletContext(), getServletConfig());}postProcessWebApplicationContext(wac);applyInitializers(wac);wac.refresh();}

个函数用于配置和刷新Web应用上下文。首先根据上下文的id是否与默认值相同来为其设置一个更有用的id。然后设置上下文的servletContext、servletConfig和命名空间,并添加一个监听器。接下来,通过调用wac环境的initPropertySources方法来初始化属性源。最后,调用postProcessWebApplicationContext和applyInitializers方法对上下文进行后处理和初始化,并刷新上下文。

5、刷新WebApplicationContext

onRefresh是FrameworkServlet类中提供的模板方法,在其子类DispatcherServlet中进行了重写,主要用于刷新Spring在Web功能实现中所必须使用的全局变量。DispatcherServlet中onRefresh函数的源码如下:

/*** 这个方法调用了 {@link #initStrategies} 方法。*/
@Override
protected void onRefresh(ApplicationContext context) {initStrategies(context);
}/*** 初始化这个 servlet 使用的策略对象。* <p>对于需要初始化更多策略对象的情况,可以被子类重写。*/
protected void initStrategies(ApplicationContext context) {initMultipartResolver(context);initLocaleResolver(context);initThemeResolver(context);initHandlerMappings(context);initHandlerAdapters(context);initHandlerExceptionResolvers(context);initRequestToViewNameTranslator(context);initViewResolvers(context);initFlashMapManager(context);
}
初始化MultipartResolver

在Spring中,MultipartResolver主要是处理文件上传。默认情况下,Spring是没有Multipart处理的,因为很多开发者想要自己处理它们。如果想使用Spring的Multipart,则需要在Web应用的上下文中添加Multipart解析器。这样,每个请求就会被检查是否包含Multipart。然而如果请求中包含Multipart,那么上下文中定义的MultipartResolver就会解析它,这样请求中的Multipart属性就会想其他属性一样被处理。常用配置如下:

<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"><property name="maximumFileSize"><value>100000</value></property>
</bean>

那么MultipartResolver就是在initMultipartResolver中被加入到DispatcherServlet中的。

	/*** 初始化用于此类的 MultipartResolver。* <p>如果在 BeanFactory 中未定义给定名称的 bean,则不提供多部分处理。*/private void initMultipartResolver(ApplicationContext context) {try {this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);if (logger.isTraceEnabled()) {logger.trace("检测到 " + this.multipartResolver);}else if (logger.isDebugEnabled()) {logger.debug("检测到 " + this.multipartResolver.getClass().getSimpleName());}}catch (NoSuchBeanDefinitionException ex) {// 默认情况下没有 multipart 解析器。this.multipartResolver = null;if (logger.isTraceEnabled()) {logger.trace("未声明 '" + MULTIPART_RESOLVER_BEAN_NAME + "' multipart 解析器");}}}

因为之前的步骤已经完成了Spring中配置文件的解析,所以在这里只要在配置文件注册过都可以通过ApplicationContext提供的getBean方法来直接获取对应的bean,进而初始化MultipartResolver中的multipartResolver变量。

初始化LocaleResolver

在Spring的国际化配置中一共有三种使用方式。

  • 基于URL参数的配置。通过URL参数来控制国际化,而提供这个功能的就是AcceptHeaderLocaleResolver,默认的参数名为local,注意大小写。
  • 基于session的配置。它通过检验用户会话中预置的属性来解析区域。最常用的是根据用户本次会话过程中的语言设定决定语言中来。
  • 基于cookie的国际配置。CookieLocaleResolver用于通过浏览器的cookie设置Locale对象。这种策略在应用程序中不支持会话或者状态必须保存在客户端有用。

这三种方式都可以解决国际化问题,但是对于LocaleResolver的使用基础是在DispatcherServlet中的初始化。

	/*** 初始化该类使用的LocaleResolver。* <p>如果BeanFactory中没有给定名称的bean,将默认使用AcceptHeaderLocaleResolver。*/private void initLocaleResolver(ApplicationContext context) {try {this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);if (logger.isTraceEnabled()) {logger.trace("检测到 " + this.localeResolver);}else if (logger.isDebugEnabled()) {logger.debug("检测到 " + this.localeResolver.getClass().getSimpleName());}}catch (NoSuchBeanDefinitionException ex) {// 需要使用默认的LocaleResolverthis.localeResolver = getDefaultStrategy(context, LocaleResolver.class);if (logger.isTraceEnabled()) {logger.trace("没有LocaleResolver '" + LOCALE_RESOLVER_BEAN_NAME +"': 使用默认 [" + this.localeResolver.getClass().getSimpleName() + "]");}}}

提取配置文件中设置的LocaleResolver来初始化DispatcherServlet中的localeResolver属性。

初始化ThemeResolver

initThemeResolver未来会被遗弃,这里不做详细介绍,只是简单的展示源码。

/*** 初始化由该类使用的ThemeResolver。* 如果BeanFactory中没有给定名称的bean定义此命名空间,默认为FixedThemeResolver。*/
@Deprecated
private void initThemeResolver(ApplicationContext context) {try {this.themeResolver = context.getBean(THEME_RESOLVER_BEAN_NAME, ThemeResolver.class);if (logger.isTraceEnabled()) {logger.trace("检测到 " + this.themeResolver);}else if (logger.isDebugEnabled()) {logger.debug("检测到 " + this.themeResolver.getClass().getSimpleName());}}catch (NoSuchBeanDefinitionException ex) {// 需要使用默认策略this.themeResolver = getDefaultStrategy(context, ThemeResolver.class);if (logger.isTraceEnabled()) {logger.trace("没有ThemeResolver '" + THEME_RESOLVER_BEAN_NAME +"': 使用默认 [" + this.themeResolver.getClass().getSimpleName() + "]");}}
}
初始化HandlerMappings

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

在基于SpringMVC的Web应用中,可以为DispatcherServlet提供多个HandlerMapping供其应用。DispatcherServlet在选用HandlerMapping的过程中,将根据我们所指定的一些列HandlerMapping的优先级进行排序,然后优先使用优先级在前的HandlerMapping。如果当前的HandlerMapping能够返回可用的Handler,DispatcherServlet则使用当前返回的Handler来进行Web请求的处理,而不再继续询问其他的HandlerMapping。否则,DispatcherServlet将继续按照各个HandlerMapping的优先级进行询问,直到获取一个可用的Handler为止。初始化配置的源码如下:

/*** 初始化用于此类的HandlerMappings。* <p>如果BeanFactory中未定义此命名空间的HandlerMapping bean,默认为BeanNameUrlHandlerMapping。*/
private void initHandlerMappings(ApplicationContext context) {this.handlerMappings = null;if (this.detectAllHandlerMappings) {// 在ApplicationContext中查找所有HandlerMappings,包括祖先上下文。Map<String, HandlerMapping> matchingBeans =BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);if (!matchingBeans.isEmpty()) {this.handlerMappings = new ArrayList<>(matchingBeans.values());// 我们保持HandlerMappings的排序。AnnotationAwareOrderComparator.sort(this.handlerMappings);}}else {try {HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);this.handlerMappings = Collections.singletonList(hm);}catch (NoSuchBeanDefinitionException ex) {// 忽略,之后我们将添加一个默认HandlerMapping。}}// 如果找不到其他HandlerMapping,则通过注册默认HandlerMapping来确保至少有一个HandlerMapping。if (this.handlerMappings == null) {this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);if (logger.isTraceEnabled()) {logger.trace("未声明servlet '" + getServletName() +"': 使用DispatcherServlet.properties中的默认策略");}}for (HandlerMapping mapping : this.handlerMappings) {if (mapping.usesPathPatterns()) {this.parseRequestPath = true;break;}}
}

这个Java函数用于初始化HandlerMappings,根据配置从ApplicationContext中获取所有的HandlerMapping实例,并按照指定的顺序排序。如果没有定义HandlerMapping,则使用默认的BeanNameUrlHandlerMapping。最后,根据找到的HandlerMapping实例设置一些属性值。如果只期望SpringMVC加载指定的HandlerMapping时,可以修改web.xml中的DispatcherServlet的初始参数,将detectAllHandlerMappings设置为false,此时SpringMVC将会查找名为“handlerMapping”的bean,并作为当前系统中唯一的HandlerMapping。

初始化HandlerAdapters

该步骤适用于初始化适配器,源码如下:

/*** 初始化该类使用的HandlerAdapters。* <p>如果该命名空间的BeanFactory中没有定义HandlerAdapter Bean,则默认使用SimpleControllerHandlerAdapter。*/
private void initHandlerAdapters(ApplicationContext context) {this.handlerAdapters = null;if (this.detectAllHandlerAdapters) {// 在ApplicationContext中,包括祖先上下文,查找所有的HandlerAdapters。Map<String, HandlerAdapter> matchingBeans =BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);if (!matchingBeans.isEmpty()) {this.handlerAdapters = new ArrayList<>(matchingBeans.values());// 我们按排序顺序保存HandlerAdapters。AnnotationAwareOrderComparator.sort(this.handlerAdapters);}} else {try {HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class);this.handlerAdapters = Collections.singletonList(ha);} catch (NoSuchBeanDefinitionException ex) {// 忽略,稍后添加默认HandlerAdapter。}}// 确保我们至少有一些HandlerAdapters,如果找不到其他HandlerAdapters,则注册默认HandlerAdapters。if (this.handlerAdapters == null) {this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class);if (logger.isTraceEnabled()) {logger.trace("对于servlet '" + getServletName() + "'没有声明HandlerAdapters:使用DispatcherServlet.properties中的默认策略");}}
}

同样在初始化的过程中涉及了一个变量detectAllHandlerAdapters,和detectAllHandlerMappings作用很相似,只不过作用对象是HandlerAdapter。可以通过修改web.xml中的DispatcherServlet的初始参数,将detectAllHandlerAdapters设置为false,使得SpringMVC查找bean名称为“handlerAdapter”的HandlerAdapter实例。

作为总控制器的派遣servlet通过处理器映射得到处理器后,会轮询处理器适配器模板,查找能够处理当前HTTP请求的处理器适配器的实现,处理器适配器模块根据处理器映射返回的处理器类型,例如简单的适配器类型、注解控制器类型或者远程调用处理器类型,来选择一个适当的处理器适配器的实现,从而适配当前的HTTP请求。

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

基于HandlerExceptionResolver接口的异常处理,使用这种方法只需要实现resolveException方法,该方法返回一个ModelAndView对象,在方法内部对异常的类型进行判断,然后尝试生成对应的ModelAndView对象,如果该方法返回了null,则Spring会继续寻找其他实现了HandlerExceptionResolver接口的bean。换句话说,Spring会搜索所有注册在其环境中实现了HandlerExceptionResolver接口bean,逐个执行,直到返回一个ModelAndView对象。

public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionResolver {/*** 检查是否应该应用此解析器(即,如果提供的处理器与配置的* {@linkplain #setMappedHandlers 处理器} 或 {@linkplain #setMappedHandlerClasses 处理器类} 中的任何一个匹配)* 然后委托给 {@link #doResolveException} 模板方法。*/@Override@Nullablepublic ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {if (shouldApplyTo(request, handler)) {prepareResponse(ex, response);ModelAndView result = doResolveException(request, response, handler, ex);if (result != null) {// 在 warn 日志启用时打印调试消息。if (logger.isDebugEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {logger.debug(buildLogMessage(ex, request) + (result.isEmpty() ? "" : " to " + result));}// 在 logException 方法中显式配置的 warn 日志器。logException(ex, request);}return result;}else {return null;}}@Override@Nullableprotected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {try {// ErrorResponse exceptions that expose HTTP response detailsif (ex instanceof ErrorResponse errorResponse) {ModelAndView mav = null;if (ex instanceof HttpRequestMethodNotSupportedException theEx) {mav = handleHttpRequestMethodNotSupported(theEx, request, response, handler);}else if (ex instanceof HttpMediaTypeNotSupportedException theEx) {mav = handleHttpMediaTypeNotSupported(theEx, request, response, handler);}else if (ex instanceof HttpMediaTypeNotAcceptableException theEx) {mav = handleHttpMediaTypeNotAcceptable(theEx, request, response, handler);}else if (ex instanceof MissingPathVariableException theEx) {mav = handleMissingPathVariable(theEx, request, response, handler);}else if (ex instanceof MissingServletRequestParameterException theEx) {mav = handleMissingServletRequestParameter(theEx, request, response, handler);}else if (ex instanceof MissingServletRequestPartException theEx) {mav = handleMissingServletRequestPartException(theEx, request, response, handler);}else if (ex instanceof ServletRequestBindingException theEx) {mav = handleServletRequestBindingException(theEx, request, response, handler);}else if (ex instanceof MethodArgumentNotValidException theEx) {mav = handleMethodArgumentNotValidException(theEx, request, response, handler);}else if (ex instanceof HandlerMethodValidationException theEx) {mav = handleHandlerMethodValidationException(theEx, request, response, handler);}else if (ex instanceof NoHandlerFoundException theEx) {mav = handleNoHandlerFoundException(theEx, request, response, handler);}else if (ex instanceof NoResourceFoundException theEx) {mav = handleNoResourceFoundException(theEx, request, response, handler);}else if (ex instanceof AsyncRequestTimeoutException theEx) {mav = handleAsyncRequestTimeoutException(theEx, request, response, handler);}return (mav != null ? mav :handleErrorResponse(errorResponse, request, response, handler));}// Other, lower level exceptionsif (ex instanceof ConversionNotSupportedException theEx) {return handleConversionNotSupported(theEx, request, response, handler);}else if (ex instanceof TypeMismatchException theEx) {return handleTypeMismatch(theEx, request, response, handler);}else if (ex instanceof HttpMessageNotReadableException theEx) {return handleHttpMessageNotReadable(theEx, request, response, handler);}else if (ex instanceof HttpMessageNotWritableException theEx) {return handleHttpMessageNotWritable(theEx, request, response, handler);}else if (ex instanceof MethodValidationException theEx) {return handleMethodValidationException(theEx, request, response, handler);}else if (ex instanceof BindException theEx) {return handleBindException(theEx, request, response, handler);}}catch (Exception handlerEx) {if (logger.isWarnEnabled()) {logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx);}}return null;}/*** 处理 {@link ErrorResponse} 异常。* <p>默认实现将状态和头信息设置为从 {@code ErrorResponse} 获取到的值。* 如果可用,{@link ProblemDetail#getDetail()} 将用作* {@link HttpServletResponse#sendError(int, String)} 的消息。* @param errorResponse 需要处理的异常* @param request 当前的 HTTP 请求* @param response 当前的 HTTP 响应* @param handler 执行的处理器* @return 一个空的 {@code ModelAndView},表示异常已处理* @throws IOException 可能从 {@link HttpServletResponse#sendError} 抛出* @since 6.0*/
protected ModelAndView handleErrorResponse(ErrorResponse errorResponse,HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {if (!response.isCommitted()) {HttpHeaders headers = errorResponse.getHeaders();headers.forEach((name, values) -> values.forEach(value -> response.addHeader(name, value)));int status = errorResponse.getStatusCode().value();String message = errorResponse.getBody().getDetail();if (message != null) {response.sendError(status, message);}else {response.sendError(status);}}else {logger.warn("忽略异常,响应已提交。: " + errorResponse);}return new ModelAndView();
}}
初始化RequestToViewNameTranslator

当Controller处理器方法没有返回一个View对象或逻辑视图名称,并且在该方法中没有直接往Response的输出流里面写数据的时候,Spring就会采用约定好的方式提供一个逻辑视图名称。这个逻辑视图名称是通过Spring定义的RequestToViewNameTranslator接口的getViewName方法来实现的。首先看一下初始化RequestToViewNameTranslator的源码如下:

	/*** 初始化该servlet实例使用的RequestToViewNameTranslator。* <p>如果没有配置实现,则默认使用DefaultRequestToViewNameTranslator。*/private void initRequestToViewNameTranslator(ApplicationContext context) {try {this.viewNameTranslator = context.getBean(REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME, RequestToViewNameTranslator.class);if (logger.isTraceEnabled()) {logger.trace("检测到 " + this.viewNameTranslator.getClass().getSimpleName());}else if (logger.isDebugEnabled()) {logger.debug("检测到 " + this.viewNameTranslator);}}catch (NoSuchBeanDefinitionException ex) {// 需要使用默认实现this.viewNameTranslator = getDefaultStrategy(context, RequestToViewNameTranslator.class);if (logger.isTraceEnabled()) {logger.trace("未检测到RequestToViewNameTranslator '" + REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME +"': 使用默认 [" + this.viewNameTranslator.getClass().getSimpleName() + "]");}}}

Spring已经给我们提供了一个它自己的实现,就是DefaultRequestToViewNameTranslator,源码如下:

public class DefaultRequestToViewNameTranslator implements RequestToViewNameTranslator {private static final String SLASH = "/";private String prefix = "";private String suffix = "";private String separator = SLASH;private boolean stripLeadingSlash = true;private boolean stripTrailingSlash = true;private boolean stripExtension = true;/*** Translates the request URI of the incoming {@link HttpServletRequest}* into the view name based on the configured parameters.* @throws IllegalArgumentException if neither a parsed RequestPath, nor a* String lookupPath have been resolved and cached as a request attribute.* @see ServletRequestPathUtils#getCachedPath(ServletRequest)* @see #transformPath*/@Overridepublic String getViewName(HttpServletRequest request) {String path = ServletRequestPathUtils.getCachedPathValue(request);return (this.prefix + transformPath(path) + this.suffix);}/*** Transform the request URI (in the context of the webapp) stripping* slashes and extensions, and replacing the separator as required.* @param lookupPath the lookup path for the current request,* as determined by the UrlPathHelper* @return the transformed path, with slashes and extensions stripped* if desired*/@Nullableprotected String transformPath(String lookupPath) {String path = lookupPath;if (this.stripLeadingSlash && path.startsWith(SLASH)) {path = path.substring(1);}if (this.stripTrailingSlash && path.endsWith(SLASH)) {path = path.substring(0, path.length() - 1);}if (this.stripExtension) {path = StringUtils.stripFilenameExtension(path);}if (!SLASH.equals(this.separator)) {path = StringUtils.replace(path, SLASH, this.separator);}return path;}}
初始化ViewResolvers

在SpringMVC中,当Controller将请求处理结果放入到ModelAndView中以后,DispatcherServlet会根据ModelAndView选择合适的视图进行渲染。ViewResolver接口定义了resolveViewName方法,根据viewName创建合适类型的View实现。初始化ViewResolvers的源码如下:

	/*** 初始化用于此类的 ViewResolvers。* <p如果在此命名空间的 BeanFactory 中未定义 ViewResolver 象,我们将默认使用 InternalResourceViewResolver。*/private void initViewResolvers(ApplicationContext context) {this.viewResolvers = null;if (this.detectAllViewResolvers) {// 在 ApplicationContext 中查找所有命名空间的 ViewResolvers,包括祖先层级。Map<String, ViewResolver> matchingBeans =BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);if (!matchingBeans.isEmpty()) {this.viewResolvers = new ArrayList<>(matchingBeans.values());// 我们按升序排列 ViewResolvers。AnnotationAwareOrderComparator.sort(this.viewResolvers);}} else {try {ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class);this.viewResolvers = Collections.singletonList(vr);}catch (NoSuchBeanDefinitionException ex) {// 忽略,后面我们会添加一个默认的 ViewResolver。}}// 如果没有找到其他 ViewResolver,则通过从 DispatcherServlet.properties 中注册一个默认的 ViewResolver 来确保至少有一个 ViewResolver。if (this.viewResolvers == null) {this.viewResolvers = getDefaultStrategies(context, ViewResolver.class);if (logger.isTraceEnabled()) {logger.trace("未为 servlet '" + getServletName() +" 声明 ViewResolvers:使用默认的 ViewResolver 策略");}}}
初始化FlashMapManager

SpringMVC Flush提供了一个请求存储属性,可供其他请求使用。在使用重定向的时候非常必要,例如POST/GET/DELETE。初始化FlashMapManager的源码如下:

	/*** 初始化由此servlet实例使用的FlashMapManager。* <p>如果未配置实现,则默认为{@code org.springframework.web.servlet.support.DefaultFlashMapManager}。*/private void initFlashMapManager(ApplicationContext context) {try {this.flashMapManager = context.getBean(FLASH_MAP_MANAGER_BEAN_NAME, FlashMapManager.class);if (logger.isTraceEnabled()) {logger.trace("Detected " + this.flashMapManager.getClass().getSimpleName());}else if (logger.isDebugEnabled()) {logger.debug("Detected " + this.flashMapManager);}}catch (NoSuchBeanDefinitionException ex) {// 需要使用默认实现this.flashMapManager = getDefaultStrategy(context, FlashMapManager.class);if (logger.isTraceEnabled()) {logger.trace("No FlashMapManager '" + FLASH_MAP_MANAGER_BEAN_NAME +"': using default [" + this.flashMapManager.getClass().getSimpleName() + "]");}}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/227588.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HBase 超大表迁移、备份、还原、同步演练手册:全量快照 + 实时同步(Snapshot + Replication)不停机迁移方案

博主历时三年精心创作的《大数据平台架构与原型实现&#xff1a;数据中台建设实战》一书现已由知名IT图书品牌电子工业出版社博文视点出版发行&#xff0c;点击《重磅推荐&#xff1a;建大数据平台太难了&#xff01;给我发个工程原型吧&#xff01;》了解图书详情&#xff0c;…

自动化测试系列 之 Python单元测试框架unittest

一、概述 什么是单元测试 单元测试是一种软件测试方法&#xff0c;是测试最小的可测试单元&#xff0c;通常是一个函数或一个方法。 在软件开发过程中&#xff0c;单元测试作为一项重要的测试方法被广泛应用。 为什么需要单元测试 单元测试是软件开发中重要的一环&#xf…

TG7050CKN,TG7050SKN ,TG7050CMN,TG7050SMN

爱普生推出的温补晶振型号&#xff1a;TG7050CKN&#xff0c;TG7050SKN &#xff0c;TG7050CMN&#xff0c;TG7050SMN频率范围为 10mhz ~ 54mhz 适用于广泛的频率需求。这几款的特点就是耐高温&#xff0c;温度可达105℃高温&#xff0c;而且都是高稳定性温补晶振&#xff0c;&…

21 UVM printer

uvm_printer 类提供了以不同格式打印 uvm_objects 的灵活性。我们已经讨论了使用 uvm_field_* 宏的 print() 方法&#xff0c;或者如果不使用 utils_begin/ end 宏&#xff0c;则编写 do_print() 方法。 UVM printer提供四种内置printer。 uvm_printeruvm_table_printeruvm_t…

键盘字符(#键)显示错误

当屏幕上显示的键与键盘上按下的键不同时&#xff0c;尤其是 # 键。大多数情况下&#xff0c;此错误是由于 raspbian 和 NOOBS 软件的默认英国键盘配置所致。 解决方案&#xff1a; 要解决此问题&#xff0c;您需要将配置更改为您自己的键盘或语言的配置。这可以通过转到树莓派…

EasyCVR无人机推流+人数统计AI算法,助力公共场所人群密度管控

一、背景与需求 在公共场所和大型活动的管理中&#xff0c;人数统计和人群密度控制是非常重要的安全问题。传统的方法可能存在效率低下或准确度不足的情况&#xff0c;无法满足现代社会的需求。TSINGSEE青犀可以利用无人机推流AI人流量统计算法&#xff0c;基于计算机视觉技术…

项目实战--Message Queue

一. 概念篇 我们在学习多线程的时候,学习过生产者-消费者模型,为了实现解耦合和削峰填谷,引入了阻塞队列. 在实际的后端开发中,跨主机之间使用生产者消费者模型,也是非常普遍的需求,因此,阻塞队列会被封装成一个独立的服务器程序,实现更丰富的功能.这样的程序称为"消息队列…

Spark编程实验四:Spark Streaming编程

目录 一、目的与要求 二、实验内容 三、实验步骤 1、利用Spark Streaming对三种类型的基本数据源的数据进行处理 2、利用Spark Streaming对Kafka高级数据源的数据进行处理 3、完成DStream的两种有状态转换操作 4、把DStream的数据输出保存到文本文件或MySQL数据库中 四…

MFC - 给系统菜单(About Dialog)发消息

文章目录 MFC - 给系统菜单(About Dialog)发消息概述笔记resource.h菜单的建立菜单项的处理MSDN上关于系统菜单项值的说法END MFC - 给系统菜单(About Dialog)发消息 概述 做了一个对话框程序, 在系统菜单(在程序上面的标题栏右击)中有"关于"的菜单. 这个是程序框架…

【数据结构】八、查找

一、基本概念 静态查找&#xff1a;只查找&#xff0c;不改变集合内数据元素 动态查找&#xff1a;有则输出元素&#xff0c;无则添加元素 二、静态查找表 2.1顺序查找 在线性表、链表、树中依次查找 2.2折半查找&#xff08;二分查找&#xff09; 在有序的线性表中&…

基于轻量级GhostNet模型开发构建生活场景下生活垃圾图像识别系统

轻量级识别模型在我们前面的博文中已经有过很多实践了&#xff0c;感兴趣的话可以自行移步阅读&#xff1a; 《移动端轻量级模型开发谁更胜一筹&#xff0c;efficientnet、mobilenetv2、mobilenetv3、ghostnet、mnasnet、shufflenetv2驾驶危险行为识别模型对比开发测试》 《基…

【Java JVM】Java 实例对象的访问定位

Java 程序会通过栈上的 reference 数据来操作堆上的具体对象。 但是 reference 类型在《Java虚拟机规范》里面只规定了它是一个指向对象的引用, 并没有定义这个引用应该通过什么方式去定位, 访问到堆中对象的具体位置, 所以对象访问方式也是由虚拟机实现而定的&#xff0c;主流…

深入探索小红书笔记详情API:解锁内容创新的无尽潜力

一、引言 在当今信息爆炸的时代&#xff0c;内容创新已经成为品牌和个人脱颖而出的关键。小红书&#xff0c;作为全球最大的消费类口碑库之一&#xff0c;每天产生大量的用户生成内容。而小红书笔记详情API&#xff0c;作为一个强大的工具&#xff0c;能够为内容创作者提供深入…

UG装配-添加组件

添加组件命令位置在如下位置&#xff1a;菜单-装配-组件-添加组件 添加组件命令位置在如下位置&#xff1a;菜单-装配-组件-添加组件

Java关键字(1)

Java中的关键字是指被编程语言保留用于特定用途的单词。这些关键字不能用作变量名或标识符。以下是Java中的一些关键字&#xff1a; public&#xff1a;表示公共的&#xff0c;可以被任何类访问。 private&#xff1a;表示私有的&#xff0c;只能被定义该关键字的类访问。 cl…

060:vue中markdown编辑器mavon-editor的应用示例

第060个 查看专栏目录: VUE ------ element UI 专栏目标 在vue和element UI联合技术栈的操控下&#xff0c;本专栏提供行之有效的源代码示例和信息点介绍&#xff0c;做到灵活运用。 &#xff08;1&#xff09;提供vue2的一些基本操作&#xff1a;安装、引用&#xff0c;模板使…

(学习打卡1)重学Java设计模式之设计模式介绍

前言&#xff1a;听说有本很牛的关于Java设计模式的书——重学Java设计模式&#xff0c;然后买了(*^▽^*) 开始跟着小傅哥学Java设计模式吧&#xff0c;本文主要记录笔者的学习笔记和心得。 打卡&#xff01;打卡&#xff01; 设计模式介绍 一、设计模式是什么&#xff1f; …

2023年03月17日_微软和谷歌办公AI的感慨

2023年3月17日 最近这个科技圈的消息 有点爆炸的让人应接不暇了 各种大公司简直就是神仙打架 你从来没有见过这么密集的 这么高频的产品发布 昨天微软是发布了Office 365 Copilot 在里边提供了大量的AI的功能 然后谷歌呢也发布了这个Google Workspace AI 也是跟365 Cop…

据报道,微软的下一代 Surface 笔记本电脑将是其首款真正的“人工智能 PC”

明年&#xff0c;微软计划推出 Surface Laptop 6和 Surface Pro 10&#xff0c;这两款设备将提供 Arm 和 Intel 两种处理器选项。不愿意透露姓名的不透露姓名人士透露&#xff0c;这些新设备将引入先进的人工智能功能&#xff0c;包括配备下一代神经处理单元 (NPU)。据悉&#…

uniapp打包Android、Ios、微信小程序

首先我们需要在我们的代码中&#xff0c;把我们所要用到的配置信息配置好&#xff0c;在检查一下我们测试的内容是否有打开&#xff08;取消注释&#xff09;&#xff0c;在检查一下我们的版本信息是否正确&#xff0c;查看一下接口ip是否是正式线 这里的配置信息一定要配置好…