1. springmvc简介
同其他的mvc框架一样,springmvc的核心还是控制流和数据流,只不过它提供了一套更为灵活的解决方案,具体体现在两点:一、springmvc的数据绑定特别灵活;二、springmvc的视图解析;这主要基于springmvc提供了一套强大的组件工具,此外springmvc还能更方便的构建基于rest的web站点;这些特性只有在后续使用中我们才会慢慢体会到,此处只是略作提及,在下一章节我们将先从springmvc的发展历史来加深对它的理解。
2. springmvc后世前缘
关于springmvc的由来我们先从一幅图说起:
从图中我们可以发现,所有的MVC框架都是从基本的Servlet模型发展而来。那么我们就先来回忆一下servlet的请求-响应模型:
配置Servlet及其映射关系(在web.xml中)
<servlet> <servlet-name>registerServlet</servlet-name> <servlet-class>com.demo2do.springmvc.web.RegisterServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>registerServlet</servlet-name> <url-pattern>/register</url-pattern> </servlet-mapping>
在Servlet实现类中完成响应逻辑
public class RegisterServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 从request获取参数 String name = req.getParameter("name"); String birthdayString = req.getParameter("birthday"); // 做必要的类型转化 Date birthday = null; try { birthday = new SmpleDateFormat("yyyy-MM-dd").parse(birthdayString); } catch (ParseException e) { e.printStackTrace(); } // 初始化User类,并设置字段到user对象中去 User user = new User(); user.setName(name); user.setBirthday(birthday); // 调用业务逻辑代码完成注册 UserService userService = new UserService(); userService.register(user); // 设置返回数据 request.setAttribute("user", user); // 返回成功页面 req.getRequestDispatcher("/success.jsp").forward(req, resp); } }
这是我们大家都熟悉的servlet的请求-响应模型,因为这是我们做j2ee开发的基石;对整个过程加以抽象会发现servlet规范无外乎做了这样两件事:
- 维护请求响应的映射关系定义(web.xml)并根据这个定义找到对应的业务处理servlet
- 对请求数据和响应数据的处理
注:第一个就是软件工程中的控制流的概念,而第二个就是数据流的概念;这两个概念是众多mvc框架的核心,想深入了解的可到网上自行查阅相关资料。
springmvc归根结底还是要做servlet规范做的两件事,只不过在Servlet编程模型之下会遇到了各种各样的问题,而springmvc就是针对这些问题提供了自己的解决方案。
那么在Servlet编程模型之下遇到的第一个问题就是:项目规模扩大之后,请求-响应的映射关系全部定义在web.xml中,将造成web.xml的不断膨胀而变得难以维护。
针对这个问题,SpringMVC提出的方案就是:提炼一个核心的Servlet覆盖对所有Http请求的处理。
这一被提炼出来的Servlet,通常被我们称之为:核心分发器。在SpringMVC中,核心分发器就是org.springframework.web.servlet.DispatcherServlet。
有了DispatcherServlet,我们至少从表面上解决了上面的问题。至少在web.xml中,我们的配置代码就被固定了下来:
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/**</url-pattern>
</servlet-mapping>
有了DispatcherServlet,我们只相当于迈出了坚实的第一步,因为对核心Servlet的提炼不仅仅是将所有的Servlet集中在一起那么简单,我们还将面临两大问题:
- 核心Servlet应该能够建立起一整套完整的对所有Http请求进行规范化处理的流程。比如在哪解析数据、捕获异常等等。
- 核心Servlet应该能够根据一定的规则对不同的Http请求分发到不同的Servlet对象上去进行处理。
以上两点都是根据servlet规范模型抽象出来的。对此,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中对于最基本的组件的一些说明:
我们在之后篇文章中将重点对这里所提到的所有组件做深入的分析。大家在这里需要理解的是SpringMVC定义这些组件的目的和初衷。
这些组件一旦被定义,自然而然也就引出了下一个问题:这些组件是如何串联在一起的?这个过程,是在DispatcherServlet中完成的。有关这一点,我们可以从两个不同的角度加以证明。
- 从DispatcherServlet自身数据结构的角度
如图中所示,DispatcherServlet中包含了众多SpringMVC的组件,这些组件是实现DispatcherServlet核心逻辑的基础。 从DispatcherServlet的核心源码的角度
try { // 这里省略了部分代码 // 获取HandlerMapping组件返回的执行链 mappedHandler = getHandler(processedRequest, false); if (mappedHandler == null || mappedHandler.getHandler() == null) { noHandlerFound(processedRequest, response); return; } // 获取HandlerAdapter组件 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 这里省略了部分源码 // 调用HandlerAdapter组件 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); // 这里省略了部分源码 }catch (ModelAndViewDefiningException ex) { logger.debug("ModelAndViewDefiningException encountered", ex); mv = ex.getModelAndView(); }catch (Exception ex) { Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); // 调用HandlerExceptionResolver进行异常处理 mv = processHandlerException(processedRequest, response, handler, ex); errorView = (mv != null); }
从上面的代码片段中,我们可以看到DispatcherServlet的核心逻辑不过是对组件的获取和调用。
除此之外,SpringMVC对处理流程的规范化和组件化所引出的另外一个问题就是如何针对所有的组件进行管理。
先说说管理。其实管理这些组件对于SpringMVC来说完全不是问题,因为SpringMVC作为Spring Framework的一部分,其自身的运行环境就是Spring所定义的容器之中。我们知道,Spring Framework的核心作用之一就是对整个应用程序的组件进行管理。所以SpringMVC对于这些已定义组件的管理,只不过是借用了Spring自身已经提供的容器功能而已,这里就不做赘述。
有了组件,也有了DispatcherServlet对所有组件的串联,我们之前所提出的两个问题似乎已经可以迎刃而解。所以,我们可以说:SpringMVC就是通过DispatcherServlet将一堆组件串联起来的Web框架。
3. DispatcherServlet
DispatcherServlet —— 串联起整个逻辑主线,是整个框架的心脏
3.1 DispatcherServlet的体系结构
通过不同的角度来观察DispatcherServlet会得到不同的结论。我们在这里选取了三个不同的角度:运行主线、继承结构和数据结构。
【运行主线】
从DispatcherServlet所实现的接口来看,DispatcherServlet的核心本质:是一个Servlet。这个结论似乎很幼稚,不过这个幼稚的结论却蕴含了一个对整个框架都至关重要的内在原则:Servlet可以根据其特性进行运行主线的划分。
根据Servlet规范的定义,Servlet中的两大核心方法init方法和service方法,它们的运行时间和触发条件都截然不同:
- init方法
在整个系统启动时运行,且只运行一次。因此,在init方法中我们往往会对整个应用程序进行初始化操作。这些初始化操作可能包括对容器(WebApplicationContext)的初始化、组件和外部资源的初始化等等。
- service方法
在整个系统运行的过程中处于侦听模式,侦听并处理所有的Web请求。因此,在service及其相关方法中,我们看到的则是对Http请求的处理流程。
因而在这里,Servlet的这一特性就被SpringMVC用于对不同的逻辑职责加以划分,从而形成两条互不相关的逻辑运行主线:
初始化主线 —— 负责对SpringMVC的运行要素进行初始化
Http请求处理主线 —— 负责对SpringMVC中的组件进行逻辑调度完成对Http请求的处理
对于一个MVC框架而言,运行主线的划分非常重要。因为只有弄清楚不同的运行主线,我们才能针对不同的运行主线采取不同的研究策略。而我们在这个系列中的绝大多数分析的切入点,也是围绕着不同的运行主线进行的。
注:SpringMVC运行主线的划分依据是Servlet对象中不同方法的生命周期。事实上,几乎所有的MVC都是以此为依据来进行运行主线的划分。这进一步可以证明所有的MVC框架的核心基础还是Servlet规范,而设计理念的差异也导致了不同的框架走向了完全不同的发展道路。
【继承结构】
除了运行主线的划分以外,我们再关注一下DispatcherServlet的继承结构:
在这个继承结构中,我们可以看到DispatcherServlet在其继承树中包含了2个Spring的支持类:HttpServletBean和FrameworkServlet。我们分别来讨论一下这两个Spring的支持类在这里所起到的作用。
HttpServletBean是Spring对于Servlet最低层次的抽象。在这一层抽象中,Spring会将这个Servlet视作是一个Spring的bean,并将init-param中的值作为bean的属性注入进来:
public final void init() throws ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Initializing servlet '" + getServletName() + "'");
}
// Set bean properties from init parameters.
try {
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.environment));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
throw ex;
}
// Let subclasses do whatever initialization they like.
initServletBean();
if (logger.isDebugEnabled()) {
logger.debug("Servlet '" + getServletName() + "' configured successfully");
}
}
从源码中,我们可以看到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对象之中:
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
if (this.logger.isInfoEnabled()) {
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
}
long startTime = System.currentTimeMillis();
try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
} catch (ServletException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
} catch (RuntimeException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}
if (this.logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
elapsedTime + " ms");
}
}
上面的这段代码就是FrameworkServlet初始化的核心代码。从中我们可以看到这个FrameworkServlet将调用其内部的方法initWebApplicationContext()对Spring的容器(WebApplicationContext)进行初始化。同时,FrameworkServlet还暴露了与之通讯的结构可供子类调用:
public abstract class FrameworkServlet extends HttpServletBean {
/** WebApplicationContext for this servlet */
private WebApplicationContext webApplicationContext;
// 这里省略了其他所有的代码
/**
* Return this servlet's WebApplicationContext.
*/
public final WebApplicationContext getWebApplicationContext() {
return this.webApplicationContext;
}
}
我们在这里暂且不对Spring容器(WebApplicationContext)的初始化过程详加探查,稍后我们会讨论一些WebApplicationContext初始化过程中的配置选项。不过读者可以在这里体会到:FrameworkServlet在其内部初始化了一个Spring的容器(WebApplicationContext)并暴露了相关的操作接口,因而继承自FrameworkServlet的DispatcherServlet,也就直接拥有了与WebApplicationContext进行通信的能力。
通过对DispatcherServlet继承结构的研究,我们可以明确:
结论 DispatcherServlet的继承体系架起了DispatcherServlet与Spring容器进行沟通的桥梁。
【数据结构】
在上一篇文章中,我们曾经提到过DispatcherServlet的数据结构:
我们可以把在上面这张图中所构成DispatcherServlet的数据结构主要分为两类(我们在这里用一根分割线将其分割开来):
配置参数 —— 控制SpringMVC组件的初始化行为方式
核心组件 —— SpringMVC的核心逻辑处理组件
可以看到,这两类数据结构都与SpringMVC中的核心要素组件有关。因此,我们可以得出这样一个结论:
结论 组件是整个DispatcherServlet的灵魂所在:它不仅是初始化主线中的初始化对象,同样也是Http请求处理主线中的逻辑调度载体。
注:我们可以看到被我们划为配置参数的那些变量都是boolean类型的,它们将在DispatcherServlet的初始化主线中起到一定的作用,我们在之后会使用源码进行说明。而这些boolean值可以通过web.xml中的init-param值进行设定覆盖(这是由HttpServletBean的特性带来的)。
SpringMVC的运行体系
DispatcherServlet继承结构和数据结构,实际上表述的是DispatcherServlet与另外两大要素之间的关系:
继承结构 —— DispatcherServlet与Spring容器(WebApplicationContext)之间的关系
数据结构 —— DispatcherServlet与组件之间的关系
所以,其实我们可以这么说:SpringMVC的整个运行体系,是由DispatcherServlet、组件和容器这三者共同构成的。
在这个运行体系中,DispatcherServlet是逻辑处理的调度中心,组件则是被调度的操作对象。而容器在这里所起到的作用,是协助DispatcherServlet更好地对组件进行管理。这就相当于一个工厂招了一大批的工人,并把工人划分到一个统一的工作车间而便于管理。在工厂要进行生产活动时,只需要从工作车间把工人分派到相应的生产流水线上即可。
笔者在这里引用Spring官方reference中的一幅图,对三者之间的关系进行简单的描述:
注:在这幅图中,我们除了看到在图的左半边DispatcherServlet、组件和容器这三者之间的调用关系以外,还可以看到SpringMVC的运行体系与其它运行体系之间存在着关系。有关这一点,我们在之后的讨论中会详细展开。
既然是三个元素之间的关系表述,我们必须以两两关系的形式进行归纳:
DispatcherServlet - 容器 —— DispatcherServlet对容器进行初始化
容器 - 组件 —— 容器对组件进行全局管理
DispatcherServlet - 组件 —— DispatcherServlet对组件进行逻辑调用
值得注意的是,在上面这幅图中,三大元素之间的两两关系其实表现得并不明显,尤其是“容器 - 组件”和“DispatcherServlet - 组件”之间的关系。这主要是由于Spring官方reference所给出的这幅图是一个静态的关系表述,如果从动态的观点来对整个过程加以审视,我们就不得不将SpringMVC的运行体系与之前所提到的运行主线联系在一起,看看这些元素在不同的逻辑主线中所起到的作用。
以后,我们就分别研究DispatcherServlet的两条运行主线。