对基于特定代码的应用来说,其架构目标很难确定下来。在开始开发之前制定文档化的目标,这种情况只是空想,因为一旦开始开发,代码通常就会朝一个与架构目标不同的方向前进,这样实际上最后的应用特征是基于代码而不是架构。这种情况很难发现并且是开发过程中逐步形成的产物而不是计划的产物,其结果就会造成不同的程序包及特性之间的不一致。
本章我们将会讨论5种基于Struts2代码的特征。从2002年以来,这些架构元素随着代码的发展仍然存在--从开始的WebWork,经过WebWork分解为WebWork2和XWork,一直到最后转变为Struts2.
4、1 概念的分解
作为一个web应用开发人员,有多种级别的功能需要进行阐述:
(1)在request/response周期期间,需要对作为其核心目标的每个action的逻辑进行说明;
(2)访问或拥有可以用来执行action逻辑或访问资源的业务对象;
(3)编译、映射和转换,以便把HTML中基于字符的值转化为原始类型,以及把视图对象转换为业务对象或数据表来进行表示;
(4)为应用中分组的action或所有的action提供功能的横切关注点。
在Struts2架构中,每一个概念都是独立的。功能和逻辑不需要放置在专门的action里。我们来看看上面所提到的概念以及它们是如何处理的:
(1)每个Action逻辑--这是最简单的概念。每个action负责它需要提供的逻辑或功能;
(2)编译、映射和转换--3者是各自有所不同的概念,但是有一个相同点就是它们都是作为核心action逻辑的补充。编译和类型转换由框架本身进行处理。来自于HTML的字符值在action开始处理之前被转换为基本类型或被注入到action中--所有需要的都在此处。映射由一个特定的拦截器进行处理。通过对一个action进行配置,说明其所拥有的域模型,并准确指明其在HTML中所对应的域,框架将会把UI映射到该域模型上。它甚至会生成对象图。
(3)横切关注点--拦截器是提供横切功能的主要角色。开发者可以实现拦截器,然后把它们应用到所有action、某个特定包中的所有action或者选择某些指定的action中去。另一个横切关注点是用户接口层。Struts2可以使用其所提供的称为“主题(themes)”的标签来为此提供帮助。不同的主题可以被开发来提供不同的层选项,并被应用到单个标签或整个应用(通过把它设为默认值)。
4、2 松耦合
WebWork的早期目标之一就是提供一个松耦合的框架。其2.0版更加强调了这点:把代码分成了两个项目,一个是XWork--提供普通的命令模式(generic command-pattern)框架,另一个是WebWork--为XWork提供明确的web接口。这个WebWork架构基础上的变化创建了二者的共生关系。原来赖以成名的“WebWork”现在实际上是WebWork和XWork的结合体。
作为一个独立的项目,XWork现在可以被用于其它项目的一部分了--实际上也确实是这样。Swingwork就是一个这样的项目--它是一个基于Swing的、底层使用XWork的MVC框架。另外一个例子就是JMS前端--执行或共享与web UI相关的XWork action。这些例子提供了高水平松耦合的典型案例。Struts2是又一个采用XWork的项目。
松耦合思想已经更近一步,集成到整个框架--从处理action的第一步直到最后一步。实际上,在Struts2里几乎没有什么不可配置的--个人认为这个特点既是Struts2发展的极大原动力之一,但同时也是其重大缺陷之一。
松耦合配置的一般案例包括:
(1)把URL映射到action;
(2)把一个action的不同结果映射到提交的页面;
(3)把处理期间产生的异常映射到提交的异常页面;
不常使用及Struts2特定的案例包括:
(1)如果你不想使用Sping框架,就得配置业务对象工厂;
(2)改变URL映射到一个action类的方式;
(3)为action结果增加新的结果类型;
(4)为新的框架功能增加插件;
(5)通过拦截器配置框架级功能;
松耦合系统的好处是众所周知的--增强了可测试性、易于扩展框架特征等。但有一个缺陷,由于可置配的灵活性(尤其是与拦截器相关的),导致一个特定action的处理路径不易被开发者所理解。这一点在进行调试的时候比较明显。由于不理解发生了什么事,一个不了解系统的开发人员将不能快速高效的进行调试。问题本身可能比较简单,比如由一个没有进行正确配置的拦截器引起的,或者甚至只是因为拦截器的顺序不对引起的。只要理解处理路径上的每一个细节,很快就会找到问题的解决办法。
4、3 易测试性(Testability)
在过去几年中,单元测试已经成为软件开发过程中事实上的标准。它不仅保证了类逻辑的一致性,而且通过在类开发过程中甚至开发之前实现单元测试,将会减少设计的复杂性,并且会使设计更为健壮。
Struts2的前身,WebWork就是建立在这种环境之上的。由于框架元素的松散耦合,测试很容易进行。在web应用开发过程中,action、拦截器、结果、对象工厂及其它被开发的组件能够独立地进行测试。由于action和拦截器最为常用,我们就对这两种组件进行详细的探讨。
Actions
通常在框架中是通过执行“execute()”方法来调用action的,或者在配置时调用任何一个返回值为字符值(String)的方法来进行调用的。从易测性的角度来看,这再简单不过了。
我们来看一个例子。下面是一个数字累加的action类:
public class MyAction {
private int number;
public int getnumber() { return number; }
public void setNumber( int n ) { number =
n; }
public String execute() {
number += 10;
return “success”;
}
}
由于该action是POJO,单元测试只需把action实例化、调用方法,并在断点出得到预期的结果就可以了。所有搜数据和资源都通过setter方法提供给action。因此action需要的任何数据都能够直接在action里进行设置。
在本例中我们需要两个断点--一个为“execute”方法的输出进行设置,另一个则是为验证action的状态是否是我们预期的。单元测试如下所示:
public class myActionTest extends TestCase
{
…
public void testExecute() {
MyAction action = new MyAction();
Action.setNumber(5);
assertEquals("success",
action.execute());
assertEquals(15,action.getNumber());
}
}
对于资源稍微有点麻烦,类似jMock这样的库可以被用来提供资源的模拟实现,测试在action和资源之间的交互是否正确。
尽管本例是用JUnit写成的,但是TestNG或其它任何框架都可以使用。
拦截器(Interceptors)
当你编译拦截器的时候,测试会稍微有点复杂。不过也有一些额外的帮助可以利用。使用拦截器进行工作有两种情景。
第一种情景是使用在调用时与ActionInvocation对象进行交互的拦截器。在执行后,你能够通过在断点出得到拦截器本身的状态来验证逻辑的正确性。在这种情景下,你能够利用与测试action同样的方法来测试拦截器。实例化拦截器、创建一个ActionInvocation对象的模拟实现(该对象带有在测试拦截器时将会用到的值)、调用intercept方法、在断点处得到预期的结果。这些可能发生在拦截器本身、被调用方法的返回结果或是由系统抛出的异常。
第二种情景是使用与其环境或拦截器堆栈中的其它拦截器发生交互的拦截器。在这种情况下,测试需要通过ActionProxy类与action进行交互,还将会需要访问拦截器本身不能访问的其它环境对象。
XWork库通过提供XWorkTestCase类来为JUnit测试提供帮助、通过提供TestNGStrutsTestCase
和TestNGXWorkTestCase类来为TestNG测试提供帮助。这些为ConfigurationManager,Configuration,
Container 和
ActionProxyFactory类的实例提供了测试实现。涉及到的类还有XWorkTestCaseHelper 和 MockConfiguration.等。
现在我们已经具备了安装环境的基础,测试本身变得容易了--遵循在第一种情景里所描述的步骤即可。唯一的不同点在于,不是调用拦截器的intercept()方法,而是需要调用ActionProxy类的execute()方法。如下述代码所示:
ActionProxy proxy =
actionProxyFactory.createActionProxy(NAMESPACE,NAME,null);
assertEquals("success",
proxy.execute());
在这种情况下,测试将会设置断点来获取预期的action结果值、action值或值堆栈的值。被执行的action能够在执行前或执行后通过下面的调用进行访问:
MyAction
action=(MyAction)proxy.getInvocation().getAction();
而值堆栈则可以通过下面的调用进行访问:
proxy.getInvocation().getStack();
4、4 模块化(Modularization)
随着应用系统越来越庞大,把web应用分离为许多模块就变得越来越重要。模块化允许在一个项目中开发的功能或新的框架特征能够独立打包,并可以在其它项目中进行重用。Struts2已经把模块化作为其架构的基础部分,允许开发者独立工作和编译彼此的项目。
下面是一些对应用进行模块化的方法:
(1)配置信息能够被分为多个文件--这并不影响应用的打包,由于配置信息根据功能界限进行逻辑分离而易于查找,从而简化的开发人员的工作。
(2)独立的应用模块可以以插件的形式进行创建--为了提供摸个特定特征所需的一切都可以打包在一起并作为插件独立发布。这包括action、拦截器、拦截器堆栈、视图模板(JSP除外)等。浏览器插件的配置就是一个例子,该插件提供了一个完整的模块,当你把该模块添加到你的应用中时,其提供了一个web接口来查看配置信息。
(3)可以创建新的框架功能插件-非特定应用的新功能可以以插件形式捆绑并在不同的应用中使用。
从技术角度来说,所有这些对应用进行模块化的方式都是一样的--都具有同样的配置元素(除了名称不同之外,“struts-plugin.xml”是系统自动加载插件时的配置文件)、具有相同的目录结构,并且它们也包含同样的框架和应用元素。
插件的两种类型之间的唯一不同就是你在概念上如何看待它们,以及哪些元素和配置被放入分发包中。
4.4.1 其它配置元素
由于插件为核心框架功能提供了可选实现,所以有一些额外的配置元素。尽管在“struts.xml”和“struts-default.xml”配置文件中可以使用这些元素,但是通常情况下它们还是在插件配置文件中进行配置。
对于插件来说,可选实现的配置分为两步:
1、使用<bean .../>标签中来提供可选接口的实现,使用一个唯一的键来识别它。
2、使用<constant … />标签在已配置的接口实现中选择一个。
我们来更详细地看一下这两个步骤。
<bean .../>标签允许插件提供扩展点的实现信息。下面的例子就是一个展示在"struts-default.xml"文件中为一个对象工厂进行配置的情况:
<bean name="struts"
type="com.opensymphony.xwork2.ObjectFactory"
class="org.apache.struts2.impl.StrutsObjectFactory"
/>
属性提供了在Struts2中创建和使用一个可选对象实现的所有信息。可以使用的属性如下所示:
class--提供类的全名;
type--指明类需要实现的接口;
name--对每个类进行唯一识别的简称;
static--指明是否把静态类方法注入到类的实例;
scope--指明所用实例的范围,其值可能为:"default"、"request"、"session"、 "singleton"或"thread"。
optional--如果值为“true”,既是在创建类实例时出现错误,将仍然继续加载。
接下来,<constant
… />标签允许开发人员选择使用哪个配置。只有两个属性--一个属性“name”提供了你的新实现所改变的扩展点的名称,另一个属性“value”就是在<bean … />标签中配置的“name”名称。
<constant name="struts.objectFactory"
value="plexus" />
<constant … />标签是把一个新值赋给一个已知属性的一种方式。该值也可以在"web.xml"配置文件中使用“init-param”进行修改,或者在““struts.properties”配置文件中作为一个名值对(name-value pair)进行修改。
如果你没在开发一个插件,而只是使用常规"struts-xml"配置文件中的这些技巧,这里还有一个捷径。在<constant ... />标签里,使用你通常放在<bean ... />标签里的类值---这样做可以避免对<bean
... />标签的需求。
下面这张表列出了可配置扩展点的接口和属性名称:(表省略)
4.4.2 惯例优先原则(Convention over configuration)
惯例优先配置的原则是Rails带给主流应用开发的一个概念。不是提供在很多应用中都很类似的配置文件,而是假定在多数情况下开发人员将会遵从一个特定的模式。这个模式非常普遍以至于可以被认为是一个惯例,这样就可以在框架中提供一个默认的配置而不是为每个新的应用提供一个配置。默认情况下,开发人员不需要提供配置信息。但是如果有与惯例配置信息不同的需要,就得提供相应的配置信息来代替默认配置信息。
Struts2已经采用了这种理念。松耦合给Struts2提供大的弹性,但是同时这也意味着该框架很难进行配置。而惯例优先配置原则则为这两种对立的力量提供了平衡,使开发人员的开发可以更加简单和高效。
在Struts2中,惯例优先配置的例子包括:
(1)
隐式配置文件加载—配置文件“Struts-default.xml”和“struts-plugin.xml”是自动加载而不是显式加载的。
(2)
插件中的代码—当利用插件中的代码时,使用action名称和结果字符二者的结合来自动搜索结果模板,所以对于“/user/add.action”来说,将会为一个“success”的结果返回“/user/add-success.jsp”模板,而为一个“error”的结果返回“/user/add-error.jsp”模板。
(3)
默认结果及结果类型—当对action进行配置时,当使用默认的“success”和JSP时,不必指明结果和类型。
(4)
绑定Spring业务服务—当安装了Spring框架插件后,不必为每个action配置其所需的基于Spring提供的业务,因为这些业务会自动与action进行绑定。
在前面的章节中,我们已经看到了几个默认配置,以及如何通过配置使用新值来代替默认值。更多的配置选项,以及更多的惯例将会在接下来的章节中进行探讨。