spring源码分析 SpringMVC

目录

ContextLoaderListener

ServletContextListener的使用

Spring中的ContextLoaderListener

DispatcherServlet

servlet的使用

DispatcherServlet的初始化

WebApplicationContext的初始化

寻找或创建对应的WebApplicationContext实例

configureAndRefreshWebApplicationContext

刷新

DisnatcherServlet的逻辑处理

MultipartContent类型的request处理

根据request信息寻找对应的Handler

根据request查找对应的Handler

加入拦截器到执行链

没找到对应的Handler的错误处理

根据当前Handler寻找对应的HandlerAdapter

缓存处理

Handlerlnterceptor的处理

逻辑处理

异常视图的处理

根据视图跳转页面

解析视图名称

页面跳转


注意:本文转自 spring源码深度解析

Spring框架提供了构建Web,应用程序的全功能MVC模块。通过策略接口,Spring框架是高度可配置的,而且支持多种视图技术,例如JavaServer Pages (JSP)、Velocity、Tiles、iText和POI。SpringMVC框架并不知道使用的视图,所以不会强迫您只使用JSP技术。SpringMVC分离了控制器、模型对象、分派器以及处理程序对象的角色,这种分离让它们更容易进行定制。

Spring的MVC是基于Servlet功能实现的,通过实现Servlet接口的DispatcherServlet来封装其核心功能实现,通过将请求分派给处理程序,同时带有可配置的处理程序映射、视图解析、本地语言、主题解析以及上载文件支持。默认的处理程序是非常简单的Controller接口,只有一个方法ModelAndView handleRequest(request,response)。Spring提供了一个控制器层次结构,可以派生子类。如果应用程序需要处理用户输入表单,那么可以继承AbstractFormController。如果需要把多页输入处理到一个表单,那么可以继承AbstractWizardFormController。

对SpringMVC或者其他比较成熟的MVC框架而言,解决的问题无外乎以下几点。

将Web,页面的请求传给服务器。

根据不同的请求处理不同的逻辑单元。

返回处理结果数据并跳转至响应的页面。

ContextLoaderListener

对于SpringMVC功能实现的分析,我们首先从web.Xml开始,在web.Xml文件中我们首先配置的就是ContextLoaderListener,那么它所提供的功能有哪些,又是如何实现的呢?

当使用编程方式的时候我们可以直接将Spring配置信息作为参数传入Spring容器中,如

ApplicationContext ac=new ClassPathXmlApplicationContext("applicationContext.Xml");

但是在Web下,我们需要更多的是与Web环境相互结合,通常的办法是将路径以Context-Param的方式注册并使用ContextLoaderListener进行监听读取。

ContextLoaderListener的作用就是启动Web容器时,自动装配ApplicationContext的配置信息。因为它实现了ServletContextListener这个接口,在web.Xml配置这个监听器,启动容器时,就会默认执行它实现的方法,使用ServletContextListener接口,开发者能够在为客户端请求提供服务之前向ServletContext中添加任意的对象。这个对象在ServletContext启动的时候被初始化,然后在ServletContext整个运行期间都是可见的。

每一个Web应用者都有一个ServletContext与之相关联。ServletContext对象在应用启动时被创建,在应用关闭的时候被销毁。ServletContext在全局范围内有效,类似于应用中的一个全局变量。

在ServletContextListener中的核心逻辑便是初始化WebApplicationContext实例并存放至ServletContext中。

ServletContextListener的使用

正式分析代码前我们同样还是首先了解ServletContextListener的使用。

1. 创建自定义ServletContextlistener

首先我们创建ServletContextListener,目标是在系统启动时添加自定义的属性,以便于在全局范围内可以随时调用。系统启动的时候会调用ServletContextListener实现类的Contextlnitialized方法,所以需要在这个方法中实现我们的初始化逻辑。

Spring中的ContextLoaderListener

分析了ServletContextListener的使用方式后再来分析Spring中的ContextLoaderListener的实现就容易理解得多,虽然ContextLoaderListener实现的逻辑要复杂得多,但是大致的套路还是万变不离其宗。

ServletContext启动之后会调用ServletContextListener的Contextlnitialized方法,那么,我们就从这个函数开始进行分析。

