内容


AOP@Work

AOP 和元数据:完美的匹配,第 1 部分

元数据增强的 AOP 的概念和结构

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: AOP@Work

敬请期待该系列的后续内容。

此内容是该系列的一部分:AOP@Work

敬请期待该系列的后续内容。

新的 Java 元数据功能(facility)是 J2SE 5.0 的一部分,它可能是当前 Java 语言中最重要的增强。通过提供为程序元素附加额外数据的标准方法,元数据功能具有简化和改进许多应用程序开发领域的潜在能力,其中包括配置管理、框架实现和代码生成。这个功能还对面向方面的编程(即 AOP)具有特殊意义的影响。

元数据与 AOP 的结合带来了一些重要的问题,其中包括:

  • 元数据功能对 AOP 有什么影响?
  • 元数据增强的 AOP 是可选的还是必需的?
  • 在哪儿可以找到在 AOP 中有效地使用元数据的准则?
  • 这种结合对 AOP 的采用有什么影响?

在这个由两部分组成的系列文章中,我将回答这些问题,这是新的 AOP@Work 系列的第二篇文章。 在这篇文章的前半部分中,我首先将对元数据和 Java 元数据功能的概念进行介绍,还将说明提供元数据和消费它的区别,并提供一些适合使用元数据注释的常见编程场景。下一步,我将快速地回顾 AOP 的连接点模型的基本内容,并说明它从元数据增强中可以获得哪些好处。最后是一个实际的例子,将使用元数据增强的 AOP 分五步完善一个设计。在第 2 部分,我将展示一种将元数据视为多维关注点空间中的 签名的一种创新方法、讨论元数据对 AOP 采用的影响,最后提供一些有效结合 AOP 与元数据的指导。

在本文中,我将采用三种重要的 AOP 实现的例子,将这些概念应用到实例中,这三种实现是 AspectJ、AspectWerkz 和 JBoss APO。请参阅参考资料,以获得关于 Java 元数据功能和面向方面编程的一组介绍文章的清单。

元数据的概念

元数据 是关于数据的数据。在编程语言上下文中,元数据是添加到程序元素如方法、字段、类和包上的额外信息。元数据是用称为注释 的程序元素表示的。与元数据相关的语义范围很广,从纯粹的文档,到执行行为的修改。例如,可以用元数据描述类的作者和 版权所有者,这时它对程序的执行没有影响,也可以用它描述方法属性,比如 事务特性,这很可能改变方法的行为,正如我在本文后面所描述的。

虽然在 Java 语言中有众多的元数据工具(最著名的是 XDoclet), 但是元数据注释直到发行 Java 5.0 时才被添加到 Java 语言的核心中。 Java 元数据功能(JSR 175,请参阅参考资料)包括一 种机制,该机制允许在 Java 代码中添加自定义注释,并允许通过反射(reflection),以编程方式访问元数据注释。

理解和使用元数据的关键是供应和消费的概念。元数据的供应者 是将注释实例 与程序元素关联的工具,消费者 是读取、解释以及对注释实例进行操作的工具。在下一节中,我将更详细地讨论这些概念。

提供元数据

元数据功能定义了向程序元素提供注释的机制。元数据功能也可以指定一种定义注释类型的方法。 与类指定一个创建 对象的模版很相像,注释类型指定创建注释实例的模版。之后,元数据功能可以检查注释实例的注释类型。元数据功能也可以指定一种消费注释的一般方式。元数据功能不会做的一件事是定义 与注释相关的解释和语义。这是留给消费者做的,正如我将在 下一节中所讨论的那样。

元数据功能的能力是不同的。例如,Javadoc 规范使您可以在与 程序元素相关联的备注中指定注释。其他一些元数据功能则使用单独的文档(通常是 XML 文档)表示元数据。例如,EJB 部署描述符指定企业 bean 的额外特性。与 Javadoc 功能不同,这种 方式将程序元素与元数据松散地结合,不利的方面是,它需要开发人员修改 多处描述同一元素的地方。

Java 元数据功能增加了新的语言支持,以便允许元数据声明注释类型和注释(annotate)程序元素。它还使得在类文件中以源 代码级别保留元数据、并在运行时由保持(retention)策略控制成为可能。

元数据的供应者可以使用一种以横切(crosscutting)方式附加某种注释、而不是向多个元素单独提供注释的功能。 由于这种注释的横切本性,用 AOP 提供它们是很好 的一种方法。我将在本文后面详细介绍这种功能的细节。

元数据功能的选择会影响表示元数据的方式,但是将附加数据与程序元素相关联的基本想法对所有元数据功能而言是相同的。

