springMvc理解

一.网站:

原文博主主页:http://downpour.iteye.com/

1.SpringMVC深度探险(一) —— SpringMVC前传http://downpour.iteye.com/blog/1330537

2.SpringMVC深度探险(二) —— SpringMVC概览http://downpour.iteye.com/blog/1330596

3.SpringMVC深度探险(三) —— DispatcherServlet与初始化主线http://downpour.iteye.com/blog/1341459

4.SpringMVC深度探险(四) —— SpringMVC核心配置文件详解http://downpour.iteye.com/blog/1389285

二.备份

1.SpringMVC深度探险(一) —— SpringMVC前传

在我们熟知的建立在三层结构(表示层、业务逻辑层、持久层)基础之上的J2EE应用程序开发之中,表示层的解决方案最多。因为在表示层自身的知识触角很多,需要解决的问题也不少,这也就难免造成与之对应的解决方案层出不穷。 

笔者在很多讨论中经常可以看到类似“某某框架已死”,或者“某某框架已经足以打败所有其他的框架”的言论。事实上,每一种解决方案都有着自身独有的存在价值和历史背景。如果单单从某一个方面或者某几个方面去看一个框架,那么评论难免有失偏颇。 

所以,整个系列的第一篇文章,我们脱开SpringMVC框架本身,把SpringMVC放到一个更大的知识体系范围之中,讲一讲整个Web开发领域、尤其是MVC框架的发展历程。正如“认识历史才能看清未来”,当我们能够正确审视整个MVC框架的发展历程,也就能够分析它的发展趋势,并且站在一个更高的高度来对所有的解决方案进行评价。 

两种模型 

从整个B/S程序的运行结构来看,J2EE的表示层解决方案实际上是对“请求-响应”模式的一种实现。既然谓之“请求-响应”也就势必存在着两大沟通角色: 

c9fad437-f776-35a5-a7ac-238a45b71b43.png 

由于这两大角色的承载载体和编程语言实现基础都不同,因而也就产生了两种截然不同的针对表示层的解决方案的设计思路: 
 

  • 以服务器端应用程序为主导来进行框架设计
  • 以浏览器页面组件(及其自身的事件触发模型)为主导来进行框架设计

业界对于上述这两种不同的设计模型也赋予了不同的名词定义:前一种被称之为MVC模型;后一种则被称之为组件模型,也有称之为事件模型。 

注:笔者个人对于这两种模型的概念定义并不是非常认同。因为在笔者个人的观点认为,MVC模型的定义角度所针对的是编程元素的划分;而组件模型(事件模型)的定义角度是动态交互方式的表述。所以我们在这里强调的是解决方案自身所设立的基准和侧重点的不同。 

从使用者的社区力量上来看,无疑MVC模型获得了更多程序员的青睐。这里面的原因很多,我们在这里也不想过多展开对两种不同编程模型之间的讨论。不过在这里,我们将针对同一个业务场景(用户注册)分别给出基于这两个编程模型的代码示例,帮助读者了解这两种编程模型在设计思想上的不同之处。 

【MVC模型】 

在MVC模型中,我们选取当前比较热门的两大框架Struts2和SpringMVC作为代码示例。 

首先,我们将用户注册场景中最为核心的“用户类”定义出来: 
 

Java代码 

 收藏代码

  1. public class User {  
  2.       
  3.     private String email;  
  4.   
  5.     private String password;  
  6.   
  7.     // 省略了setter和getter方法  
  8. }  



紧接着是一个简单的JSP表单: 
 

Html代码 

 收藏代码

  1. <form method="post" action="/register">  
  2. <label>Email:</label><input type="text" name="email" />  
  3. <label>Password:</label><input type="password" name="password" />  
  4. <input type="submit" value="submit" />  
  5. </form>  



上述这两段代码无论是SpringMVC还是Struts2,都可以共用。而在请求响应处理类(也就是Controller)上的设计差异是两个框架最大的不同。 

如果使用SpringMVC,那么Controller的代码看上去就像这样: 
 

Java代码 

 收藏代码

  1. @Controller  
  2. @RequestMapping  
  3. public class UserController {  
  4.       
  5.     @RequestMapping("/register")  
  6.     public ModelAndView register(String email, String password) {  
  7.         // 在这里调用具体的业务逻辑代码  
  8.         return new ModelAndView("register-success");  
  9.     }  
  10.   
  11. }  



如果使用Struts2,那么Controller的代码看上去就稍有不同: 
 

Java代码 

 收藏代码

  1. public class UserController {  
  2.       
  3.     private String email;  
  4.   
  5.     private String password;  
  6.       
  7.     public String register() {  
  8.         // 在这里调用具体的业务逻辑代码  
  9.         return "register-success";  
  10.     }  
  11.        
  12.     // 这里省略了setter和getter方法  
  13.   
  14. }  



除此之外,Struts2还需要在某个配置文件中进行请求映射的配置: 
 

Xml代码 

 收藏代码

  1. <action name="register" class="com.demo2do.sandbox.web.UserController" method="register">  
  2.     <result name="success">/register-success.jsp</result>  
  3. </action>  



从上面的代码示例中,我们可以为整个MVC模型的实现总结归纳出一些特点: 

1. 框架本身并不通过某种手段来干预或者控制浏览器发送Http请求的行为方式。 

从上面的代码中我们就可以看到,无论是SpringMVC还是Struts2,它们在请求页面的实现中都使用了原生HTML代码。就算是Http请求的发送,也借助于HTML之中对Form提交请求的支持。 

2. 页面(View层)和请求处理类(Controller)之间的映射关系通过某一种配置形式维系起来。 

我们可以看到在浏览器和Web服务器之间的映射关系在不同的框架中被赋予了不同的表现形式:在SpringMVC中,使用了Annotation注解;在Struts2中,默认采取XML配置文件。不过无论是哪一种配置形式,隐藏在其背后的都是对于请求映射关系的定义。 

3. Controller层的设计差异是不同MVC框架之间最主要的差异。 

这一点实际上是我们在对于MVC模型自身进行定义时就反复强调的一点。在上面的例子中,我们可以看到SpringMVC使用方法参数来对请求的数据进行映射;而Struts2使用Controller类内部的属性来进行数据请求的映射。 

在MVC模型中,浏览器端和服务器端的交互关系非常明确:无论采取什么样的框架,总是以一个明确的URL作为中心,辅之以参数请求。因此,URL看上去就像是一个明文的契约,当然,真正蕴藏在背后的是Http协议。所有的这些东西都被放在了台面上,我们可以非常明确地获取到一次交互中所有的Http信息。这也是MVC模型中最为突出的一个特点。 

【组件模型】 

在组件模型中,我们则选取较为成熟的Tapestry5作为我们的代码示例。 

首先,我们来看看请求页面的情况: 
 

Html代码 

 收藏代码

  1. <form t:type="form" t:id="form">  
  2. <t:label for="email"/>:<input t:type="TextField" t:id="email" t:validate="required,minlength=3" size="30"/>  
  3. <t:label for="password"/>:<input t:type="PasswordField" t:id="password" t:validate="required,minlength=3" size="30"/>  
  4. <input type="submit" value="Login"/>  
  5. </form>  



在这里,请求的页面不再是原生的HTML代码,而是一个扩展后的HTML,这一扩展包含了对HTML标签的扩展(增加了新的标签,例如<t:label>),也包含了对HTML自身标签中属性的扩展(增加新的支持属性,例如t:type,t:validate)。 

接着我们来看看服务器端响应程序: 
 

Java代码 

 收藏代码

  1. public class Register {  
  2.   
  3.     private String email;  
  4.   
  5.     private String password;  
  6.   
  7.     @Component(id = "password")  
  8.     private PasswordField passwordField;  
  9.   
  10.     @Component  
  11.     private Form form;  
  12.   
  13.     String onSuccess() {  
  14.   
  15.         return "PostRegister";  
  16.     }  
  17.   
  18.     // 这里省略了setter和getter方法  



从上面的代码示例中,我们可以看到一些与MVC模型截然不同的特点: 

1. 框架通过对HTML进行行为扩展来干预和控制浏览器与服务器的交互过程。 

我们可以发现,Tapestry5的请求页面被加入了更多的HTML扩展,这些扩展包括对HTML标签的扩展以及HTML标签中属性的扩展。而这些扩展中,有不少直接干预了浏览器与服务器的交互。例如,上面例子中的t:validate="required,minlength=3"扩展实际上就会被自动映射到服务器端程序中带有@Component(id="password")标注的PasswordField组件上,并在提交时自动进行组件化校验。而当页面上的提交按钮被点击触发时,默认在服务器端的onSuccess方法会形成响应并调用其内部逻辑。 

2. 页面组件的实现是整个组件模型的绝对核心 

从上述的例子中,我们可以看到组件模型的实现不仅需要服务器端实现,还需要在页面上指定与某个特定组件进行事件绑定。两者缺一不可,必须相互配合,共同完成。因此整个Web程序的交互能力完全取决于页面组件的实现好坏。 

3. 页面组件与服务器端响应程序之间的映射契约并不基于Http协议进行 

在上面的例子中,从页面组件到服务器端的响应程序之间的映射关系是通过名称契约而定的。而页面上的每个组件可以指定映射到服务器端程序的具体某一个方法。我们可以看到这种映射方式并不是一种基于URL或者Http协议的映射方式,而是一种命名指定的方式。 

在组件模型中,浏览器端和服务器端的交互关系并不以一个具体的URL为核心,我们在上述的例子中甚至完全没有看到任何URL的影子。不过这种事件响应式的方式,也提供给我们另外一个编程的思路,而这种基于契约式的请求-响应映射也得到了一部分程序员的喜爱。因而组件模型的粉丝数量也是很多的。 

MVC模型的各种形态 

之前我们已经谈到,MVC模型是一种以服务器响应程序(也就是Controller)为核心进行程序设计的,因而所有的MVC框架的历史发展进程实际上是一个围绕着Controller不断进行重构和改造的过程。而在这个过程中,不同的MVC框架也就表现出了不同的表现形态。接下来,我们就给出一些具有代表意义的MVC框架表现形态。 

注:笔者在这里将提到三种不同的MVC框架的表现形态,实际上与请求-响应的实现模式有着密切的联系,有关这一方面的内容,请参阅另外一篇博文的内容:《Struts2技术内幕》 新书部分篇章连载(五)—— 请求响应哲学 

【Servlet】 

Servlet规范是最基本的J2EE规范,也是我们进行Web开发的核心依赖。它虽然自身并不构成开发框架,但是我们不得不承认所有的MVC框架都是从最基本的Servlet规范发展而来。因此,我们可以得出一个基本结论: 
 

downpour 写道

Servlet是MVC模型最为基本的表现形态。



在Servlet规范中所定义的请求处理响应接口是这样的: 

dbb787ef-c5da-3d2e-b7d1-4deef5763600.png 

我们可以看到,Servlet的基本接口定义中: 

参数列表 —— Http请求被封装为一个HttpServletRequest对象(或者ServletRequest对象),而Http响应封装为一个HttpServletResponse对象(或者ServletResponse对象) 
返回值 —— 方法不存在返回值(返回值为void)
 

在这个设计中,HttpServletRequest和HttpServletResponse承担了完整的处理Http请求的任务。而这两个Servlet对象的职责也有所分工: 

HttpServletRequest对象 —— 主要用于处理整个Http生命周期中的数据。 
HttpServletResponse对象 —— 主要用于处理Http的响应结果。
 

这里实际上有一点“数据与行为分离”的意味。也就是说,在Servlet处理请求的过程中,其实也是Servlet中响应方法内部的逻辑执行过程中,如果需要处理请求数据或者返回数据,那么我们需要和HttpServletRequest打交道;如果需要处理执行完毕之后的响应结果,那么我们需要和HttpServletResponse打交道。 

这样的设计方式,是一种骨架式的设计方式。因为Servlet是我们进行Web开发中最底层的标准,所以我们可以看到接口设计中的返回值对于一个最底层标准而言毫无意义。因为不存在一个更底层的处理程序会对返回值进行进一步的处理,我们不得不在Servlet的过程中自行处理浏览器的行为控制。 

MVC模型的这一种形态,被笔者冠以一个名称:参数-参数(Param-Param)实现模式。因为在响应方法中,数据与行为的操作载体都以参数的形式出现。 

Servlet的设计模型是所有MVC模型表现形态中最为基础也是最为底层的一种模型,所有其他模型都是建立在这一模型的基础之上扩展而来。 

【Struts1.X】 

Struts1.X是一个较为早期的MVC框架实现,它的历史最早可以追溯到2000年,作为Apache开源组织的一个重要项目,取名为“Struts”,有“基础构建”的含义。在那个程序框架尚处于朦胧阶段的年代,“基础构建”无疑是每个程序员梦寐以求的东西。 

对于Struts1.X,我们还是把关注的重点放在Struts中的Controller层的定义上: 
 

Java代码 

 收藏代码

  1. public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response);  



如果和之前的Servlet模型加以比较我们就可以发现,Struts1.X对于基本的Servlet模型做了一定的扩展和重构: 
 

  • 保留了HttpServletRequest和HttpServletResponse这两大接口作为参数
  • 将返回值改为ActionForward,并由Struts1.X框架通过处理ActionForward完成对响应结果的处理
  • 增加了ActionMapping和ActionForm两大参数,前者表示Http请求的一个简要概括,后者表示一个数据模型,用以承载整个请求生命周期中的数据

经过一番扩展和重构,我们可以发现Struts1.X相比较于原始的Servlet模型已经有了一定的进步。比如,我们可以不再直接操作HttpServletResponse这样的原生Servlet对象来进行Http返回的处理;再比如,对于一些简单的请求数据读取,我们可以不必直接操作生硬的HttpServletRequest接口,而通过ActionForm来完成。 

MVC模型发展到了这里,我们可以看到响应方法中的“返回值”已经能够被调动起来用在整个Http请求的处理过程中。因此,这种在响应方法中参数和返回值同时参与到Http请求的处理过程中的表现形态,被笔者冠以另外一个名称:参数-返回值(Param-Return)实现模式。 

由于Struts1.X已经不再是一个底层的实现规范,于是响应方法“返回值”被框架引入,加入到了整个处理过程之中。我们可以看到,在这里最大的进步之处就在于:引入了新的编程元素,从而优化整个逻辑处理过程。编程元素的引入非常重要,因为对于一个任何一个程序员而言,充分调用所有可以利用的编程要素是衡量一个程序写得好坏的重要标准。之后,我们还可以看到其他的框架在引入编程元素这个方面所做的努力。 

【Webwork2 / Struts2】 

随着时间的推进,越来越多的程序员在使用Struts1.X进行开发的过程中发现Struts1.X在设计上存在的一些不足。而与此同时,各种各样的Web层的解决方案也如雨后春笋般涌现出来。不仅仅是以MVC模型为基础的开发框架,还有包括JSF和Tapestry之类的基于组件模型的开发框架也在这个时期诞生并不断发展壮大。因此,这个时期应该是整个Web层解决方案的大力发展时期。 

而在这些框架中,有一个来自于Opensymphony开源社区的优秀框架Webwork2探索了一条与传统Servlet模型不同的解决方案,逐渐被大家熟识和理解,不断发展并得到了广大程序员的认可。2004年,Webwork2.1.7版本发布,成为Webwork2的一个重要里程碑,它以优秀的设计思想和灵活的实现,吸引了大批的Web层开发人员投入它的怀抱。 

或许是看到了Struts1.X发展上的局限性,Apache社区与Opensymphony开源组织在2005年底宣布未来的Struts项目将与Webwork2项目合并,并联合推出Struts2,通过Apache社区的人气优势与OpenSymphony的技术优势,共同打造下一代的Web层开发框架。这也就是Struts2的由来。 

从整个过程中,我们可以发现,Webwork2和Struts2是一脉相承的Web层解决方案。而两者能够在一个相当长的时间段内占据开发市场主导地位的重要原因在于其技术上的领先优势。而这一技术上的领先优势,突出表现为对Controller的彻底改造: 
 

Java代码 

 收藏代码

  1. public class UserController {  
  2.   
  3.     private User user  
  4.   
  5.     public String execute() {  
  6.         // 这里加入业务逻辑代码  
  7.         return "success";  
  8.     }  
  9.   
  10.     // 这里省略了setter和getter方法  
  11. }  



从上面的代码中,我们可以看到Webwork2 / Struts2对于Controller最大的改造有两点: 
 

  • 在Controller中彻底杜绝引入HttpServletRequest或者HttpServletResponse这样的原生Servlet对象。
  • 将请求参数和响应数据都从响应方法中剥离到了Controller中的属性变量。

