SpringMVC解析3-DispatcherServlet组件初始化
在spring中,ContextLoaderListener只是辅助功能,用于创建WebApplicationContext类型实例,而真正的逻辑实现其实是在DispatcherServlet中进行的,DispatcherServlet是实现servlet接口的实现类。
servlet是一个Java编写的程序,此程序是基于HTTP协议的,在服务器端运行的(如Tomcat),是按照servlet规范编写的一个java类。主要是处理客户端的请求并将其结果发送到客户端。servlet的生命周期是由servlet的容器来控制的,它可以分为3个阶段:初始化,运行和销毁。
(1)初始化阶段。
- servlet容器加载servlet类,把servlet类的.class文件中的数据读到内存中。
- servlet容器创建一个ServletConfig对象。ServletConfig对象包含了servlet的初始化配置信息。
- servlet容器创建一个servlet对象
- servlet容器调用servlet对象的init方法进行初始化
(2)运行阶段
当servlet容器接收到一个请求时,servlet容器会针对这个请求创建serlvetRequest和servletResponse对象,然后调用service方法。并把这两个参数传递给service方法。service方法通过servletRequest对象获得请求的信息。并处理该请求。再通过servletResponse对象生成这个请求的相应结果。然后销毁servletResponse和servletRequest对象。
(3)销毁阶段
当web应用终止时,servlet容器会先调用servlet对象的destory方法,然后再销毁servlet对象,同时销毁servlet关联的ServletConfig对象。我们可以在destroy方法的实现中,释放servlet所占用的资源,如关闭数据库连接,关闭输入输出流等。
DispatcherServlet的初始化
servlet初始化阶段会调用其init方法,所以我们看下DispatcherServle的init方法。在其父类HttpServletBean中找到了该方法。
public final void init() throws ServletException { if (logger.isDebugEnabled()) { logger.debug("Initializing servlet '" + getServletName() + "'"); } // Set bean properties from init parameters. try { //解析init-param并封装在pvs中 PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); //将当前的这个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) { logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex); throw ex; } // Let subclasses do whatever initialization they like. //留给子类扩展 initServletBean(); if (logger.isDebugEnabled()) { logger.debug("Servlet '" + getServletName() + "' configured successfully"); } }
DispatcherServlet的初始化过程主要是通过将当前的servlet类型实例转换为BeanWrapper类型实例,以便使用Srping中提供的注入功能进行对应属性的注入。这些属性如contextAttribute,contxtClass,nameSpace,contextConfigLocation等,都可以在web.xml文件中以初始化参数的方式配置在servlet声明中。DispatcherServlet继承自FramworkServlet,FrameworkServlet类上包含对应的同名属性,属性注入主要包含如下步骤:
- 封装及验证初始化参数
- 将当前servlet实例转化成BeanWrapper实例,PropertyAccessorFactory.forBeanPropertyAccess是Spring中提供的工具方法,主要用于将指定实例转化为Spring中可以处理的BeanWrapper类型的实例。
- 注册相对于Resource的属性编辑器
- 属性注入
- servletBean的初始化
封装及验证初始化参数
public ServletConfigPropertyValues(ServletConfig config, Set<String> requiredProperties) throws ServletException { Set<String> missingProps = (requiredProperties != null && !requiredProperties.isEmpty()) ? new HashSet<String>(requiredProperties) : null; Enumeration en = config.getInitParameterNames(); while (en.hasMoreElements()) { String property = (String) en.nextElement(); Object value = config.getInitParameter(property); addPropertyValue(new PropertyValue(property, value)); if (missingProps != null) { missingProps.remove(property); } } // Fail if we are still missing properties. if (missingProps != null && missingProps.size() > 0) { throw new ServletException( "Initialization from ServletConfig for servlet '" + config.getServletName() + "' failed; the following required properties were missing: " + StringUtils.collectionToDelimitedString(missingProps, ", ")); } }
servletBean的初始化
在ContextLoaderListener加载的时候已经创建了WebApplicationContext实例,而在这个函数最重要的就是对这个实例进行进一步的补充初始化。
继续查看initServletBean()。父类FrameworkServlet覆盖了HttpServletBean中的initServletBean,函数如下:
protected final void initServletBean() throws ServletException { getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'"); if (this.logger.isInfoEnabled()) { this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started"); } long startTime = System.currentTimeMillis(); try { this.webApplicationContext = initWebApplicationContext(); initFrameworkServlet(); } catch (ServletException ex) { this.logger.error("Context initialization failed", ex); throw ex; } catch (RuntimeException ex) { this.logger.error("Context initialization failed", ex); throw ex; } if (this.logger.isInfoEnabled()) { long elapsedTime = System.currentTimeMillis() - startTime; this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " + elapsedTime + " ms"); } }
上面的函数设计了计时器来统计初始化的执行时间,而且提供了一个扩展方法initFrameworkServelt()用于子类的覆盖操作,而作为关键的初始化逻辑实现委托给了initWebApplicationContext().initWebApplicationContext函数的主要工作就是创建或者刷新WebApplicationContext实例并对servlet功能所使用的变量进行初始化。
protected WebApplicationContext initWebApplicationContext() { WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); WebApplicationContext wac = null; if (this.webApplicationContext != null) { // A context instance was injected at construction time -> use it //context实例在构造函数中被注入 wac = this.webApplicationContext; if (wac instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac; if (!cwac.isActive()) { // The context has not yet been refreshed -> provide services such as // setting the parent context, setting the application context id, etc if (cwac.getParent() == null) { // The context instance was injected without an explicit parent -> set // the root application context (if any; may be null) as the parent cwac.setParent(rootContext); } //刷新上下文环境,初始化Spring环境包括加载配置文件等 configureAndRefreshWebApplicationContext(cwac); } } } if (wac == null) { // No context instance was injected at construction time -> see if one // has been registered in the servlet context. If one exists, it is assumed // that the parent context (if any) has already been set and that the // user has performed any initialization such as setting the context id //根据contextAttribute属性加载WebApplicationContext wac = findWebApplicationContext(); } if (wac == null) { // No context instance is defined for this servlet -> create a local one wac = createWebApplicationContext(rootContext); } if (!this.refreshEventReceived) { // Either the context is not a ConfigurableApplicationContext with refresh // support or the context injected at construction time had already been // refreshed -> trigger initial onRefresh manually here. onRefresh(wac); } if (this.publishContext) { // Publish the context as a servlet context attribute. String attrName = getServletContextAttributeName(); getServletContext().setAttribute(attrName, wac); if (this.logger.isDebugEnabled()) { this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() + "' as ServletContext attribute with name [" + attrName + "]"); } } return wac; }
上面代码前面都是在获取webApplicationContext,后面有这样一行代码,我们看下:configureAndRefreshWebApplicationContext
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... ServletContext sc = getServletContext(); if (sc.getMajorVersion() == 2 && sc.getMinorVersion() < 5) { // Servlet <= 2.4: resort to name specified in web.xml, if any. String servletContextName = sc.getServletContextName(); if (servletContextName != null) { wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + servletContextName + "." + getServletName()); } else { wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + getServletName()); } } else { // Servlet 2.5's getContextPath available! wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath()) + "/" + getServletName()); } } } 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); //加载配置文件及整合parent到wac wac.refresh(); }
无论调用方式如何变化,只要是使用ApplicationContext所提供的功能,最后都免不了使用公共父类AbstractApplicationContext提供的refresh()进行配置文件加载。对于本函数中的初始化主要包含几个部分。
1.寻找或创建对应的WebApplicationContext实例
- 通过构造函数的注入进行初始化。当进入initWebApplicationContext函数后通过判断this.webApplicationContext != null后,便可以确定this.webApplicationContext是否是通过构造函数来初始化的。在Web中包含SpringWeb的核心逻辑的DispatcherServlet只可以被声明为一次,在Spring中已经存在验证,所以这就确保了如果this.webApplicationContext != null,则可以直接判定this.webApplicationContext已经通过构造函数初始化。
- 通过contextAttribute进行初始化,findWebApplicationContext()。通过在web.xml文件中配置的servlet参数contextAttribute来查找ServletContext中对应的属性,默认为WebApplicationContext.class.getName() + ".ROOT",也就是在ContextLoaderListener加载时会创建WebApplicationContext实例,并将实例以WebApplicationContext.class.getName() + ".ROOT"为key放入ServletContext中,当然可以重写初始化逻辑使用自己创建WebApplicationContext,并在servlet的配置中通过初始化参数contextAttribute指定key。
- 重新创建WebApplicationContext实例,createWebApplicationContext。如果通过以上两种方式并没有找到任何突破,那就没办法了,只能在这里重新创建新的实例了。
2.configureAndRefreshWebApplicationContext。无论是通过构造函数注入还是单独创建,都免不了会调用configureAndRefreshWebApplicationContext方法来对已经创建的WebApplicationContext实例进行配置及刷新。
3.DispatcherServlet的刷新
DispatcherServlet的刷新
onRefresh是FrameworkServletl类中提供的模板方法,在其子类DispatcherServlet中进行了重写,主要用于刷新Spring在web功能实现中所必须使用的全局变量。介绍一下初始化过程以及使用场景,具体的使用细节在下一篇解析再做详细介绍。
protected void onRefresh(ApplicationContext context) { initStrategies(context); } protected void initStrategies(ApplicationContext context) { //初始化MultipartResolver initMultipartResolver(context); //初始化LocaleResolver initLocaleResolver(context); //初始化ThemeResolver initThemeResolver(context); //初始化HandlerMappings initHandlerMappings(context); //初始化HandlerAdapter initHandlerAdapters(context); //初始化HandlerExcpetionResolvers initHandlerExceptionResolvers(context); //初始化RequestToViewNameTranslator initRequestToViewNameTranslator(context); //初始化ViewResolvers initViewResolvers(context); //初始化FlashMapManager 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="resolveLazily" value="true"/>
<!--btye为单位,5M-->
<property name="maxUploadSize" value="5242880"/>
</bean>
因为之前的步骤已经完成了Spring中配置文件的解析,所以在这里只要配置文件注册过都可以通过ApplicationContext提供了getBean方法获取对应的bean,进而初始化MultipartResolver中的multipartResolver变量。
初始化LocaleResolver
在Spring的国际化配置中一共有3种使用方式。
(1)基于URL参数的配置。通过URL参数来控制国际化,比如你在页面上加一句简体中文来控制项目中使用的国际化参数。
<a href="myTest.jsp?locale=zh_CN">简体中文</a>
而提供这个功能的就是AcceptHeaderLocaleResolver,默认的参数名为locale,注意大小写。里面放的就是你的提交参数,比如en_US、zh_CN之类的,具体配置如下;
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver"></bean>
(2)基于session的配置。它通过检验用户会话中预置的属性来解析区域。最常用的是根据用户本次会话过程中的语言设定决定语言种类(例如,用户登录时选择语言种类,则此次登录周期内统一使用此语言设定),如果该会话属性不存在,它会根据accept-language HTTP头部确定默认区域。
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"></bean>
(3)基于Cookie的国际化配置。CookieLocaleResolver用于通过浏览器的cookie设置取得Locale对象。这种策略在应用程序不支持会话或者状态必须保存在客户端时有用,配置如下:
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
这三种方式都可以解决国际化的问题,对应的拦截方式:
<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor" />
在mapping url映射的时候,可以调用这个属性来跳转的时候,进行国际化的拦截。
初始化ThemeResolver
在Web开发中经常会遇到主题Theme来控制网页风格,这将进一步改善用户体验,简单说就是一个主题就是一组静态资源,可以影响应用程序的视觉效果。Spring中的主题功能和国际化功能非常相似,构成Spring主题功能主要包括如下内容。
(1)主题资源。Spring中的ThemeSource是Spring中主题资源的接口,Spring的主题需要通过ThemeSource接口来实现存放主题信息的资源。 ResourceBundleThemeSource是ThemeSource接口默认实现类,在Spring中的配置如下:
<bean id="themsSource" class="org.Springframework.ui.context.support.ResourceBundleThemeSource"> </bean>
默认状态下是在类路径根目录下查找相应的资源文件,也可以通过beasenamePrefix指定查找资源文件的包路径。
(2)主题解析器。
ThemeSource定义了一些主题资源,ThemeResolver是主题解析器的接口,主题解析的工作则是其子类完成的。
对于主题解析器,主要有三个比较常见的实现,以主题summer.properties为例。
FixedThemeResolver:用于选择一个固定的主题。
<bean id="themeResolver" class="org.Springframework.web.servlet.theme.FixedThemeResolver"> <property name="defaultThemeName" value="主题名" /> </bean>
CookieThemeResolver:用于实现用户所选的主题,以Cookie的方式存放在客户端的机器上。
<bean id="themeResolver" class="org.Springframework.web.servlet.theme.CookieThemeResolver"> <property name="defaultThemeName" value="主题名" /> </bean>
SessionThemeResolver:用户主题保存在用户的HTTP Session中。
<bean id="themeResolver" class="org.Springframework.web.servlet.theme.SessionThemeResolver"> <property name="defaultThemeName" value="主题名" /> </bean>
以上配置用于设置主题名称,并将该名称保存在用户的HttpSession中。
另外,对于FixedThemeResolver和SessionThemeResolver,共同继承了AbstractThemeResolver类。用户也可以自己实现自己的解析器继承AbstractThemeResolver类。
(3)拦截器。
如果需要根据用户请求来改变主题,那么Spring提供了一个已经实现的拦截器ThemeChangeInterceptor拦截器,具体的配置如下:
<bean id="themeChangeInterceptor" class="org.Springframework.web.servlet.theme.ThemeChangeInterceptor"> <property name="paramName" value="themeName"></property> </bean>
其中设置用户请求参数名为themeName,即URL为?themeName=具体的主题名称。此外还需要在handlerMapping中配置拦截器。
<property name="interceptors"> <list> <ref local="themeChangeInterceptor" /> </list> </property>
初始化HandlerMappings
当客户端发出Request时DispatcherServlet会将Request提交给HandlerMaping,然后HandlerMapping根据WebApplicationContext的配置来回传给DispatcherServlet相应的Controller.
在基于SpringMVC的web应用程序中,我们可以为DispatcherServlet提供多个HandlerMapping供其使用。DispatcherServlet在选用HandlerMapping的过程中,将根据我们所制定的一系列HandlerMapping的优先级进行排序,然后优先使用优先级在前面的handlerMapping.如果当前的HandlerMapping能够返回可用的Handler,DispatcherSevlet则使用当前返回的Handler进行Web请求的处理。而不再继续询问其他的HandlerMapping。否则,DispatcherServlet将继续按照各个handlerMapping的优先级进行询问,直到获得一个可用的Handler为止。初始化配置如下:
private void initHandlerMappings(ApplicationContext context) { this.handlerMappings = null; if (this.detectAllHandlerMappings) { // Find all HandlerMappings in the ApplicationContext, including ancestor contexts. Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values()); // We keep HandlerMappings in sorted order. OrderComparator.sort(this.handlerMappings); } } else { 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. if (this.handlerMappings == null) { this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class); if (logger.isDebugEnabled()) { logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default"); } } }
默认情况下,SpringMVC将加载当前系统中所有实现了HandlerMapping接口的bean。如果只期望SpringMVC加载指定的handlermapping时,可以修改web.xml中的DispatcherServlet的初始化参数,将detectAllHandlerMappings的值设置为false:
<init-param>
<param-name>detectAllHandlerMappings</param-name>
<param-value>false</param-value>
</init-param>
此时,SpringMVC将查找名为“handlerMapping”的bean,并作为当前系统中唯一的handlermapping.如果没有没有定义handlerMapping的话,则SpringMVC将按照org.Springframeword.web.servlet.DispatcherServlet所在目录下的DispatcherServlet.properties中所定义的org.Springframeword.web.servlet.HandlerMapping的内容来加载默认的handlerMapping(用户没有自定义Strategies的情况下)
DispatcherServlet.properties
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.annotation.DefaultAnnotationHandlerMapping
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\
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
初始化HandlerAdapters
从名字也能联想到这个一个典型的适配器模式的使用,在计算机编程中,适配器模式将一个类的接口适配成用户所期待的。使用适配器,可以使接口而无法一起工作的的类协同工作,做法是将类自己的接口包裹在一个已经存在的类中。那么在处理handler中为什么会使用适配模式呢?我们看下他的初始化逻辑。
private void initHandlerAdapters(ApplicationContext context) { this.handlerAdapters = null; 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<HandlerAdapter>(matchingBeans.values()); // We keep HandlerAdapters in sorted order. OrderComparator.sort(this.handlerAdapters); } } else { 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. if (this.handlerAdapters == null) { this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class); if (logger.isDebugEnabled()) { logger.debug("No HandlerAdapters found in servlet '" + getServletName() + "': using default"); } } }
同样在初始化的过程中涉及了一个变量detectAllHandlerAdapters,detectAllhandlerAdapters作用和detectAllHandlerMappings类似,只不过作用对象为handlerAdapter。也可以通过如下配置强制系统只加载beanname为“handlerAdapteer”的handlerAdapter。
<init-param>
<param-name>detectAllhandlerAdapters</param-name>
<param-value>false</param-value>
</init-param>
如果无法找得到对应的bean,那么系统会尝试加载默认的适配器。
protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) { String key = strategyInterface.getName(); String value = defaultStrategies.getProperty(key); if (value != null) { String[] classNames = StringUtils.commaDelimitedListToStringArray(value); List<T> strategies = new ArrayList<T>(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( "Error loading DispatcherServlet's default strategy class [" + className + "] for interface [" + key + "]: problem with class file or dependent class", err); } } return strategies; } else { return new LinkedList<T>(); } }
在getDefaultStrategies函数中,Spring会尝试从defaultStrategies中加载对应的HandlerAdapter属性,那么defaultStrategies是如何初始化的呢?
在当前类DispatcherServlet中存在这样一段初始化代码块:
static { // Load default strategy implementations from properties file. // This is currently strictly internal and not meant to be customized // by application developers. try { ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class); defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); } catch (IOException ex) { throw new IllegalStateException("Could not load 'DispatcherServlet.properties': " + ex.getMessage()); } }
在系统加载的时候,defaultStrategies根据当前路径DispatcherServlet.properties来初始化本身,查看DispatcherServlet.properties中对应于HandlerAdapter的属性:
org.Springframework.web.servlet.HandlerAdapter=org.Springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.Springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.Springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
由此得知,如果程序开发人员没有在配置文件中定义自己的适配器,那么Spring会默认加载配置文件中的3个适配器。
作为总控制器的派遣器servlet通过处理器映射得到处理器后,会轮询处理器适配器模块,查找能够处理当前HTTP请求的处理器适配器的实现,处理器适配器模块根据处理器映射返回的处理器类型,例如简单的控制器类型、注解控制器类型或者远程调用处理器类型,来选择某一个适当的处理器适配器的实现,从而适配当前的HTTP请求。
- HTTP请求处理器适配器(HttpRequestHandlerAdapter)。HTTP请求处理器适配器仅仅支持对HTTP请求处理器的适配。它简单地将HTTP请求对象和响应对象传递给HTTP请求处理器的实现,它并不需要返回值。它主要应用在基于HTTP的远程调用的实现上。
- 简单控制器处理器适配器(SimpleControllerHandlerAdapter)。这个实现类将HTTP请求适配到一个控制器的实现进行处理。这里控制器的实现是一个简单的控制器接口的实现。简单控制器处理器适配器被设计成一个框架类的实现,不需要被改写,客户化的业务逻辑通常是在控制器接口的实现类中实现的。
- 注解方法处理器适配器(AnnotationMethodHandlerAdapter)。这个类的实现是基于注解的实现,它需要结合注解方法映射和注解方法处理器协同工作。它通过解析声明在注解控制器的请求映射信息来解析相应的处理器方法来处理当前的HTTP请求。在处理的过程中,它通过反射来发现探测处理器方法的参数,调用处理器方法,并且映射返回值到模型和控制器对象,最后返回模型和控制器对象给作为主控制器的派遣器Servlet。
Spring中所使用的Handler并没有任何特殊的联系,但是为了统一处理,Spring提供了不同情况下的适配器。
初始化HandlerExceptionResolvers
@Component public class ExceptionHandler implements HandlerExceptionResolver { private static final Log logs = LogFactory.getLog(ExceptionHandler.class); @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object obj, Exception exception) { request.setAttribute("exception", exception.toString()); request.setAttribute("exceptionStack", exception); logs.error(exception.toString(), exception); return new ModelAndView("error/exception"); } }
使用这种异常处理方式,需要实现resolveException方法,该方法返回一个ModelAndView对象,在方法内部对异常的类型进行判断,然后尝试生成对应的ModelAndView对象,如果该方法返回了null,则Spring会继续寻找其他的实现了HandlerExceptionResolver接口的bean,直到返回一个ModelAndView对象。
配置如下:
<bean id="exceptionHandler" class="com.test.exception.ExceptionHandler" />
初始化RequestToViewNameTranslator
当Controller处理器方法没有返回一个View对象或逻辑视图名称,并且在该方法中没有直接往response的输出流里面写数据的时候,Spring就会采用约定好的方式提供一个逻辑视图名称。这个逻辑视图名称是通过Spring定义的org.Springframework.web.servlet.RequestToView NameTranslator接口的getViewName方法来实现的,我们可以实现自己的Request ToViewName Translator接口来约定好没有返回视图名称的时候如何确定视图名称。Spring已经给我们提供了一个它自己的实现,那就是org.Springframework.web.servlet.view.DefaultRequestToViewNameTranslator。
在介绍DefaultRequestToViewNameTranslator是如何约定视图名称之前,先来看一下它支持用户定义的属性。
prefix:前缀,表示约定好的视图名称需要加上的前缀,默认是空串。
suffix:后缀,表示约定好的视图名称需要加上的后缀,默认是空串。separator:分隔符,默认是斜杠“/”。
stripLeadingSlash:如果首字符是分隔符,是否要去除,默认是true。
stripTrailingSlash:如果最后一个字符是分隔符,是否要去除,默认是true。
stripExtension:如果请求路径包含扩展名是否要去除,默认是true。
urlDecode:是否需要对URL解码,默认是true。它会采用request指定的编码或者
ISO-8859-1编码对URL进行解码。
当我们没有在SpringMVC的配置文件中手动的定义一个名为viewNameTranlator的Bean的时候,Spring就会为我们提供一个默认的viewNameTranslator,即DefaultRequestToViewName Translator。
接下来看一下,当Controller处理器方法没有返回逻辑视图名称时,DefaultRequestToView NameTranslator是如何约定视图名称的。DefaultRequestToViewNameTranslator会获取到请求的URI,然后根据提供的属性做一些改造,把改造之后的结果作为视图名称返回。这里以请求路径http://localhost/app/test/index.html为例,来说明一下DefaultRequestToViewNameTranslator是如何工作的。该请求路径对应的请求URI为/test/index.html,我们来看以下几种情况,它分别对应的逻辑视图名称是什么。
- prefix和suffix如果都存在,其他为默认值,那么对应返回的逻辑视图名称应该是prefixtest/indexsuffix。
- stripLeadingSlash和stripExtension都为false,其他默认,这时候对应的逻辑视图名称是/product/index.html。
- 都采用默认配置时,返回的逻辑视图名称应该是product/index。
如果逻辑视图名称跟请求路径相同或者相关关系都是一样的,那么我们就可以采用Spring为我们事先约定好的逻辑视图名称返回,这可以大大简化我们的开发工作。
初始化ViewResolvers
在SpringMVC中,当Controller将请求处理结果放入到ModelAndView中以后,DispatcherServlet会根据ModelAndView选择合适的视图进行渲染。那么在SpringMVC中是如何选择合适的View呢?View对象是如何创建的呢?答案在ViewResolver中。ViewResolver接口定义了resolverViewName方法,根据viewName创建合适类型的View实现。
那么如何配置ViewResolver呢?在Spring中,ViewResolver作为SpringBean存在,可以在配置文件中进行配置,例如下面的代码,配置了JSP相关的veiwResolver.
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/view/" /> <property name="suffix" value=".jsp" /> </bean>
初始化FlashMapManager
SpringMVC Flash attributs提供了一个请求存储属性,可供其他请求使用。在使用重定向的时候非常必要,例如Post/Redirect/Get模式。Flash attributes在重定向之前暂存(就像存在session中)以便重定向之后还能使用,并立即删除。
SpringMVC有两个主要的抽象来支持flasth attributes。FlashMap用于保持flash attributes,而FlashMapManager用于存储,检索,管理FlashMap实例。
flash attribute支持默认开始(“on”)并不需要显示启用,它永远不会导致HTTPSession的创建。这两个FlashMap实例都可以通过静态方法RequestContextUtils从SpringMVC的任何位置访问。