消费元数据

在元数据注释中创建一些值是为了消费元数据。元数据可以有不同的 消费方式,理解这些用法会帮助理解 AOP 与元数据的结合。 下面的用例有助于读者理解如何在 AOP 实现中消费为非 AOP 目的提供的注释。

代码生成
代码生成也许是使用元数据的最熟悉的方式。使用类似 XDoclet 的工具,可以消费在 Javadoc 标签中指定的注释,从而生成像 XML 文档或者 Java 代码这样的人工内容。生成的代码又会影响所注释元素的运行时行为。一个支持元数据功能的新 XDoclet 版本 已经开发出来了。命令行工具 apt (注释 处理工具),作为 Java 2 SDK 5.0 发布的一部分,也提供了一种通过编写 插件程序处理注释的方法。例如,一个最近发布的契约增强工具 Contract4J 使用 apt 生成某些方面,以增强契约式设计(DBC)的契约。

程序式行为修改
标准的元数据功能提供了让注释在运行时可用的方法。它还使您可以以编程方式利用反射访问 注释实例。然后可以像其他对象一样,用注释实例 修改程序的行为。这种程序式的消费还可以让程序员 跳过应用程序的代码生成例程,生成的代码只允许读取在注释中编码的信息。

框架消费
元数据常常用于协助程序元素与框架或者 EJB、EMF 和 TestNG 这样的工具之间的通信。框架本身可以选择使用代码生成、反射访问或者将某种逻辑应用到执行中的 AOP。在 EJB 3.0 中,注释的建议用法,如 @Remove@Session,将告诉框架程序元素的作用。Eclipse Modeling Framework 使用注释(当前表示为 Javadoc 标签)创建 UML 模型和 XML 持久化支持。在工具方面,(比如)TestNG 使用元数据在测试用例与测试执行工具之间通信。

语言扩展
对元数据的这种使用扩展了底层编程语言和编译器。将语义属性与元数据相关联意味着编译的类可以与没有它们的类具有不同的结构和行为(请参阅 参考资料提供的对这一主题的进一步讨论)。 最近宣布的 Pluggable Annotation Processing API(JSR 269)会带来一种 处理这种注释的标准方法。使用元数据扩展 Java 语言能带来强大功效,但又 危险:一方面,注释使我们可以不用修改核心语言就可在 Java 语言中添加新功能, 使核心语言成为一种开放式语言,在最好的情况下,有原则的扩展会克服原语言中 的一些限制。另一方面,非标准的注释、特殊注释或者不一致的注释 会带来难以理解的代码。

顺便说一下,在纯面向对象的语言中启用 AOP 是使用元数据进行语言扩展 的一个例子。AspectWerkz 和 JBoss AOP 使用元数据将类的语义转换为一个方面、将数据字段转换为一个切入点、将方法转换为一个通知,等等。AspectJ 5 同样也将支持 @AspectJ 语法,这是 AspectJ 与 AspectWerkz 项目合并的结果。请参阅参考资料,以了解更多关于不同 AOP 实现和 Java 语言扩展的内容。

在下一节中,我将快速地回顾 AOP 的连接点模型(join point model)的基本内容, 然后说明是如何用元数据增强它。

元数据和连接点模型

连接点 是系统执行中的一个可标识的点。连接点模型 是 AOP 中最基本和最独特的概念,它定义了系统中哪个连接点是公开的,以及如何捕获它们。要用方面实现横切功能,需要用名为切入点的编程结构捕获所需要的连接点。

切入点 选择连接点,并收集所选的连接点处的上下文。 所有 AOP 系统都提供一种定义切入点的语言。切入点语言的 复杂程度是不同 AOP 系统的一种区分元素。切入点语言越成熟, 越容易编写健壮的切入点。请参阅 AOP@Work 系列的 第一篇文章,学习关于切入点语言的重要性的详细讨论(请参阅参考资料)。

捕获连接点

切入点指定程序给定元素的属性。编写好的方面的要点 在于编写强壮的切入点,其他重要部分是 良好设计的方面继承关系。当系统发展时,捕获比预计多的连接点或者错过预计连接点 的切入点都会使系统容易崩溃。编写好的切入点是掌握好 AOP 的关键,尽管这对于新手来说通常不是一件容易的事。