这两大改造被看作是框架的神来之笔。因为通过这一改造,整个Controller类彻底与Web容器解耦,可以方便地进行单元测试。而摆脱了Servlet束缚的Controller,也为整个编程模型赋予了全新的定义。 

当然,这种改造的前提条件在于Webwork2 / Struts2引入了另外一个重要的编程概念:ThreadLocal模式。使得Controller成为一个线程安全的对象被Servlet模型所调用,这也就突破了传统Servlet体系下,Servlet对象并非一个线程安全的对象的限制条件。 

注:有关ThreadLocal模式相关的话题,请参考另外一篇博文:《Struts2技术内幕》 新书部分篇章连载(七)—— ThreadLocal模式 

从引入新的编程元素的角度来说,Webwork2 / Struts2无疑也是成功的。因为在传统Servlet模式中的禁地Controller中的属性变量被合理利用了起来作为请求处理过程中的数据部分。这样的改造不仅使得表达式引擎能够得到最大限度的发挥,同时使得整个Controller看起来更像是一个POJO。因而,这种表现形态被笔者冠以的名称是:POJO实现模式。 

POJO实现模式是一种具有革命性意义的模式,因为它能够把解耦合这样一个观点发挥到极致。从面向对象的角度来看,POJO模式无疑也是所有程序员所追求的一个目标。这也就是Webwork2 / Struts2那么多年来经久不衰的一个重要原因。 

【SpringMVC】 

相比较Webwork2 / Struts2,SpringMVC走了一条比较温和的改良路线。因为SpringMVC自始至终都没有突破传统Servlet编程模型的限制,而是在这过程中不断改良,不断重构,反而在发展中开拓了一条崭新的道路。 

我们可以看看目前最新版本的SpringMVC中对于Controller的定义: 
 

Java代码 

 收藏代码

  1. @Controller  
  2. @RequestMapping  
  3. public class UserController {  
  4.       
  5.     @RequestMapping("/register")  
  6.     public ModelAndView register(String email, String password) {  
  7.         // 在这里调用具体的业务逻辑代码  
  8.         return new ModelAndView("register-success");  
  9.     }  
  10.   
  11. }  



我们在这里引用了在之前的讲解中曾经使用过的代码片段。不过这一代码片段刚刚好可以说明SpringMVC在整个Controller改造中所涉及到的一些要点: 

1. 使用参数-返回值(Param-Return)实现模式来打造Controller 

方法的参数(email和password)被视作是Http请求参数的概括。而在这里,它们已经被SpringMVC的框架有效处理并屏蔽了内在的处理细节,呈现出来的是与请求参数名称一一对应的参数列表。而返回值ModelAndView则表示Http的响应是一个数据与视图的结合体,表示Http的处理结果。 

2. 引入Annotation来完成请求-响应的映射关系 

引入Annotation来完成请求-响应的映射关系,是SpringMVC的一个重大改造。在早期的SpringMVC以及其他的MVC框架中,通常都是使用XML作为基础配置的。而Annotation的引入将原本分散的关注点合并到了一起,为实现配置简化打下了坚实的基础。 

3. 泛化参数和返回值的含义 

这是一个蕴含的特点。事实上,SpringMVC在响应方法上,可以支持多种多样不同的参数类型和返回值类型。例如,当参数类型为Model时,SpringMVC将会自动将请求参数封装于Model内部而传入请求方法;当返回值类型是String时,直接表示SpringMVC需要返回的视图类型和视图内容。当然,这些泛化的参数和返回值的内容全部都由SpringMVC在框架内部处理了。 

如果我们来评述一下这些特点就会发现,SpringMVC虽然是一个温和的改良派,却是在改良这个领域做得最为出色的。以引入Annotation为例,引入Annotation来完成请求-响应映射,不正是我们反复强调的引入并合理使用新的编程元素来完成处理任务嘛?而泛化后的参数和返回值,则可以让程序员在写Controller的代码时可以随心所欲,不再受到任何契约的束缚,这样一来接口的逻辑语义也就能够更加清晰。 

MVC模型的发展轨迹 

之前讲了那么多MVC模型的实现形态,我们是否能从中总结出一条发展轨迹呢?答案是肯定的,笔者在这里作了一副图: 

93e16090-54df-3ef2-a318-3b710a59db40.png

从图中,我们可以看到三类完全不同的发展方向。目前,Struts1.X这一条路被证明已经穷途末路;另外的两条发展轨迹总体来说实力相当,SpringMVC大有赶超之势。 

那么,为什么曾经一度占领了大部分市场的Struts2会在近一段时间内被SpringMVC大幅赶超呢?这里面的原因多种多样,有自身架构上的原因,有设计理念上的原因,但是笔者认为,其本质原因还是在于Struts2对于技术革新的力度远不及SpringMVC。 

如果我们回顾一下Struts2过去能够独占鳌头的原因就可以发现,Struts2的领先在于编程模型上的领先。其引入的POJO模型几乎是一个杀手级的武器。而基于这一模型上的拦截器、OGNL等技术的支持使得其他的编程模型在短时间很难超越它。 

但是随着时代的发展,Struts2在技术革新上的作为似乎步子就迈得比较小。我们可以看到,在JDK1.5普及之后,Annotation作为一种新兴的Java语法,逐渐被大家熟知和应用。这一点上SpringMVC紧跟了时代的潮流,直接用于请求-响应的映射。而Struts2却迟迟无法在单一配置源的问题上形成突破。当然,这只是技术革新上的一个简单的例子,其他的例子还有很多。 

有关Struts2和SpringMVC的比较话题,我们在之后的讨论中还会有所涉及,不过笔者并不希望在这里引起框架之间的争斗。大家应该客观看待每个框架自身设计上的优秀之处和不足之处,从而形成个人自己的观点。 

从整个MVC框架的发展轨迹来看,我们可以得出一个很重要的结论: 
 

downpour 写道

MVC框架的发展轨迹,始终是伴随着技术的革新(无论是编程模型的改变还是引入新的编程元素)共同向前发展。而每一次的技术革新,都会成为MVC框架发展过程中的里程碑。



小结 

在本文中所讲的一些话题触角涉及到了Web开发的各个方面。作为SpringMVC的前传,笔者个人认为将整个MVC框架的发展历程讲清楚,大家才能更好地去了解SpringMVC本身。而我们在这里所谈到的一些概念性的话题,也算是对过去十年以来MVC框架的一个小结,希望对读者有所启示。 

 

2.SpringMVC深度探险(二) —— SpringMVC概览

对于任何事物的研究,总是由表及里、由浅入深地进行。在本系列的第二篇文章中,我们将通过不同的观察视角,对SpringMVC做一些概要性的分析,帮助大家了解SpringMVC的基本构成要素、SpringMVC的发展历程以及SpringMVC的设计原则。 

SpringMVC的构成要素 

了解一个框架的首要任务就是搞清楚这个框架的基本构成要素。当然,这里所说的构成要素实际上还可以被挖掘为两个不同的层次: 
 

  • 基于框架所编写的应用程序的构成要素
  • 框架自身的运行主线以及微观构成要素

我们在这里首先来关注一下第一个层次,因为第一个层次是广大程序员直接能够接触得到的部分。而第二个层次的讨论,我们不得不在第一个层次的讨论基础之上通过不断分析和逻辑论证慢慢给出答案。 

在上一篇文章中,我们曾经列举了一段SpringMVC的代码示例,用于说明MVC框架的构成结构。我们在这里不妨将这个示例细化,总结归纳出构成SpringMVC应用程序的基本要素。 

1. 指定SpringMVC的入口程序(在web.xml中) 
 