这里涉及了一个常用类WebApplicationContext:在Web应用中,我们会用到WebApplicationContext,WebApplicationContext继承自ApplicationContext,在AppIicationContext的基础上又追加了一些特定于Web的操作及属性,非常类似于我们通过编程方式使用Spring使用的ClassPathXmlApplicationContext类提供的功能。继续跟踪代码:

initWebApplicationContext函数主要是体现了创建WebApplicationContext实例的一个功能 架构,从函数中我们看到了初始化的大致步骤。

1 WebApplicationContext存在性的验证。

在配置中只允许声明一次ServletContextListener,多次声明会扰乱Spring的执行逻辑,所以这里首先做的就是对此验证,在Spring中如果创建WebApplicationContext实例会记录在ServletContext中以方便全局调用,而使用的key就是WebApplicationContext.ROOT_WEB_APPLICATION_ CONTEXT_ATTRIBUTE,所以验证的方式就是查看ServIetContext实例中是否有对应key的属性。

2 创建WebApplicationContext实例。

如果通过验证,则Spring将创建WebApplicationContext实例的工作委托给了createWebApplicationContext函数。

根据以上静态代码块的内容,我们推断在当前类ContextLoader同样目录下必定会存在属性文件ContextLoader.properties,查看后果然存在,内容如下:

org.Springframework.web.Context.WebApplicationContext=org.Springframework.web.Context.support.XmlWebApplicationContext

综合以上代码分析,在初始化的过程中,程序首先会读取ContextLoader类的同目录下的属性文件ContextLoader.properties,并根据其中的配置提取将要实现WebApplicationContext接口的实现类,并根据这个实现类通过反射的方式进行实例的创建。

3 将实例记录在servletContext中。

4 映射当前的类加载器与创建的实例到全局变量currentContextPerThread中。

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容器会针对这个请求创建servletRequest和 servletResponse对象,然后调用service方法。并把这两个参数传递给service方法。service方法通过servletRequest对象获得请求的信息。并处理该请求。再通过servletResponse对象生成这个请求的响应结果。然后销毁servIetRequest和servletResponse对象。我们不管这个请求是post提交的还是get提交的,最终这个请求都会由service方法来处理。

3 销毁阶段

当Web应用被终止时,servlet容器会先调用servlet对象的destrory方法,然后再销毁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()、doCiet()、doOptions()、doPost()、doPut()和doTrace()。

servlet的使用

我们同样还是以最简单的servlet来快速体验其用法。

1 建立servlet

麻雀虽小,五脏俱全。实例中包含了对init方法和getJpost方法的处理,init方法保证在servlet加载的时候能做一些逻辑操作,而HttpServlet类则会帮助我们根据方法类型的不同而将逻辑引入不同的函数。在子类中我们只需要重写对应的函数逻辑便可,如以上代码重写了doGet和doPost方法并将逻辑处理部分引导至handleLogic函数中,最后,又将页面跳转至index.jsp。

2 添加配置

为了使servlet能够正常使用,需要在web.xml文件中添加以下配置:

配置后便可以根据对应的配置访问相应的路径了。

DispatcherServlet的初始化

通过上面的实例我们了解到,在servlet初始化阶段会调用其init方法,所以我们首先要查看在DispatcherServlet中是否重写了init方法。我们在其父类HttpServletBean中找到了该方法。

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

1 封装及验证初始化参数

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

从代码中得知,封装属性主要是对初始化的参数进行封装,也就是servlet中配置的<init-param>中配置的封装。当然,用户可以通过对requiredProperties参数的初始化来强制验证某些属性的必要性,这样,在属性封装的过程中,一旦检测到requiredProperties中的属性没有指定初始值,就会抛出异常。

2 将当前servlet实例转化成BeanWrapper实例

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

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

属性编辑器,我们在上文中已经介绍并且分析过其原理,这里使用属性编辑器的目的是在对当前实例(DispatcherServlet)属性注入过程中一旦遇到Resource类型的属性就会使用ResourceEditor去解析。

4 属性注入

BeanWrapper为Spring中的方法,支持Spring的自动注入。其实我们最常用的属性注入无非是ContextAttribute、ContextClass、nameSpace、ContextConfigLocation等。

5 servletBean的初始化

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

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