目前,捕获连接点的最常用方法是利用程序元素的隐式属性,包括 静态属性,如签名(它包括类型和方法名、参数类型、返回类型和异常 类型等)和词汇排列(lexical placement),以及动态属性(如控制流程)。 在连接点签名中明智地使用通配符通常可以产生好的、简洁的连接点定义。 还可以将单独的连接点组合为更复杂的连接点。基于程序元素的隐式属性的 连接点模型非常强大并且很有用,AOP 在当前生产系统中的成功证明了 这一点。

在于程序元素中可用的隐式信息通常足以捕获所需要的连接点。在这 种有时称为动态横切的模型中,隐式数据、通配符和动态属性(如 控制流程的结合)使您不用修改所捕获的程序的元素就可以捕获连接点。 例如,可以通过指定在实现了 Remote 接口 的类中抛出 RemoteException 的操作来捕获所 有 RMI 调用。一个像 execution(* Remote+.*(..) throws RemoteException) (在 AspectJ 中定义)这样的连接点可以很好地 捕获所有 RMI 操作,而无需修改程序元素,并且保证有一个强壮的切入点。 这里很好的一点是可以不需要加入比 RMI 基础设施所需要的更多的协作就可以捕获 连接点。

用元数据捕获连接点

基于签名的切入点不能捕获实现某种横切功能所需要的连接点。 例如,如何捕获需要事务管理或者授权的连接点呢? 与 RMI 的例子不同,在元素名或者签名中没有什么内在的东西说明事务性或者授权特性。 在这种情况下,所需要的切入点可能会变得很难处理,从下面的例子中就可看到。 (这是 AspectJ 的例子,但是在其他系统中的切入点在概念上是相同的。)

pointcut transactedOps() 
    : execution(public void Account.credit(..))
      || execution(public void Account.debit(..))
      || execution(public void Customer.setAddress(..))  
      || execution(public void Customer.addAccount(..))
      || execution(public void Customer.removeAccount(..));

像这样的情况就要用元数据捕获所需要的连接点。例如,可以编写 如下所示的切入点,以捕获所有带有 @Transactional 注释 的方法的执行。

pointcut execution(@Transactional * *.*(..));

元数据和模块化

虽然上述例子使使用元数据捕获连接点看起来不用费什么脑子, 但是对这种使用的潜在影响加以考虑是很重要的,特别是涉及到模块化(modularity)的时候。 一旦开始在切入点中使用元数据,方法中必须携带相应的注释,以便在使用 它们的方面的横切实现中进行协作,如下所示:

public class Account {
    ...
    @Transactional(kind=Required)
    public void credit(float amount)  {
        ...
    }
    @Transactional(kind=Required)
    public void debit(float amount)  {
        ...
    }
	
    public float getBalance() {
        ...
    }
    ...
}

与此类似,Customer 类中的 addAccount()removeItem()setAddress() 方法现在必须携带 @Transactional 注释。

大多数 AOP 实践者目前用现有的 AOP 支持实现事务和授权功能,通常是 通过使用方面继承的设计模式。不过,正如在本文将会看到的,在 AOP 系统中添加元数据可以显著改进它们。我将进一步讨论添加元数据如何影响 AOP 系统的模块化,并在本文的第二部分中讨论元数据发挥最大作用的场景。在下一节中,我将开始更具体地说明如何扩展 AOP 实现来添加元数据。

元数据增强的 AOP

AOP 系统及它们的连接点模型可以通过使用元数据注释扩展。JBoss AOP、Spring AOP、AspectWerkz 和 AspectJ 都提供或者计划提供利用 元数据的机制。JBoss AOP 和 AspectWerkz 的当前版本支持元数据。Spring AOP 通过实现 org.springframework.aop.Pointcut 接口,允许通过编程方式 编写切入点来支持元数据。新的 AspectJ 版本将通过修改 AspectJ 语言支持元数据。

在上一节中,我展示了 AOP 如何消费元数据的基本内容,使用了用 @Transactional 注释选取方法的例子。 在这一节和本文其余部分,我将重点介绍结合 AOP 和元数据的细节。

虽然本文中的重点是支持元数据的 AOP 实现,如果利用代码生成支持,即使在核心 AOP 系统不直接 支持消费元数据时,也可以做到这一点。 例如,Barter 是一种开源工具,它使用注释和代码生成预先执行步骤,以增强不支持用 Javadoc 标签捕获连接点的老版本 AspectJ 上的 DBC 合同。今天,Contract4J 用 Java 元数据功能样式的注释执行 类似的任务。请参阅参考资料,以学习更多关于这种 工具的内容。

AOP 系统中的元数据支持

为了支持基于元数据的横切,AOP 系统需要提供一种消费和提供 注释的方法。我将在这里介绍这两种支持的基本内容。在下一节我将提供关于每 种方法的更多细节。