Xml代码 

 收藏代码

  1. <!-- Processes application requests -->  
  2. <servlet>  
  3.     <servlet-name>dispatcher</servlet-name>  
  4.     <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
  5.     <load-on-startup>1</load-on-startup>  
  6. </servlet>  
  7.           
  8. <servlet-mapping>  
  9.     <servlet-name>dispatcher</servlet-name>  
  10.     <url-pattern>/**</url-pattern>  
  11. </servlet-mapping>  



以一个Servlet作为入口程序是绝大多数MVC框架都遵循的基本设计方案。这里的DispatcherServlet被我们称之为核心分发器,是SpringMVC最重要的类之一,之后我们会对其单独展开进行分析。 

2. 编写SpringMVC的核心配置文件(在[servlet-name]-servlet.xml中) 
 

Xml代码 

 收藏代码

  1. <beans xmlns="http://www.springframework.org/schema/beans"  
  2.        xmlns:mvc="http://www.springframework.org/schema/mvc"  
  3.        xmlns:context="http://www.springframework.org/schema/context"  
  4.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  5.        xsi:schemaLocation="  
  6.             http://www.springframework.org/schema/beans  
  7.             http://www.springframework.org/schema/beans/spring-beans-3.1.xsd  
  8.             http://www.springframework.org/schema/context   
  9.             http://www.springframework.org/schema/context/spring-context-3.1.xsd  
  10.             http://www.springframework.org/schema/mvc  
  11.             http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd"   
  12.        default-autowire="byName">  
  13.       
  14.     <!-- Enables the Spring MVC @Controller programming model -->  
  15.     <mvc:annotation-driven />  
  16.       
  17.     <context:component-scan base-package="com.demo2do" />  
  18.       
  19.       
  20.     <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">  
  21.         <property name="prefix" value="/" />  
  22.         <property name="suffix" value=".jsp" />  
  23.     </bean>  
  24.       
  25. </beans>  



SpringMVC自身由众多不同的组件共同构成,而每一个组件又有众多不同的实现模式。这里的SpringMVC核心配置文件是定义SpringMVC行为方式的一个窗口,用于指定每一个组件的实现模式。有关SpringMVC组件的概念,在之后的讨论中也会涉及。 

3. 编写控制(Controller)层的代码 
 

Java代码 

 收藏代码

  1. @Controller  
  2. @RequestMapping  
  3. public class UserController {  
  4.   
  5.     @RequestMapping("/login")  
  6.     public ModelAndView login(String name, String password) {  
  7.        // write your logic here   
  8.            return new ModelAndView("success");  
  9.     }  
  10.   
  11. }  



控制(Controller)层的代码编写在一个Java文件中。我们可以看到这个Java文件是一个普通的Java类并不依赖于任何接口。只是在响应类和响应方法上使用了Annotation的语法将它与Http请求对应起来。 

从这个例子中,我们实际上已经归纳了构成基于SpringMVC应用程序的最基本要素。它们分别是: 
 

  • 入口程序 —— DispatcherServlet
  • 核心配置 —— [servlet-name]-servlet.xml
  • 控制逻辑 —— UserController

从应用程序自身的角度来看,入口程序和核心配置一旦确定之后将保持固定不变的,而控制逻辑则随着整个应用程序功能模块的扩展而不断增加。所以在这种编程模式下,应用程序的纵向扩展非常简单并且显得游刃有余。 

基于SpringMVC的应用程序能够表现为现在这个样子,经历了一个不断重构不断改造的过程。接下来的讨论,我们就来试图为大家揭秘这个过程。 

SpringMVC的发展历程 

在上一篇文章中,我们曾经讨论过MVC的发展轨迹。当时我们总结了一个MVC框架的发展轨迹图: 
 

93e16090-54df-3ef2-a318-3b710a59db40.png



从图中我们可以发现,所有的MVC框架都是从基本的Servlet模型发展而来。因此,要了解SpringMVC的发展历程,我们还是从最基本的Servlet模型开始,探究SpringMVC对于Servlet模型的改造过程中究竟经历了哪些阶段、碰到了哪些问题、并看看SpringMVC是如何解决这些问题的。 

【核心Servlet的提炼】 

在Servlet模型中,请求-响应的实现依赖于两大元素的共同配合: 

1. 配置Servlet及其映射关系(在web.xml中) 
 

Xml代码 

 收藏代码

  1. <servlet>  
  2.     <servlet-name>registerServlet</servlet-name>  
  3.     <servlet-class>com.demo2do.springmvc.web.RegisterServlet</servlet-class>  
  4.     <load-on-startup>1</load-on-startup>  
  5. </servlet>  
  6.           
  7. <servlet-mapping>  
  8.     <servlet-name>registerServlet</servlet-name>  
  9.     <url-pattern>/register</url-pattern>  
  10. </servlet-mapping>  



在这里,<url-pattern>定义了整个请求-响应的映射载体:URL;而<servlet-name>则将<servlet>节点和<servlet-mapping>节点联系在一起形成请求-响应的映射关系;<servlet-class>则定义了具体进行响应的Servlet实现类。 

2. 在Servlet实现类中完成响应逻辑 
 

Java代码 

 收藏代码

  1. public class RegisterServlet extends  HttpServlet {  
  2.   
  3.      @Override  
  4.      protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
  5.       
  6.          // 从request获取参数  
  7.          String name = req.getParameter("name");  
  8.          String birthdayString = req.getParameter("birthday");  
  9.            
  10.          // 做必要的类型转化  
  11.          Date birthday = null;  
  12.          try {  
  13.              birthday = new SmpleDateFormat("yyyy-MM-dd").parse(birthdayString);  
  14.          } catch (ParseException e) {  
  15.          e.printStackTrace();  
  16.          }  
  17.   
  18.          // 初始化User类,并设置字段到user对象中去  
  19.          User user = new User();  
  20.          user.setName(name);  
  21.          user.setBirthday(birthday);  
  22.   
  23.          // 调用业务逻辑代码完成注册  
  24.          UserService userService = new UserService();  
  25.          userService.register(user);  
  26.            
  27.          // 设置返回数据  
  28.          request.setAttribute("user", user);  
  29.   
  30.          // 返回成功页面  
  31.          req.getRequestDispatcher("/success.jsp").forward(req, resp);  
  32.      }  
  33. }  



Servlet实现类本质上是一个Java类。通过Servlet接口定义中的HttpServletRequest对象,我们可以处理整个请求生命周期中的数据;通过HttpServletResponse对象,我们可以处理Http响应行为。 

整个过程并不复杂,因为作为一个底层规范,所规定的编程元素和实现方式应该尽可能直观和简单。在这一点上,Servlet规范似乎可以满足我们的要求。如果将上述过程中的主要过程加以抽象,我们可以发现有两个非常重要概念蕴含在了Servlet的规范之中: 
 

a7de7025-2205-33bf-b55a-8158751f980e.png



控制流和数据流的问题几乎贯穿了所有MVC框架的始末,因而我们不得不在这里率先提出来,希望对读者有一些警示作用。 

:对于控制流和数据流的相关概念,请参考另外一篇博客:《Struts2技术内幕》 新书部分篇章连载(五)—— 请求响应哲学。这一对概念,几乎是所有MVC框架背后最为重要的支撑,读者应该尤其重视! 

所有MVC框架的核心问题也由控制流和数据流这两大体系延伸开来。比如,在Servlet编程模型之下,“请求-响应映射关系的定义”这一问题就会随着项目规模的扩大而显得力不从心: 
 

问题1 写道

项目规模扩大之后,请求-响应的映射关系全部定义在web.xml中,将造成web.xml的不断膨胀而变得难以维护。



针对这个问题,SpringMVC提出的方案就是:提炼一个核心的Servlet覆盖对所有Http请求的处理。。 

这一被提炼出来的Servlet,通常被我们称之为:核心分发器。在SpringMVC中,核心分发器就是org.springframework.web.servlet.DispatcherServlet。 

:核心分发器的概念并非SpringMVC独创。我们可以看到,核心分发器的提炼几乎是所有MVC框架设计中的必经之路。在Struts2中,也有核心分发器(Dispatcher)的概念,只是它并不以Servlet的形式出现。因此,读者应该把关注点放在核心分发器这个概念的提炼之上,而不是纠结于其形式。 

有了DispatcherServlet,我们至少从表面上解决了上面的问题。至少在web.xml中,我们的配置代码就被固定了下来: 
 

Xml代码 

 收藏代码

  1. <!-- Processes application requests -->  
  2. <servlet>  
  3.     <servlet-name>dispatcherServlet</servlet-name>  
  4.     <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
  5.     <load-on-startup>1</load-on-startup>  
  6. </servlet>  
  7.           
  8. <servlet-mapping>  
  9.     <servlet-name>dispatcherServlet</servlet-name>  
  10.     <url-pattern>/**</url-pattern>  
  11. </servlet-mapping>  



有了DispatcherServlet,我们只相当于迈出了坚实的第一步,因为对核心Servlet的提炼不仅仅是将所有的Servlet集中在一起那么简单,我们还将面临两大问题: 
 

问题2 写道

核心Servlet应该能够根据一定的规则对不同的Http请求分发到不同的Servlet对象上去进行处理。

 

问题3 写道

核心Servlet应该能够建立起一整套完整的对所有Http请求进行规范化处理的流程。



而这两大问题的解决,涉及到了DispatcherServlet的设计核心。我们也不得不引入另外一个重要的编程元素,那就是:组件。 

【组件的引入】 

DispatcherServlet的引入是我们通过加入新的编程元素来对基本的Servlet规范进行抽象概括所迈出的第一步。不过接下来,有关DispatcherServlet的设计问题又一次摆到了我们的面前。 

如果仔细分析一下上一节末尾所提出的两个问题,我们可以发现这两个问题实际上都涉及到了DispatcherServlet的处理过程,这一处理过程首先必须是一剂万能药,能够处理所有的Http请求;同时,DispatcherServlet还需要完成不同协议之间的转化工作(从Http协议到Java世界的转化)。 

对此,SpringMVC所提出的方案是:将整个处理流程规范化,并把每一个处理步骤分派到不同的组件中进行处理。 

这个方案实际上涉及到两个方面: 
 

  • 处理流程规范化 —— 将处理流程划分为若干个步骤(任务),并使用一条明确的逻辑主线将所有的步骤串联起来
  • 处理流程组件化 —— 将处理流程中的每一个步骤(任务)都定义为接口,并为每个接口赋予不同的实现模式

在SpringMVC的设计中,这两个方面的内容总是在一个不断交叉、互为补充的过程中逐步完善的。 

处理流程规范化是目的,对于处理过程的步骤划分和流程定义则是手段。因而处理流程规范化的首要内容就是考虑一个通用的Servlet响应程序大致应该包含的逻辑步骤: 
 

  • 步骤1 —— 对Http请求进行初步处理,查找与之对应的Controller处理类(方法)
  • 步骤2 —— 调用相应的Controller处理类(方法)完成业务逻辑
  • 步骤3 —— 对Controller处理类(方法)调用时可能发生的异常进行处理
  • 步骤4 —— 根据Controller处理类(方法)的调用结果,进行Http响应处理

这些逻辑步骤虽然还在我们的脑海中,不过这些过程恰恰正是我们对整个处理过程的流程化概括,稍后我们就会把它们进行程序化处理。 

所谓的程序化,实际上也就是使用编程语言将这些逻辑语义表达出来。在Java语言中,最适合表达逻辑处理语义的语法结构是接口,因此上述的四个流程也就被定义为了四个不同接口,它们分别是: 
 

  • 步骤1 —— HandlerMapping
  • 步骤2 —— HandlerAdapter
  • 步骤3 —— HandlerExceptionResolver
  • 步骤4 —— ViewResolver

结合之前我们对流程组件化的解释,这些接口的定义不正是处理流程组件化的步骤嘛?这些接口,就是组件。 

除了上述组件之外,SpringMVC所定义的组件几乎涵盖了每一个处理过程中的重要节点。我们在这里引用Spring官方reference中对于最基本的组件的一些说明: 
 

bcd4f964-d766-3981-b703-f8b635e5d505.png



我们在之后篇文章中将重点对这里所提到的所有组件做深入的分析。大家在这里需要理解的是SpringMVC定义这些组件的目的和初衷。 

这些组件一旦被定义,自然而然也就引出了下一个问题:这些组件是如何串联在一起的?这个过程,是在DispatcherServlet中完成的。有关这一点,我们可以从两个不同的角度加以证明。 

1. 从DispatcherServlet自身数据结构的角度 

5066c9c3-3958-36f1-b1c2-1bcbb335491b.png 

如图中所示,DispatcherServlet中包含了众多SpringMVC的组件,这些组件是实现DispatcherServlet核心逻辑的基础。 

2. 从DispatcherServlet的核心源码的角度 
 

Java代码 

 收藏代码

  1. try {  
  2.     // 这里省略了部分代码  
  3.   
  4.     // 获取HandlerMapping组件返回的执行链  
  5.     mappedHandler = getHandler(processedRequest, false);  
  6.     if (mappedHandler == null || mappedHandler.getHandler() == null) {  
  7.         noHandlerFound(processedRequest, response);  
  8.         return;  
  9.     }  
  10.   
  11.     // 获取HandlerAdapter组件  
  12.     HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());  
  13.   
  14.     // 这里省略了部分源码  
  15.       
  16.     // 调用HandlerAdapter组件  
  17.     mv = ha.handle(processedRequest, response, mappedHandler.getHandler());  
  18.   
  19.     // 这里省略了部分源码  
  20.   
  21. }catch (ModelAndViewDefiningException ex) {  
  22.     logger.debug("ModelAndViewDefiningException encountered", ex);  
  23.     mv = ex.getModelAndView();  
  24. }catch (Exception ex) {  
  25.     Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);  
  26.     // 调用HandlerExceptionResolver进行异常处理  
  27.     mv = processHandlerException(processedRequest, response, handler, ex);  
  28.     errorView = (mv != null);  
  29. }  



从上面的代码片段中,我们可以看到DispatcherServlet的核心逻辑不过是对组件的获取和调用。 

除此之外,SpringMVC对处理流程的规范化和组件化所引出的另外一个问题就是如何针对所有的组件进行管理。 

先说说管理。其实管理这些组件对于SpringMVC来说完全不是问题,因为SpringMVC作为Spring Framework的一部分,其自身的运行环境就是Spring所定义的容器之中。我们知道,Spring Framework的核心作用之一就是对整个应用程序的组件进行管理。所以SpringMVC对于这些已定义组件的管理,只不过是借用了Spring自身已经提供的容器功能而已。 

:SpringMVC在进行组件管理时,会单独为SpringMVC相关的组件构建一个容器环境,这一容器环境可以独立于应用程序自身所创建的Spring容器。有关这一点,我们在之后的讨论中将详细给出分析。 

而SpringMVC对这些组件的管理载体,就是我们在上一节中所提到的核心配置文件。我们可以看到,核心配置文件在整个SpringMVC的构成要素中占有一席之地的重要原因就是在于:我们必须借助一个有效的手段对整个SpringMVC的组件进行定义,而这一点正是通过核心配置文件来完成的。 

如果我们把上面的整个过程重新梳理一下,整个逻辑看起来就像这样: 
 

38f796af-0541-3de5-93d2-32e02a638181.png



这四个方面的内容,我们是顺着设计思路的不断推进而总结归纳出来的。这也恰好证明之前所提到的一个重要观点,我们在这里强调一下: 
 

downpour 写道

处理流程的规范化和组件化,是在一个不断交叉、互为补充的过程中逐步完善的。



【行为模式的扩展】 

有了组件,也有了DispatcherServlet对所有组件的串联,我们之前所提出的两个问题似乎已经可以迎刃而解。所以,我们可以说: 
 

downpour 写道

SpringMVC就是通过DispatcherServlet将一堆组件串联起来的Web框架。



在引入组件这个概念的时候,我们所强调的是处理流程的抽象化,因而所有组件的外在表现形式是接口。接口最重要意义是定义操作规范,所以接口用来表达每一个处理单元的逻辑语义是最合适不过的。但光有接口,并不能完整地构成一个框架的行为模式。从操作规范到行为模式的变化,是由接口所对应的实现类来完成的。 

在Java语言中,一个接口可以有多个不同的实现类,从而构成一个树形的实现体系。而每一个不同的实现分支,实际上代表的是对于相同的逻辑语义的不同解读方式。结合上面我们的描述,也可以说:一个接口的每一个不同的实现分支,代表了相同操作规范的不同行为模式。 

我们可以通过之前曾经提到过的一个SpringMVC组件HandlerMapping为例进行说明。 

b8333d33-aa53-3c70-9870-b1cfb4ebf854.png 

上图就是HandlerMapping接口的树形实现体系。在这个实现体系结构中,每一个树形结构的末端实现都是SpringMVC中比较具有典型意义的行为模式。我们可以截取其中的几个实现来加以说明: 
 

  • BeanNameUrlHandlerMapping —— 根据Spring容器中的bean的定义来指定请求映射关系
  • SimpleUrlHandlerMapping —— 直接指定URL与Controller的映射关系,其中的URL支持Ant风格
  • DefaultAnnotationHandlerMapping —— 支持通过直接扫描Controller类中的Annotation来确定请求映射关系
  • RequestMappingHandlerMapping —— 通过扫描Controller类中的Annotation来确定请求映射关系的另外一个实现类

有关这几个实现类的具体示例和使用说明,读者可以参考不同版本的Spring官方文档来获取具体的细节。 

:我们在这里之所以要强调不同版本的Spring官方文档的原因在于这些不同的实现类,正代表了不同版本SpringMVC在默认行为模式上选择的不同。在下图中,我们列出了不同重大版本的SpringMVC的实现体系结构,并用红色框圈出了每个版本默认的实现类。 

7791ba91-af53-3d43-bdaf-a8d2d8ad16ee.png

我们可以看到,上述这些不同的HandlerMapping的实现类,其运行机制和行为模式完全不同。这也就意味着对于HandlerMapping这个组件而言,可以进行选择的余地就很大。我们既可以选择其中的一种实现模式作为默认的行为模式,也可以将这些实现类依次串联起来成为一个执行链。不过这已经是实现层面和设计模式上的小技巧了。 

单就HandlerMapping一个组件,我们就能看到各种不同的行为模式。如果我们将逻辑主线中所有的组件全部考虑进来,那么整个实现机制就会随着这些组件实现体系的不同而表现出截然不同的行为方式了。因此,我们的结论是: 
 

downpour 写道

SpringMVC各种不同的组件实现体系成为了SpringMVC行为模式扩展的有效途径。



有关SpringMVC的各种组件和实现体系,我们将在之后的讨论中详细展开。 

SpringMVC的设计原则 

最后我们来讨论一下SpringMVC的设计原则。任何框架在设计的时候都必须遵循一些基本的原则,而这些原则也成为整个框架的理论基础。对于那些有一定SpringMVC使用经验的程序员来说,这些基本的设计原则本身也一定是给大家留下深刻印象的那些闪光点,所以我们非常有必要在这里加以总结。 

【Open for extension / closed for modification】 

这条重要的设计原则被写在了Spring官方的reference中SpringMVC章节的起始段: 
 

Spring Reference 写道

A key design principle in Spring Web MVC and in Spring in general is the “Open for extension, closed for modification” principle.



SpringMVC在整个官方reference的起始就强调这一原则,可见其对于整个框架的重要性。那么我们又如何来理解这段话的含义呢?笔者在这里从源码的角度归纳了四个方面: 

1. 使用final关键字来限定核心组件中的核心方法 

有关这一点,我们还可以在Spring官方的reference中找到非常明确的说明: 
 

Spring Reference 写道

Some methods in the core classes of Spring Web MVC are marked final. As a developer you cannot override these methods to supply your own behavior. This has not been done arbitrarily, but specifically with this principle in mind.



在SpringMVC的源码中,HandlerAdapter实现类RequestMappingHandlerAdapter中,核心方法handleInternal就被定义为final: 

0ca0e53f-af29-373b-bab6-e93ef5c3bc9b.png 
 

downpour 写道

结论  As a developer you cannot override these methods to supply your own behavior



2. 大量地在核心组件中使用private方法 

我们依然以SpringMVC默认的HandlerAdapter实现RequestMappingHandlerAdapter为例进行说明: 

27d44927-8d82-36b1-92c4-6a653c991e7a.png


可以看到,几乎所有的核心处理方法全部被定义成了带有红色标记的private方法,这就充分表明了SpringMVC对于“子类扩展”这种方式的态度: 
 

downpour 写道

结论  子类不允许通过继承的方式改变父类的默认行为。



3. 限定某些类对外部程序不可见 

有关这一点,有好几个类可以加以证明,我们不妨来看看它们的源码定义: 
 

Java代码 

 收藏代码

  1. class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {  
  2.     // 这里省略了所有的代码  
  3. }  
  4.   
  5. class DefaultServletHandlerBeanDefinitionParser implements BeanDefinitionParser {  
  6.     // 这里省略了所有的代码  
  7. }  
  8.   
  9. class InterceptorsBeanDefinitionParser implements BeanDefinitionParser {  
  10.     // 这里省略了所有的代码  
  11. }  
  12.   
  13. class ResourcesBeanDefinitionParser implements BeanDefinitionParser {  
  14.     // 这里省略了所有的代码  
  15. }  


 

downpour 写道

结论  不允许外部程序对这些系统配置类进行访问,从而杜绝外部程序对SpringMVC默认行为的任何修改。



在这些类的定义中,我们并未看到public修饰符。也就是说,这些类只能在SpringMVC的内部被调用,对于框架以外的应用程序是不可见的。有关这些类的作用,我们将在之后的讨论中详细展开。 

4. 提供自定义扩展接口,却不提供完整覆盖默认行为的方式 

这一点,需要深入到SpringMVC的请求处理内部才能够体会得到,我们在这里截取了其中的一段源码加以说明: 
 

Java代码 

 收藏代码

  1. private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {  
  2.     List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();  
  3.   
  4.     // Annotation-based argument resolution  
  5.     resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));  
  6.     resolvers.add(new RequestParamMapMethodArgumentResolver());  
  7.     resolvers.add(new PathVariableMethodArgumentResolver());  
  8.     resolvers.add(new ServletModelAttributeMethodProcessor(false));  
  9.     resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters()));  
  10.     resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters()));  
  11.     resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));  
  12.     resolvers.add(new RequestHeaderMapMethodArgumentResolver());  
  13.     resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));  
  14.     resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));  
  15.   
  16.     // Type-based argument resolution  
  17.     resolvers.add(new ServletRequestMethodArgumentResolver());  
  18.     resolvers.add(new ServletResponseMethodArgumentResolver());  
  19.     resolvers.add(new HttpEntityMethodProcessor(getMessageConverters()));  
  20.     resolvers.add(new RedirectAttributesMethodArgumentResolver());  
  21.     resolvers.add(new ModelMethodProcessor());  
  22.     resolvers.add(new MapMethodProcessor());  
  23.     resolvers.add(new ErrorsMethodArgumentResolver());  
  24.     resolvers.add(new SessionStatusMethodArgumentResolver());  
  25.     resolvers.add(new UriComponentsBuilderMethodArgumentResolver());  
  26.   
  27.     // Custom arguments  
  28.     if (getCustomArgumentResolvers() != null) {  
  29.         resolvers.addAll(getCustomArgumentResolvers());  
  30.     }  
  31.   
  32.     // Catch-all  
  33.     resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));  
  34.     resolvers.add(new ServletModelAttributeMethodProcessor(true));  
  35.   
  36.     return resolvers;  
  37. }  



这是RequestMappingHandlerAdapter内部的一个重要方法,用以获取所有的参数处理实现类(HandlerMethodArgumentResolver)。从源码中,我们可以看到虽然这个方法是一个private的方法,但是它在源码中却提供了getCustomArgumentResolvers()方法作为切入口,允许用户自行进行扩展。不过我们同样可以发现,用户自定义的扩展类,只是被插入到整个寻址过程中,并不能通过用户自定义的扩展类来实现对其他HandlerMethodArgumentResolver行为的覆盖;也不能改变HandlerMethodArgumentResolver的处理顺序。也就是说: 
 

downpour 写道

结论  SpringMVC提供的扩展切入点无法改变框架默认的行为方式。



上述这四个方面,都是这一条设计原则在源码级别的佐证。或许有的读者会产生这样的疑虑:这个不能改,那个也不能改,我们对于SpringMVC的使用岂不是丧失了很多灵活性?这个疑虑的确存在,但是只说对了一半。因为SpringMVC的这一条设计原则说的是:不能动其根本,只能在一定范围内进行扩展。 

至于说到SpringMVC为什么会基于这样一条设计原则,这里面的原因很多。除了之前所提到的编程模型和组件模型的影响,其中更加牵涉到一个编程哲学的取向问题。有关这一点,我们在之后的文章中将陆续展开。 

【形散神不散】 

这一条编程原则实际上与上一条原则只是在表达方式上有所不同,其表达的核心意思是比较类似的。那么我们如何来定义这里的“形”和“神”呢? 
 

  • 神 —— SpringMVC总是沿着一条固定的逻辑主线运行
  • 形 —— SpringMVC却拥有多种不同的行为模式

SpringMVC是一个基于组件的开发框架,组件的不同实现体系构成了“形”;组件的逻辑串联构成了“神”。因此,“形散神不散”,实际上是说: 
 

downpour 写道

结论  SpringMVC的逻辑主线始终不变,而行为模式却可以多种多样。



我们在之前有关组件的讨论中,已经见识到了组件的实现体系,也领略了在不同的SpringMVC版本中,组件的行为模式的不同。这些已经能够充分证明“形散”的事实。接下来,我们再通过源码来证明一下“神不散”: 

61c47726-1b41-3d12-a050-f76dc396c24f.png

图中的代码是DispatcherServlet中的核心方法doDispatch,我们这里使用了比较工具将Spring3.1中的实现代码和Spring2.0.8中的实现代码做了比较,其中的区别之处比较工具使用了不同的颜色标注了出来。 

我们可以很明显地看到,虽然Spring2.0到Spring3.1之间,SpringMVC的行为方式已经有了翻天覆地的变化,然而整个DispatcherServlet的核心处理主线却并没有很大的变化。这种稳定性,恰巧证明了整个SpringMVC的体系结构设计的精妙之处。 

【简化、简化、还是简化】 

在Spring2.5之前的SpringMVC版本并没有很强的生命力,因为它只是通过组件将整个MVC的概念加以诠释,从开发流程的简易度来看并没有很明显的提升。有关SpringMVC发展的里程碑,我们将在之后篇文章中重点讲述。我们在这里想要谈到的SpringMVC的另外一大设计原则,实际上主要是从Spring2.5这个版本之后才不断显现出来的。这条设计原则可以用2个字来概括:简化。 

这里说的简化,其实包含的内容非常广泛。笔者在这里挑选了两个比较重要的方面来进行说明: 
 

  • Annotation —— 简化各类配置定义
  • Schema Based XML —— 简化组件定义

先谈谈Annotation。Annotation是JDK5.0带来的一种全新的Java语法。这种语法的设计初衷众说纷纭,并没有一个标准的答案。笔者在这里给出一个个人观点以供参考: 
 

downpour 写道

结论  Annotation的原型是注释。作为一种对注释的扩展而被引入成为一个语法要素,其本身就是为了对所标注的编程元素进行补充说明,从而进一步完善编程元素的逻辑语义。



从这个结论中,我们可以看到一层潜在的意思:在Annotation出现之前,Java自身语法所定义的编程元素已经不足以表达足够多的信息或者逻辑语义。在这种情况下,过去经常使用的方法是引入新的编程元素(例如使用最多的就是XML形式的结构化配置文件)来对Java程序进行补充说明。而在Annotation出现之后,可以在一定程度上有效解决这一问题。因此Annotation在很长一段时间都被当作是XML配置文件的替代品。 

这也就是Annotation经常被用来和XML进行比较的原因。孰优孰劣其实还是要视具体情况而定,并没有什么标准答案。不过我们在这里想强调的是Annotation在整个SpringMVC中所起到的作用,并非仅仅是代替XML那么简单。我们归纳了有三个不同的方面: 

1. 简化请求映射的定义 

在Spring2.5之前,所有的Http请求与Controller核心处理器之间的映射关系都是在XML文件中定义的。作为XML配置文件的有效替代品,Annotation接过了定义映射关系的重任。我们可以将@RequestMapping加在Controller的class-level和method-level进行Http请求的抽象。 

2. 消除Controller对接口的依赖 

在Spring2.5之前,SpringMVC规定所有的Controller都必须实现Controller接口: 
 

Java代码 

 收藏代码

  1. public interface Controller {  
  2.   
  3.     /** 
  4.      * Process the request and return a ModelAndView object which the DispatcherServlet 
  5.      * will render. A <code>null</code> return value is not an error: It indicates that 
  6.      * this object completed request processing itself, thus there is no ModelAndView 
  7.      * to render. 
  8.      * @param request current HTTP request 
  9.      * @param response current HTTP response 
  10.      * @return a ModelAndView to render, or <code>null</code> if handled directly 
  11.      * @throws Exception in case of errors 
  12.      */  
  13.     ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;  
  14.   
  15. }  