上面的函数设计了计时器来统计初始化的执行时间,而且提供了一个扩展方法 initFrameworkServlet()用于子类的覆盖操作,而作为关键的初始化逻辑实现委托给了 initWebApplicationContext()。

WebApplicationContext的初始化

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

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

寻找或创建对应的WebApplicationContext实例

WebApplicationContext的寻找及创建包括以下几个步骤。

1 通过构造函数的注入进行初始化。

当进入initWebApplicationContext函数后通过判断this.webApplicationContext != null后,便可以确定this.webApplicationContext是否是通过构造函数来初始化的。可是有读者可能会有疑问,在initServletBean函数中明明是把创建好的实例记录在了this.webApplicationContext中:

this.webApplicationContext=initWebApplicationContext();

何以判定这个参数是通过构造函数初始化,而不是通过上一次的函数返回值初始化呢?如果存在这个问题,那么就是读者忽略一个问题了:在Web中包含Spring Web的核心逻辑的 DispatcherServlet只可以被声明为一次,在Spring中已经存在验证,所以这就确保了如果 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。

3 重新创建WebApplicationContext实例。

如果通过以上两种方式并没有找到任何突破,那就没办法了,只能在这里重新创建新的实例了。

configureAndRefreshWebApplicationContext

无论是通过构造函数注入还是单独创建,都会调用configureAndRefreshWebApplicationContext 方法来对已经创建的WebApplicationContext实例进行配置及刷新,那么这个步骤又做了哪些工作呢?

无论调用方式如何变化,只要是使用ApplicationContext所提供的功能最后都免不了使用公共父类AbstractApplicationContext提供的refresh()进行配置文件加载。

刷新

onRefresh是FrameworkServlet类中提供的模板方法,在其子类DispatcherServlet中进行了重写,主要用于刷新Spring在Web功能实现中所必须使用的全局变量。下面我们会介绍它们的初始化过程以及使用场景,而至于具体的使用细节会在稍后的章节中再做详细介绍。

1 初始化MultipartResolver。

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

当然,CommonsMultipartResolver还提供了其他功能用于帮助用户完成上传功能,有兴趣的读者可以进一步查看。

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

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

2 初始化LocaleResolver。

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

基于URL参数的配置

通过URL参数来控制国际化,比如你在页面上加一句<a href="?locale=zh_CN">简体中文</a>来控制项目中使用的国际化参数。而提供这个功能的就是AcceptHeaderLocaleResolver,默认的参数名为locale,注意大小写。里面放的就是你的提交参数,比如en_US、zh_CN之类的,具体配置如下

<bean id="localeResolver" class="org.Springframework.web.servlet.il8n.AcceptHeaderLocaleResolver"/>

基于session的配置

它通过检验用户会话中预置的属性来解析区域。最常用的是根据用户本次会话过程中的语言设定决定语言种类(例如,用户登录时选择语言种类,则此次登录周期内统一使用此语言设定),如果该会话属性不存在,它会根据accept-language HTTP头部确定默认区域。

<bean id="localeResolver" class="org.Springframework.web.servlet.il8n.SessionLocaleResolver"/>

基于cookie的国际化配置

CookieLocaleResolver用于通过浏览器的cookie设置取得Locale对象。这种策略在应用程序不支持会话或者状态必须保存在客户端时有用,配置如下:

<bean id="localeResolver" class="org.Springframework.web.servlet.il8n.CookieLocaleResolver"/>

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

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

3 初始化ThemeResolver

在Web开发中经常会遇到通过主题Theme来控制网页风格,这将进一步改善用户体验。

简单地说,一个主题就是一组静态资源(比如样式表和图片),它们可以影响应用程序的视觉效果。Spring中的主题功能和国际化功能非常类似。Spring主题功能的构成主要包括如下内容。

主题资源

org.Springframework.ui.Context.ThemeSource是Spring中主题资源的接口,Spring的主题需要通过ThemeSource接口来实现存放主题信息的资源。

org.Springframework.ui.Context.support.ResourceBundleThemeSource是ThemeSource接口默认实现类(也就是通过ResourceBundle资源的方式定义主题),在Spring中的配置如下:

<bean id="themeSource" class="org.Springframe.iork.ui.Context.support.ResourceBundle