支持消费注释
支持消费注释的 AOP 系统使您可以基于与程序元素相关联的注释选择连接点。当前提供这种支持的 AOP 系统实现了这一点,它们是通过扩展不同签名样式的定义来指定注释类型和属性的方式实现的。例如,一个切入点可以选择所有携带 类型为 Timing 的注释的方法。而且,它可以进一步 只选择(比如说)Value 属性超过 25 的方法。要实现取决于注释类型和属性的通知(advice), 系统可以包括那些捕获与连接点相关联的注释实例的切入点语法。最后,系统 还可以让通知通过反射 API 来访问注释实例。

支持提供注释
在标准的 Java 元数据功能中,要对每一个已注释的程序元素声明 一个注释实例。如果多个程序元素有同样的注释声明,那么就会 产生不必要的混乱。可以利用 AOP 的横切机制对所有受影响的元素进行一次注释。一个支持提供注释的 AOP 系统可以以横切方式将注释附加到程序元素上。例如,可以用一个简单的声明将 @Secure 注释附加到 Account 类的所有方法上,而无需在这种方法中分别加入注释。

并不是所有 AOP 系统支持这里提到的所有方法,在下面的讨论中可以了解更 多细节。我首先分析几种 AOP 系统是如何提供对消费注释的支持的。

在 AOP 中消费注释

切入点语法在不同的元数据增强的 AOP 系统中是不同的。通过分析每种系统 是如何处理捕获所有携带 @Transactional 注释实例的方法的切入点,可以了解 这种区别。在这些例子中,我将重点放在通过注释类型选择连接点上,然后我将 进一步解释在选择连接点时,其他会起作用的因素。

AspectJ5
AspectJ 5 语法(在编写本文的时候,它处于重要转折阶段) 扩展了类型、方法和字段的定义,以将注释作为签名的一部分加入,如下所示:

pointcut transactedOps(): execution(@Transactional * *.*(..));

如果想要在 AspectJ 5 中使用 @AspectJ 样式的定义,那么同样的 切入点将有如下定义:

@Pointcut("execution(@Transactional * *.*(..))")
void transactedOps();

AspectWerkz
像大多数其他 AOP 工具一样,AspectWerkz 的切入点语法与 AspectJ 的语法非常相像。下面代码段中的切入点声明使用了元数据类型的 注释,而 XML 类型具有同样的切入点表示:

@Expression("execution(@Transactional * *.*(..))")
Pointcut transactedOps;

注意,AspectWerkz 使用元数据扩展 Java 编程语言,以便支持 AOP, 如上述例子所示。因此,AspectWerkz 出于两种目的使用元数据:扩展编程元素的语义和实现基于元数据的横切。 在上面的例子中,我着重分析了后一种用途。

JBoss AOP
JBoss AOP 在概念上与其他 AOP 系统没有很大区别,尽管它使用了不同的语法。 下面显示的切入点与其他例子相同,但是是用 JBos XML 语法表示的:

<pointcut name="transactedOps" expr="* *->@Transactional(..)"/>

可以看出,AOP 系统在根据附加到连接点上的元数据注释捕获 它们的方式上没有概念上的差别。

按注释属性选择

在选择连接点时,类型不是惟一要考虑的事项:还可以考虑属性。例如,下面的切入点将捕获 value 属性设置为 RequiredNew 的、带有 @Transactional 注释的所有方法:

execution(@Transactional(value==RequiredNew) *.*(..))

在编写本文的时候,还没有支持基于注释属性的切入点的 AOP 系统。 相反,每个系统都是根据通知中的动态决定来检查属性,并调用相应的逻辑(或者 至少使用 if() 切入点动态检查)。基于注释属性的切入点有某些好处,特别是对于编译时检查和静态选择的效率。 下一版本的 AOP 系统支持这种切入点。

公开注释实例

由于通知逻辑可以取决于元数据注释的类型实例属性,每一个注释实例 都必须使用与连接点上的其他上下文相同的方式公开上下文(例如,对象、方法参数等)。AspectJ 5 扩展了现有的切入点,并增加了几个新的切入点来公开注释。 例如,下面的切入点收集与捕获的连接点相关联的、类型为 Transactional 的注释实例:

pointcut transactedOps(Transactional tx)
    : execution(@Transactional * *.*(..)) && @annotation(tx);

捕获注释实例后,可以用与任何其他上下文相同的方式使用注释实例。 例如,在下面的通知中,查询捕获的 注释,以获得其属性:

Object around(Transactional tx) : transactedOps(tx) {
    if(tx.value() == Required) {
        ... implement the required transaction behavior
    } else if(tx.value() == RequiredNew) {
        ... implement the required-new transaction behavior
    }
    ...
}

大多数 AOP 系统只使用反射 API 公开捕获的连接点上的 注释实例,并且不允许将注释绑定为连接点上下文。 在这种情况下,可以查询表示已通知连接点的对象来获得相关的注释。AspectJ 提供了反射访问和传统的连接点上下文。请参阅参考资料,以了解更多关于 AspectJ 对 公开注释实例的支持。

在 AOP 中提供注释

使用 AOP 结构提供注释背后的基本想法是避免将程序元素定义与 注释相混淆。概念上,这种结构可以以横切的方式在程序元素 上附加注释。初看之下,使用 AOP 构造提供注释、然后使用这些注释捕获连接点似乎是不必要的、多余的。总之,如果可以确定需要 注释的连接点,那么就可以编写一个切入点,并直接通知这些 连接点。不过,以横切的方式提供注释是很有用的。 首先,这种声明可以作为与非 AOP 客户通信的管道。其次,以横切机制提供 注释使得设计一种更松散耦合的系统、同时避免注释混乱成为 可能。

在本文最后,我将说明使用 AOP 构造提供注释带来的一些 设计可能性。现在,我将展示以横切方式提供注释的基本语法。

提供注释语法

AspectJ 建议的语法扩展了当前的静态横切构造,以创建一个新的 declare annotation 语法。下面的代码段将附加一个类型为 Authenticated 的、permission 属性设置为 banking 的注释:

declare annotation : * Account.*(..) 
                   : @Authenticated(permission="banking");

@AspectJ 切入点还通过使用 @DeclareAnnotation 支持 同样的功能,可以像下面这样编写同样的声明:

@DeclareAnnotation("* Account.*(..)")
@Authenticated(permission="banking") 
void bankAccountMethods();

在 JBoss AOP 中,当使用 XML 样式的方面时,可以用 annotation-introduction 元素附加注释。invisible 属性指出 在运行时是否保留注释(等同于标准 Java 元数据功能中的 RetentionPolicy.SOURCE)。

<annotation-introduction expr="method(* Account->*(..))"
                         invisible="false">
      @Authenticated(permission="banking")
</annotation-introduction>

可以看出,提供注释的原理在不同的 AOP 系统中是相同的, 尽管语法不一样。

使用元数据的 AOP 设计

从前面几节中可以看出,将元数据与 AOP 结合是相当简单的。重要的是 知道什么时候使用基于元数据的横切、什么时候不使用它。 在本节中,通过考虑系统如何从一个使用隐式连接点属性的 AOP 实现进化为 一个结合了基于元数据切入点的实现,我将回答这个问题。在第 2 部分中,我将 探讨选择元数据驱动方式的概念性问题。

本节的讨论应当在两个方面提供帮助:首先,作为 AOP 实践者,使用元数据并不总是第一或者惟一的选择,理解这一点很重要。其次,这里的示例实现可以指导您在决定使用基于元数据的横切后, 如何改进设计。

可以将一个事务管理程序作为练习的例子。虽然我使用了 AspectJ 开发这个例子的 所有代码,但是在其他 AOP 系统中的实现在概念上是相同的。将这个练习中的每一 步都看成是对原设计的改造。目标是逐渐分离系统并改进其模块性。

版本 1: 原生方面

我模块化一个横切功能的第一次尝试是使用 特定于系统方面,它包含了切入点定义和这个切入点的通知。 这是非常简单的方案,并且通常是学习 AOP 时遇到的第一个设计。 图 1 显示了这个使用一个方面的设计示意图:

图 1. 用 AOP 实现事务管理的第一个努力
图 1. 用 AOP 实现事务管理的第一个努力
图 1. 用 AOP 实现事务管理的第一个努力

清单 1 实现上述设计

清单 1: 银行系统的事务管理方面
public aspect BankingTxMgmt {
    pointcut transactedOps() 
        : execution(void Customer.setAddress(..))
          || execution(void Customer.addAccount(..))
          || execution(void Customer.removeAccount(..))
          || execution(void Account.credit(..))
          || execution(void Account.debit(..));
          
    Object around() : transactedOps() {
         try {
             beginTransaction();
             Object result = proceed();
             endTransaction();
             return result;
         } catch (Exception ex) {
             rollbackTransaction();
             return null;
         }
    }
    
    ... implementation of beginTransaction() etc.
}