也就是说,应用程序不得不严重依赖于接口所规定的处理模式。而我们看到Controller接口除了对处理接口的返回值做了一次封装以外,我们依然需要面对原生的HttpServletRequest和HttpServletResponse对象进行操作。 

而在Spring2.5之后,我们可以通过@Controller来指定SpringMVC可识别的Controller,彻底消除了对接口的依赖:
 
 

Java代码 

 收藏代码

  1. @Controller  
  2. public class UserController {  
  3.       // 这里省略了许多代码  
  4. }  



3. 成为框架进行逻辑处理的标识 

之前已经谈到,Annotation主要被用于对编程元素进行补充说明。因而Spring就利用这一特性,使得那些被加入了特殊Annotation的编程元素可以得到特殊的处理。例如,SpringMVC引入的@SessionAttribute、@RequestBody、@ModelAttribute等等,可以说既是对Controller的一种逻辑声明,也成为了框架本身对相关元素进行处理的一个标识符。 

再谈谈Schema Based XML。Schema Based XML并不是一个陌生的概念,早在Spring2.0时代就被用于进行XML配置的简化。SpringMVC在进入到3.0版本之后,正式将其引入并作为SpringMVC组件定义的一个重要手段。 

在XML中引入Schema,只需要在XML文件的开头加入相关的定义。例如: 
 

Xml代码 

 收藏代码

  1. <beans xmlns="http://www.springframework.org/schema/beans"  
  2.        xmlns:mvc="http://www.springframework.org/schema/mvc"  
  3.        xmlns:context="http://www.springframework.org/schema/context"  
  4.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  5.        xsi:schemaLocation="  
  6.             http://www.springframework.org/schema/beans  
  7.             http://www.springframework.org/schema/beans/spring-beans-3.1.xsd  
  8.             http://www.springframework.org/schema/context   
  9.             http://www.springframework.org/schema/context/spring-context-3.1.xsd  
  10.             http://www.springframework.org/schema/mvc  
  11.             http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd">  
  12.   
  13.   
  14. </beans>  



而Schema的具体处理,则位于Spring的JAR中的/META-INF/spring.handlers文件中进行定义: 
 

Xml代码 

 收藏代码

  1. http\://www.springframework.org/schema/mvc=org.springframework.web.servlet.config.MvcNamespaceHandler  



我们会在之后的讨论中详细分析MvcNamespaceHandler的源码。不过我们可以明确的是,在我们使用Schema Based XML的同时,有许多SpringMVC的内置对象会被预先定义成为组件,我们的配置将是对这些预先定义好的组件的一个二次配置的过程。可以想象,二次配置一定会比较省力,因为它至少省去了很多内置对象的定义过程。这也就是Schema Based XML带来的简化效果了。 

小结 

本文从逻辑上讲,可以分成三个部分: 
 

  • SpringMVC的构成要素 —— 是什么 —— 阐述框架的主体结构
  • SpringMVC的发展历程 —— 为什么 —— 阐述框架各要素产生的内因
  • SpringMVC的设计原则 —— 怎么样 —— 阐述框架的共性思想

“是什么”是框架最根本的问题。我们从SpringMVC的三要素入手,帮助大家分析构成SpringMVC的基本元素主要是为了让读者对整个SpringMVC的架构有一个宏观的认识。在之后的分析中,我们研究的主体内容也将始终围绕着这些SpringMVC的构成要素,并进行逐一分析。 

“为什么”是框架的存在基础。我们可以看到,整个SpringMVC的发展历程是一个对于开发模式不断进行优化的过程,也是不断解决Web开发中所面临的一个又一个问题的过程。之前我们也曾经提到过一个重要观点:任何框架无所谓好与坏、优与劣,它们只是在不同的领域解决问题的方式不同。所以,我们分析这些SpringMVC基本构成要素产生的原因实际上也是对整个Web开发进行重新思考的过程。 

“怎么样”是一种深层次的需求。对于SpringMVC而言,了解其基本构成和用法并不是一件难事,但是要从中提炼并总结出一些共性的东西就需要我们能够站在一个更高的高度来进行分析。也只有了解了这些共性的东西,我们才能进一步总结出使用框架的最佳实践。 

读到这里,希望读者能够回味一下本文的写作思路,并且能够举一反三将这种思考问题的方式运用到其他一些框架的学习中去。这样,本文的目的也就达到了。 

 

3.SpringMVC深度探险(三) —— DispatcherServlet与初始化主线

在上一篇文章中,我们给出了构成SpringMVC应用程序的三要素以及三要素的设计过程。让我们来归纳一下整个设计过程中的一些要点: 
 

  • SpringMVC将Http处理流程抽象为一个又一个处理单元
  • SpringMVC定义了一系列组件(接口)与所有的处理单元对应起来
  • SpringMVC由DispatcherServlet贯穿始终,并将所有的组件串联起来

在整个过程中,组件和DispatcherServlet总是维持着一个相互支撑的关系: 
 

  • DispatcherServlet —— 串联起整个逻辑主线,是整个框架的心脏
  • 组件 —— 逻辑处理单元的程序化表示,起到承上启下的作用,是SpringMVC行为模式的实际承载者

在本系列接下来的两篇文章中,我们将分别讨论DispatcherServlet和组件的相关内容。本文讨论DispatcherServlet,而下一篇则重点分析组件。 

有关DispatcherServlet,我们想从构成DispatcherServlet的体系结构入手,再根据不同的逻辑主线分别加以分析,希望能够帮助读者整理出学习SpringMVC核心类的思路。 

DispatcherServlet的体系结构 

通过不同的角度来观察DispatcherServlet会得到不同的结论。我们在这里选取了三个不同的角度:运行特性、继承结构和数据结构。 

【运行主线】 

从DispatcherServlet所实现的接口来看,DispatcherServlet的核心本质:是一个Servlet。这个结论似乎很幼稚,不过这个幼稚的结论却蕴含了一个对整个框架都至关重要的内在原则:Servlet可以根据其特性进行运行主线的划分。 

根据Servlet规范的定义,Servlet中的两大核心方法init方法和service方法,它们的运行时间和触发条件都截然不同: 

1. init方法 

在整个系统启动时运行,且只运行一次。因此,在init方法中我们往往会对整个应用程序进行初始化操作。这些初始化操作可能包括对容器(WebApplicationContext)的初始化、组件和外部资源的初始化等等。 

2. service方法 

在整个系统运行的过程中处于侦听模式,侦听并处理所有的Web请求。因此,在service及其相关方法中,我们看到的则是对Http请求的处理流程。 

因而在这里,Servlet的这一特性就被SpringMVC用于对不同的逻辑职责加以划分,从而形成两条互不相关的逻辑运行主线: 
 

  • 初始化主线 —— 负责对SpringMVC的运行要素进行初始化
  • Http请求处理主线 —— 负责对SpringMVC中的组件进行逻辑调度完成对Http请求的处理

对于一个MVC框架而言,运行主线的划分非常重要。因为只有弄清楚不同的运行主线,我们才能针对不同的运行主线采取不同的研究策略。而我们在这个系列中的绝大多数分析的切入点,也是围绕着不同的运行主线进行的。 

:SpringMVC运行主线的划分依据是Servlet对象中不同方法的生命周期。事实上,几乎所有的MVC都是以此为依据来进行运行主线的划分。这进一步可以证明所有的MVC框架的核心基础还是Servlet规范,而设计理念的差异也导致了不同的框架走向了完全不同的发展道路。 

【继承结构】 

除了运行主线的划分以外,我们再关注一下DispatcherServlet的继承结构: 

a5ae8920-b395-322b-a023-3278418992e0.png 

在这个继承结构中,我们可以看到DispatcherServlet在其继承树中包含了2个Spring的支持类:HttpServletBean和FrameworkServlet。我们分别来讨论一下这两个Spring的支持类在这里所起到的作用。 

HttpServletBean是Spring对于Servlet最低层次的抽象。在这一层抽象中,Spring会将这个Servlet视作是一个Spring的bean,并将init-param中的值作为bean的属性注入进来: 
 

Java代码 

 收藏代码

  1. public final void init() throws ServletException {  
  2.     if (logger.isDebugEnabled()) {  
  3.         logger.debug("Initializing servlet '" + getServletName() + "'");  
  4.     }  
  5.   
  6.     // Set bean properties from init parameters.  
  7.     try {  
  8.         PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);  
  9.         BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);  
  10.         ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());  
  11.         bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.environment));  
  12.         initBeanWrapper(bw);  
  13.         bw.setPropertyValues(pvs, true);  
  14.     }  
  15.     catch (BeansException ex) {  
  16.         logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);  
  17.         throw ex;  
  18.     }  
  19.   
  20.     // Let subclasses do whatever initialization they like.  
  21.     initServletBean();  
  22.   
  23.     if (logger.isDebugEnabled()) {  
  24.         logger.debug("Servlet '" + getServletName() + "' configured successfully");  
  25.     }  
  26. }  



从源码中,我们可以看到HttpServletBean利用了Servlet的init方法的执行特性,将一个普通的Servlet与Spring的容器联系在了一起。在这其中起到核心作用的代码是:BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);将当前的这个Servlet类转化为一个BeanWrapper,从而能够以Spring的方式来对init-param的值进行注入。BeanWrapper的相关知识属于Spring Framework的内容,我们在这里不做详细展开,读者可以具体参考HttpServletBean的注释获得更多的信息。 

FrameworkServlet则是在HttpServletBean的基础之上的进一步抽象。通过FrameworkServlet真正初始化了一个Spring的容器(WebApplicationContext),并引入到Servlet对象之中: 
 

Java代码 

 收藏代码

  1. protected final void initServletBean() throws ServletException {  
  2.     getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");  
  3.     if (this.logger.isInfoEnabled()) {  
  4.         this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");  
  5.     }  
  6.     long startTime = System.currentTimeMillis();  
  7.   
  8.     try {  
  9.         this.webApplicationContext = initWebApplicationContext();  
  10.         initFrameworkServlet();  
  11.     } catch (ServletException ex) {  
  12.         this.logger.error("Context initialization failed", ex);  
  13.         throw ex;  
  14.     } catch (RuntimeException ex) {  
  15.         this.logger.error("Context initialization failed", ex);  
  16.         throw ex;  
  17.     }  
  18.   
  19.     if (this.logger.isInfoEnabled()) {  
  20.         long elapsedTime = System.currentTimeMillis() - startTime;  
  21.         this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +  
  22.                 elapsedTime + " ms");  
  23.     }  
  24. }  



上面的这段代码就是FrameworkServlet初始化的核心代码。从中我们可以看到这个FrameworkServlet将调用其内部的方法initWebApplicationContext()对Spring的容器(WebApplicationContext)进行初始化。同时,FrameworkServlet还暴露了与之通讯的结构可供子类调用: 
 

Java代码 

 收藏代码

  1. public abstract class FrameworkServlet extends HttpServletBean {  
  2.   
  3.     /** WebApplicationContext for this servlet */  
  4.     private WebApplicationContext webApplicationContext;  
  5.   
  6.         // 这里省略了其他所有的代码  
  7.   
  8.     /** 
  9.      * Return this servlet's WebApplicationContext. 
  10.      */  
  11.     public final WebApplicationContext getWebApplicationContext() {  
  12.         return this.webApplicationContext;  
  13.     }  
  14. }  



我们在这里暂且不对Spring容器(WebApplicationContext)的初始化过程详加探查,稍后我们会讨论一些WebApplicationContext初始化过程中的配置选项。不过读者可以在这里体会到:FrameworkServlet在其内部初始化了一个Spring的容器(WebApplicationContext)并暴露了相关的操作接口,因而继承自FrameworkServlet的DispatcherServlet,也就直接拥有了与WebApplicationContext进行通信的能力。 

通过对DispatcherServlet继承结构的研究,我们可以明确: 
 

downpour 写道

结论 DispatcherServlet的继承体系架起了DispatcherServlet与Spring容器进行沟通的桥梁。



【数据结构】 

在上一篇文章中,我们曾经提到过DispatcherServlet的数据结构: 

6ea52d3d-fe87-3579-976e-d6edd1f0deb3.png 

我们可以把在上面这张图中所构成DispatcherServlet的数据结构主要分为两类(我们在这里用一根分割线将其分割开来): 
 

  • 配置参数 —— 控制SpringMVC组件的初始化行为方式
  • 核心组件 —— SpringMVC的核心逻辑处理组件

可以看到,这两类数据结构都与SpringMVC中的核心要素组件有关。因此,我们可以得出这样一个结论: 
 

downpour 写道