ThemeSource">

<property name="basenamePrefix" value="com.test. "></property> </bean>

默认状态下是在类路径根目录下查找相应的资源文件,也可以通过basenamePrefix来制定。

这样,DispatcherServlet就会在com.test包下查找资源文件。

主题解析器

ThemeSource定义了一些主题资源,那么不同的用户使用什么主题资源由谁定义呢?org.Springframework.web.servlet.ThemeResolver是主题解析器的接口,主题解析的工作便由它的子类来完成。

对于主题解析器的子类主要有3个比较常用的实现。以主题文件summer.properties为例。

FixedThemeResolver用于选择一个固定的主题。

以上配置的作用是设置主题文件为summer.properties,在整个项目内固定不变。

CookieThemeResolver用于实现用户所选的主题,以cookie的形式存放在客户端的机器上,配置如下:

<bean id="themeResolver" class="org.Springframework.web.servlet.theme.CookieThemeResolver"> <property name="defaultThemeName" value="surnmer"/>

</bean>

SessionThemeResolver用于主题保存在用户的HTTP Session中。

<bean id="themeResolver" class="org.Springframework.web.servlet.theme.SessionThemeResolver"> <property name="defaultThemeName" value="surnmer"/>

</bean>

以上配置用于设置主题名称,并且将该名称保存在用户的HttpSession中。

AbstractThemeResolver是一个抽象类被SessionThemeResolver和FixedThemeResolver继承,用户也可以继承它来自定义主题解析器。

拦截器

如果需要根据用户请求来改变主题,那么Spring提供了一个已经实现的拦截器-ThemeChangelnterceptor拦截器了,配置如下:

<bean id="themeChangeinterceptor" class="org.Springframework.web.servlet.theme.ThemeChangeinterceptor">

<property name="paramName" value="themeName"></property> </bean>

其中设置用户请求参数名为themeName,即URL为?themeName=具体的主题名称。此外,还需要在handlerMapping中配置拦截器。当然需要在HandleMapping中添加拦截器。

<property name="interceptors"> <list>

<ref loca!="themeChangeinterceptor" /> </list>

</property>

了解了主题文件的简单使用方式后,再来查看解析器的初始化工作,与其他变量的初始化工作相同,主题文件解析器的初始化工作并没有任何需要特别说明的地方。

4 初始化HandlerMappings。

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

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

默认情况下,SpringMVC将加载当前系统中所有实现了HandlerMapping接口的bean。如果只期望SpringMVC加载指定的handlermapping时,可以修改web.Xml中的DispatcherServlet的初始参数,将detectAllHandlerMappings的值设置为false:

<init-param> <param-name>ctetectAllHandlerMappings</param-name> <param-value>false</param-value>

</init-param>

此时,SpringMVC将查找名为"handlerMapping"的bean,并作为当前系统中唯一的 handlermapping。如果没有定义handlerMapping的话,则SpringMVC将按照org.Springframework. web.servlet.DispatcherServlet所在目录下的DispatcherServlet.prope巾es中所定义的org. Springframework.web.servlet.HandlerMapping的内容来加载默认的handlerMapping(用户没有自定义Strategies的情况下)。

5 初始化HandlerAdapters。

从名字也能联想到这是一个典型的适配器模式的使用,在计算机编程中,适配器模式将一个类的接口适配成用户所期待的。使用适配器,可以使接口不兼容而无法在一起工作的类协同工作,做法是将类自己的接口包裹在一个已存在的类中。那么在处理handler时为什么会使用适配器模式呢?回答这个问题我们首先要分析它的初始化逻辑。

同样在初始化的过程中涉及了一个变量detectAIIHandlerAdapters,detectAllHandlerAdapters作用和detectAIIHandlerMappings类似,只不过作用对象为handlerAdapter。亦可通过如下配置来强制系统只加载beanname为handlerAdapterhandlerAdapter。

<init-pararn> <pararn-name>detectAllHandlerAdapters</pararn-name> <pararn-value>false</pararn-value>

</init-pararn>

如果无法找到对应的bean,那么系统会尝试加载默认的适配器。

在getDefaultStrategies函数中,Spring会尝试从defaultStrategies中加载对应的HandlerAdapter的属性,那么defaultStrategies是如何初始化的呢?

