用静态横切的强大功能建立高度松散的系统
级别:中级
Andrew Glover (aglover@vanwardtechnologies.com)
CTO,Vanward Technologies
2004 年 3 月
许多 Java 开发人员已经接受了面向方面编程(AOP)的非强制性风格和灵活性,特别是在用于建立高度松散和可扩展的企业系统时。在本文中,您将看到 AOP 的功能设计概念之一(静态横切)如何把可能是一大堆混乱的紧密耦合的代码转变成一个强大的、可扩展的企业应用程序。
在将商业需求转换成软件功能的快速开发周期中,面向方面是一种可以利用的强有力的设计原理。通过将首要的设计重点从面向对象编程(OOP)的传统化特性转移出来,AOP 和设计原理允许软件架构师用一种和面向对象相当而又互补的方式来考虑设计。
在本文里,您将学习如何实现 AOP 中最没有得到充分利用的特性之一。横切(crosscutting)是一种蕴含强大力量的相对简单的设计和编程技术,尤其是当它被用于建立松散耦合的、可扩展的企业系统时。虽然动态横切(其中对象的运行时行为可以改变)被认为是 AOP 的根基之一,但静态横切却是一种远不为人所知的技术。我将在本文中尝试弥补这个缺憾。我将首先概述动态和静态横切,然后迅速切入一个实现场景来展示后一种技术。您将亲自体会到静态横切是多么方便地克服了如下这个最常见的企业挑战之一:如何在利用第三方代码的同时保持应用程序代码库(codebase)的灵活性。
请注意,尽管我首先简要地从概念上概述面向方面编程,但本文并不是一篇关于 AOP 的介绍。请参阅 参考资料 一节以获得关于该主题的介绍性文章的列表。
AOP 概述
面向对象设计最根本的魅力在于,它能够将真实世界领域中的实体及各自的行为建模为抽象的对象。以面向对象方式设计的系统产生了很多有效的业务对象,比如 Person、Account、Order 以及 Event。面向对象设计的缺点在于,这样的业务对象会因为混合的属性和与对象最初意图不一致的操作而变得混乱。
通过使设计者运用动态和静态横切,用一种非强制性的整洁和模块化的方法来添加对象行为,面向方面编程有效地解决了这一问题。
什么是横切?
横切 是面向方面编程的专有名词。它指的是在一个给定的编程模型中穿越既定的职责部分(比如日志记录和性能优化)的操作。在横切的世界里,横切有两种类型:动态横切和静态横切。在本文中,尽管我将简要地同时讨论二者,但我主要关注静态横切。
动态横切
动态横切 是通过 切入点 和 连接点 在一个 方面 中创建行为的过程,连接点可以在执行时横向地应用于现有对象。动态横切通常用于帮助向对象层次中的各种方法添加日志记录或身份认证。下面让我们花点时间了解一下动态横切中的一些实际概念:
方面(aspect)类似于 Java 编程语言中的类。方面定义切入点和通知(advice),并由诸如 AspectJ 这样的方面编译器来编译,以便将横切(包括动态的和静态的)织入(interweave)现有的对象中。
一个 连接点(join point) 是程序执行中一个精确执行点,比如类中的一个方法。例如,对象 Foo 中的方法 bar() 就可以是一个连接点。连接点 是个抽象的概念;不用主动定义一个连接点。
一个 切入点(pointcut) 本质上一个用于捕捉连接点的结构。例如,可以定义一个切入点来捕捉对对象 Foo 中的方法 bar() 的所有调用。和连接点相反,切入点需要在方面中定义。
通知(advice) 是切入点的可执行代码。一个经常定义的通知是添加日志记录功能,其中切入点捕捉对对象 Foo 中的 bar() 的每个调用,然后该通知动态地插入一些日志记录功能,比如捕捉 bar() 的参数。
这些概念是动态横切的核心,虽然正如我们即将看到的,它们并不全都是静态横切所必需的。请参阅 参考资料 来了解关于动态横切的更多内容。
静态横切
静态横切 和动态横切的区别在于它不修改一个给定对象的执行行为。相反,它允许通过引入附加的方法字段和属性来修改对象的 结构。此外,静态横切可以把扩展和实现附加到对象的基本结构中。
虽然现在还无法谈及静态横切的普遍使用——它看起来是 AOP 的一个相对未被探索(尽管非常具有吸引力)的特性——然而这一技术蕴含的潜力是巨大的。使用静态横切,架构师和设计者能用一种真正面向对象的方法有效地建立复杂系统的模型。静态横切允许您不用创建很深的层次结构,以一种本质上更优雅、更逼真于现实结构的方式,插入跨越整个系统的公共行为。
在本文剩下的篇幅中,我将重点讲解静态横切的技术和应用。
创建静态横切
创建静态横切的语法和动态横切有很大的不同,即没有切入点和通知。给定一个对象(比如下面定义的 Foo),静态横切使得创建新方法、添加附加的构造函数,甚至改变继承层次都变得十分简单。我们将用一个例子来更好地展示静态横切是怎样在一个现有的类中实现的。清单 1 显示了一个简单的没有方面的 Foo。
清单 1. 没有方面的 Foo
public class Foo {
public Foo() {
super();
}
}
如清单 2 所示,在一个对象中添加一个新的方法和在一个方面中定义一个方法是同样简单的。
清单 2. 向 Foo 添加一个新方法
public aspect FooBar {
void Foo.bar() {
System.out.println("in Foo.bar()");
}
}
构造函数略有区别的地方在于 new 关键字是必需的,如清单 3 所示。
清单 3. 向 Foo 添加一个新的构造函数
public aspect FooNew {
public Foo.new(String parm1){
super();
System.out.println("in Foo(string parm1)");
}
}
改变对象的继承层次需要一个 declare parents 标签。比如,为了变成多线程的,Foo 将需要实现 Runnable,或者扩展 Thread。清单 4 显示了用 declare parents 标签来改变 Foo 的继承层次。
清单 4. 改变 Foo 的继承层次
public aspect FooRunnable {
declare parents: Foo implements Runnable;
public void Foo.run() {
System.out.println("in Foo.run()");
}
}
现在,您可能开始独自设想静态横切的含意了,特别是在与创建松散耦合、高度可扩展的系统有关时。在下面的几小节中,我将带您看一个一个真实的设计和实现场景,以展示使用静态横切来扩展您的企业应用的灵活性是多么容易。
实现场景
企业系统经常被设计来利用第三方的产品和库。为了不把整个结构和所需产品耦合在一起,通常在设计来与外部厂商代码交互的应用中包括进一个抽象层。在插入其他厂商的实现乃至自主开发的代码时,这个抽象层在对系统的一致性产生最小破坏的情况下,为该体系结构提供了高度的灵活性。
在这个实现场景中,设想系统在某一操作发生后,通过各不相同的通讯渠道通知客户。这个例子系统使用了一个 Email 对象来代表直接电子邮件通讯的一个实例。如清单 5 所示,Email 对象包含了诸如发件人地址、收件人地址、主题栏和消息正文等属性。
清单 5. 例子 Email 对象
public class Email implements Sendable {
private String body;
private String toAddress;
private String fromAddress;
private String subject;
public String getBody() {
return body;
}
public String getFromAddress() {
return fromAddress;
}
public String getSubject() {
return subject;
}
public String getToAddress() {
return toAddress;
}
public void setBody(String string) {
body = string;
}
public void setFromAddress(String string) {
fromAddress = string;
}
public void setSubject(String string) {
subject = string;
}
public void setToAddress(String string) {
toAddress = string;
}
}
整合第三方代码
除了建立一个发送电子邮件、传真、短消息等的自定义通讯系统之外,体系结构团队决定整合进一个供应商的产品,该产品能遵循特定的规则,发送基于任意对象的消息。该产品非常灵活,并且通过 XML 提供了一个映射机制,允许将自定义客户端对象映射到与厂商的特定渠道实现。该厂商的系统严重依赖于这一映射文件和 Java 平台的反射能力来与普通 Java 对象协同工作。为了体现灵活性,体系结构团队建立了一个 Sendable 接口模型,如清单 6 所示。
清单 6. 例子 Sendable 接口
public interface Sendable {
String getBody();
String getToAddress();
}
图 1 显示了 Email 对象和 Sendable 接口的类图。
图 1. Email 和 Sendable 的类图
设计挑战
除了通过不同渠道发送各种格式的消息的能力之外,供应商通过一个已给的接口,提供一个钩子(hook)来允许进行收件人地址验证。供应商的文档表明,实现这个接口的任何对象都将遵循一个预定义的生命周期,它 validateAddress() 方法将被调用,并正确地处理相应的结果行为。如果 validateAddress() 返回 false,供应商的通讯系统将不再试图进行相应的通讯。清单 7 显示了供应商的 validateAddress() 接口。
清单 7. Sendable 接口的地址验证
package com.acme.validate;
public interface Validatable {
boolean validateAddress();
}
运用基本的面向对象设计原理,体系结构团队决定修改 Sendable 接口来扩展供应商的 Validatable 接口。但是,这一决定将导致对供应商代码的直接依赖和耦合。如果开发团队接下来决定用另一个供应商的工具,就不得不重构代码库,以删除 Sendable 接口中的 extends 语句和对象层次中已实现的行为。
一个更优雅且根本上更灵活的解决方案就是使用静态横切来对预期对象添加行为。
静态横切带来援助
运用面向方面的原理,该团队可以创建一个方面来声明 Email 对象实现供应商的 Validatable 接口;此外,在方面中,体系结构团队将预期的行为编码在 ValidataAddress() 方法中。因为 Email 对象中既不包含任何供应商包的导入,也没有定义 validateAddress() 方法,这样代码就更好地消除了耦合。Email 对象根本没意识到它是 validatabl e 类型的对象!清单 8 显示了结果方面,其中 Email 静态地得到增强以实现供应商的 Validatable 接口。
清单 8. Email 可验证方面
import com.acme.validate.Validatable;
public aspect EmailValidateAspect {
declare parents: Email implements Validatable;
public boolean Email.validateAddress(){
if(this.getToAddress() != null){
return true;
}else{
return false;
}
}
}
测试一下!
您可以利用 JUnit 来证明 EmailValidateAspect 确实改变了 Email 对象。在一个 JUnit 测试套件中, Email 对象可以用缺省值创建,然后一系列测试案例可以检验 Email 的确是 Validatable 的一个实例;此外,可以通过一个测试案例来断言,如果 toAddress 为 null,对 validateAddress() 的调用将返回 false。另外,还可以用另一个测试案例来检验:一个非 null 的 toAddress 将导致 validateAddress() 返回 true。
1-2-3,用 JUnit 进行测试
您可以首先创建这样一个结构,它构造带有简单值的 Email 对象实例。注意在清单 9 中,该实例确实有一个有效(意为非 nul)的toAddress 值。
清单 9. JUnit setUp()
import com.acme.validate.Validatable;
public class EmailTest extends TestCase {
private Email email;
protected void setUp() throws Exception {
//set up an email instance
this.email = new Email();
this.email.setBody("body");
this.email.setFromAddress("dev@dev.com");
this.email.setSubject("validate me");
this.email.setToAddress("ag@ag.com");
}
protected void tearDown() throws Exception {
this.email = null;
}
//EmailTest continued...
}
对于一个有效的 Email 对象,estEmailValidateInstanceof() 确保实例是 Validatable 类型的,如清单 10 所示。
清单 10. JUnit 校验实例
public void testEmailValidateInstanceof() throws Exception{
TestCase.assertEquals("Email object should be of type Validatable",
true, this.email instanceof Validatable);
}
如清单 11 所示,下一个测试案例故意把 toAddress 字段设置为 null ,然后检验 validateAddress() 将返回 false。
清单 11. JUnit null toAddress 检查
public void testEmailAddressValidateNull() throws Exception{
//force a false
this.email.setToAddress(null);
Validatable validtr = (Validatable)this.email;
TestCase.assertEquals("validateAddress should return false",
false, validtr.validateAddress());
}
最后一步是出于稳健的考虑:testEmailAddressValidateTrue() 测试案例用 Email 实例的初始值调用 validateAddress(),即 toAddress 域的值为 ag@ag.com。
清单 12. JUnit 非 null toAddress 检查
public void testEmailAddressValidateTrue() throws Exception{
Validatable validtr = (Validatable)this.email;
TestCase.assertEquals("validateAddress should return true",
true, validtr.validateAddress());
}
重构该例子
体系结构团队想方设法使用 Sendable 接口来抽象通讯实现;然而,他们的第一次尝试就好像忽略了这个接口。从静态横切民Email 对象中吸取教训之后,他们通过把契约行为在对象层次中提升到 Sendable 基接口,从而进一步精炼了其策略。
新的方面创建了一个用供应商的 Validatable 接口来对 Sendable 接口进行的扩展。此外,他们在方面中创建了已实现的行为。这次,validateAddress() 方法是为另一个通信对象定义的:Fax,如清单 13 所示。
清单 13. 一个更好的方面
import com.acme.validate.Validatable;
public aspect SendableValidateAspect {
declare parents: Sendable extends Validatable;
public boolean Email.validateAddress(){
if(this.getToAddress() != null){
return true;
}else{
return false;
}
}
public boolean Fax.validateAddress(){
if(this.getToAddress() != null
&& this.getToAddress().length() >= 11){
return true;
}else{
return false;
}
}
}
永远不要停止重构
您可能注意到清单 13 中的方面稍有不足,因为所有 Sendable 的实现者各自都有在同一个方面中定义的 validateAddress() 方法。这很容易导致代码膨胀。另外,如果不谨慎处理,改变一个接口的静态结构将出现很多不希望的副作用:必须找到目标接口的所有实现者。因此,这里的教训很简单:永远不要停止重构。
结束语
虽然这里的 API 例子是人为的,但它有望证明在企业体系结构中应用静态横切是多么简单。静态横切应用于本文描述的这一类场景(它可以在其中用于非强制性地改变对象的行为甚至定义)尤为有效,不过它还有其他很多用处。譬如,您可以在开发时用静态横切来“EJB 化”POJO(传统的普通 Java 对象);或者您可以在业务对象中用它来利用诸如 Hibernate 的持久框架的生命周期接口(请参阅参考资料)。
静态横切为很多影响企业代码有效性的轻微缺陷提供了优雅的解决方案。通过本文,您已经学习了该技术的基础知识和它最基本的应用之一。请参阅 参考资料 来学习更多关于面向方面编程和其他横切技术的知识。
参考资料
下载本文中用到的源代码。
您可以从 eclipse.org/aspectj 下载 AspectJ 及其相关工具。该网站还包括一个FAQ、邮件列表、精彩文档和关于 AOP 的其他资源的链接,是一个开始进一步研究的好地方。
AspectWerkz 是一个针对 Java 平台的动态、轻量级和高性能的 AOP/AOSD 框架。
Eclipse IDE 特别提供了一个 AspectJ 插件。
要获得关于面向方面软件开发的综合信息资源,请试试 AOSD.net。
JBoss 团队已经创建了一个有趣的 AOP 框架。
Hibernate 是针对 Java 平台的一个强大的、超高性能的对象/关系型持久性和查询服务。
Codehaus一个包含很多有趣的开放源代码项目的大型知识库,其中包括 AspectWerkz 和 Nanning(另外一个针对 Java 平台的 Aspect 实现)。
访问 Developer Bookstore 以获得技术书籍的全面列表,其中包括 Ivan Kiselev 的 Aspect-Oriented Programming with AspectJ (Sams Publishing,2002)和 Ramnivas Laddad 的 AspectJ in Action (Manning Publishing,2003),以及其他的大量 Java 相关的书籍。
在 developerWorks Java 技术专区 可以找到关于 Java 编程各个方面的文章。
也可以从 developerWorks 浏览 Java 技术专区教程主页 ,获得针对 Java 的免费教程的详尽列表。
关于作者
Andrew Glover 是 Vanward Technologies 的首席技术官(CTO),该公司是一家位于华盛顿特区市中心的公司,专业从事自动测试框架的构建,以减少软件中的 bug 数量,降低集成和测试次数,并提高整体代码稳定性。