结论 组件是整个DispatcherServlet的灵魂所在:它不仅是初始化主线中的初始化对象,同样也是Http请求处理主线中的逻辑调度载体。



:我们可以看到被我们划为配置参数的那些变量都是boolean类型的,它们将在DispatcherServlet的初始化主线中起到一定的作用,我们在之后会使用源码进行说明。而这些boolean值可以通过web.xml中的init-param值进行设定覆盖(这是由HttpServletBean的特性带来的)。 

SpringMVC的运行体系 

DispatcherServlet继承结构和数据结构,实际上表述的是DispatcherServlet与另外两大要素之间的关系: 
 

  • 继承结构 —— DispatcherServlet与Spring容器(WebApplicationContext)之间的关系
  • 数据结构 —— DispatcherServlet与组件之间的关系

所以,其实我们可以这么说:SpringMVC的整个运行体系,是由DispatcherServlet、组件和容器这三者共同构成的。 

在这个运行体系中,DispatcherServlet是逻辑处理的调度中心,组件则是被调度的操作对象。而容器在这里所起到的作用,是协助DispatcherServlet更好地对组件进行管理。这就相当于一个工厂招了一大批的工人,并把工人划分到一个统一的工作车间而便于管理。在工厂要进行生产活动时,只需要从工作车间把工人分派到相应的生产流水线上即可。 

笔者在这里引用Spring官方reference中的一幅图,对三者之间的关系进行简单的描述: 

17a6f37b-340c-31a7-9352-944f87bb6a01.png

:在这幅图中,我们除了看到在图的左半边DispatcherServlet、组件和容器这三者之间的调用关系以外,还可以看到SpringMVC的运行体系与其它运行体系之间存在着关系。有关这一点,我们在之后的讨论中会详细展开。 

既然是三个元素之间的关系表述,我们必须以两两关系的形式进行归纳: 
 

  • DispatcherServlet - 容器 —— DispatcherServlet对容器进行初始化
  • 容器 - 组件 —— 容器对组件进行全局管理
  • DispatcherServlet - 组件 —— DispatcherServlet对组件进行逻辑调用

值得注意的是,在上面这幅图中,三大元素之间的两两关系其实表现得并不明显,尤其是“容器 - 组件”和“DispatcherServlet - 组件”之间的关系。这主要是由于Spring官方reference所给出的这幅图是一个静态的关系表述,如果从动态的观点来对整个过程加以审视,我们就不得不将SpringMVC的运行体系与之前所提到的运行主线联系在一起,看看这些元素在不同的逻辑主线中所起到的作用。 

接下来,我们就分别看看DispatcherServlet的两条运行主线。 

DispatcherServlet的初始化主线 

对于DispatcherServlet的初始化主线,我们首先应该明确几个基本观点: 
 

  • 初始化主线的驱动要素 —— servlet中的init方法
  • 初始化主线的执行次序 —— HttpServletBean -> FrameworkServlet -> DispatcherServlet
  • 初始化主线的操作对象 —— Spring容器(WebApplicationContext)和组件

这三个基本观点,可以说是我们对之前所有讨论的一个小结。明确了这些内容,我们就可以更加深入地看看DispatcherServlet初始化主线的过程: 

61b32fbb-1c8f-35ae-91cd-05dfd027b123.png

在这幅图中,我们站在一个动态的角度将DispatcherServlet、容器(WebApplicationContext)和组件这三者之间的关系表述出来,同时给出了这三者之间的运行顺序和逻辑过程。读者或许对其中的绝大多数细节还很陌生,甚至有一种无从下手的感觉。这没有关系,大家可以首先抓住图中的执行线,回忆一下之前有关DispatcherServlet的继承结构和数据结构的内容。接下来,我们就图中的内容逐一进行解释。 

【WebApplicationContext的初始化】 

之前我们讨论了DispatcherServlet对于WebApplicationContext的初始化是在FrameworkServlet中完成的,不过我们并没有细究其中的细节。在默认情况下,这个初始化过程是由web.xml中的入口程序配置所驱动的: 
 