在当前类DispatcherServlet中存在这样一段初始化代码块:

在系统加载的时候,defaultStrategies根据当前路径DispatcherServlet.properties来初始化本身,查看DispatcherServlet.properties中对应于HandlerAdapter的属性:

由此得知,如果程序开发人员没有在配置文件中定义自己的适配器,那么Spring会默认加载配置文件中的3个适配器。

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

HTTP请求处理器适配器(HttpRequestHandlerAdapter)。

HTTP请求处理器适配器仅仅支持对HTTP请求处理器的适配。它简单地将HTTP请求对象和响应对象传递给HTTP请求处理器的实现,它并不需要返回值。它主要应用在基于HTTP的远程调用的实现上。

简单控制器处理器适配器(SimpleControllerHandlerAdapter)。

这个实现类将HTTP请求适配到一个控制器的实现进行处理。这里控制器的实现是一个简单的控制器接口的实现。简单控制器处理器适配器被设计成一个框架类的实现,不需要被改写,客户化的业务逻辑通常是在控制器接口的实现类中实现的。

注解方法处理器适配器(AnnotationMethodHandlerAdapter)。

这个类的实现是基于注解的实现,它需要结合注解方法映射和注解方法处理器协同工作。

它通过解析声明在注解控制器的请求映射信息来解析相应的处理器方法来处理当前的HTTP请求。在处理的过程中,它通过反射来发现探测处理器方法的参数,调用处理器方法,并且映射返回值到模型和控制器对象,最后返回模型和控制器对象给作为主控制器的派遣器Servlet。

所以我们现在基本上可以回答之前的问题了,Spring中所使用的Handler并没有任何特殊的联系,但是为了统一处理,Spring提供了不同情况下的适配器。

6 初始化HandlerExceptionResolvers。

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

7 初始化RequestToViewNameTranslator。

当Controller处理器方法没有返回一个View对象或逻辑视图名称,并且在该方法中没有直接往response的输出流里面写数据的时候,Spring就会采用约定好的方式提供一个逻辑视图名称。这个逻辑视图名称是通过Spring定义的org.Springframework.web. servlet.RequestToViewNameTranslator接口的getViewName方法来实现的,我们可以实现自己的RequestToViewNameTranslator接口来约定好没有返回视图名称的时候如何确定视图名称。Spring已经给我们提供了一个它自己的实现,那就是org.Springframework.web.servlet. view.DefaultRequestToViewNameTranslator。

在介绍DefaultRequestToViewNameTranslator是如何约定视图名称之前,先来看一下它支持用户定义的属性。

prefix:前缀,表示约定好的视图名称需要加上的前缀,默认是空串。

suffix:后缀,表示约定好的视图名称需要加上的后缀,默认是空串。

separator: 分隔符,默认是斜杠"/"。

stripLeadingSlash:如果首字符是分隔符,是否要去除,默认是true。

stripTrailingSlash:如果最后一个字符是分隔符,是否要去除,默认是true。

stripExtension:如果请求路径包含扩展名是否要去除,默认是true。

urlDecode:是否需要对URL解码,默认是true。它会采用request指定的编码或者IS0-8859-1编码对URL进行解码。

当我们没有在SpringMVC的配置文件中手动的定义一个名为viewNameTranlator的Bean的时候,Spring就会为我们提供一个默认的viewNameTranslator,即DefaultRequestToViewNameTranslator。

接下来看一下,当Controller处理器方法没有返回逻辑视图名称时,DefaultRequestToViewName Translator是如何约定视图名称的。DefaultRequestToViewNameTranslator会获取到请求的URI, 然后根据提供的属性做一些改造,把改造之后的结果作为视图名称返回。这里以请求路径http://localhost/app/test/index.html为例,来说明一下DefaultRequestToViewNameTranslator是如何工作的。该请求路径对应的请求URI为/test/index.html,我们来看以下几种情况,它分别对应的逻辑视图名称是什么。

prefix和suffix如果都存在,其他为默认值,那么对应返回的逻辑视图名称应该是prefixtest/indexsuffix。

stripLeaclingSlash和stripExtension都为false,其他默认,这时候对应的逻辑视图名称是/product/index.html。