对于需要很少底层系统信息的方面,这个方案可以工作得很好。 例如,如果希望启用池功能,可以编写一个一般性的方面, 通知对池中资源进行创建和销毁调用。可是对于不能用一般方法捕获所需连接点的横切功能,这种方法存在局限性。 首先,这个方面不是可重用的,因为切入点定义是特定于 系统的。其次,对系统的改变可能使这个方面也作出改变。 换句话说,系统的第一个版本使我们得到程序元素与切入点之间的一个 N 对一的 耦合。因为这不是最佳选择,所以我还要再努力。

版本 2:可重用的方面

我的第二个努力通过使之可重用来改进这个示例方面。我抽取了方面的 可重用的部分,并增加了一个以特定于系统的方式定义切入点的子方面(subaspect)。 图 2 显示了提取了基本方面的结构:

图 2. 提取一个可重用的事务管理方面
图 2. 提取一个可重用的事务管理方面
图 2. 提取一个可重用的事务管理方面

清单 2 显示了这个基本方面,它现在是可重用的了。可以注意 与清单 1 相比的两个改变:这个方面标记为 abstract,并且 transactedOps() 切入点也标记 为 abstract,而且删除了对它的定义:

清单 2. 可重用的事务管理基本方面
public abstract aspect TxMgmt {
    public abstract pointcut transactedOps(); 
          
    Object around() : transactedOps() {
         try {
             beginTransaction();
             Object result = proceed();
             commitTransaction();
             return result;
         } catch (Exception ex) {
             rollbackTransaction();
             return null;
         }
    }
   
    ... implementation of beginTransaction() etc.
}

下一步,需要为这个基本方面编写一个子方面。下面的子方面定义了一个捕获需要事务管理支持的连接点的切入点。清单 3 显示了一个特定于银行的子方面,它扩展了清单 2 中的 TxMgmt 方面。这个子方面定义了具有与清单 1 相同定义的 transactedOps() 切入点。

清单 3. 特定于系统的子方面
public aspect BankingTxMgmt extends TxMgmt {
    public pointcut transactedOps() 
        : execution(void Customer.setAddress(..))
          || execution(void Customer.addAccount(..))
          || execution(void Customer.removeAccount(..))
          || execution(void Account.credit(..))
          || execution(void Account.debit(..));
}

虽然有了改进,但是这种设计仍然是一个子方面与类之间的 N 对 一依赖关系。银行系统的事务要求的任何改变都需要修改 BankingTxMgmt 的切入点定义。这与理想差得还很远,我将继续努力。

版本 3: Participant 模式

我在上面解决了重用性的问题,但是仍然需要避免 N 对一的依赖关系。可以使用 Participant 模式(请参阅参考资料)做到这一点。不是在 整个系统中使用一个子方面,而是使用许多子方面 —— 每个子系统一个子方面,这使得编写相对稳定的切入点成为可能。 在这个上下文中,一个子系统 可以是一个包、一组包,甚至是一个类。图 3 显示了不同元素之间的结构关系:

图 3. 使用 participant 设计模式
图 3. 使用 participant 设计模式
图 3. 使用 participant 设计模式

清单 4 显示了具有参与者子方面的 Customer 类, 它负责定义嵌入类的切入点。

清单 4. 带有嵌入参与者方面的 Customer 类
public class Customer {
    public void setAddress(Address addr) {
        ...
    }
    public void addAccount(Account acc) {
        ...
    }
    public void removeAccount(Account acc) {
        ...
    }
    ...
    private static aspect TxMgmtParticipant extends TxMgmt {
        public pointcut transactedOps() 
            : execution(void Customer.setAddress(..))
              || execution(void Customer.addAccount(..))
              || execution(void Customer.removeAccount(..));
    }
}

示例 Customer 类中的子方面只是枚举所有以通配符作为参数的方法。不过在现实中,可能会使用 通配符简化切入点的定义。例如,可以使用下面的定义,声明 transactedOps() 切入点捕获类的所有公共方法:

public pointcut transactedOps() 
    : execution(public * Customer.*(..));

在清单 5 中,可以看到 Account 类是如何嵌入一个子方面,从而参与系统的事务管理功能。

清单 5. 带有嵌入参与者方面的 Account 类
public class Account {
    public void credit(float amount) {
        ...
    }
    public void debit(float amount) {
        ...
    }
    public float getBalance() {
        ...
    }
    ...
    private static aspect TxMgmtParticipant extends TxMgmt {
        public pointcut transactedOps() 
            : execution(void Account.credit(..))
              || execution(void Account.debit(..));
    }
}