Xml代码 

 收藏代码

  1. <!-- Processes application requests -->  
  2. <servlet>  
  3.     <servlet-name>dispatcher</servlet-name>  
  4.     <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
  5.     <load-on-startup>1</load-on-startup>  
  6. </servlet>  
  7.           
  8. <servlet-mapping>  
  9.     <servlet-name>dispatcher</servlet-name>  
  10.     <url-pattern>/**</url-pattern>  
  11. </servlet-mapping>  



我们已经不止一次提到过这段配置,不过在这之前都没有对这段配置做过什么很详细的分析。事实上,这段入口程序的配置中隐藏了SpringMVC的两大要素(核心分发器Dispatcher和核心配置文件[servlet-name]-servlet.xml)之间的关系表述: 
 

downpour 写道

在默认情况下,web.xml配置节点中<servlet-name>的值就是建立起核心分发器DispatcherServlet与核心配置文件之间联系的桥梁。DispatcherServlet在初始化时会加载位置在/WEB-INF/[servlet-name]-servlet.xml的配置文件作为SpringMVC的核心配置。



SpringMVC在这里采用了一个“命名约定”的方法进行关系映射,这种方法很廉价也很管用。以上面的配置为例,我们就必须在/WEB-INF/目录下,放一个名为dispatcher-servlet.xml的Spring配置文件作为SpringMVC的核心配置用以指定SpringMVC的基本组件声明定义。 

这看上去似乎有一点别扭,因为在实际项目中,我们通常喜欢把配置文件放在classpath下,并使用不同的package进行区分。例如,在基于Maven的项目结构中,所有的配置文件应置于src/main/resources目录下,这样才比较符合配置文件统一化管理的最佳实践。 

于是,Spring提供了一个初始化的配置选项,通过指定contextConfigLocation选项来自定义SpringMVC核心配置文件的位置: 
 

Xml代码 

 收藏代码

  1. <!-- Processes application requests -->  
  2. <servlet>  
  3.     <servlet-name>dispatcher</servlet-name>  
  4.     <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
  5.     <init-param>  
  6.         <param-name>contextConfigLocation</param-name>  
  7.         <param-value>classpath:web/applicationContext-dispatcherServlet.xml</param-value>  
  8.     </init-param>  
  9.     <load-on-startup>1</load-on-startup>  
  10. </servlet>  
  11.           
  12. <servlet-mapping>  
  13.     <servlet-name>dispatcher</servlet-name>  
  14.     <url-pattern>/</url-pattern>  
  15. </servlet-mapping>  



这样一来,DispatcherServlet在初始化时,就会自动加载在classpath下,web这个package下名为applicationContext-dispatcherServlet.xml的文件作为其核心配置并用以初始化容器(WebApplicationContext)。

当然,这只是DispatcherServlet在进行WebApplicationContext初始化过程中的配置选项之一。我们可以在Spring的官方reference中找到相应的配置选项,有兴趣的读者可以参照reference的说明进行尝试: 

2a2f0d65-b1de-3dcc-8c54-3c4e9d87f6c2.png

所有的这些配置选项,实际上都是为了让DispatcherServlet对WebApplicationContext的初始化过程显得更加自然。不过这只是完成了容器(WebApplicationContext)的构建工作,那么容器所管理的那些组件,又是如何进行初始化的呢? 
 

downpour 写道

结论 SpringMVC核心配置文件中所有的bean定义,就是SpringMVC的组件定义,也是DispatcherServlet在初始化容器(WebApplicationContext)时,所要进行初始化的组件。



在上一篇文章我们谈到组件的时候就曾经提到,SpringMVC自身对于组件并未实现一套完整的管理机制,而是借用了Spring Framework核心框架中容器的概念,将所有的组件纳入到容器中进行管理。所以,SpringMVC的核心配置文件使用与传统Spring Framework相同的配置形式,而整个管理的体系也是一脉相承的。 

:Spring3.0之后,单独为SpringMVC建立了专用的schema,从而使得我们可以使用Schema Based XML来对SpringMVC的组件进行定义。不过我们可以将其视作是传统Spring配置的一个补充,而不要过于纠结不同的配置格式。 

我们知道,SpringMVC的组件是一个个的接口定义,当我们在SpringMVC的核心配置文件中定义一个组件时,使用的却是组件的实现类: 
 

Xml代码 

 收藏代码

  1. <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">  
  2.     <property name="prefix" value="/" />  
  3.     <property name="suffix" value=".jsp" />  
  4. </bean>  



这也就是Spring管理组件的模式:用具体的实现类来指定接口的行为方式。不同的实现类,代表着不同的组件行为模式,它们在Spring容器中是可以共存的: 
 

Xml代码 

 收藏代码

  1. <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">  
  2.     <property name="prefix" value="/" />  
  3.     <property name="suffix" value=".jsp" />  
  4. </bean>  
  5.   
  6. <bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">  
  7.     <property name="prefix" value="/" />  
  8.     <property name="suffix" value=".ftl" />  
  9. </bean>  



所以,Spring的容器就像是一个聚宝盆,它只负责承载对象,管理好对象的生命周期,而并不关心一个组件接口到底有多少种实现类或者行为模式。这也就是我们在上面那幅图中,画了多个HandlerMappings、HandlerAdapters和ViewResolvers的原因:一个组件的多种行为模式可以在容器中共存,容器将负责对这些实现类进行管理。而具体如何使用这些对象,则由应用程序自身来决定。 

如此一来,我们可以大致概括一下WebApplicationContext初始化的两个逻辑层次: 
 

  • DispatcherServlet负责对容器(WebApplicationContext)进行初始化。
  • 容器(WebApplicationContext)将读取SpringMVC的核心配置文件进行组件的实例化。

整个过程,我们把应用程序的日志级别调低,可以进行非常详细的观察: 
 

引用

14:15:27,037 DEBUG StandardServletEnvironment:100 - Initializing new StandardServletEnvironment 
14:15:27,128 DEBUG DispatcherServlet:115 - Initializing servlet 'dispatcher' 
14:15:27,438  INFO DispatcherServlet:444 - FrameworkServlet 'dispatcher': initialization started 
14:15:27,449 DEBUG DispatcherServlet:572 - Servlet with name 'dispatcher' will try to create custom WebApplicationContext context of class 'org.springframework.web.context.support.XmlWebApplicationContext', using parent context [null] 
1571 [main] INFO /sample - Initializing Spring FrameworkServlet 'dispatcher' 
14:15:27,505 DEBUG StandardServletEnvironment:100 - Initializing new StandardServletEnvironment 
14:15:27,546  INFO XmlWebApplicationContext:495 - Refreshing WebApplicationContext for namespace 'dispatcher-servlet': startup date [Mon Feb 06 14:15:27 CST 2012]; root of context hierarchy 
14:15:27,689  INFO XmlBeanDefinitionReader:315 - Loading XML bean definitions from class path resource [web/applicationContext-dispatcherServlet.xml] 
14:15:27,872 DEBUG PluggableSchemaResolver:140 - Loading schema mappings from [META-INF/spring.schemas] 
14:15:28,442 DEBUG PathMatchingResourcePatternResolver:550 - Looking for matching resources in directory tree [D:\Work\Demo2do\Sample\target\classes\com\demo2do\sample\web\controller] 
14:15:28,442 DEBUG PathMatchingResourcePatternResolver:612 - Searching directory [D:\Work\Demo2do\Sample\target\classes\com\demo2do\sample\web\controller] for files matching pattern [D:/Work/Demo2do/Sample/target/classes/com/demo2do/sample/web/controller/**/*.class] 
14:15:28,450 DEBUG PathMatchingResourcePatternResolver:351 - Resolved location pattern [classpath*:com/demo2do/sample/web/controller/**/*.class] to resources [file [D:\Work\Demo2do\Sample\target\classes\com\demo2do\sample\web\controller\BlogController.class], file [D:\Work\Demo2do\Sample\target\classes\com\demo2do\sample\web\controller\UserController.class]] 
14:15:28,569 DEBUG ClassPathBeanDefinitionScanner:243 - Identified candidate component class: file [D:\Work\Demo2do\Sample\target\classes\com\demo2do\sample\web\controller\BlogController.class] 
14:15:28,571 DEBUG ClassPathBeanDefinitionScanner:243 - Identified candidate component class: file [D:\Work\Demo2do\Sample\target\classes\com\demo2do\sample\web\controller\UserController.class] 
14:15:28,634 DEBUG BeanDefinitionParserDelegate:497 - Neither XML 'id' nor 'name' specified - using generated bean name [org.springframework.web.servlet.view.InternalResourceViewResolver#0] 
14:15:28,635 DEBUG XmlBeanDefinitionReader:216 - Loaded 19 bean definitions from location pattern [classpath:web/applicationContext-dispatcherServlet.xml] 
14:15:28,635 DEBUG XmlWebApplicationContext:525 - Bean factory for WebApplicationContext for namespace 'dispatcher-servlet': org.springframework.beans.factory.support.DefaultListableBeanFactory@2321b59a: defining beans [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#0,org.springframework.format.support.FormattingConversionServiceFactoryBean#0,org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#0,org.springframework.web.servlet.handler.MappedInterceptor#0,org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#0,org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver#0,org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver#0,org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,blogController,userController,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.web.servlet.resource.ResourceHttpRequestHandler#0,org.springframework.web.servlet.handler.SimpleUrlHandlerMapping#0,org.springframework.web.servlet.view.InternalResourceViewResolver#0]; root of factory hierarchy 
14:15:29,015 DEBUG RequestMappingHandlerMapping:98 - Looking for request mappings in application context: WebApplicationContext for namespace 'dispatcher-servlet': startup date [Mon Feb 06 14:15:27 CST 2012]; root of context hierarchy 
14:15:29,037  INFO RequestMappingHandlerMapping:188 - Mapped "{[/blog],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.web.servlet.ModelAndView com.demo2do.station.web.controller.BlogController.index() 
14:15:29,039  INFO RequestMappingHandlerMapping:188 - Mapped "{[/login],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.web.servlet.ModelAndView com.demo2do.station.web.controller.UserController.login(java.lang.String,java.lang.String) 
14:15:29,040 DEBUG DefaultListableBeanFactory:458 - Finished creating instance of bean 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#0' 
14:15:29,460 DEBUG BeanNameUrlHandlerMapping:71 - Looking for URL mappings in application context: WebApplicationContext for namespace 'dispatcher-servlet': startup date [Mon Feb 06 14:15:27 CST 2012]; root of context hierarchy 
14:15:29,539 DEBUG DefaultListableBeanFactory:458 - Finished creating instance of bean 'org.springframework.web.servlet.resource.ResourceHttpRequestHandler#0' 
14:15:29,540 DEBUG DefaultListableBeanFactory:217 - Creating shared instance of singleton bean 'org.springframework.web.servlet.handler.SimpleUrlHandlerMapping#0' 
14:15:29,555  INFO SimpleUrlHandlerMapping:314 - Mapped URL path [/static/**] onto handler 'org.springframework.web.servlet.resource.ResourceHttpRequestHandler#0' 
14:15:29,556 DEBUG DefaultListableBeanFactory:458 - Finished creating instance of bean 'org.springframework.web.servlet.handler.SimpleUrlHandlerMapping#0' 
14:15:29,827 DEBUG DispatcherServlet:523 - Published WebApplicationContext of servlet 'dispatcher' as ServletContext attribute with name [org.springframework.web.servlet.FrameworkServlet.CONTEXT.dispatcher] 
14:15:29,827  INFO DispatcherServlet:463 - FrameworkServlet 'dispatcher': initialization completed in 2389 ms 
14:15:29,827 DEBUG DispatcherServlet:136 - Servlet 'dispatcher' configured successfully 
4047 [main] INFO org.mortbay.log - Started SelectChannelConnector@0.0.0.0:8080 
Jetty Server started, use 4267 ms 



在这段启动日志(笔者进行了一定删减)中,笔者刻意将WebApplicationContext的初始化的标志日志使用红色的标进行区分,而将核心配置文件的读取位置和组件定义初始化的标志日志使用蓝色标记加以区分。相信有了这段日志的帮助,读者应该可以对WebApplicationContext的初始化过程有了更加直观的认识。 

:启动日志是我们研究SpringMVC的主要途径之一,之后我们还将反复使用这种方法对SpringMVC的运行过程进行研究。读者应该仔细品味每一条日志的作用,从而能够与之后的分析讲解呼应起来。 

或许读者对WebApplicationContext对组件进行初始化的过程还有点困惑,大家不妨先将这个过程省略,把握住整个DispatcherServlet的大方向。我们在之后的文章中,还将对SpringMVC的组件、这些组件的定义以及组件的初始化方式做进一步的分析和探讨。 

到此为止,图中顺着FrameworkServlet的那些箭头,我们已经交代清楚,读者可以回味一下整个过程。 

【独立的WebApplicationContext体系】 

独立的WebApplicationContext体系,是SpringMVC初始化主线中的一个非常重要的概念。回顾一下刚才曾经提到过的DispatcherServlet、容器和组件三者之间的关系,我们在引用的那副官方reference的示意图中,实际上已经包含了这一层意思: 
 

downpour 写道

结论 在DispatcherServlet初始化的过程中所构建的WebApplicationContext独立于Spring自身的所构建的其他WebApplicationContext体系而存在。



稍有一些Spring编程经验的程序员,对于下面的配置应该非常熟悉: 
 

Xml代码 

 收藏代码

  1. <context-param>  
  2.     <param-name>contextConfigLocation</param-name>  
  3.     <param-value>classpath:context/applicationContext-*.xml</param-value>  
  4. </context-param>  
  5.       
  6. <listener>  
  7.     <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>  
  8. </listener>  



在上面的代码中,我们定义了一个Listener,它会在整个Web应用程序启动的时候运行一次,并初始化传统意义上的Spring的容器。这也是一般情况下,当并不使用SpringMVC作为我们的表示层解决方案,却希望在我们的Web应用程序中使用Spring相关功能时所采取的一种配置方式。 

如果我们要在这里引入SpringMVC,整个配置看上去就像这样: 
 

Xml代码 

 收藏代码

  1. <context-param>  
  2.     <param-name>contextConfigLocation</param-name>  
  3.     <param-value>classpath:context/applicationContext-*.xml</param-value>  
  4. </context-param>  
  5.       
  6. <listener>  
  7.     <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>  
  8. </listener>  
  9.       
  10. <!-- Processes application requests -->  
  11. <servlet>  
  12.     <servlet-name>dispatcher</servlet-name>  
  13.     <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
  14.     <init-param>  
  15.         <param-name>contextConfigLocation</param-name>  
  16.         <param-value>classpath:web/applicationContext-dispatcherServlet.xml</param-value>  
  17.     </init-param>  
  18.     <load-on-startup>1</load-on-startup>  
  19. </servlet>  
  20.           
  21. <servlet-mapping>  
  22.     <servlet-name>dispatcher</servlet-name>  
  23.     <url-pattern>/</url-pattern>  
  24. </servlet-mapping>  



在这种情况下,DispatcherServlet和ContextLoaderListener会分别构建不同作用范围的容器(WebApplicationContext)。我们可以引入两个不同的概念来对其进行表述:ContextLoaderListener所初始化的容器,我们称之为Root WebApplicationContext;而DispatcherServlet所初始化的容器,是SpringMVC WebApplicationContext。 

同样采取日志分析的方法,加入了ContextLoaderListener之后,整个启动日志变成了这样: 
 

引用

[main] INFO /sample - Initializing Spring root WebApplicationContext 
14:56:42,261  INFO ContextLoader:272 - Root WebApplicationContext: initialization started 
14:56:42,343 DEBUG StandardServletEnvironment:100 - Initializing new StandardServletEnvironment 
14:56:42,365  INFO XmlWebApplicationContext:495 - Refreshing Root WebApplicationContext: startup date [Mon Feb 06 14:56:42 CST 2012]; root of context hierarchy 
14:56:42,441 DEBUG PathMatchingResourcePatternResolver:550 - Looking for matching resources in directory tree [D:\Work\Demo2do\Sample\target\classes\context] 
14:56:42,442 DEBUG PathMatchingResourcePatternResolver:612 - Searching directory [D:\Work\Demo2do\Sample\target\classes\context] for files matching pattern [D:/Work/Demo2do/Sample/target/classes/context/applicationContext-*.xml] 
14:56:42,446 DEBUG PathMatchingResourcePatternResolver:351 - Resolved location pattern [classpath:context/applicationContext-*.xml] to resources [file [D:\Work\Demo2do\Sample\target\classes\context\applicationContext-configuration.xml]] 
14:56:42,447  INFO XmlBeanDefinitionReader:315 - Loading XML bean definitions from file [D:\Work\Demo2do\Sample\target\classes\context\applicationContext-configuration.xml] 
14:56:42,486 DEBUG PluggableSchemaResolver:140 - Loading schema mappings from [META-INF/spring.schemas] 
14:56:42,597 DEBUG DefaultBeanDefinitionDocumentReader:108 - Loading bean definitions 
14:56:42,658 DEBUG PathMatchingResourcePatternResolver:550 - Looking for matching resources in directory tree [D:\Work\Demo2do\Sample\target\classes\com\demo2do\sample] 
14:56:42,699 DEBUG ClassPathBeanDefinitionScanner:243 - Identified candidate component class: file [D:\Work\Demo2do\Sample\target\classes\com\demo2do\sample\service\impl\BlogServiceImpl.class] 
14:56:42,750 DEBUG XmlBeanDefinitionReader:216 - Loaded 5 bean definitions from location pattern [classpath:context/applicationContext-*.xml] 
14:56:42,750 DEBUG XmlWebApplicationContext:525 - Bean factory for Root WebApplicationContext: org.springframework.beans.factory.support.DefaultListableBeanFactory@478e4327: defining beans [blogService,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor]; root of factory hierarchy 
14:56:42,860 DEBUG ContextLoader:296 - Published root WebApplicationContext as ServletContext attribute with name [org.springframework.web.context.WebApplicationContext.ROOT] 
14:56:42,860  INFO ContextLoader:301 - Root WebApplicationContext: initialization completed in 596 ms 


14:56:42,935 DEBUG DispatcherServlet:115 - Initializing servlet 'dispatcher' 
14:56:42,974  INFO DispatcherServlet:444 - FrameworkServlet 'dispatcher': initialization started 
14:56:42,974 DEBUG DispatcherServlet:572 - Servlet with name 'dispatcher' will try to create custom WebApplicationContext context of class 'org.springframework.web.context.support.XmlWebApplicationContext', using parent context [Root WebApplicationContext: startup date [Mon Feb 06 14:56:42 CST 2012]; root of context hierarchy] 
14:56:42,979  INFO XmlWebApplicationContext:495 - Refreshing WebApplicationContext for namespace 'dispatcher-servlet': startup date [Mon Feb 06 14:56:42 CST 2012]; parent: Root WebApplicationContext 
14:56:42,983  INFO XmlBeanDefinitionReader:315 - Loading XML bean definitions from class path resource [web/applicationContext-dispatcherServlet.xml] 
14:56:42,987 DEBUG PluggableSchemaResolver:140 - Loading schema mappings from [META-INF/spring.schemas] 
14:56:43,035 DEBUG DefaultBeanDefinitionDocumentReader:108 - Loading bean definitions 
14:56:43,075 DEBUG PathMatchingResourcePatternResolver:550 - Looking for matching resources in directory tree [D:\Work\Demo2do\Sample\target\classes\com\demo2do\sample\web\controller] 
14:56:43,075 DEBUG PathMatchingResourcePatternResolver:612 - Searching directory [D:\Work\Demo2do\Sample\target\classes\com\demo2do\sample\web\controller] for files matching pattern [D:/Work/Demo2do/Sample/target/classes/com/demo2do/sample/web/controller/**/*.class] 
14:56:43,077 DEBUG PathMatchingResourcePatternResolver:351 - Resolved location pattern [classpath*:com/demo2do/sample/web/controller/**/*.class] to resources [file [D:\Work\Demo2do\Sample\target\classes\com\demo2do\sample\web\controller\BlogController.class], file [D:\Work\Demo2do\Sample\target\classes\com\demo2do\sample\web\controller\UserController.class]] 
14:56:43,079 DEBUG ClassPathBeanDefinitionScanner:243 - Identified candidate component class: file [D:\Work\Demo2do\Sample\target\classes\com\demo2do\sample\web\controller\BlogController.class] 
14:56:43,080 DEBUG ClassPathBeanDefinitionScanner:243 - Identified candidate component class: file [D:\Work\Demo2do\Sample\target\classes\com\demo2do\sample\web\controller\UserController.class] 
14:56:43,089 DEBUG XmlBeanDefinitionReader:216 - Loaded 19 bean definitions from location pattern [classpath:web/applicationContext-dispatcherServlet.xml] 
14:56:43,089 DEBUG XmlWebApplicationContext:525 - Bean factory for WebApplicationContext for namespace 'dispatcher-servlet': org.springframework.beans.factory.support.DefaultListableBeanFactory@5e6458a6: defining beans [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#0,org.springframework.format.support.FormattingConversionServiceFactoryBean#0,org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#0,org.springframework.web.servlet.handler.MappedInterceptor#0,org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#0,org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver#0,org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver#0,org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,blogController,userController,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.web.servlet.resource.ResourceHttpRequestHandler#0,org.springframework.web.servlet.handler.SimpleUrlHandlerMapping#0,org.springframework.web.servlet.view.InternalResourceViewResolver#0]; parent: org.springframework.beans.factory.support.DefaultListableBeanFactory@478e4327 
14:56:43,323 DEBUG RequestMappingHandlerMapping:98 - Looking for request mappings in application context: WebApplicationContext for namespace 'dispatcher-servlet': startup date [Mon Feb 06 14:56:42 CST 2012]; parent: Root WebApplicationContext 
14:56:43,345  INFO RequestMappingHandlerMapping:188 - Mapped "{[/blog],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.web.servlet.ModelAndView com.demo2do.sample.web.controller.BlogController.index() 
14:56:43,346  INFO RequestMappingHandlerMapping:188 - Mapped "{[/login],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.web.servlet.ModelAndView com.demo2do.sample.web.controller.UserController.login(java.lang.String,java.lang.String) 
14:56:43,707 DEBUG BeanNameUrlHandlerMapping:71 - Looking for URL mappings in application context: WebApplicationContext for namespace 'dispatcher-servlet': startup date [Mon Feb 06 14:56:42 CST 2012]; parent: Root WebApplicationContext 
14:56:43,828  INFO SimpleUrlHandlerMapping:314 - Mapped URL path [/static/**] onto handler 'org.springframework.web.servlet.resource.ResourceHttpRequestHandler#0' 
14:56:43,883 DEBUG DispatcherServlet:523 - Published WebApplicationContext of servlet 'dispatcher' as ServletContext attribute with name [org.springframework.web.servlet.FrameworkServlet.CONTEXT.dispatcher] 
14:56:43,883  INFO DispatcherServlet:463 - FrameworkServlet 'dispatcher': initialization completed in 909 ms 
14:56:43,883 DEBUG DispatcherServlet:136 - Servlet 'dispatcher' configured successfully 
2687 [main] INFO org.mortbay.log - Started SelectChannelConnector@0.0.0.0:8080 
Jetty Server started, use 2901 ms



整个启动日志被我们分为了2段。第一段的过程初始化的是Root WebApplicationContext;而第二段的过程初始化的是SpringMVC的WebApplicationContext。我们还是使用了红色的标记和蓝色标记指出了在整个初始化过程中的一些重要事件。其中,有这样一段内容值得我们注意: 
 

引用

14:56:42,979  INFO XmlWebApplicationContext:495 - Refreshing WebApplicationContext for namespace 'dispatcher-servlet': startup date [Mon Feb 06 14:56:42 CST 2012]; parent: Root WebApplicationContext



在这段日志中,非常明确地指出了SpringMVC WebApplicationContext与Root WebApplicationContext之间的关系:从属关系。因为根据这段日志的表述,SpringMVC WebApplicationContext能够感知到Root WebApplicationContext的存在,并且将其作为parent容器。 

Spring正是使用这种Parent-Child的容器关系来对不同的编程层次进行划分。这种我们俗称的父子关系实际上不仅仅是一种从属关系,更是一种引用关系。从刚才的日志分析中,我们可以看出:SpringMVC中所定义的一切组件能够无缝地与Root WebApplicationContext中的组件整合。 

到此为止,我们针对图中以web.xml为核心的箭头分支进行了讲解,读者可以将图中的内容与上面的文字说明对照再次加以理解。 

【组件默认行为的指定】 

DispatcherServlet的初始化主线的执行体系是顺着其继承结构依次进行的,我们在之前曾经讨论过它的执行次序。所以,只有在FrameworkServlet完成了对于WebApplicationContext和组件的初始化之后,执行权才被正式转移到DispatcherServlet中。我们可以来看看DispatcherServlet此时究竟干了哪些事: 
 

Java代码 

 收藏代码

  1. /** 
  2.  * This implementation calls {@link #initStrategies}. 
  3.  */  
  4. @Override  
  5. protected void onRefresh(ApplicationContext context) {  
  6.     initStrategies(context);  
  7. }  
  8.   
  9. /** 
  10.  * Initialize the strategy objects that this servlet uses. 
  11.  * <p>May be overridden in subclasses in order to initialize further strategy objects. 
  12.  */  
  13. protected void initStrategies(ApplicationContext context) {  
  14.     initMultipartResolver(context);  
  15.     initLocaleResolver(context);  
  16.     initThemeResolver(context);  
  17.     initHandlerMappings(context);  
  18.     initHandlerAdapters(context);  
  19.     initHandlerExceptionResolvers(context);  
  20.     initRequestToViewNameTranslator(context);  
  21.     initViewResolvers(context);  
  22.     initFlashMapManager(context);  
  23. }  



onRefresh是FrameworkServlet中预留的扩展方法,在DispatcherServlet中做了一个基本实现:initStrategies。我们粗略一看,很容易就能明白DispatcherServlet到底在这里干些什么了:初始化组件。 

读者或许会问,组件不是已经在WebApplicationContext初始化的时候已经被初始化过了嘛?这里所谓的组件初始化,指的又是什么呢?让我们来看看其中的一个方法的源码: 
 

Java代码 

 收藏代码

  1. /** 
  2.  * Initialize the MultipartResolver used by this class. 
  3.  * <p>If no bean is defined with the given name in the BeanFactory for this namespace, 
  4.  * no multipart handling is provided. 
  5.  */  
  6. private void initMultipartResolver(ApplicationContext context) {  
  7.     try {  
  8.         this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);  
  9.         if (logger.isDebugEnabled()) {  
  10.             logger.debug("Using MultipartResolver [" + this.multipartResolver + "]");  
  11.         }  
  12.     } catch (NoSuchBeanDefinitionException ex) {  
  13.         // Default is no multipart resolver.  
  14.         this.multipartResolver = null;  
  15.         if (logger.isDebugEnabled()) {  
  16.             logger.debug("Unable to locate MultipartResolver with name '" + MULTIPART_RESOLVER_BEAN_NAME +  
  17.                     "': no multipart request handling provided");  
  18.         }  
  19.     }  
  20. }  



原来,这里的初始化,指的是DispatcherServlet从容器(WebApplicationContext)中读取组件的实现类,并缓存于DispatcherServlet内部的过程。还记得我们之前给出的DispatcherServlet的数据结构吗?这些位于DispatcherServlet内部的组件实际上只是一些来源于容器缓存实例,不过它们同样也是DispatcherServlet进行后续操作的基础。 

:我们在第一篇文章中就曾经提到过Servlet实例内部的属性的访问有线程安全问题。而在这里,我们可以看到所有的组件都以Servlet内部属性的形式被调用,充分证实了这些组件本身也都是无状态的单例对象,所以我们在这里不必考虑线程安全的问题。 

如果对上面的代码加以详细分析,我们会发现initMultipartResolver的过程是查找特定MultipartResolver实现类的过程。因为在容器中查找组件的时候,采取的是根据特定名称(MULTIPART_RESOLVER_BEAN_NAME)进行查找的策略。由此,我们可以看到DispatcherServlet进行组件初始化的特点: 
 

downpour 写道

结论 DispatcherServlet中对于组件的初始化过程实际上是应用程序在WebApplicationContext中选择和查找组件实现类的过程,也是指定组件在SpringMVC中的默认行为方式的过程。



除了根据特定名称进行查找的策略以外,我们还对DispatcherServlet中指定SpringMVC默认行为方式的其他的策略进行的总结: 
 

  • 名称查找 —— 根据bean的名字在容器中查找相应的实现类
  • 自动搜索 —— 自动搜索容器中所有某个特定组件(接口)的所有实现类
  • 默认配置 —— 根据一个默认的配置文件指定进行实现类加载

这三条策略恰巧在initHandlerMappings的过程中都有体现,读者可以从其源码中找到相应的线索: 
 

Java代码 

 收藏代码

  1. private void initHandlerAdapters(ApplicationContext context) {  
  2.     this.handlerAdapters = null;  
  3.   
  4.     if (this.detectAllHandlerAdapters) {  
  5.         // Find all HandlerAdapters in the ApplicationContext, including ancestor contexts.  
  6.         Map<String, HandlerAdapter> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);  
  7.         if (!matchingBeans.isEmpty()) {  
  8.             this.handlerAdapters = new ArrayList<HandlerAdapter>(matchingBeans.values());  
  9.             // We keep HandlerAdapters in sorted order.  
  10.             OrderComparator.sort(this.handlerAdapters);  
  11.         }  
  12.     }  
  13.     else {  
  14.         try {  
  15.             HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class);  
  16.             this.handlerAdapters = Collections.singletonList(ha);  
  17.         }  
  18.         catch (NoSuchBeanDefinitionException ex) {  
  19.             // Ignore, we'll add a default HandlerAdapter later.  
  20.         }  
  21.     }  
  22.   
  23.     // Ensure we have at least some HandlerAdapters, by registering  
  24.     // default HandlerAdapters if no other adapters are found.  
  25.     if (this.handlerAdapters == null) {  
  26.         this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class);  
  27.         if (logger.isDebugEnabled()) {  
  28.             logger.debug("No HandlerAdapters found in servlet '" + getServletName() + "': using default");  
  29.         }  
  30.     }  
  31. }  