都采用默认配置时,返回的逻辑视图名称应该是product/index。

如果逻辑视图名称跟请求路径相同或者相关关系都是一样的,那么我们就可以采用Spring为我们事先约定好的逻辑视图名称返回,这可以大大简化我们的开发工作,而以上功能实现的关键属性viewNameTranslator,则是在initRequestToViewNameTranslator中完成。

8 初始化ViewResolvers。

在SpringMVC中,当Controller将请求处理结果放入到ModelAndView中以后,DispatcherServlet会根据ModelAndView选择合适的视图进行渲染。那么在SpringMVC中是如何选择合适的View呢?View对象是是如何创建的呢?答案就在ViewResolver中。ViewResolver接口定义了resolverViewName方法,根据viewName创建合适类型的View实现。

那么如何配置ViewResolver呢?在Spring中,ViewResolver作为SpringBean存在,可以在Spring配置文件中进行配置,例如下面的代码,配置了JSP相关的ViewResolver。

9 初始化FlashMapManager。

SpringMVC Flash attributes提供了一个请求存储属性,可供其他请求使用。在使用重定向时候非常必要,例如Post/Redirect/Get模式。Flash attributes在重定向之前暂存(就像存在session中)以便重定向之后还能使用,并立即删除。

SpringMVC有两个主要的抽象来支待flash attributes。FlashMap用于保持flash attributes,而FlashMapManager用于存储、检索、管理FlashMap实例。

flash attribute支持默认开启("on")并不需要显式启用,它永远不会导致HTTP Session的创建。这两个FlashMap实例都可以通过静态方法RequestContextUtils从SpringMVC的任何位置访问。

flashMapManager的初始化在initFlashMapManager中完成。

DisnatcherServlet的逻辑处理

根据之前的示例,我们知道在HttpServlet类中分别提供了相应的服务方法,它们是doDelete()、doGet()、doOptions()、doPost()、doPut()和doTrace(),它会根据请求的不同形式将程序引导至对应的函数进行处理。这几个函数中最常用的函数无非就是doGet()和doPost(),那么我们就直接查看DispatcherServlet中对于这两个函数的逻辑实现。

函数中已经开始了对请求的处理,虽然把细节转移到了doService函数中实现,但是我们不难看出处理请求前后所做的准备与处理工作。

1 为了保证当前线程的LocaleContext以及RequestAttributes可以在当前请求后还能恢复,提取当前线程的两个属性。

2 根据当前request创建对应的LocaleContext和RequestAttributes,并绑定到当前线程。

3 委托给doService方法进一步处理。

4 请求处理结束后恢复线程到原始状态。

5 请求处理结束后无论成功与否发布事件通知。继续查看doService方法。

我们猜想对请求处理至少应该包括一些诸如寻找Handler并页面跳转之类的逻辑处理,但是,在doService中我们并没有看到想看到的逻辑,相反却同样是一些准备工作,但是这些准备工作却是必不可少的。Spring将已经初始化的功能辅助工具变量,比如localeResolver、

themeResolver等设置在request属性中,而这些属性会在接下来的处理中派上用场。

经过层层的准备工作,终于在doDispatch函数中看到了完整的请求处理过程。

doDispatch函数中展示了Spring请求处理所涉及的主要逻辑,而我们之前设置在request 中的各种辅助属性也都有被派上了用场。下面回顾一下逻辑处理的全过程。

MultipartContent类型的request处理

对于请求的处理,Spring首先考虑的是对于Multipart的处理,如果是MultipartContent类型的request,则转换request为MultipartHttpServletRequest类型的request。

根据request信息寻找对应的Handler

在Spring中最简单的映射处理器配置如下:

<bean id="simpleUrlMapping" class="org.Springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="mappings">

<props>

<prop key="/userlist.htm">userController</prop> </props>

</property>

</bean>

在Spring加载的过程中,Spring会将类型为SimpleUrlHandlerMapping的实例加载到 this.HandlerMappings中,按照常理推断,根据request提取对应的Handler,无非就是提取当前实例中的userController,但是userController为继承自AbstractController类型实例,与HandlerExecutionChain并无任何关联,那么这一步是如何封装的呢?