Customer 类一样,增加这一步会简化这个切入点。 例如,如果发现除了 getBalance() 方法之外,所有公共方法都需要在事务管理中执行怎么办? 可以定义这个切入点,按下方所示方法捕获这种实现:

public pointcut transactedOps() 
   : execution(public void Account.*(..))
     && !execution(float Account.getBalance());

现在,如果类发生改变,那么只需要修改类中嵌套的子方面即可。我已经将系统耦合减少到每个子方面所捕获的更少的程序元素数(比如说从 n 减少到 1),以此取代了那个大大的 N。而且,如果类改变了事务管理需求,只需要改变嵌入 的参与者方面中的切入点即可,这样可以防止局部性。

这个例子展示了在关于 AOP 的讨论中常常会漏掉的重要一点: 如果试图找到整个系统的签名模式,那么就会看到一个令人不快的意外 —— 不稳定的、复杂的和不正确的切入点。不过,考虑系统的子集时,通常会 发现对整个子系统都适用的签名模式。使用每个类具有一个方面的 Participant 模式, 将每个类视为一个子系统,而将任何逻辑分为子系统都可以做得很好。

这种解决方案对于大多数情况是合理的。它的不利之处是类直接依赖于基本方面, 因此基本方面必须总是出现在系统中。这种解决方案的另一个问题是 它的横切功能不能使用,除非类通过嵌入嵌套的子方面显式“参与”协作。 这个问题更多时候与横切功能的本性有关、而不是与解决方案有关。 并且,很快您就会看到,可以对它稍加改进。

版本 4: 基于元数据的切入点

在这个回合中,我准备修改每一个方法,让它有一个注释,并退回到只在系统中使用 一个子方面(像在 版本 2 中一样)。不过,这次我的子方面将使用 一个基于元数据的切入点来捕获所需要的连接点,该连接点携带我提供的注释的方法。 这个子方面本身可以在系统中重用。图 4 显示了这个版本的示意图。

图 4. 元数据驱动的事务管理
图 4. 元数据驱动的事务管理
图 4. 元数据驱动的事务管理

利用基于元数据的子方面,当类中的连接点改变其特性时,只有 这个连接点的注释需要改变。清单 6 显示了扩展 版本 2 中的 TxMgmt方面的子方面,并通过 捕获所有携带类型为 Transactional 的注释的所有方法来定义 transactedOps() 切入点。

清单 6. 元数据驱动的事务管理子方面
public aspect MetadataDrivenTxMgmt extends TxMgmt {
    public pointcut transactedOps() 
        : execution(@Transactional * *.*(..));
}

这个类必须通过向每一个需要在事务管理中执行的方法上附加类型为 Transactional 的注释与子方面进行协作。 清单 7 显示了类 Customer 的实现,其方法中包含以下注释:

清单 7. 带有注释的 Customer 类
public class Customer {
    @Transactional
    public void setAddress(Address addr) {
        ...
    }
    @Transactional
    public void addAccount(Account acc) {
        ...
    }
    @Transactional
    public void removeAccount(Account acc) {
        ...
    }
    ...
}

清单 8 显示了 Account 类的类似实现:

清单 8. 带注释的 Account 类
public class Account {
    @Transactional
    public void credit(float amount) {
        ...
    }
    @Transactional
    public void debit(float amount) {
        ...
    }
    public float getBalance() {
        ...
    }
    ...
}

这时,我已经建立方法与协作方面之间的一对一依赖关系。 我去除了方面与类之间的直接依赖关系。结果,在想要改变基本方面时,现在可以不用对系统的任何地方做任何改变。

基本方面的使用是可选的(也就是说您可以减少分层结构)。不过,将基本方面与元数据驱动的子方面分离具有若干好处。首先,派生 的方面可以选择注释类型。在一个系统中,可以使用 Transactional 作为注释类型来捕获 连接点,而在另外的系统中,注释类型可以是 Tx。其次,它为派生的方面提供了 Participant 模式与元数据驱动的方法的选择。第三,这种方法使得从像 @Purchase 或者 @OrderProcessing 这样的业务注释中派生 出事务切入点成为可能。最后,它使元数据驱动的方法与基于 Participant 的方法的结合成为可能。

通过借助注释的合作,参与责任被转移给了每个方法(而不是参与者子方面)。MetadataDrivenTxMgmt 与类之间的依赖关系局限于注释类型及它们相关的语义。

在大多数情况下,这个版本已经足够好了。不过,还有一个特殊的场景,我可以再进 一步努力,以得到最佳的结果。

版本 5: Aspect 作为元数据供应者