这里有必要对“默认策略”做一个简要的说明。SpringMVC为一些核心组件设置了默认行为方式的说明,这个说明以一个properties文件的形式位于SpringMVC分发包(例如spring-webmvc-3.1.0.RELEASE.jar)的内部: 

52159265-14d6-38ed-a4a3-6766a9876c53.png 

我们可以观察一下DispatcherServlet.properties的内容: 
 

引用

# Default implementation classes for DispatcherServlet's strategy interfaces. 
# Used as fallback when no matching beans are found in the DispatcherServlet context. 
# Not meant to be customized by application developers. 

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.DefaultFlashMapManager



结合刚才initHandlerMappings的源码,我们可以发现如果没有开启detectAllHandlerAdapters选项或者根据HANDLER_ADAPTER_BEAN_NAME的名称没有找到相应的组件实现类,就会使用DispatcherServlet.properties文件中对于HandlerMapping接口的实现来进行组件默认行为的初始化。 

由此可见,DispatcherServlet.properties中所指定的所有接口的实现方式在Spring的容器WebApplicationContext中总有相应的定义。这一点,我们在组件的讨论中还会详谈。 

这个部分我们的侧重点是图中DispatcherServlet与容器之间的关系。读者需要理解的是图中为什么会有两份组件定义,它们之间的区别在哪里,以及DispatcherServlet在容器中查找组件的三种策略。 

小结 

在本文中,我们对SpringMVC的核心类:DispatcherServlet进行了一番梳理。也对整个SpringMVC的两条主线之一的初始化主线做了详细的分析。 

对于DispatcherServlet而言,重要的其实并不是这个类中的代码和逻辑,而是应该掌握这个类在整个框架中的作用以及与SpringMVC中其他要素的关系。 

对于初始化主线而言,核心其实仅仅在于那张笔者为大家精心打造的图。读者只要掌握了这张图,相信对整个SpringMVC的初始化过程会有一个全新的认识。 

 

4.SpringMVC深度探险(四) —— SpringMVC核心配置文件详解

在上一篇文章中,我们从DispatcherServlet谈起,最终为读者详细分析了SpringMVC的初始化主线的全部过程。整个初始化主线的研究,其实始终围绕着DispatcherServlet、WebApplicationContext和组件这三大元素之间的关系展开。 

在文章写完之后,也陆续收到了一些反馈,其中比较集中的问题,是有关WebApplicationContext对组件进行初始化的过程交代的不够清楚。所以,本文作为上一篇文章的续文,就试图来讲清楚这个话题。 

SpringMVC的核心配置文件 

SpringMVC的核心配置文件,我们从整个专栏的第一篇文章就开始接触。所以,我们在这里首先对SpringMVC的核心配置文件做一些概括性的回顾。 
 

downpour 写道

结论 SpringMVC的核心配置文件是构成SpringMVC应用程序的必要元素之一。



这是我们在讲有关SpringMVC的构成要素时就曾经提到过的一个重要结论。当时我们所说的另外两大必要元素就是DispatcherServlet和Controller。因而,SpringMVC的核心配置文件在整个应用程序中所起到的作用也是举足轻重的。这也就是我们在这里需要补充对这个文件进行详细分析的原因。 
 

downpour 写道

结论 SpringMVC的核心配置文件与传统的Spring Framework的配置文件是一脉相承的。



这个结论很容易理解。作为Spring Framework的一部分,我们可以认为SpringMVC是整个Spring Framework的一个组件。因而两者的配置体系和管理体系完全相同也属情理之中。实际上,SpringMVC所采取的策略,就是借用Spring Framework强大的容器(ApplicationContext)功能,而绝非自行实现。 
 

downpour 写道

结论 SpringMVC的核心配置文件是架起DispatcherServlet与WebApplicationContext之间的桥梁。



我们在web.xml中指定SpringMVC的入口程序DispatcherServlet时,实际上蕴含了一个对核心配置文件的指定过程([servlet-name]-servlet.xml)。当然,我们也可以明确指定这个核心配置文件的位置。这些配置选项,我们已经在上一篇文章中详细介绍过,这里不再重复。 

而上面这一结论,除了说明两者之间的配置关系之外,还包含了一层运行关系:DispatcherServlet负责对WebApplicationContext进行初始化,而初始化的依据,就是这个SpringMVC的核心配置文件。所以,SpringMVC的核心配置文件的内容解读将揭开整个SpringMVC初始化主线的全部秘密。 

如果我们把这个结论与上一个结论结合起来来看,也正因为SpringMVC的核心配置文件使用了与Spring Framework相同的格式,才使其成为DispatcherServlet驾驭Spring的窗口。 
 

downpour 写道

结论 SpringMVC的核心配置文件是SpringMVC中所有组件的定义窗口,通过它我们可以指定整个SpringMVC的行为方式。



这个结论告诉了我们SpringMVC核心配置文件在整个框架中的作用。组件行为模式的多样化,决定了我们必须借助一个容器(WebApplicationContext)来进行统一的管理。而SpringMVC的核心配置文件,就是我们进行组件管理的窗口。 

核心配置文件概览 

说了那么多有关SpringMVC核心配置文件的结论,我们不妨来看一下这个配置文件的概况: 
 

