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:
downpour 写道
结论 As a developer you cannot override these methods to supply your own behavior
2. 大量地在核心组件中使用private方法
我们依然以SpringMVC默认的HandlerAdapter实现RequestMappingHandlerAdapter为例进行说明:
可以看到,几乎所有的核心处理方法全部被定义成了带有红色标记的private方法,这就充分表明了SpringMVC对于“子类扩展”这种方式的态度:
downpour 写道
结论 子类不允许通过继承的方式改变父类的默认行为。
3. 限定某些类对外部程序不可见
有关这一点,有好几个类可以加以证明,我们不妨来看看它们的源码定义:
Java代码
class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
// 这里省略了所有的代码
}
class DefaultServletHandlerBeanDefinitionParser implements BeanDefinitionParser {
// 这里省略了所有的代码
}
class InterceptorsBeanDefinitionParser implements BeanDefinitionParser {
// 这里省略了所有的代码
}
class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
// 这里省略了所有的代码
}
downpour 写道
结论 不允许外部程序对这些系统配置类进行访问,从而杜绝外部程序对SpringMVC默认行为的任何修改。
在这些类的定义中,我们并未看到public修饰符。也就是说,这些类只能在SpringMVC的内部被调用,对于框架以外的应用程序是不可见的。有关这些类的作用,我们将在之后的讨论中详细展开。
4. 提供自定义扩展接口,却不提供完整覆盖默认行为的方式
这一点,需要深入到SpringMVC的请求处理内部才能够体会得到,我们在这里截取了其中的一段源码加以说明: Java代码
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();
// Annotation-based argument resolution
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver());
resolvers.add(new ServletModelAttributeMethodProcessor(false));
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters()));
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters()));
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
resolvers.add(new HttpEntityMethodProcessor(getMessageConverters()));
resolvers.add(new RedirectAttributesMethodArgumentResolver());
resolvers.add(new ModelMethodProcessor());
resolvers.add(new MapMethodProcessor());
resolvers.add(new ErrorsMethodArgumentResolver());
resolvers.add(new SessionStatusMethodArgumentResolver());
resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
// Custom arguments
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}
// Catch-all
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
resolvers.add(new ServletModelAttributeMethodProcessor(true));
return resolvers;
}
这是RequestMappingHandlerAdapter内部的一个重要方法,用以获取所有的参数处理实现类(HandlerMethodArgumentResolver)。从源码中,我们可以看到虽然这个方法是一个private的方法,但是它在源码中却提供了getCustomArgumentResolvers()方法作为切入口,允许用户自行进行扩展。不过我们同样可以发现,用户自定义的扩展类,只是被插入到整个寻址过程中,并不能通过用户自定义的扩展类来实现对其他HandlerMethodArgumentResolver行为的覆盖;也不能改变HandlerMethodArgumentResolver的处理顺序。也就是说:
downpour 写道
结论 SpringMVC提供的扩展切入点无法改变框架默认的行为方式。
上述这四个方面,都是这一条设计原则在源码级别的佐证。或许有的读者会产生这样的疑虑:这个不能改,那个也不能改,我们对于SpringMVC的使用岂不是丧失了很多灵活性?这个疑虑的确存在,但是只说对了一半。因为SpringMVC的这一条设计原则说的是:不能动其根本,只能在一定范围内进行扩展。
至于说到SpringMVC为什么会基于这样一条设计原则,这里面的原因很多。除了之前所提到的编程模型和组件模型的影响,其中更加牵涉到一个编程哲学的取向问题。有关这一点,我们在之后的文章中将陆续展开。
【形散神不散】
这一条编程原则实际上与上一条原则只是在表达方式上有所不同,其表达的核心意思是比较类似的。那么我们如何来定义这里的“形”和“神”呢?
- 神 —— SpringMVC总是沿着一条固定的逻辑主线运行
- 形 —— SpringMVC却拥有多种不同的行为模式
SpringMVC是一个基于组件的开发框架,组件的不同实现体系构成了“形”;组件的逻辑串联构成了“神”。因此,“形散神不散”,实际上是说:
downpour 写道
结论 SpringMVC的逻辑主线始终不变,而行为模式却可以多种多样。
我们在之前有关组件的讨论中,已经见识到了组件的实现体系,也领略了在不同的SpringMVC版本中,组件的行为模式的不同。这些已经能够充分证明“形散”的事实。接下来,我们再通过源码来证明一下“神不散”:
图中的代码是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代码
public interface Controller {
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
也就是说,应用程序不得不严重依赖于接口所规定的处理模式。而我们看到Controller接口除了对处理接口的返回值做了一次封装以外,我们依然需要面对原生的HttpServletRequest和HttpServletResponse对象进行操作。
而在Spring2.5之后,我们可以通过@Controller来指定SpringMVC可识别的Controller,彻底消除了对接口的依赖:
Java代码
@Controller
public class UserController {
// 这里省略了许多代码
}
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代码
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd">
</beans>
而Schema的具体处理,则位于Spring的JAR中的/META-INF/spring.handlers文件中进行定义:
Xml代码
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而言,了解其基本构成和用法并不是一件难事,但是要从中提炼并��结出一些共性的东西就需要我们能够站在一个更高的高度来进行分析。也只有了解了这些共性的东西,我们才能进一步总结出使用框架的最佳实践。
读到这里,希望读者能够回味一下本文的写作思路,并且能够举一反三将这种思考问题的方式运用到其他一些框架的学习中去。这样,本文的目的也就达到了。