在某些情况下,类中的大多数方法需要携带注释(如 版本 4 中所示)。 而且,许多横切特性需要每个方法有一个或者多个注释。这种条件会使每个方法 声明许多注释,这种情况通常称为注释地狱。结合 Participant 模式与注释者-供应者方面可以减少注释混乱。在有明确的方法表示特定的 连接点时,这是一种有用的选择。在这种情况下,有一种注释者-供应者设计避免了错过连接 点的注释的风险。

图 5. Aspect 作为元数据供应者
图 5. Aspect 作为元数据供应者
图 5. Aspect 作为元数据供应者

注释者方面只是使用一个或者多个 declare annotation:例如,declare annotation : <Method pattern> : <Annotation definition>;。在这个例子中, 我使用了 Participant 模式类型的协作,每个类有一个注释者方面。不过, 这样做不是这种设计的必备要求。比如说,您可以为每个包实现一个注释者。 核心思想是找出一个合适的子系统,它具有明确的签名模式或者动态上下文信息 (控制流程等),可以捕获所需要的连接点,并避免这种情况下的注释混乱。 清单 9 显示了带有注释者方面的 Customer 类。

清单 9. 具有嵌入注释者方面的 Customer 类
public class Customer {
    public void setAddress(Address addr) {
        ...
    }
    public void addAccount(Account acc) {
        ...
    }
    public void removeAccount(Account acc) {
        ...
    }
    ...
    private static aspect Annotator {
        declare annotation: public Customer.*(..): @Transactional;
    }
}

与此类似,清单 10 中的 Account 类包括一个注释者方面。

清单 10. 带有嵌入注释者方面的 Account 类
public class Account {
    public void credit(float amount) {
        ...
    }
    public void debit(float amount) {
        ...
    }
    ...
    private static aspect Annotator {
        declare annotation: public Account.*(..): @Transactional;
    }
}

现在比较这个实现与 版本 3 中使用 Participant 模式 的实现。版本 3 有一个很大的缺点:它使一个类与特定的方面关联在一起。 从某种意义上说,它是一种非常积极的参与 —— 必须总是存在一些基本方面 (由于它们是所有参与方面的基本方面)。请使用注释者方面方法,参与只发生在对注释类型的共同理解这一级别。

连接注释类型

这种技术的一种变化是用注释者方面作为服务于业务目的的注释和方面实现所 使用的注释之间的桥梁。例如,如果知道所有具有 @Purchase@OrderProcessing 注释的方法都必须是事务管理的,那么可以编写如清单 11 所示的方面。

清单 11. 将业务注释转换为横切 2005-3-20 注释
public aspect BusinessTransactionBridge {
    declare annotation: @Purchase *.*(..): @Transactional;
    declare annotation: @OrderProcessing  *.*(..): @Transactional;
}

这个方面将 @Transactional 注释附加到所有具有 @Purchase 或者 @OrderProcessing 注释的方法中。将这种方法与 清单 2清单 6 中的方面结合,就可以将事务管理逻辑用于方法的执行。

结束语

元数据是表示关于程序元素的额外信息的方法。Java 编程语言中新的元数据功能使得使用有类型的注释成为可能。使用元数据很简单,尽管消费它会有许多选择。面向方面的编程本身就表现为原则性的元数据消费者。带元数据参数的连接点模型通过帮助横切功能使用更简单的切入点,使 AOP 更易被接受,而用稳定的、基于签名的切入点难于指定这种横切功能。

在这由两部分组成的系列文章的第 1 部分中,我对元数据概念做了高层次的介绍,并说明了 AOP 如何利用包含在程序元素的元数据中的信息。我还简要分析了 不同 AOP 系统中支持基于元数据的切入点所涉及的机制,并讲解了一个分五 步的设计改造,以展示如何在 AOP 系统中使用元数据。

在本文的第 2 部分中,我将深入研究让 AOP 作为消费者和供应者时,定义和使用元数据时的设计考虑。我将讨论添加元数据会对 AOP 系统中的 obliviousness 原则产生怎样的影响,以及元数据如何影响 AOP 系统的采用。我还要 介绍一种让 AOP 作为多维功能空间中的签名的创新方法,这是一种在日常 AOP 实践中、以及在为非 AOP 目的设计注释类型时有用的概念。

致谢
我要感谢 Ron Bodkin、Wes Isberg、Mik Kersten、Nicholas Lesiecki 和 Rick Warren 对本文的审阅。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=58313
ArticleTitle=AOP@Work: AOP 和元数据:完美的匹配,第 1 部分
publish-date=03082005