6.2.1. 鸟瞰SpringMVC
SpringMVC框架在处理控制器(Controller)的实现方面与其他的request驱动的web框架在总体思路上是相似的,
就跟我们所说的那样,通过引入Front Controller和Page
Controller的概念来分离流程控制逻辑与具体的web请求处理逻辑。
org.springframework.web.servlet.DispatcherServlet就是SpringMVC框架中的Front
Controller,他负责接收并处理所有的web请求,只不过具体的处理逻辑他会委派给他的下一级控制器实现,即
org.springframework.web.servlet.mvc.Controller,
而org.springframework.web.servlet.mvc.Controller[
]
则对应Page Controller的角色定义:
当然,仅仅有他们两个还远远称不上一个完整的web开发框架,要使得整个的web开发框架能够运作起来,我们还需要更多的角色相助!下面,让我带您走过DispatcherServlet这道长廊,并乘机一一为您介绍SpringMVC当中的各位“掌权者
”。
在我们开始DispatcherServlet的旅程之前,我们不妨先回顾一下通常的控制器Servlet都会做哪些工作吧!
就以我们JSP Model 2架构中提到的MockMagicServlet为例(之前我特意请您关注过它),
对于它在处理web请求的过程中所做的工作,我们可以简单归纳为三点:
-
获取请求信息,比如请求的路径,各种参数值等等;
String parameter1 = request.getParameter("paramName1");
String parameter2 = request.getParameter("paramName2");
// ...
-
根据请求信息调用具体的service对象处理具体的web请求;
List<InfoBean> infoList = MockServletService.query(parameter1,parameter2);
-
处理完成后,将要显示给视图的模型数据通过request传递,然后通过RequestDispatcher选择具体的jsp视图显示;
request.setAttribute("infoList", infoList);
forward(request,response,"view.jsp");
现在让我们再看DispatcherServlet在整个web请求处理过程中所做的事情,你将发现,总的原则上并没有太多改变,唯一改变的是,DispatcherServlet将各项工作细化并分离给了较为独立的角色来完成。
Procedure 6.1.
DispatcherServlet处理流程概略[
]
-
HandlerMapping先生(web请求的处理协调人)
既然DispatcherServlet是整个框架的FrontController,当我们将它注册到web.xml的时候,就注定了它要服务于规定的一组web请求,而不是单独的一个web请求:
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
不能够像“一个web请求对应一个servlet
”那样获取web容器对URL映射匹配的支持,我们的DispatcherServlet现在只好自己来处理具体的web请求与具体的处理类之间的映射关系匹配了。
对于web请求与具体的处理类之间的映射匹配来说,具体的处理方式或者说策略可能多种多样,完全可能随着应用程序甚至每一个具体的web请求的不同而发生变化:
-
最常见的,就是那种“掐头去尾
”的处理方式,将web请求的URL路径去除前面的上下文路径(Context path)和最后的扩展名,取最终剩下的路径信息作为匹配的结果,比如:
http://www.nosuchname.com/yourapp/resource.html
最终以resource
作为匹配结果。
-
以web请求的URL中存在的某个参数的值作为匹配的标准,比如:
http://www.nosuchname.com/yourapp/resource.html?controller=yourController
当发现请求的路径中存在controller这个参数的话,其值就会被作为匹配结果用来调用具体的处理类,在这里是yourController。
-
以cookie或者session中的某些信息作为匹配标准,比如针对某一个客户的web请求全部转发给单独的一个处理类进行处理。
甚至于,结合Ruby On
Rails的理念,我们在开发中规定一些的惯例(Convention),然后以这些惯例解析web请求的URL路径信息以获取具体的处理类匹配。
可见,要将映射匹配的逻辑写死到DispatcherServlet是无法有效扩展的,而且,匹配的方式也可能随着需求而变化,所以,SpringMVC
为了能够灵活的处理映射的匹配,
引入了org.springframework.web.servlet.HandlerMapping专门管理web请求到具体的处理类之间的映射关
系。
当web请求到达DispatcherServlet之后,Dispatcher将寻求具体的HandlerMapping实例以获取对应当前web请求
的具体处理类(即org.springframework.web.servlet.Controller)。
-
org.springframework.web.servlet.Controller(web请求的具体处理者)
org.springframework.web.servlet.Controller是对应DispatcherServlet的次级控制器
(sub-controller),它本身实现了对应某个具体web请求的处理逻辑。当你所使用的HandlerMapping查找到当前web请求对应
哪一个org.springframework.web.servlet.Controller具体实例之后,
DispatcherServlet即可获得HandlerMapping所返回的结果,并调用
org.springframework.web.servlet.Controller的处理方法来处理当前的web请求。
org.springframework.web.servlet.Controller的处理方法执行完毕之后将返回一个
org.springframework.web.servlet.ModelAndView实例,ModelAndView包含了两部分信息:
有了ModelAndView所包含的视图与模型二者的信息之后,DispatcherServlet就可着手视图的渲染工作了。
-
ViewResolver和View(视图独立战争的领导者)
按照MockMagicServlet的处理流程,我们已经走到了最后一步,即选择并转到最终的JSP视图文件:
request.setAttribute("infoList", infoList);
forward(request,response,"view.jsp");
但是,对于一个web框架来说,我们是不可以这么简单的处理的,为什么那?不要忘了,现在可用的视图技术可不仅仅JSP一
家,Velocity,Freemarker等通用的模板引擎都可以帮助我们构建相应的视图,
而他们是不依赖于request对象来传递模型数据的,甚至于,我们也不仅仅依赖于JSP专用的RequestDispatcher来输出最终的视图,否
则,我们也没有必要通过ModelAndView来返回视图以及模型数据了,直接在
org.springframework.web.servlet.Controller内部完成视图的渲染工作就可以了,不是吗?鉴于此,Spring
提出了一套基于ViewResolver和View接口的web视图处理抽象层, 以屏蔽web框架在使用不同的web视图技术时候的差异性。
那么,SpringMVC是如何以统一的方式将相同的模型数据纳入不同的视图显示的那?
实际上,撇开JSP使用的RequestDispatcher不论,servlet自身就提供了两种最基本的视图输出方式,不知道您还是否记得我们最初的out.println?!
基本来说,我们要向客户端输出的视图类型可以分为“文本
”以及“二进制
”两种方式,比如JSP/JSTL,Velocity/Freemarker等最终的输出结果都是以(X)HTML等标记文本形式表现的,而PDF/Excel之类则属于二进制内容行列。
对于这两种形式的视图内容的输出,Servlet自身所暴露给我们的HttpServletResponse已经足够应付:
// markup text view output with servlet
String markupText = ...;
PrintWriter writer = response.getWriter();
writer.write(markupText);
writer.close();
...
// binary view output with servlet
byte[] binaryContext = ...;
ServletOutputStream out = response.getOutputStream();
out.write(binaryContext);
out.flush();
out.close();
在HttpServletResponse可以同时支持文本形式以及二进制形式的视图输出的前提下,我们只要在最终将视图数据通过HttpServletResponse输出之前,
借助于不同的视图技术API,结合模型数据和相应的模板文件生成最终的视图结果:
1 获取模型数据Model;
2 获取视图模板文件(比如*.jsp, *.vm, *.fm, *.xls,etc.);
3 结合视图模板和模型数据,使用相应的视图技术API生成最终视图结果;
4 将视图结果通过HttpServletResponse输出到客户端;
5 DONE!
这样,不管最终生成的视图内容如何,我们都可以以几乎相同的方式输出他们。但唯一的问题在于,我们不可能将每一个视图的生成代码都纳入DispatcherServlet的职权范围,
毕竟每一种视图技术的生成代码是不同的,而且,所使用的视图技术也可能随着具体环境而变化。
SpringMVC通过引入org.springframework.web.servlet.View接口定义来统一的抽象视图的生成策略,
这样,DispatcherServlet只需要根据Spring Controller处理完毕后通过ModelAndView返回的逻辑视图名称查找到具体的View实现,然后委派该具体的View实现来根据模型数据输出具体的视图内容即可:
现在,视图模板与模型数据的合并逻辑以及合并后的视图结果的输出逻辑全部封装到了相应的View实现类中,DispatcherServlet只需要根据
ModelAndView返回的信息选择具体的View实现类做最终的具体工作即可。
不过,与HandlerMapping帮助DispatcherServlet来查找具体的Spring
Controller以处理web请求类似,DispatcherServlet现在需要依赖于某一个
org.springframework.web.servlet.ViewResolver来帮他处理逻辑视图名与具体的View实例之间的映射对应关
系,
ViewResolver将根据ModelAndView中的逻辑视图名查找相应的View实现类,然后将查找的结果返回
DispatcherServlet,DispatcherServlet最终将ModelAndView中的模型数据交给返回的View处理最终的视图
渲染工作。
至此,整个DispatcherServlet的处理流程即告结束。
如果你还不足以从以上流程概略中分出哪一个角色对应什么样的工作,那下面这幅Sequence图或许可以帮你加深一下印象:
当我们了解了SpringMVC中的基本概念之后,让我们开始着手开发我们的第一个基于SpringMVC的web应用程序...