Struts 作为 MVC 2的 Web 框架,自推出以来不断受到开发者的追捧,得到广泛的应用。作为最成功的 Web 框架,Struts 自然拥有众多的优点:MVC 2模型的使用、功能齐全的标志库(Tag Library)、开放源代码。
但是,正所谓“没有最好,只有更好”,Struts1.x 自身也有不少的缺点:需要编写的代码过多,容易引起“类爆炸”、单元测试困难。这些缺点随着 Web 的发展越来越明显。这就促生了 Struts 2,它的诞生能很好的解决上述问题。
一、引言
Struts 的第一个版本是在2001年5月份发布的。它的最初设想是通过结合 JSP 和 Servlet,使 Web 应用的视图和业务/应用逻辑得以清晰地分离开来。在 Struts 之前,最常见的做法是在 JSP 中加入业务和应用逻辑,或者在 Servlet 中通过 println()来生成视图。
自从第一版发布以来,Struts 实际上已成为业界公认的 Web 应用标准。它的炙手可热也为自己带来了改进和变更,所以不但要跟上对 Web 应用框架不断变化的需求,而且要与日渐增多竞争激烈的众多框架的特性相融合。
到最后,产生了几个下一代 Struts 的解决方案。其中两个最受瞩目的方案是 Shale 和 Struts Ti。Shale 是一个基于构件的框架,并在最近成为 Apache 的顶级项目。而 Struts Ti 则是在 Struts 的成功经验基础上继续坚持对前端控制器(Front Controller)和MVC(model-view-controller)模式进行改进。
WebWork 项目是在2002年3月发布的,它对 Struts 式框架进行了革命性改进,引进了不少新的思想、概念和功能,但和原 Struts 代码并不兼容。WebWork 是一个成熟的框架,经过了好几次重大的改进与发布。
在2005年12月,WebWork 与 Struts Ti 宣布合并。与此同时,Struts Ti 改名为 Struts Action Framework 2.0,成为 Struts 真正的继承者。
最后要注意的是,并不是说 Struts 或 WebWork 项目已经停止开发了。由于人们对这两个项目的兴趣仍然很高,而且也有很多开发者仍然愿意使用它们,因此这两个项目还在继续开发中,继续修复 Bug,改进功能和继续添加新功能。
二、 Action 的区别
对于有着丰富的 Struts1.x 开发经验的朋友来说,都十分的清楚 Action 是整个 Struts 框架的核心内容,当然 Struts2也不例外。不过,Struts1.x 与 Struts2的 Action 模型很大的区别。
Struts2和 Struts1.x 的差别,最明显的就是 Struts2是一个 pull-MVC 架构。这是什么意思呢?从开发者角度看,就是说需要显示给用户的数据可以直接从 Action 中获取,而不像 Struts1.x 那样,必须把相应的 Bean 存到 Page、Request 或者 Session 中才能获取。Struts1.x 必须继承org.apache.struts.action.Action 或者其子类,表单数据封装在 FormBean 中。Struts 2无须继承任何类型或实现任何接口,表单数据包含在 Action 中,通过 Getter 和 Setter 获取(如下面的 ActionForStruts2的代码示例)。
虽然,在理论上 Struts2的 Action 无须实现任何接口或者是继承任何的类,但是,在实际编程过程中,为了更加方便的实现 Action,大多数情况下都会继承 com.opensymphony.xwork2.ActionSupport 类,并且重载(Override)此类里的 String execute()方法。如下所示:
import java.text.DateFormat;
import java.util.Date;
import com.opensymphony.xwork2.ActionSupport;
public class ActionForStruts2 extends ActionSupport {
private String message;
public String getMessage() {
return message;
}
@Override
public String execute() {
message = " This is hello from strtuts2. Now is: " +
DateFormat.getInstance().format( new Date());
return SUCCESS;
}
}
首先,从 ActionForStruts2可以看出,返回的对象不是 ActionForward,而是 String。如果你不喜欢以字符串的形式出现在你的代码中,有个 Helper 接口 Action 可以以常量方式提供常见结果,如“success”、“none”、“error”、“input”和“login”。
另外, 按照惯例,在 Struts1.x 中只有“execute”方法能调用 Action, 但在 Struts2中并非必要,任何声明为 public String methodName() 方法,都能通过配置来调用 Action。
最后,和 Struts1.x 最大的革命性的不同是,Struts2处理 Action 过程中调用的方法(“execute”方法)是不带参数的。那如何获取所需要的对象呢?答案是使用 IoC(反转控制,Inversion of Control),也叫“依赖注入(Dependency Injection)”的模式(想更多地了解这方面信息请看Martin Fowler的文章http://www.martinfowler.com/articles/injection.html)。Spring框架使得这个模式流行起来,然而 Struts2的前身(WebWork)也同时应用上了这个模式。
三、IoC
IoC(Inversion of Control,以下译为控制反转),随着 Java 社区中轻量级容器(Lightweight Contianer)的推广而越来越为大家耳熟能详。在此,无需再多费唇舌来解释“什么是控制反转”和“为什么需要控制反转”。因为互联网上已经有非常多 的文章对诸如此类的问题作了精彩而准确的回答。读者可以去读一下 Rod Johnson 和 Juergen Hoeller 合著的《Expert one-on-one J2EE Development without EJB》或Martin Fowler所写的《Inversion of Control Containers and the Dependency Injection pattern》。
众所周知,Struts2是以 Webwork 2作为基础发展出来。而在 Webwork 2.2之前的 Webwork 版本,其自身有一套控制反转的实现,Webwork 2.2在 Spring 框架的如火如荼发展的背景下,决定放弃控制反转功能的开发,转由 Spring 实现。值得一提的是,Spring 确实是一个值得学习的框架,因为有越来越多的开源组件(如 iBATIS 等)都放弃与 Spring 重叠的功能的开发。因此,Struts2推荐大家通过 Spring 实现控制反转。
为了更好地了解反转控制,下面来看一个例子,如何利用 IoC 在 Action 处理过程中可以访问到当前请求 HttpServerRequest 对象。
在例子中,使用的依赖注入机制是接口注入。就如其名称一样,接口注入需要的是已经被实现了的接口。这个接口包含了相应属性的 setter,为 Action 提供值。例子中使用了 ServletRequestAware 接口,如下:
public void setServletRequest(HttpServletRequest request);
}
当继承这个接口后,原本简单的 Action 看起来有点复杂了,但是这时可以获取 HttpServerRequest 对象来使用了。
private HttpServletRequest request;
public void setServletRequest(HttpServletRequest request) {
this.request = request;
}
public String execute() throws Exception {
// 可以开始使用request对象进行工作了
return Action.SUCCESS;
}
}
看起来现在这些属性是类级别的,并不是线程安全的,会出现问题。其实在 Struts2里并没有问题,因为每个请求过来的时候都会产生一个新的 Action 对象实例,它并没有和其他请求共享一个对象,所以不需要考虑线程安全问题。
四、拦截器
Interceptor(以下译为拦截器),在 AOP(Aspect-Oriented Programming)中用于在某个方法或字段被访问之前,进行拦截然后在之前或之后加入某些操作。拦截是 AOP 的一种实现策略。
在 Webwork 的中文文档的解释为——拦截器是动态拦截 Action 调用的对象。它提供了一种机制可以使开发者定义在一个 action 执行的前后执行的代码,也可以在一个 action 执行前阻止其执行。同时也提供了一种可以提取 action 中可重用的部分的方式。
Struts1.x 的标准框架中不提供任何形式的拦截器,虽一个名为 SAIF 的附加项目则实现了这样的功能,但它的适用的范围还很有限。
拦截器是 Struts2的一个强有力的工具,有许多功能(feature)都是构建于它之上,如国际化、转换器,校验等。谈到拦截器,还有一个流行的词——拦截器 链(Interceptor Chain,在 Struts2中称为拦截器栈 Interceptor Stack)。拦截器链就是将拦截器按一定的顺序联结成一条链。在访问被拦截的方法或字段时,拦截器链中的拦截器就会按其之前定义的顺序被调用。
Struts 2的拦截器实现相对比较简单。当请求到达 Struts2的 ServletDispatcher 时,Struts 2会查找配置文件,并根据其配置实例化相对的拦截器对象,然后串成一个列表(list),最后一一地调用列表中的拦截器。
Struts 2已经提供丰富多样功能齐全的拦截器实现。读者可以到 struts2-all-2.0.6.jar 或 struts2-core-2.0.6.jar 包的 struts-default.xml 查看关于默认的拦截器与拦截器链的配置。
作为“框架(framework)”,可扩展性是不可缺少的,因为世上没有放之四海而皆准的东西。虽然,Struts 2为我们提供如此丰富的拦截器实现,但是这并不意味我们失去创建自定义拦截器的能力,恰恰相反,在 Struts 2自定义拦截器是相当容易的一件事。
五、Struts2和 Struts1.x的全面比较
为了对 Struts2和 Strtus1.x进行全面的比较,让读者了解这两种框架各自的优缺点,以便于在自己的项目中,根据实际情况,选择合适的框架,对它们两者进行比较,总结了如下表分析比较。
特性比较:
Action 类
Struts1.x 要求 Action 类要扩展自一个抽象基类。Struts1.x 的一个共有的问题是面向抽象类编程而不是面向接口编程。
Struts2的 Action 类实现了一个 Action 接口,连同其他接口一起来实现可选择和自定义的服务。Struts2提供一个名叫 ActionSupport 的基类来实现一般使用的接口。当然,Action接口不是必须的。任何使用 execute 方法的 POJO 对象可以被当作 Struts 2的 Action 对象来使用。
线程模型
Struts1.x Action 类是单例类,因为只有一个实例来控制所有的请求。单例类策略造成了一定的限制,并且给开发带来了额外的烦恼。Action 资源必须是线程安全或者同步的。
Struts2 Action 对象为每一个请求都实例化对象,所以没有线程安全的问题。(实践中,servlet容器给每一个请求产生许多丟弃的对象,并且不会导致性能和垃圾回收问题)。
Servlet 依赖
Struts1.x 的 Action 类依赖于 servlet API,当 Action 被调用时,以 HttpServletRequest 和HttpServletResponse 作为参数传给 execute 方法。
Struts2的 Action 和容器无关。Servlet 上下文被表现为简单的 Maps,允许 Action 被独立的测试。Struts2的 Action 可以访问最初的请求(如果需要的话)。但是,尽可能避免或排除其他元素直接访问 HttpServletRequest 或 HttpServletResponse。
易测性
测试 Struts1.x的主要问题是 execute 方法暴露了 Servlet API 这使得测试要依赖于容器)。第三方的扩展,如 Struts TestCase,提供了一套 Struts1的模拟对象(来进行测试)。
Struts2的 Action 可以通过初始化、设置属性、调用方法来测试。依赖注入的支持也是测试变得更简单。
捕获输入
Struts1.x 使用 ActionForm 对象来捕获输入。象 Action 一样,所有的 ActionForm 必须扩展基类。因为其他的 JavaBean 不能作为 ActionForm 使用,开发者经常创建多余的类来捕获输入。DynaBeans 可以被用来作为替代 ActionForm 的类来创建。但是,开发者可能是在重新描述(创建)已经存在的 JavaBean(仍然会导致有冗余的 javabean)。
Struts2直接使用 Action 属性作为输入属性,消除了对第二个输入对象的需求。输入属性可能是有自己(子)属性的rich对象类型。Action 属性能够通过web页面上的taglibs访问。Struts2也支持 ActionForm 模式。rich对象类型,包括业务对象,能够用作输入/输出对象。这种 ModelDriven 特性简化了 taglib 对 POJO 输入对象的引用。
表达式语言
Struts1.x 整合 JSTL,所以它使用 JSTL 的表达式语言。表达式语言有基本的图形对象移动,但是对集合和索引属性的支持很弱。
Struts2使用 JSTL,但是也支持一个更强大和灵活的表达式语言--"Object Graph Notation Language" (OGNL)。
将值绑定到页面
Struts1.x 使用标准 JSP 机制来绑定对象到页面上下文。
Struts2使用“ValueStack”技术,使 taglib 能够访问值而不需要把你的页面(view)和对象绑定起来。ValueStack 策略允许通过一系列名称相同但类型不同的属性重用页面(view)。
类型转换
Struts1.x 的 ActionForm 属性经常都是 String。Struts 1.x 使用 Commons-Beanutils 来进行类型转换。转换每一个类,而不是为每一个实例配置。
Struts2使用 OGNL 进行类型转换。提供基本和常用对象的转换器。
验证
Struts1.x 支持在 ActionForm 的 validate 方法中手动校验,或者通过 Commons Validator 的扩展来校验。同一个类可以有不同的校验内容,但不能校验子对象。
Struts2支持通过 validate 方法和 XWork 校验框架来进行校验。XWork 校验框架使用为属性类类型定义的校验和内容校验,来支持 chain 校验子属性
Action 执行控制
Struts1.x 支持每一个模块有单独的 Request Processors(生命周期),但是模块中的所有 Action 必须共享相同的生命周期。
Struts2支持通过拦截器堆栈(Interceptor Stacks)为每一个 Action 创建不同的生命周期。堆栈能够根据需要和不同的 Action 一起使用。
六、结论
前面已经简要介绍了 Struts2的起源,并详细对比了 Struts2和 Struts1.x 的差异,读者应该对 Struts2的基础有所了解了——包括高层的框架概念和基础的请求流程,并理解 Struts1.x 和 Struts2两者之间在 Action 方面的差别,Struts2加强了对拦截器与 IoC 的支持,而在 Struts1.x 中,这些特性是很难想象的。
同时,读者应该明白:Struts2是 WebWork 的升级,而不是 Struts 1.x 的升级。虽然 Struts 2提供了与 Struts1.x 的兼容,但已经不是 Struts1.x 的升级。对于已有 Struts1.x 开发经验的开发者而言,Struts1.x 的开发经验对于 Struts2并没有太大的帮助;相反,对于已经有 WebWork 开发经验的开发者而言,WebWork 的开发经验对 Struts2的开发将有很好的借鉴意义。