在之前的内容我们提过,在系统启动时Spring会将所有的映射类型的bean注册到 this.handlerMappings变量中,所以此函数的目的就是遍历所有的HandlerMapping,并调用其getHandler方法进行封装处理。以SimpleUrlHandlerMapping为例查看其getHandler方法如下:

函数中首先会使用getHandlerlntemal方法根据request信息获取对应的Handler,如果以SimpleUrlHandlerMapping为例分析,那么我们推断此步骤提供的功能很可能就是根据URL找到匹配的Controller并返回,当然如果没有找到对应的Controller处理器那么程序会尝试去查找配置中的默认处理器,当然,当查找的controller为String类型时,那就意味着返回的是配置的bean名称,需要根据bean名称查找对应的bean,最后,还要通过getHandlerExecutionChain方法对返回的Handler进行封装,以保证满足返回类型的匹配。下面详细分析这个过程。

根据request查找对应的Handler

首先从根据request查找对应的Handler开始分析。

根据URL获取对应Handler的匹配规则代码实现起来虽然很长,但是并不难理解,考虑了直接匹配与通配符两种情况。其中要提及的是buildPathExposingHandler函数,它将Handler封装成了HandlerExecutionChain类型。

在函数中我们看到了通过将Handler以参数形式传入,并构建HandlerExecutionChain类型实例,加入了两个拦截器。此时我们似乎已经了解了Spring这样大番周折的目的。链处理机制,是Spring中非常常用的处理方式,是AOP中的重要组成部分,可以方便地对目标对象进行扩展及拦截,这是非常优秀的设计。

加入拦截器到执行链

getHandlerExecutionChain函数最主要的目的是将配置中的对应拦截器加入到执行链中,以保证这些拦截器可以有效地作用于目标对象。

没找到对应的Handler的错误处理

每个请求都应该对应着一Handler,因为每个请求都会在后台有相应的逻辑对应,而逻辑的实现就是在Handler中,所以一旦遇到没有找到Handler的情况(正常情况下如果没有Url匹配的Handler,开发人员可以设置默认的Handler来处理请求,但是如果默认请求也未设置就会出现Handler为空的清况),就只能通过response向用户返回错误信息。

根据当前Handler寻找对应的HandlerAdapter

在WebApplicationContext的初始化过程中我们讨论了HandlerAdapters的初始化,了解了在默认清况下普通的Web请求会交给SimpleControllerHandlerAdapter去处理。下面我们以 SimpleControllerHandlerAdapter为例来分析获取适配器的逻辑。

分析到这里,一切已经明了,SimpleController HandlerAdapter就是用于处理普通的Web请求的,而且对于SpringMVC来说,我们会把逻辑封装至Controller的子类中,例如我们之前的引导示例UserController就是继承自AbstractController,而AbstractController实现Controller接口。

缓存处理

在研究Spring对缓存处理的功能支持前,我们先了解一个概念:Last-Modified缓存机制。

1 在客户端第一次输入URL时,服务器端会返回内容和状态码200,表示请求成功,同时会添加一个"Last-Modified"的响应头,表示此文件在服务器上的最后更新时间,例如,"Last-Modified:Wed,14Mar2012 10:22:42 GMT"表示最后更新时间为(2012-03-14 10:22)。

2 客户端第二次请求此URL时,客户端会向服务器发送请求头"If-Modified-Since",询间服务器该时间之后当前请求内容是否有被修改过,如"If-Modified-Since:Wed, 14 Mar 2012 10:22:42 GMT", 如果服务器端的内容没有变化,则自动返回HTTP304状态码(只要响应头,内容为空,这样就节省了网络带宽)。

Spring提供的对Last-Modified机制的支持,只需要实现LastModified接口,如下所示:

HelloWorldLastModifiedCacheController只需要实现LastModified接口的getLastModified方法,保证当内容发生改变时返回最新的修改时间即可。

Spring判断是否过期,通过判断请求的"If-Modified-Since"是否大于等于当前的getLastModified方法的时间戳,如果是,则认为没有修改。上面的controller与普通的controller并无太大差别,声明如下:

<bean name="/helloLastModified" class="com.test.controller.HelloWorldLastModifiedCacheController"/>

Handlerlnterceptor的处理