Xml代码 

 收藏代码

  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <beans xmlns="http://www.springframework.org/schema/beans"  
  3.        xmlns:mvc="http://www.springframework.org/schema/mvc"  
  4.        xmlns:context="http://www.springframework.org/schema/context"  
  5.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  6.        xsi:schemaLocation="http://www.springframework.org/schema/beans  
  7.             http://www.springframework.org/schema/beans/spring-beans-3.1.xsd  
  8.             http://www.springframework.org/schema/context   
  9.             http://www.springframework.org/schema/context/spring-context-3.1.xsd  
  10.             http://www.springframework.org/schema/mvc  
  11.             http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd">  
  12.   
  13.     <!-- Enables the Spring MVC @Controller programming model -->  
  14.     <mvc:annotation-driven />  
  15.       
  16.     <context:component-scan base-package="com.demo2do.sample.web.controller" />  
  17.       
  18.     <!-- Handles HTTP GET requests for /static/** by efficiently serving up static resources in the ${webappRoot}/static/ directory -->  
  19.     <mvc:resources mapping="/static/**" location="/static/" />  
  20.   
  21.     <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">    
  22.         <property name="prefix" value="/" />    
  23.         <property name="suffix" value=".jsp" />    
  24.     </bean>    
  25.   
  26. </beans>  



这是一个非常典型的SpringMVC核心配置文件。虽然我们在这里几乎对每一段重要的配置都做了注释,不过可能对于毫无SpringMVC开发经验的读者来说,这段配置基本上还无法阅读。所以接下来,我们就试图对这个文件中的一些细节加以说明。 

【头部声明】 

配置文件中首先进入我们眼帘的是它的头部的一大段声明: 
 

Xml代码 

 收藏代码

  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <beans xmlns="http://www.springframework.org/schema/beans"  
  3.        xmlns:mvc="http://www.springframework.org/schema/mvc"  
  4.        xmlns:context="http://www.springframework.org/schema/context"  
  5.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  6.        xsi:schemaLocation="http://www.springframework.org/schema/beans  
  7.             http://www.springframework.org/schema/beans/spring-beans-3.1.xsd  
  8.             http://www.springframework.org/schema/context   
  9.             http://www.springframework.org/schema/context/spring-context-3.1.xsd  
  10.             http://www.springframework.org/schema/mvc  
  11.             http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd">  
  12.       ......  
  13. </beans>  



这个部分是整个SpringMVC核心配置文件的关键所在。这一段声明,被称之为Schema-based XML的声明部分。有关Schema-based XML的概念,读者可以参考Spring官方的reference: 

Appendix C. XML Schema-based configuration 

Appendix D. Extensible XML authoring 

为了帮助读者快速理解,我们稍后会专门开辟章节针对Schema-based XML的来龙去脉进行讲解。 

【组件定义】 

除了头部声明部分的其他配置部分,就是真正的组件定义部分。在这个部分中,我们可以看到两种不同类型的配置定义模式: 

1. 基于Schema-based XML的配置定义模式 
 

Xml代码 

 收藏代码

  1. <mvc:annotation-driven />  



2. 基于Traditional XML的配置定义模式 
 

Xml代码 

 收藏代码

  1. <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">    
  2.        <property name="prefix" value="/" />    
  3.        <property name="suffix" value=".jsp" />    
  4. lt;/bean>  



两种不同的组件定义模式,其目的是统一的:对SpringMVC中的组件进行声明,指定组件的行为方式。 

虽然两种不同的组件定义模式的外在表现看上去有所不同,但是SpringMVC在对其进行解析时最终都会将其转化为组件的定义而加载到WebApplicationContext之中进行管理。所以我们需要理解的是蕴藏在配置背后的目的而非配置本身的形式。 

至于这两种不同的配置形式之间的关系,我们稍后会在Schema-based XML的讲解中详细展开。 

Schema-based XML 

【基本概念】 

Schema-based XML本身并不是SpringMVC或者Spring Framework独创的一种配置模式。我们可以看看W3C对于其用途的一个大概解释: 
 

W3C 写道

The purpose of an XSD schema is to define and describe a class of XML documents by using schema components to constrain and document the meaning, usage and relationships of their constituent parts: datatypes, elements and their content and attributes and their values. Schemas can also provide for the specification of additional document information, such as normalization and defaulting of attribute and element values. Schemas have facilities for self-documentation. Thus, XML Schema Definition Language: Structures can be used to define, describe and catalogue XML vocabularies for classes of XML documents.



这个解释稍微有点抽象。所以我们可以来看看Spring官方reference对于引入Schema-based XML的说法: 
 

Spring Reference 写道

The central motivation for moving to XML Schema based configuration files was to make Spring XML configuration easier. The 'classic' <bean/>-based approach is good, but its generic-nature comes with a price in terms of configuration overhead.



也就是说,我们引入Schema-based XML是为了对Traditional的XML配置形式进行简化。通过Schema的定义,把一些原本需要通过几个bean的定义或者复杂的bean的组合定义的配置形式,用另外一种简单而可读的配置形式呈现出来。 

所以,我们也可以由此得出一些有用的推论: 
 

downpour 写道

Schema-based XML可以代替Traditional的XML配置形式,在Spring容器中进行组件的定义。



这里的代替一词非常重要,这就意味着传统的XML配置形式在这里会被颠覆,我们在对Schema-based XML进行解读时,需要使用一种全新的语义规范来理解。 
 

downpour 写道

Schema-based XML可以极大改善配置文件的可读性并且缩小配置文件的规模。



这是从引入Schema-based XML的目的反过来得出的推论。因为如果引入Schema-based XML之后,整个配置变得更加复杂,那么Schema-based XML的引入也就失去了意义。 

同时,笔者在这里需要特别强调的是Schema-based XML的引入,实际上是把原本静态的配置动态化、过程化。有关这一点,我们稍后会有说明。 

【引入目的】 

在早期的Spring版本中,只有Traditional XML一种组件定义模式。当时,XML作为Java最好的朋友,自然而然在整个框架中起到了举足轻重的作用。根据Spring的设计原则,所有纳入WebApplicationContext中管理的对象,都被映射为XML中的一个<bean>节点,通过对于<bean>节点的一个完整描述,我们可以有效地将整个应用程序中所有的对象都纳入到一个统一的容器中进行管理。 

这种统一化的描述,带来的是管理上的便利,不过同时也带来了逻辑上的困扰。因为统一的节点,降低了配置的难度,我们几乎只需要将<bean>节点与Java的对象模型对应起来即可。(有一定经验的Spring程序员可以回忆一下,我们在编写Spring配置文件时,是否也是一个将配置选项与Java对象中属性或者方法对应起来的过程)但是这样的配置形式本身并不具备逻辑语义,也就是说我们无法非常直观地看出某一个特定的<bean>定义,从逻辑上它到底想说明什么问题? 

这也就是后来Schema-based XML开始被引入并流行开来的重要原因。从形式上看,Schema-based XML相比较Traditional XML至少有三个方面的优势: 
 

  • namespace —— 拥有很明确的逻辑分类
  • element —— 拥有很明确的过程语义
  • attributes —— 拥有很简明的配置选项

这三方面的优势,我们可以用一幅图来进行说明: 

168ebe76-c51f-35ab-859b-14d2c8fde863.png 

在图中,我们分别用上下两层来说明某一个配置节点的结构名称以及它们的具体作用。由此可见,Schema-based XML中的配置节点拥有比较鲜明的功能特性,通过namespace、element和attributes这三大元素之间的配合,共同完成对一个动态过程的描述。 

例如,<mvc:annotation-driven />这段配置想要表达的意思,就是在mvc的空间内实现Annotation驱动的配置方式。其中,mvc表示配置的有效范围,annotation-driven则表达了一个动态的过程,实际的逻辑含义是:整个SpringMVC的实现是基于Annotation模式,请为我注册相关的行为模式。 

这种配置方式下,可读性大大提高:我们无需再去理解其中的实现细节。同时,配置的简易性也大大提高:我们甚至不用去关心哪些bean被定义了。 

所以总体来说,Schema-based XML的引入,对于配置的简化是一个极大的进步。 

【构成要素】 

在Spring中,一个Schema-based XML有两大构成要素:过程实现配置定义。 

先谈谈过程实现。所谓过程实现,其实就是我们刚才所举的那个例子中,实现实际背后逻辑的过程。这个过程由两个Java接口来进行表述: 
 

  • NamespaceHandler —— 对Schema定义中namespace的逻辑处理接口
  • BeanDefinitionParser —— 对Schema定义中element的逻辑处理接口

很显然,NamespaceHandler是入口程序,它包含了所有的属于该namespace定义下所有element的处理调用,所以BeanDefinitionParser的实现就成为了NamespaceHandler的调用对象了。这一点,我们可以通过NamesapceHandler的MVC实现类来加以证明: 
 

Java代码 

 收藏代码

  1. public void init() {  
  2.     registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser());  
  3.     registerBeanDefinitionParser("default-servlet-handler", new DefaultServletHandlerBeanDefinitionParser());  
  4.     registerBeanDefinitionParser("interceptors", new InterceptorsBeanDefinitionParser());         
  5.     registerBeanDefinitionParser("resources", new ResourcesBeanDefinitionParser());  
  6.     registerBeanDefinitionParser("view-controller", new ViewControllerBeanDefinitionParser());  
  7. }  



我们可以看到,MvcNamespaceHandler的执行,只不过依次调用了不同的BeanDefinitionParser的实现类而已,而每一个BeanDefinitionParser的实现,则对应于Schema定义中的element逻辑处理。例如,AnnotationDrivenBeanDefinitionParser对应于:<mvc:annotation-driven />这个element实现;ResourcesBeanDefinitionParser则对应于<mvc:resources />的实现等等。 

所以,要具体了解每个element的行为过程,只要研究每一个BeanDefinitionParser的实现类即可。我们以整个MVC空间中最重要的一个节点<mvc:annotation-driven />为例,对AnnotationDrivenBeanDefinitionParser进行说明,其源码如下: 
 

Java代码 

 收藏代码

  1. public BeanDefinition parse(Element element, ParserContext parserContext) {  
  2.     Object source = parserContext.extractSource(element);  
  3.   
  4.     CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source);  
  5.     parserContext.pushContainingComponent(compDefinition);  
  6.   
  7.     RootBeanDefinition methodMappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class);  
  8.     methodMappingDef.setSource(source);  
  9.     methodMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);  
  10.     methodMappingDef.getPropertyValues().add("order", 0);  
  11.     String methodMappingName = parserContext.getReaderContext().registerWithGeneratedName(methodMappingDef);  
  12.   
  13.     RuntimeBeanReference conversionService = getConversionService(element, source, parserContext);  
  14.     RuntimeBeanReference validator = getValidator(element, source, parserContext);  
  15.     RuntimeBeanReference messageCodesResolver = getMessageCodesResolver(element, source, parserContext);  
  16.   
  17.     RootBeanDefinition bindingDef = new RootBeanDefinition(ConfigurableWebBindingInitializer.class);  
  18.     bindingDef.setSource(source);  
  19.     bindingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);  
  20.     bindingDef.getPropertyValues().add("conversionService", conversionService);  
  21.     bindingDef.getPropertyValues().add("validator", validator);  
  22.     bindingDef.getPropertyValues().add("messageCodesResolver", messageCodesResolver);  
  23.   
  24.     ManagedList<?> messageConverters = getMessageConverters(element, source, parserContext);  
  25.     ManagedList<?> argumentResolvers = getArgumentResolvers(element, source, parserContext);  
  26.     ManagedList<?> returnValueHandlers = getReturnValueHandlers(element, source, parserContext);  
  27.           
  28.     RootBeanDefinition methodAdapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class);  
  29.     methodAdapterDef.setSource(source);  
  30.     methodAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);  
  31.     methodAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef);  
  32.     methodAdapterDef.getPropertyValues().add("messageConverters", messageConverters);  
  33.     if (element.hasAttribute("ignoreDefaultModelOnRedirect")) {  
  34.         Boolean ignoreDefaultModel = Boolean.valueOf(element.getAttribute("ignoreDefaultModelOnRedirect"));  
  35.   
  36. methodAdapterDef.getPropertyValues().add("ignoreDefaultModelOnRedirect", ignoreDefaultModel);  
  37.     }  
  38.     if (argumentResolvers != null) {  
  39.         methodAdapterDef.getPropertyValues().add("customArgumentResolvers", argumentResolvers);  
  40.     }  
  41.     if (returnValueHandlers != null) {  
  42.             methodAdapterDef.getPropertyValues().add("customReturnValueHandlers", returnValueHandlers);  
  43.     }  
  44.     String methodAdapterName = parserContext.getReaderContext().registerWithGeneratedName(methodAdapterDef);  
  45.   
  46.     RootBeanDefinition csInterceptorDef = new RootBeanDefinition(ConversionServiceExposingInterceptor.class);  
  47.     csInterceptorDef.setSource(source);  
  48.     csInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, conversionService);  
  49.     RootBeanDefinition mappedCsInterceptorDef = new RootBeanDefinition(MappedInterceptor.class);  
  50.     mappedCsInterceptorDef.setSource(source);  
  51.         mappedCsInterceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);  
  52.         mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, (Object) null);  
  53.         mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(1, csInterceptorDef);  
  54.     String mappedInterceptorName = parserContext.getReaderContext().registerWithGeneratedName(mappedCsInterceptorDef);  
  55.   
  56.     RootBeanDefinition methodExceptionResolver = new RootBeanDefinition(ExceptionHandlerExceptionResolver.class);  
  57.     methodExceptionResolver.setSource(source);  
  58.         methodExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);  
  59.         methodExceptionResolver.getPropertyValues().add("messageConverters", messageConverters);  
  60.         methodExceptionResolver.getPropertyValues().add("order", 0);  
  61.     String methodExceptionResolverName =  
  62. parserContext.getReaderContext().registerWithGeneratedName(methodExceptionResolver);  
  63.   
  64.     RootBeanDefinition responseStatusExceptionResolver = new RootBeanDefinition(ResponseStatusExceptionResolver.class);  
  65.     responseStatusExceptionResolver.setSource(source);  
  66.         responseStatusExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);  
  67.         responseStatusExceptionResolver.getPropertyValues().add("order", 1);  
  68.     String responseStatusExceptionResolverName =  
  69.                 parserContext.getReaderContext().registerWithGeneratedName(responseStatusExceptionResolver);  
  70.   
  71.     RootBeanDefinition defaultExceptionResolver = new RootBeanDefinition(DefaultHandlerExceptionResolver.class);  
  72.     defaultExceptionResolver.setSource(source);  
  73.         defaultExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);  
  74.         defaultExceptionResolver.getPropertyValues().add("order", 2);  
  75.     String defaultExceptionResolverName =  
  76.                 parserContext.getReaderContext().registerWithGeneratedName(defaultExceptionResolver);  
  77.   
  78.     parserContext.registerComponent(new BeanComponentDefinition(methodMappingDef, methodMappingName));  
  79.     parserContext.registerComponent(new BeanComponentDefinition(methodAdapterDef, methodAdapterName));  
  80.     parserContext.registerComponent(new BeanComponentDefinition(methodExceptionResolver, methodExceptionResolverName));  
  81.     parserContext.registerComponent(new BeanComponentDefinition(responseStatusExceptionResolver, responseStatusExceptionResolverName));  
  82.     parserContext.registerComponent(new BeanComponentDefinition(defaultExceptionResolver, defaultExceptionResolverName));  
  83.     parserContext.registerComponent(new BeanComponentDefinition(mappedCsInterceptorDef, mappedInterceptorName));  
  84.   
  85.     // Ensure BeanNameUrlHandlerMapping (SPR-8289) and default HandlerAdapters are not "turned off"   
  86.         MvcNamespaceUtils.registerDefaultComponents(parserContext, source);  
  87.   
  88.     parserContext.popAndRegisterContainingComponent();  
  89.   
  90.     return null;  
  91. }  



整个过程看上去稍显凌乱,不过我们发现其中围绕的一条主线就是:使用编程的方式来对bean进行注册。也就是说,<mvc:annotation-driven />这样一句配置,顶上了我们如此多的bean定义。难怪Schema-based XML被誉为是简化XML配置的绝佳帮手了。 

有了过程实现,我们再来谈谈配置定义。配置定义的目的非常简单,就是通过一些配置文件,将上述的过程实现类串联起来,从而完成整个Schema-based XML的定义。 

整个配置定义,也分为两个部分: 
 

  • Schema定义 —— 一个xsd文件,描述整个Schema空间中element和attribute的定义
  • 注册配置文件 —— 由META-INF/spring.handlers和META-INF/spring.schemas构成,用以注册Schema和Handler

Schema定义是由一个xsd文件完成的。这个文件在Spring发布的时候同时发布在网络上。例如SpringMVC的Schema定义,就发布在这个地址: 

http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd 

同时,这个地址在Spring的发布包中也存有一个备份。这个备份位于SpringMVC的分发包spring-webmvc的JAR包之中。 

38811a12-3a04-37b6-81ac-c1fd78199f28.png 

这样做的好处在于,我们在对Schema进行引用时,可以通过本地寻址来加快加载速度。 

:如果我们回顾一下之前的核心配置文件中的头部声明部分。其中的xsi:schemaLocation声明就是用于指定映射于本地的XSD文件。所以xsi:schemaLocation的定义不是必须的,不过声明它能够使Spring自动查找本地的缓存来进行schema的寻址。 

我们在这里不对XSD文件做过多的内容分析,因为其中不外乎是对element的定义、attributes的定义等等。这些内容是我们进行Schema-based XML配置的核心基础。 

配置定义的另外一个元素构成是META-INF/spring.handlers和META-INF/spring.schemas这两个文件。它们同样位于SpringMVC的分发包下。当我们在XML的头部声明中引用了相关的Schema定义之后,Spring会自动查找spring.schemas和spring.handlers的定义,根据其中指定的NamespaceHandler实现类加载执行。 

有关这个过程,我们在之后的日志分析中还会涉及。 

初始化日志的再分析 

有了Schema Based XML的相关知识,就可以对DispatcherServlet的初始化启动日志做进一步的详细分析。而这次的分析,我们试图弄清楚以下问题: 
 

  • Where —— 组件的声明在哪里?
  • How —— 组件是如何被注册的?
  • What —— 究竟哪些组件被注册了?

对于这三个问题的研究,我们需要结合日志和Schema based XML的运行机理来共同进行分析。 
 

引用

[main] INFO /sample - Initializing Spring FrameworkServlet 'dispatcher' 
19:49:48,670  INFO XmlWebApplicationContext:495 - Refreshing WebApplicationContext for namespace 'dispatcher-servlet': startup date [Thu Feb 16 19:49:48 CST 2012]; parent: Root WebApplicationContext 
19:49:48,674  INFO XmlBeanDefinitionReader:315 - Loading XML bean definitions from class path resource [web/applicationContext-dispatcher.xml]
 

## Schema定位和加载 (开始) ## 

19:49:48,676 DEBUG DefaultDocumentLoader:72 - Using JAXP provider [com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl] 
19:49:48,678 DEBUG PluggableSchemaResolver:140 - Loading schema mappings from [META-INF/spring.schemas] 
19:49:48,690 DEBUG PluggableSchemaResolver:118 - Found XML schema [http://www.springframework.org/schema/beans/spring-beans-3.1.xsd] in classpath: org/springframework/beans/factory/xml/spring-beans-3.1.xsd 
19:49:48,710 DEBUG PluggableSchemaResolver:118 - Found XML schema [http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd] in classpath: org/springframework/web/servlet/config/spring-mvc-3.1.xsd 
19:49:48,715 DEBUG PluggableSchemaResolver:118 - Found XML schema [http://www.springframework.org/schema/tool/spring-tool-3.1.xsd] in classpath: org/springframework/beans/factory/xml/spring-tool-3.1.xsd 
19:49:48,722 DEBUG PluggableSchemaResolver:118 - Found XML schema [http://www.springframework.org/schema/context/spring-context-3.1.xsd] in classpath: org/springframework/context/config/spring-context-3.1.xsd 

## Schema定位和加载 (结束) ## 

## NamespaceHandler执行阶段 (开始) ## 

19:49:48,731 DEBUG DefaultBeanDefinitionDocumentReader:108 - Loading bean definitions 
19:49:48,742 DEBUG DefaultNamespaceHandlerResolver:156 - Loaded NamespaceHandler mappings: {...} 

19:49:48,886 DEBUG PathMatchingResourcePatternResolver:550 - Looking for matching resources in directory tree [D:\Work\Demo2do\Sample\target\classes\com\demo2do\sample\web\controller] 


19:49:48,896 DEBUG XmlBeanDefinitionReader:216 - Loaded 18 bean definitions from location pattern [classpath:web/applicationContext-dispatcher.xml] 
19:49:48,897 DEBUG XmlWebApplicationContext:525 - Bean factory for WebApplicationContext for namespace 'dispatcher-servlet': org.springframework.beans.factory.support.DefaultListableBeanFactory@495c998a: defining beans [[ 
1. org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#0, 
2. org.springframework.format.support.FormattingConversionServiceFactoryBean#0, 
3. org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#0, 
4. org.springframework.web.servlet.handler.MappedInterceptor#0, 
5. org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#0, 
6. org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver#0, 
7. org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver#0, 
8. org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping, 
9. org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter, 
10. org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter, 
11. blogController, 
12. userController, 
13. org.springframework.context.annotation.internalConfigurationAnnotationProcessor, 
14. org.springframework.context.annotation.internalAutowiredAnnotationProcessor, 
15. org.springframework.context.annotation.internalRequiredAnnotationProcessor, 
16. org.springframework.context.annotation.internalCommonAnnotationProcessor, 
17. org.springframework.web.servlet.resource.ResourceHttpRequestHandler#0, 
18. org.springframework.web.servlet.handler.SimpleUrlHandlerMapping#0
 
]; parent: org.springframework.beans.factory.support.DefaultListableBeanFactory@6602e323 
19:49:48,949 DEBUG XmlWebApplicationContext:794 - Unable to locate MessageSource with name 'messageSource': using default [org.springframework.context.support.DelegatingMessageSource@4b2922f6] 
19:49:48,949 DEBUG XmlWebApplicationContext:818 - Unable to locate ApplicationEventMulticaster with name 'applicationEventMulticaster': using default [org.springframework.context.event.SimpleApplicationEventMulticaster@79b66b06] 
19:49:48,949 DEBUG UiApplicationContextUtils:85 - Unable to locate ThemeSource with name 'themeSource': using default [org.springframework.ui.context.support.DelegatingThemeSource@372c9557] 
19:49:49,154 DEBUG RequestMappingHandlerMapping:98 - Looking for request mappings in application context: WebApplicationContext for namespace 'dispatcher-servlet': startup date [Thu Feb 16 19:49:48 CST 2012]; parent: Root WebApplicationContext 
19:49:49,175  INFO RequestMappingHandlerMapping:188 - Mapped "{[/blog],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto 
public org.springframework.web.servlet.ModelAndView com.demo2do.sample.web.controller.BlogController.index() 
19:49:49,177  INFO RequestMappingHandlerMapping:188 - Mapped "{[/register],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto 
public org.springframework.web.servlet.ModelAndView com.demo2do.sample.web.controller.UserController.register(com.demo2do.sample.entity.User) 
19:49:49,180  INFO RequestMappingHandlerMapping:188 - Mapped "{[/login],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto 
public org.springframework.web.servlet.ModelAndView com.demo2do.sample.web.controller.UserController.login(java.lang.String,java.lang.String) 
19:49:49,632 DEBUG BeanNameUrlHandlerMapping:71 - Looking for URL mappings in application context: WebApplicationContext for namespace 'dispatcher-servlet': startup date [Thu Feb 16 19:49:48 CST 2012]; parent: Root WebApplicationContext 
19:49:49,924  INFO SimpleUrlHandlerMapping:314 - Mapped URL path [/static/**] onto handler 'org.springframework.web.servlet.resource.ResourceHttpRequestHandler#0' 

## NamespaceHandler执行阶段 (结束) ## 

19:49:49,956 DEBUG DispatcherServlet:627 - Unable to locate RequestToViewNameTranslator with name 'viewNameTranslator': using default [org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator@4d16318b] 
19:49:49,980 DEBUG DispatcherServlet:667 - No ViewResolvers found in servlet 'dispatcher': using default 
19:49:49,986 DEBUG DispatcherServlet:689 - Unable to locate FlashMapManager with name 'flashMapManager': using default [org.springframework.web.servlet.support.DefaultFlashMapManager@1816daa9] 
19:49:49,986 DEBUG DispatcherServlet:523 - Published WebApplicationContext of servlet 'dispatcher' as ServletContext attribute with name [org.springframework.web.servlet.FrameworkServlet.CONTEXT.dispatcher] 
19:49:49,986  INFO DispatcherServlet:463 - FrameworkServlet 'dispatcher': initialization completed in 1320 ms 
19:49:49,987 DEBUG DispatcherServlet:136 - Servlet 'dispatcher' configured successfully



在上面的启动日志中,笔者还是把不同的日志功能使用不同的颜色进行了区分。这里进行逐一分析: 

1. 黑色加粗标记区域 —— 容器的启动和结束标志 

这个部分的日志比较明显,位于容器的启动阶段和结束阶段,在之前的讨论中我们已经分析过,这里不再重复。 

2. 黄色注释段落 —— Schema定位和加载 

这个部分的日志反应出刚才我们所分析的Schema-based XML的工作原理。这是其中的第一步:读取META-INF/spring.schemas的内容,加载schema定义。然后找到相应的NamespaceHandler,执行其实现类。 

3. 蓝色注释部分 —— NamespaceHandler执行阶段 

这个部分的日志,可以帮助我们回答本节一开始所提出的两个问题。绝大多数的组件,都是在BeanDefinitionParser的实现类中使用编程的方式注册的。 

4. 红色标记区域 —— 组件注册细节 

这个部分的日志区域彻底回答了本节一开始所提出的最后一个问题:一共有18个组件被注册,就是红色标记的那18个bean。 

小结 

本文所涉及到的话题,主要围绕着SpringMVC的核心配置问题展开。读者可以将本文作为上一篇文章的续篇,将两者结合起来阅读。因为从宏观上说,本文的话题实际上也属于初始化主线的一个部分。 

转载于:https://my.oschina.net/Cubicluo/blog/842723

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值