共计 6079 个字符,预计需要花费 16 分钟才能阅读完成。
在当今的 MVC framework 里,似乎 Webwork2 逐渐成为主流,Webwork2+SpringFramework 的组合变得越来越流行。这似乎意味着 Spring 自带的 MVC framework 远比 Webwork2 差,所以大家纷纷用 Webwork2 来代替。确实,Spring 的 MVC framework 不算是整个 Spring 的核心部件,但它的威力却超过了很多人的想象。很多人包括 xiecc 认为 Spring 的 MVC framework 是非常优秀的,甚至比 Webwork2 更优秀。
下面列举一下 Spring 的 MVC framework 在设计时做出的一些重要的决定,并将之和相关的 MVC framework 如 Webwork2 或 struts 进行对比:
一、Spring 的整个 MVC 配置是基于 IOC 容器的
与 struts 或 webwork2 相比,这是一个 ms 有点奇怪的决定,看一下 Spring MVC 的配置文件,最先看到的不是 action 或者 form,而是一些有着特定名字的 bean,Bean 下面的配置是一些简单或有点复杂的属性。我们看到的是机器更容易的数据结构,而不是人更容易理解的元素。
但是这恰恰是 Spring 的 MVC 强大的根源! 因为它的配置就是 Spring 的核心 IOC 容器的配置,这意味着所有 IOC 容器的威力都可以在这里展现,我们可以为所欲为地对 Spring MVC 进行扩展和增强,我们可以完成在其它 MVC framwork 中很多难以想象的任务。想扩展新的 URL 映射方式吗? 要换一个 themeResolver 或 LocalReolver 的实现吗? 想在页面中显示新类型的 View(比如说 RDF,呵呵,一个小秘密:xiecc 是研究语义网的,虽然成天不务正业,不写论文,只写八卦)? 甚至想直接在 Controller 里定义 AOP 吗? 这些对 Spring 的 MVC 来说都是小菜一碟。
我没有仔细研究过 Webwork2 的扩展机制,我知道通过 Webwork2 的 interceptor 机制,可以进行很多的扩展,甚至有一个简单简单的 IOC 容器。但不管它有多强大,提供了多少扩展点。它的威力都很难和真正的 IOC 容器相比。而 struts 的 plugin 功能则是出名的滥,虽然它也提供了 plugin 机制。
Spring 采用 IOC 配置的另一个原因是使 Spring 的 MVC 与 Spring 的 IOC 容器的整合变得非常的容易。Spring 提供了与 struts 与 webwork2 的整合,但是这样整合都需要在进行间接的包装,感觉总不是很自然。而且还会导致一个概念多个配置,webwork2 就需要在 Spring 里配置 bean,再配置自己的 xwork 文件。想象一下吧,我们的 bean 直接就是一个 controller,直接可以完成 MVC 的所有任务,这是多少爽的感觉。
Rod Johnson 采用 IOC 容器来实现的另一个原因是这会减少好多开发工作量。看一下 urlMapping 吧,它提供的 property 本身就是一个 HashMap,只有配置完成,我们的 bean 里的数据就自然存在了,哈哈,好爽吧。不用象 struts 那样解析 XML,再把它的内容一项一项地读到 HashMap 里。
虽然这样的配置会有点怪异,但假如我们对 Spring 的 IOC 容器非常熟悉的话,会发现它非常的亲切,也非常的简单。
最后是一个简单的小秘密,Spring 怎么知道某个 bean 的配置就是 urlMapping? 另一个 bean 的配置就是 viewResolver? 其实很简单,把所有的 bean 全部读到内存里,然后通过 bean 的名字或类型去找就行了。通过名字去找就是简单的 getBean 方法,通过类型去找则使用了 BeanFactoryUtils.beansOfTypeIncludingAncestors 的静态方法。
二、Spring 提供了明确的 Model,View 概念和相应的数据结构
在 Spring 里有一个有趣的数据类型叫做 ModelAndView,它只是简单地把要显示的数据和显示的结果封装在一个类里。但是它却提供了明确的 MVC 概念,尤其是 model 概念的强化,使程序的逻辑变得更清晰了。
记得以前在 Struts 里写程序里的时候,为了显示数据经常自己把东西放到 HttpSession 或 HttpServletRequest 里 (或 set 到 form 里,虽然不太有用),这造成了 model 概念的模糊,而且也导致了 struts 与 JSP 页面的紧耦合。假如我们要替换成 Veloctiy,就得另外加一个 plugin,因为在 velocity 里数据是不需要不放到 request 里的。
Webwork2 里强调的是与 Web framework 解耦和它的 command 模式的简单性,因此在它的 action 里只有简单的 get 或 set 方法,假如返回数据,也只是简单地返回一个 String. 当然这样的实现有它的好处,但是它淡化了 model 和 view 的概念。Rod Johnson 认为 Webwork2 里的 Action 同时包含了 Action 和 Model 的职责,这样一个类的职责太多,不是一个很好的设计。当然 Jason Carreira 不太认同这种观点,因为 Action 里的 model 对象完成可以 delege 给其它对象。但不管怎样,这种争论的根源在于 Webwork2 里淡化了 model,view 甚至 web 的概念。仁者见仁,智者见智,最后的结果还是看个人喜欢好吧。
三、Spring 的 Controller 是 Singleton 的,或者是线程不安全的
和 Struts 一样,Spring 的 Controller 是 Singleton 的,这意味着每个 request 过来,系统都会用原有的 instance 去处理,这样导致了两个结果:我们不用每次创建 Controller,减少了对象创建和垃圾收集的时间; 由于只有一个 Controller 的 instance,当多个线程调用它的时候,它里面的 instance 变量不是线程安全的。
这也是 Webwork2 吹嘘的地方,它的每个 Action 都是线程安全的。因为每过来一个 request,它就创建一个 Action 对象。由于现代 JDK 垃圾收集功能的效率已经不成问题,所以这种创建完一个对象就扔掉的模式也得到了好多人的认可。Rod Johnson 甚至以此为例证明 J2EE 提供的 object pool 功能是没多大价值的。
但是当人们在吹嘘线程安全怎么怎么重要的时候,我想请问有多少人在多少情况下需要考虑线程安全?Rod Johnson 在分析 EJB 的时候也提出过其它问题,并不是没有了 EJB 的线程安全魔法,世界就会灭亡的,大多数情况下,我们根本不需要考虑线程安全的问题,也不考虑 object pool. 因为我们大多数情况下不需要保持 instance 状态。
至少我写了那么多的 struts Action,写了那么多的 Spring Controller,几乎没有碰到需要在 instance 变量保持状态的问题。当然也许是我写的代码不够多,Struts 的设计者 Craig R. McClanahan 曾经说当时他设计 struts 时有两个条件不成熟:当时没有测试驱动开发的概念; 当时 JVM 的垃圾收集性能太次。假如现在重新设计的话,他也会采用每个 request 生成一个新对象的设计方法,这样可以解决掉线程安全的问题了。
四、Spring 不象 Webwork2 或 tapestry 那样去隐藏 Servlet 相关的元素如 HttpServletRequest 或 HttpServletResponse
这又是一个重要的设计决定。在 Webwork2 里我们没有 HttpServletRequest 或者 HttpServletResponse,只有 getter,setter 或 ActionContext 里数据,这样的结果导致一个干净的 Action,一个与 Web 完全无关的 Action,一个可以在任何环境下独立运行的 bean. 那么 Webwork2 的这样一个基于 Command 模式的 Action 究竟给我们带来了什么? 我想主要有两点:
1、它使我们的 Action 可以非常容易地被测试。
2、用户可以在 Action 里添加业务逻辑,并被其它类重用。
然而仔细跟 Spring 比较一下,我们就会发现这两点功能所带来的好处其实并不象我们想象的那么显着。Spring 的 Controller 类也可以非常轻松被测试,看一下 spring-mock 下面的包吧,它提供的 MockHttpServletRequest,MockHttpServletResponse 还有其它一些类让测试 Controller 变得异常轻松。再看一下 Action 里的业务逻辑吧,Jason Carreira 曾经说我们可以尽情地在 Webwork2 的 Action 里加业务逻辑,因为 Action 是不依赖于 Web 的。但是有多少人真正往 Action 里加业务逻辑的? 大多数人都会业务逻辑 delegate 给另一个 Service 类或 Manager 类。因为我们很清楚,往 Action 里加业务逻辑会使整个体系的分层架构变得不清晰,不管怎样,Web 层就是 Web 层,业务层就是业务层,两者的逻辑混在一起总会带来问题的。而且往 Action 里加业务逻辑会使用这个 Action 类变得庞大,Webwork2 的 Action 是每个 request 都创建实例的,尽管带来的性能影响不太大,但并不表示每次都要把业务逻辑再 new 出来,业务逻辑在大多数的情况下应该是单例的。
不把 request 和 response 展现给用户当然还会带来功能上的损失,也许一般的场合,用用 webwork2 提供的接口已经足够了,但有时我们必须要知道 request 和 response 才能发挥出更大的威力。比如我以前的一个项目里有一个通过递归动态生成的树状结构的页面,在 jsp 页面上显示递归是痛苦或不可能的,因此我用 response 直接 write 出页面,这在 spring 里很 easy,但在 webwork 里可能比较难了(偶不敢肯定,偶研究得不够深,也许高手是有办法的)。
五、Spring 提供了不错但不够充分的 interceptor 机制
回头看一下 struts,它在架构里甚至没有给我们提供 hook point 的机会,我们没有任何机会加入自己的 interceptor. 我们只能通过重载 struts 的 RequestProcessor 类来进行一点有限的扩展。
到了 Webwork2,似乎 interceptor 一下子成了整个 Framework 的核心,除了 Action 的核心部件,其它所有的东西都是 interceptor. 它的超强的 interceptor 功能使们扩展整个架构变得非常方便。有人称这种 interceptor 为 AOP,Jason Carreira 则自豪地宣称这个叫做 pragamtic AOP. 我不认同这是 AOP,它只是简单的 interceptor 机制。但不管如何,它的 interceptor 确实有强大的功能。
Spring 也提供了它的 interceptor 机制,它的 HandlerInterceptor 三个 interceptor 方法:peHandle,postHandle,afterCompletion. 分别对应 Controller 执行前,Controller 执行后和 page render 之后。虽然大多数情况下已经够用,但是从功能上来说显然它没有 Webwork2 强大。从 AOP 的角度来看,它没有提供 around interceptor,而只有 before 与 after interceptor. 这意味着我们无法在 interceptor 前后保持状态,最简单的情况假如我们要计算一个 Controller 的执行时间,我们必须在执行完 before 后把 begintime 这个状态保持住,再在 after 里把它调出来,但是显然这个状态保持会是个问题,我们不能把它放到 instance 变量里,因为 interceptor 不是线程安全的。也许通过 ThreadLocal 可以解决这个问题,但是如此简单的功能要用到这样的方法来处理,显然这个 Interceptor 本身设计上还是有点问题的。
六、Spring 提供了 MultiActionController,使它可以在一个类里包含多个 Action
这个设计和 struts 的 DispatchAction 有点类似,只不过提供了更灵活的机制。当我们的项目变大的时候,把功能类似的方法放到同一个 Action 里完全值得的!Webwork2 缺少这样的机制。假如看一下 Spring 的源代码,会发现其实实现 MultiActionController 的工作量相当的少,只不过是用反射机制把解析出来的方法名执行一下就完事了。其实 Webwork2 也完全可以提供这样的机制。虽然从设计上来说确实不是很优雅,但是它确实很有用。
七、Spring 提供了更多的选择方式
看看 Spring 里提供的 Controller 吧,它提供了好多不同的 Controller 类。要生成 Wizard 吗? 要专门用于提交 form 的 Controller 吗? 要执多个方法的类吗?Spring 提供了丰富的子类来扩展这些选择。当然我们还可以很轻松地自己扩展这些功能。
再看看 Spring 的 ViewResolver 吧,它提供了无数不同类型的 ViewResolver. 更重要的是我们自定义我们的页面映射方式。看看 strtus,看看 webwork2,都会存在页面与 forward name 的一层间接转换,我们必须在配置文件里配置好某个字符串(典型的是 success) 对应的是那个页面。但是 Spring 里我们有了更大的自由度,我们可以采用 webwork2 的策略,也可以采用更简单的策略,如将 JSP 文件名去掉扩展名的映射方法。也许有人认为这种映射方式很幼稚,但是我觉得它是非常有用的方式,即使在大项目里。
还有新的扩展吗? 看看 Spring Web Flow 吧,它是 SpringFramework 的子项目。它为一长串的基于页面流的 Wizard 页面提供了可配置的实现方式。在 Spring 1.3 里,它将是 SpringFramework 的一部分。
八、Spring 的 tag
尽管 Spring 的 tag 数量上少得可怜,但它却是精心设计的。它的目标很简单:让美工可以轻松地编辑页面。因为在 Spring 的页面里 Text 仍然是 Text,checkbox 仍然是 CheckBox,而不象在 struts 或 webwork2 中的 Tag. 它只是用 Springbind 对输入内容进行了一下包装。所以尽管页面显示代码上会比 Webwork2 多,但这绝对是有价值的。
在接下来的几章里,我会分析一下 Spring 是如何让我们的 Web 应用不需要知道 ApplicationContext 就能够访问 IOC 容器的,然后会对 Spring 的设计和执行过程进行简单的源码分析,然后给出几个扩展 Spring MVC 的方法。