概述
大部分Java应用都是Web应用,展现层是Web应用不可忽略的重要环节。Spring为展现层提供了一个优秀的Web框架——Spring MVC。和众多其它Web框架一样,它基于MVC设计理念,此外,由于它采用了松散耦合可插拔组件结构,具有比其它MVC框架更多的扩展性和灵活性。
Spring MVC框架围绕DispatcherServlet这个核心展开,DispatcherServlet的作用是截获请求并组织一系列组件共同完成请求的处理工作。
体系结构
Spring MVC是基于Model 2实现的技术框架,Model 2是经典的MVC(Model,View,Control)模型的Web应用变体,这个改变主要源于HTTP协议的无状态性。Model 2的目的和MVC一样,也是利用处理器分离模型、视图和控制,达到不同技术层级间松散耦合的效果,提高系统灵活性、复用性和可维护性。在多数情况下,你可以将Model 2与MVC等同起来。
在利用Model 2之前,我们把所有的展现逻辑和业务逻辑集中在一起,有时也称这种应用模式为Model 1,Model 1的主要缺点就是紧耦合,复用性差,维护成本高。
由于Spring MVC就是基于Model 2实现的框架,所以它底层的机制也是MVC.
从接受请求到返回响应,Spring MVC框架的众多组件都伸胳膊挽袖子行动起来,各司其职,有条不紊地完成份内的工作。在整个框架中,DispatcherServlet处于核心的位置,它负责协调和组织不同组件,共同完成请求响应的工作。和大多数Web MVC框架一样,Spring MVC通过一个前端Servlet处理器接收所有的请求,并将具体工作委托给其它组件进行具体的处理,DispatcherServlet就是Spring MVC的前端Servlet处理器。下面我们对Spring MVC处理请求的整体过程做一下高空俯瞰:
① 整个过程开始于客户端发送一个HTTP请求;
② DispatcherServlet接收这个请求后,并将请求的处理工作委托给具体的处理器(Handler),后者负责处理请求执行相应的业务逻辑。在这之前,DispatcherServlet必须能够凭借请求信息(URL或请求参数等)按照某种机制找到请求对应的处理器,DispatcherServlet是通过垂询HandlerMapping完成这一工作的;
③ 当DispatcherServlet从HandlerMapping中得到当前请求对应的处理器后,它就将请求分派给这个处理器。处理器根据请求的信息执行相应的业务逻辑,一个设计良好的处理器应该通过调用Service层的业务对象完成业务处理,而非自己越俎代庖。
Spring提供了丰富的处理器类型,在真正处理业务逻辑前,有些处理器会事先执行两项预处理工作:
1)将HttpServletRequest请求参数绑定到一个POJO对象中;
2)对绑定了请求参数的POJO对象进行数据合法性校验;
④ 处理器完成业务逻辑的处理后将返回一个ModelAndView给DispatcherServlet,ModelAndView包含了视图逻辑名和渲染视图时需要用到的模型数据对象;
⑤ 由于ModelAndView中包含的是视图逻辑名,DispatcherServlet必须知道这个逻辑名对应的真实视图对象,这项视图解析的工作通过调用ViewResolver来完成;
⑥ 当得到真实的视图对象后,DispatcherServlet将请求分派给这个View对象,由其完成Model数据的渲染工作;
⑦ 最终客户端得到返回的响应,这可能是一个普通的HTML页面,也可能是一个Excel电子表格、甚至是一个PDF文档等不一而足的视图形式,Spring的视图类型是异常丰富和灵活的。
以上每一个步骤都包含丰富的知识点,本文将通过一个实例涵盖所有的组件,你可以从中认识到每个组件的庐山真面目,不过现在我们首先要做的第一件事是在web.xml中配置好DispatcherServlet,让这颗“启辉器”真正工作起来。
认识并配置DispatcherServlet
DispatcherServlet是Spring MVC的心脏,它负责接收HTTP请求组织并协调Spring MVC的各种组件共同完成请求的处理工作。和任何Servlet一样,你必须在web.xml中配置好DispatcherServlet。
DispatcherServlet的工作主要包括以下三项:
1) 截获满足特定模式URL请求,交由Spring MVC框架处理;
2) 初始化DispatcherServlet上下文对应的WebApplicationContext,并将其和业务层、持久层的WebApplicationContext建立关联,以便展现层的Bean可以调用业务层的Bean;
3) 初始化Spring MVC各个组件,并将它们装配到DispatcherServlet实例中。
下面,我们将逐一分析DispatcherServlet是如何完成以上各项任务的。
使用DispatcherServlet截获需要Spring MVC处理的URL
大家知道任何Servlet都可以在web.xml中通过<servlet-mapping>的配置截获特定模式的URL请求。假设我们希望DispatcherServlet截获所有以.html结束的URL请求,并进而交由Spring MVC框架进行后续处理,那么我们可以在web.xml中按以下方式配置DispatcherServlet:
代码清单 1 配置DispatcherServlet
<context-param>①业务层和持久层的Spring配置文件,这些配置文件被父Spring容器所使用
<param-name>contextConfigLocation</param-name>
<param-value>classpath:baobaotao-service.xml,classpath:baobaotao-dao.xml</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<servlet> ②声明DispatcherServlet
<servlet-name>baobaotao</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>2</load-on-startup>③Servlet自动启动的顺序号
</servlet>
<servlet-mapping> ④名为DispatcherServlet匹配的URL模式
<servlet-name>baobaotao</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
越来越多的Web应用倾向于采用“.html”后缀作为框架URL映射的模式,通过这种方法可以对使用者屏蔽服务端所使用的具体实现技术(如果用.do,客户端用户马上就能猜测到服务端使用Struts框架);另外这种URL格式容易让搜索引擎“误认为”网站各个链接都是一个静态网页,这将增加动态网站信息被收录的机率。当然,从纯技术上来说,你可以使用任何后缀模式,如*.spring、*.shtml等等。
初始化Web模块的WebApplicationContext
现在我们解决了第一个问题,但DispatcherServlet究竟如何初始化其上下文所对应的WebApplicationContext并完成和业务层、持久层WebApplicationContext的整合呢?当Web容器启动时,Spring通过ContextLoaderListener 监听器初始化业务层和持久层所对应的Spring容器(WebApplicationContext)。接着作为自动启动的DispatcherServlet开始初始化,它利用相应的Spring配置文件初始化DispatcherServlet上下文所对应的子Spring容器(WebApplicationContext),并将业务层、持久层的Spring容器作为其父容器,这种父子容器的结构带来了两个明显的好处:
1)允许展现层和业务层、持久层更好地解耦,因为展现层的Bean定义在子容器中,而业务层和持久层的Bean定义在父容器中,子容器可以访问父容器的Bean,而父容器访问不到子容器中的Bean;
2)允许分步骤初始化不同层次的Spring容器。通过ContextLoaderListener配合使用contextConfigLocation上下文参数初始化业务层、持久层的Spring容器,然后再通过DispatcherServlet初始化展现层的子Spring容器。
父Spring容器的Spring配置文件在①中定义,DispatcherServlet上下文所用到的Bean也需要一个Spring配置文件进行定义,但在②处的配置中,我们并没有看到预想中配置文件的身影。探其原因,原来是DispatcherServlet将按照默认契约机制进行工作,它自动查找WEB-INF/<servlet-name>-servlet.xml(即WEB-INF/baobaotao-servlet.xml)的配置文件,使用该配置文件初始化DispatcherServlet上下文对应的子Spring容器。
实际上,你可以配置多个DispatcherServlet分别处理不同URL模式的请求,每个DispatcherServlet上下文都对应一个自己的子Spring容器,它们拥有相同的父Spring容器(业务层、持久层Bean所在的容器).
DispatcherServlet规定了很多无需配置的默认契约,但都可以通过显式配置进行调整。DispatcherServlet拥有多个可配置的属性,但DispatcherServlet位于web.xml中而非在Spring配置文件中,我们如何配置DispatcherServlet的属性呢?
研究DispatcherServlet类的继承体系,我们可以发现它继承于FrameworkServlet,而FrameworkServlet又继承于HttpServletBean。HttpServletBean这个类的名字是否让你得到了某些启示呢?是的,HttpServletBean通过扩展HttpServlet让其具备了类似于Bean的特征 ——你可以在web.xml中通过<init-param>采用类似于在Spring配置文件中的方式配置HttpServletBean的属性,请看下面的配置片断:
代码清单 2 通过Servlet初始化参数配置DispatcherServlet属性
…
<servlet>
<servlet-name>baobaotao</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param> ①通过Servlet的初始化参数配置DispatcherServlet的属性
<param-name>namespace</param-name> ①-1 属性名
<param-value>bbt</param-value> ①-2 属性值
</init-param>
<load-on-startup>2</load-on-startup>
</servlet>
…
在①处,我们通过定义了一个namespace的初始化参数,DispatcherServlet将调用自身的setNamespace()方法将“bbt”设置为nameSpace属性的值。可按照相似方式配置的属性一并介绍如下:
lnamespace:DispatcherServlet对应的命名空间,用以构造Spring配置文件的路径,如果指定该属性后,配置文件对应的路径为:WEB-INF/<namespace>.xml而非WEB-INF/< servlet-name>.xml。按照上面配置对应的配置文件为WEB-INF/bbt.xml;
lcontextConfigLocation:如果DispatcherServlet上下文对应的Spring配置文件有多个,则可以使用该属性按照Spring资源路径的方式进行指定。如设置为“classpath:bbt1.xml,classpath:bbt2.xml”时,DispatcherServlet将使用类路径下的bbt1.xml和bbt2.xml这两个配置文件初始化WebApplicationContext;
publishContext:boolean类型属性,默认值为ture。DispatcherServlet据此属性决定是否将对应的WebApplicationContext发布到ServletContext的属性中,以便任何其它的Bean可以通过ServletContext找到DispatcherServlet上下文对应的WebApplicationContext,对应的属性名为DispatcherServlet#getServletContextAttributeName()方法返回的值。
publishEvents:boolean类型属性。当DispatcherServlet处理完一个请求后,是否需要向容器发布一个ServletRequestHandledEvent事件,默认为ture。如果容器中没有任何事件监听器,可以将此属性设置为false,以便赚取一些程序运行性能。