Servlet API定义的servlet过滤器可以在servlet处理每个Web请求的前后分别对它进行前置处理和后置处理。此外,有些时候,你可能只想处理由某些SpringMVC处理程序处理的Web请求,并在这些处理程序返回的模型属性被传递到视图之前,对它们进行一些操作。

SpringMVC允许你通过处理拦截Web请求,进行前置处理和后置处理。处理拦截是在Spring的Web应用程序上下文中配置的,因此它们可以利用各种容器特性,并引用容器中声明的任何bean。处理拦截是针对特殊的处理程序映射进行注册的,因此它只拦截通过这些处理程序映射的请求。每个处理拦截都必须实现HandlerInterceptor接口,它包含三个需要你实现的回调方法:preHandle()、postHandle()和afterCompletion()。第一个和第二个方法分别是在处理程序处理请求之前和之后被调用的。第二个方法还允许访间返回的ModelAndView对象,因此可以在它里面操作模型属性。最后一个方法是在所有请求处理完成之后被调用的(如视图呈现之后),以下是HandlerInterceptor的简单实现:

在这个拦截器的preHandler()方法中,你记录了起始时间,并将它保存到请求属性中。这个方法应该返回true,允许DispatcherServlet继续处理请求。否则,DispatcherServlet会认为这个方法已经处理了请求,直接将响应返回给用户。然后,在postHandler()方法中,从请求属性中加载起始时间,并将它与当前时间进行比较。你可以计算总的持续时间,然后把这个时间添加到模型中,传递给视图。最后,afterCompletion()方法无事可做,空着就可以了。

逻辑处理

对于逻辑处理其实是通过适配器中转调用Handler并返回视图的,对应代码如下:

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

同样,还是以引导示例为基础进行处理逻辑分析,之前分析过,对于普通的Web请求,Spring默认使用SimpleControllerHandlerAdapter类进行处理,我们进入SimpleControllerHandlerAdapter类的handle方法如下:

public ModelAndView handle(HttpServletRequest request,HttpServletResponse response,Object handler)

throws Exception {

return ((Controller)handler).handleRequest(request,response);}

但是回顾引导示例中的UserController,我们的逻辑是写在handleRequestinternal函数中而不是handleRequest函数,所以我们还需要进一步分析这期间所包含的处理流程。

异常视图的处理

有时候系统运行过程中出现异常,而我们并不希望就此中断对用户的服务,而是至少告知客户当前系统在处理逻辑的过程中出现了异常,甚至告知他们因为什么原因导致的。Spring中的异常处理机制会帮我们完成这个工作。其实,这里Spring主要的工作就是将逻辑引导至 HandlerExceptionResolver类的resolveException方法,而HandlerExceptionResolver的使用,我们在讲解WebApplicationContext的初始化的时候已经介绍过了。

根据视图跳转页面

无论是一个系统还是一个站点,最重要的工作都是与用户进行交互,用户操作系统后无论下发的命令成功与否都需要给用户一个反馈,以便于用户进行下一步的判断。所以,在逻辑处理的最后一定会涉及一个页面跳转的间题。

解析视图名称

在上文中我们提到DispatcherServlet会根据ModelAndView选择合适的视图来进行渲染,而这一功能就是在resolveViewName函数中完成的。

我们以org.Springframework.web.servlet.view.IntemalResourceViewResolver为例来分析ViewResolver逻辑的解析过程,其中resolveViewName函数的实现是在其父类AbstractCachingViewResolver中完成的。

通读以上代码,我们发现对于InternalResourceViewResolver所提供的解析功能主要考虑到 了几个方面的处理。

基于效率的考虑,提供了缓存的支持。

提供了对redirect:xx和forward:xx前缀的支持。

添加了前缀及后缀,并向View中加入了必需的属性设置。

页面跳转

当通过viewName解析到对应的View后,就可以进一步地处理跳转逻辑了。

在引导示例中,我们了解到对于ModelView的使用,可以将一些属性直接放入其中,然后在页面上直接通过JSTL语法或者原始的request获取。这是一个很方便也很神奇的功能,但是实现却并不复杂,无非是把我们将要用到的属性放入request中,以便在其他地方可以直接调用,而解析这些属性的工作就是在createMergedOutputModel函数中完成的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值