AOP@Work: 介绍 AspectJ 5
上一篇 / 下一篇 2008-01-04 10:08:41 / 个人分类:J2SE
目前,AspectJ 5 处在它的第二个里程碑版本,AspectJ 5 是 Java™ 平台上面向方面编程前进的一大步。AspectJ 5 主要的重点是对 Java 5 中引入的新 Java 语言特性(包括注释和泛型)提供支持。另外,AspectJ 5 还包含没有捆绑到 Java 5 的新特性,例如编写方面使用的基于注释的风格、改进的装入时织入以及新的方面实例化模型。现在请随这个项目的首席开发人员 Adrian Colyer 抢鲜了解 AspectJ 5,他将介绍 AspectJ 5 语言和包含 AspectJ 编译器及相关工具的版本。
AspectJ 5 (目前处在它的第二个里程碑版本)的主要重点是对 Java 5 中引入的新 Java 语言特性(包括注释和泛型)提供支持。AspectJ 5 还包含没有加入 Java 5 的新特性,例如基于注释的开发风格、改进的装入时织入和新的方面实例化模型。
在AOP@Work系列的这一期中,我概述了 AspectJ 5 语言和包含 AspectJ 编译器及相关工具的 AspectJ 5 版本。我先介绍如何用 AspectJ 5 编译器编译 Java 应用程序 (既可以用命令行编译器也可以用 AspectJ 开发工具(AJDT;请参阅参考资料)),然后,我提供了使用 Java 5 特性实现 AspectJ 应用程序的一些示例。我还讨论了擦除(erasure)对 AOP 系统的意义,这是 Java 5 中用来实现泛型的技术,我还解释了 AspectJ 解决问题的方式。这篇文章中描述的有些特性只能用即将推出的 AspectJ 5 M3 版本编译(计划在 2005 年 7 月发布)。
也可以下载以下示例中使用的 AJDT 或命令行 AspectJ 编译器。请参阅参考资料获得技术下载的链接。
AspectJ 编译器 (ajc)支持在版本1.3(及以前版本)、1.4和5.0的兼容级别上编译 Java 源代码,并生成针对 1.1 版以上 VM 的字节码。像 javac 一样,ajc 有一些限制:在 1.4 版本兼容级别上编译源代码只支持 1.4 及以上版本的目标 VM,在 5.0 版本兼容级别上编译源代码只支持 5.0 版本的目标 VM。
|
AspectJ 编译器的默认兼容级别是使用 5.0 的源代码级别,并生成针对 5.0 VM 的字节码。可以传递-1.5编译器选项,显式地把源代码兼容级别和目标级别设置为针对 Java 5。假设想用 AspectJ 5 编译器处理 Java 1.4 语言并针对 1.4 VM,那么只需传递-1.4即可。
AspectJ 5 织入器也默认在 Java 5 兼容模式下运行。在这个模式中,织入器会正确地解释 Java 5 中的新特性;例如,编译器在确定args(Integer)是否匹配int参数时,会考虑自动装箱和拆箱。如果不是从源文件编译,而是用编译后的 Java 5 .class 文件 (在inpath上),使用 AspectJ 编译器来织入方面(在aspectpath上),那么这就是想要的行为。传递-1.4或-1.3选项会禁用 Java 5 特性。
AspectJ Development Environment Guide包含更多关于新的编译器标志和选项的信息。请参阅参考资料一节访问这个指南。
如果正在用 AJDT 编译和运行 AspectJ 程序,那么 AspectJ 就继承了 Eclipse 中为 Java
编译指定的编译器选项。在这种情况下,可以对 AspectJ 进行配置,把 Java 5
模式作为工作区选项配置使用,或者以每个项目为基础使用。只要进入 Java 编译器选项配置页,把Compiler compliance level属性设置为5.0即可。如果正在从 JDK 1.4 升级,那么可能还需要在项目的build设置中把 JRE 系统库更新到 Java 5 JRE。
图 1 显示了 AJDT 的 Java 编译器选项配置页和一个用于 Java 5.0 兼容级别的选项配置设置。
图 1. 在 Eclipse 中指定 5.0 兼容级别

有了这些基础知识,现在可以把注意力转到在 AspectJ 中使用 Java 5 特性了。我选择了一个示例方面,它支持一组基本的生命周期操作,可以用于任何被注释为ManagedComponent的类型 。ManagedComponent是一个简单的标记注释,如下所示:
public @interface ManagedComponent {} |
方面本身被设计成可以表现许多 Java 5 和 AspectJ 5 语言的特性,包括枚举、注释、新风格的for循环以及泛型。LifecycleManager方面的第一部分仅定义了enum,表示托管组件可能存在的状态,还定义了托管组件将会支持的Lifecycle接口,如清单 1 所示:
清单 1. 有 State 和 Lifecycle 声明的 LifecycleManager 方面
/** |
方面的下一部分使用了一些新的 AspectJ 5 支持,以进行基于注释的类型匹配。这说明任何具有ManagedComponent注释的类型都要实现Lifecycle接口(并且因此稍后在方面中将会获得为此类组件定义的全部行为)。类型模式“@ManagedComponent *”匹配具有ManagedComponent注释、名称任意的类型,如清单 2 所示:
清单 2. 用基于注释的类型匹配声明双亲
/** |
清单 3 显示了Lifecycle中的添加/删除观察者操作中引用的LifecycleObserver接口的定义:
清单 3. LifecycleObserver 接口
/** |
对于没有提供自己的定义的所有实现者,方面提供了Lifecycle操作的默认实现。它还为所有实现者声明了私有的state和observers字段。注意state字段是枚举类型,而observers字段使用参数化类型,如清单 4 所示:
清单 4. Lifecycle 接口的默认实现
// default implementations for the state-based lifecycle events |
因为我想在这篇文章中介绍许多基础知识,所以从剩下的方面实现中我只摘录几个。对于每个生命周期事件,方面都提供before和after returning建议,以验证托管组件处于有效状态,从而执行操作并把变化通知给已注册的观察者,如下所示:
清单 5. 状态管理和通知
// these pointcuts capture the lifecycle events of managed components |
注意建议体中使用了新风格的for循环,对所有已注册的观察者进行迭代。如果生命周期操作正常返回,就执行after returning建议。如果生命周期操作通过运行时异常而退出,那么后面的建议(清单 6)就把组件转变成BROKEN状态。可以想像,方面中会有进一步的建议,防止对状态是BROKEN的托管组件执行任何操作,但是这个讨论超出了这篇文章的范围:
清单 6. 故障检测和到 BROKEN 状态的转变
/** |
示例方面已经表明,在方面中使用 Java 5 特性,就像在类中使用它们一样简单。而且从匹配的角度来看,根据注释的存在与否(在declare parents语句中),示例方面也给出了 AspectJ 5 能做什么的提示。但是在 AspectJ 5 中除了注释匹配之外还有许多东西,在下一节中会看到。
在AOP@Work系列以前的文章中,介绍了注释、元数据和面向方面编程之间的关系 (请参阅参考资料一节中的 “AOP and metadata”),所以这里不再赘述,直接介绍 AspectJ 5 能做的一些事情。
出于示例的原因,我将采用 EJB 3.0 规范中的一些注释(请参阅参考资料)。
对于具有相关事务策略的方法,可以用@Tx注释进行注释。例如:
@Tx(TxType.REQUIRED) |
如果想编写TransactionManager方面,那么可能会对带有@Tx注释的方法的执行感兴趣。编写与它们匹配的切入点很简单,如清单 7 所示:
清单 7. 匹配事务性方法的执行
public aspect TransactionManager { |
execution(@Tx * *(..))切入点表达式匹配任何方法的执行,可以使用任何名称、任何类型、任何参数,只要方法用@Tx注释。如果需要,也可以缩小范围。到事务性方法的匹配调用同样简单,只需编写“call(@Tx * *(..))”即可。
在这种情况下,实现事务策略的建议需要知道执行方法上的@Tx注释的value。使用 AspectJ,可以把连接点的上下文值绑定到切入点表达式,然后向建议公布上下文。在 AspectJ 5 中,用新的切入点指示符@annotation把这个能力扩展到了注释上。像 AspectJ 中所有的其他上下文绑定切入点指示符一样,@annotation扮演着双重角色:既把连接点匹配限制到主题(方法、字段、构造函数等)具有指定类型注释的连接点上,又公开那个值。可以很容易地重新定义TransactionManager方面,让它利用这一优点,如下所示:
清单 8. 公开注释值
public aspect TransactionManager { |
在使用@annotation来匹配注释时,注释类型必须拥有运行时持久性(否则 AspectJ 就不能在运行时公开注释值)。就像前面看到的,匹配只使用execution就能处理只具有类文件持久性的注释。
|
到目前为止所展示的技术,处理的都是基于字段的连接点。假设有个字段用@ClassifiedData注释,那么可以编写清单 9 所示的两个切入点中的一个,具体取决于是否需要公开实际的注释值:
清单 9. 带注释的字段
/** |
在结束关于注释的讨论之前,先深入研究一下 AspectJ 5 如何支持在类型上进行注释匹配的。AspectJ 允许指定类型模式,可以用注释模式限定该模式。到目前为止,我一直用的都是最简单的注释模式@Foo,它在主题有Foo注释时匹配。可以进行组合,在主题既有Foo注释又有Goo注释时,“@Foo @Goo”匹配。在主题或者有Foo注释或者有Goo注释时,“@(Foo || Goo)”匹配。请参阅AspectJ 5 Developers Guide中关于注释模式的讨论(在参考资料中),获取更多细节。
在 EJB 3.0 中,会话 bean 可以使用@Stateful或@Stateless进行注释。类型模式“@(Stateless || Stateful) *”匹配的是有这两个注释之一的类型。出于某种原因,如果想把TransactionManager方面限制到只处理会话 bean,那么可以像下面这样重新定义清单 8的transactionalMethodExecution切入点。
pointcut transactionalMethodExecution(Tx tx) : |
可以把这段代码读作“匹配在具有Stateless或Stateful注释的类型中带有Tx注释的任何方法的执行”。另一种编写它的方法是直接在切入点表达式中表达这个类型模式:execution(*
(@(Stateless || Stateful) *).*(..)),但是我认为前者更清楚。(注意,如果使用call则不是execution,那么两者之间会有显著差异:前者匹配从会话 bean 中发出的对事务方法的调用,而后者匹配对在会话 bean 中定义的事务方法的调用。)
|
AspectJ 为匹配和公开注释定义了更多切入点指示符:
- @withincode
- 匹配的连接点,由拥有指定注释的成员(方法、构造函数、建议)代码的执行产生。
- @within
- 匹配的连接点在拥有指定注释的类型内部。
- @this
- 在匹配的连接点中,对象目前绑定到有指定注释的
this。 - @target
- 匹配的连接点的目标有指定注释。
- @args
- 匹配的连接点的参数拥有指定注释。
请参阅AspectJ 5 Developers Guide获取连接点匹配和注释的更多信息。
Java 语言中对泛型的新支持是 Java 5 中引入的争议最大的变化。泛型声明时使用一个或多个类型参数,而这些类型参数在声明该类型的变量时绑定到具体的类型规范。泛型最常使用的示例是 Java 的集合类。Java 5 中的List接口是个泛型,带有一个类型参数 —— 列表中元素的类型。根据约定,单一字母用于表示类型参数,list 接口可能声明为:public interface List<E> {...}。如果想创建引用字符串列表的变量,可以把它声明为类型List<String>。泛型List<E>把自己的类型参数E绑定到String,从而创建参数化类型List<String>。
从清单 4显示的代码中摘出来的LifecycleManager方面,包含类型间声明中使用的参数化类型 (List<LifecycleObserver>)的一个示例。AspectJ 5 也允许在泛型上进行类型间声明。清单 10 显示了一个泛型DataBucket<T>,以及一个在它上面进行类型间声明的方面:
清单 10. 泛型上的类型间声明
public class DataBucket<T> { |
注意,类型间声明中使用的类型参数名称不必与DataBucket类本身声明中使用的类型参数名称对应;相反,类型的签名必须匹配(类型参数的数量,以及通过extends或super子句放在类型参数上的限制)。
这一节的其余部分,我把重点放在切入点表达式中通用签名和类型的匹配上。在随后的讨论中,把切入点指示符分成两种类型是有帮助的:一类是基于静态签名进行匹配的切入点指示符(execution、call、get、set,等等),另一类是根据运行时类型信息进行匹配的切入点指示符(this、target、args)。由于存在叫做擦除(erasure)的东西(我马上就会介绍),所以这个区分很重要。
对于基于签名进行匹配的切入点指示符,AspectJ 采取了一种简单方式:类型的类型参数规范就是签名的一部分。 例如,以下方法都是不同的签名:
void process(List<Number> numbers)void process(List<? extends Number> numbers)void process(List<?> items)
这些方法的执行可以用以下切入点分别匹配:
execution(* process(List<Number>))execution(* process(List<? extends Number>))execution(* process(List<?>))
AspectJ 在匹配类型的时候,支持*和+通配符。表达式“execution(*
process(List<*>))”匹配全部三个process方法,因为*匹配任何类型。但是,表达式“execution(*
process(List<Number+>))“只匹配第一个process方法(Number由模式Number+匹配),但是不匹配第二个或第三个。可以把模式List<Number+>扩展到与List<Float>、List<Double>、List<Integer>等匹配,但是对于List<? extends
Number>来说,这些都是不同的签名。有一个重要的区别是,请考虑这样一个事实:在process方法的方法体内,用没有通配的签名插入列表是合法的,但是在使用? extends格式的时候就不合法了。
需要记住的规则是:泛型通配符是签名的组成部分,而且 AspectJ 模式通配符被用来匹配签名。
在根据运行时类型信息进行匹配时,事情变得更有趣了。this、target和args切入点指示符全都根据运行时类型信息进行匹配。请考虑process方法的另一个变体:
void process(Number n) {...} |
可以静态地决定切入点表达式“execution(*
process(..)) &&args(Number)”以总是匹配这个方法的执行 —— 传递的参数保证是数字。相反,如果编写的是“execution(* process(..))
&&args(Double)”,那么这个表达式可能匹配这个方法的执行,具体取决于实际运行时传递的参数的类型。在这种情况下,AspectJ 应用运行时测试来判断参数是不是instanceof Double。
现在再考虑一下采用参数化类型的process方法的以下签名:
void process(List<? extends Number> ns) {...} |
然后应用相同的推断,就可以看出:
execution(* process(..)) &&args(List<? extends Number>)- 总是会匹配,因为不论传递什么类型的列表,都必须满足这个规范。
execution(* process(..)) && args(List<String>)- 永远不会匹配,因为字符串列表永远不会传递给这样的方法,该方法期待得到扩展
Number的东西的列表。 execution(* process(..)) && args(List<Number>)- 可能匹配,具体取决于实际传递的列表是数字列表、双精度列表,还是浮点列表。
在后一种情况下,可能做的工作就是在实际的参数上应用运行时测试,判断它是不是instanceof List<Number>。不幸的是,Java 5 实现泛型时采用了一种叫做擦除(erasure)的技术 —— 被擦除的就是参数化类型的运行时类型参数信息。在运行时,参数只被当作普通的List(所谓参数的“原始”类型)。
|
即使缺少必要的信息进行确定的决策,AspectJ 也必须决定这类切入点是否应当匹配。在 Java 语言中对于这类情况有一种优先级:在把原始类型(例如List)的实例传递给需要参数化类型(例如List<Number>)的方法时,调用会通过 Java 5 编译器传递,但是会生成一个“unchecked”警告,提示转换可能不是类型安全的。
类似地,当 AspectJ 认为某个切入点可能匹配指定连接点,但是不能应用运行时测试进行确定的时候,就会考虑对切入点进行匹配,而且 AspectJ 编译器会提出一个“unchecked”警告,表示实际的匹配不能被检测。就像 Java 语言支持@SuppressWarnings注释,可以在成员中抑制未检测警告一样,AspectJ 支持@SuppressAjWarnings注释,可以用它对建议进行注释,以抑制从切入点匹配发生的未检测警告。
在离开泛型主题之前,有一个要点需要考虑。回到清单 10中定义的DataBucket类,请注意不论用多少不同的类型参数去实例化DataBucket的实例(如下所示),都只有一个DataBucket类:
DataBucket<Integer> myIntBucket = new DataBucket<Integer>(); |
它的含义就是,在DataBucket类的内部,没有返回String、Integer或Food实例的getData方法执行这样的东西。相反,只有一个getData方法执行,返回类型参数T的实例。所以可以这样编写,它匹配的方法执行,是在命名类型DataBucket中返回T的getData方法,其中T是类型参数:
execution<T>(T DataBucket<T>.getData()) |
关于在 AspectJ 5 中对泛型的完整处理,请参阅AspectJ 5 Developers Guide。
介绍了 AspectJ 5 的新发行版中最重要的 Java 5 语言特性之后,我现在把重点转到一些没有显式地捆绑到 Java 5 的新特性上。其中最重要的一个是方面声明的新的基于注释的风格,称作@AspectJ注释。在 AspectJ 5 中,可以用普通的 Java 语法编写方面,然后对声明进行注释,这样,它们就可以由 AspectJ 的织入器解释。例如,在代码风格中,可以这样编写:
publicaspectTransactionManager { |
在基于注释的风格中,可以这样编写:
@Aspect |
随着 AspectJ 与 AspectWerkz 在 2005 年初的合并,@AspectJ 注释被添加到 AspectJ 中。它们使得任何标准的 Java 5 编译器都可以处理 AspectJ 源代码,而实际上任何从 Java 源代码起工作的工具都可以。在使用没有为操作 AspectJ 程序提供集成支持的 IDE 时(当然,这类环境也缺乏显示横切结构的视图),它们也为日常的编辑体验造成显著区别。
需要注意的重点是,AspectJ 5 发行版具有以下内容(虽然有两种开发风格):
- 一个语言
- 一个语义
- 一个织入器
不论选择用什么风格表达方面,它们实际表达的都是同样的东西,而且也用同样的方式发挥作用。这一重要属性使得可以容易地混合和匹配风格(所以用@AspectJ风格开发的方面可以与用代码风格开发的方面结合使用,反之亦然)。但是@AspectJ风格有些限制。例如,在使用常规的 Java 编译器时,就不支持注释风格版本的 AspectJ 构造(例如declare soft),因为这类构造需要编译时支持而不是织入时支持。
现在来看一个使用 @AspectJ 注释的示例。
我先从清单 11 显示的简化的LifecycleManager方面开始,并用@AspectJ风格重写它:
清单 11. 简化的生命周期管理器方面,代码风格
/** |
方面声明和内部的类型声明可以容易地转移到新风格,如清单 12 所示:
清单 12. 简化的生命周期管理器方面,注释风格
/** |
下面,我们来看看用 @AspectJ 风格重写切入点和建议时发生了什么。切入点是在与切入点具有相同签名的void方法上使用@Pointcut注释而编写的。建议则是在方法上使用@Before、
@Around、@AfterReturning、@AfterThrowing和@After注释而编写的,如清单 13 所示:
清单 13. 注释风格的切入点和建议
/** |
注意,在切入点表达式中,任何引用的类型都必须是全限定的(导入语句只能在源代码条件下存在,在处理注释时,对织入器不可用)。建议方法必须声明成public,并返回void(@Around建议除外,它必须返回值)。
现在剩下的就是把类型间声明也转移到 @AspectJ 风格的方面了。在注释风格中,这些声明像清单 14 所示这样进行:
清单 14. 注释风格的类型间声明
/** |
关于注释风格的开发,还有许多其他有趣的问题,例如如何在注释风格的建议方法的方法体内引用thisJoinPoint,proceed在around建议中是如何被支持的。要获取这些主题的更多信息,请参阅AspectJ 5 Developers Guide。
装入时织入指的是在类装入 VM 时织入类的过程(比照提前织入而言 —— 例如编译时织入)。从 1.1
发行版起,AspectJ 就拥有支持装入时织入必需的基础设施,但是必须编写定制的类装入器,才能真正把 AspectJ
的织入器集成进应用程序。在 AspectJ 1.2 发行版中,随着添加了aj脚本,装入时织入得到改进,aj 能够从命令行装入和运行任何 Java 应用程序,也可以在类装入时从ASPECTPATH织入方面。这个脚本支持 JDK 1.4 以上版本。
但是,命令行脚本不能方便地用在所有环境,特别是不能很好地与 Java EE 应用程序集成。在 AspectJ 5 中,通过放在类路径中的 META-INF/aop.xm,AspectJ 支持对装入时织入进行配置。这是随着 2005 年初与 AspectWerkz 的合并而带给 AspectJ 的另一个特性。
现在来看看 aop.xml 文件和它的相关元素。
aop.xml 文件包含两个主要小节:aspects元素定义用于装入时织入的方面集合,weaver元素指定控制织入器行为的选项(主要是控制应当织入哪个类型)。清单 15 显示了一个示例文件:
清单 15. 示例 aop.xml 文件
<aspectj> |
在aspects元素中,或者通过名称,或者在 aop.xml
文件内部定义,把已知的方面定义到织入器。后一种技术只能用于扩展现有抽象方面(有一个或多个抽象切入点):切入点表达式在 XML
中提供。对于“基础设施”方面,这可以是把配置(切入点表达式)外部化的很好方法。定义了织入器中的方面集合之后,如果需要(上面代码中没显示),可以使
用一个或多个可选的include和exclude元素,控制在织入过程中实际使用哪些方面。默认情况下,织入器使用所有定义的方面。
weaver元素包含传递给织入器的选项,和应当被织入(通过include语句)的类型集合的一个可选定义。如果没有指定include语句,那么所有类型都可供织入器进行织入。
如果在类路径中有多个 META-INF/aop.xml 文件,那么它们的内容就聚合在一起,形成传递给织入器的完整规范。
AspectJ 5 支持许多agents,可以把装入时能力集成到现有环境。
清单 16 显示了使用 JVMTI (Java 5) 代理时的示例 JVM 启动选项,任何符合 Java 5 规范的 JVM 都支持它:
清单 16. JVMTI 代理
-javaagent=aspectjweaver.jar |
AspectJ 5 还自带了 JRockit 代理,它支持的功能与 Java 5 之前的 JRockit VM 一样(JRockit 还支持 Java 5 上的jvmti)。等价的启动选项是-Xmanagement:class=org.aspectj.weaver.tools.
JRockitWeavingAgent。
在AspectJ 5 Developers Guide中,可以发现关于利用 AspectJ 5 进行装入时织入的更多细节。
总之,AspectJ 5 代表 AspectJ 前进的一大步。这篇文章的重点主要是新的发行版既支持对 Java 5 语言构建的完全编译,还支持基于注释和泛型的连接点匹配。
AspectJ 5 发行版中最令人兴奋的两个特性 —— 新的基于注释的开发风格和对 AspectJ 装入时织入支持的增强 —— 是 AspectJ 与 AspectWerkz 合并的结果。因为合并的重要性和特性的相关性,我在这里对它们都进行了深入讨论。
当然,一篇文章不可能覆盖 AspectJ 5 这样全面的发行版的全部增强。例如,我重点介绍了 AspectJ
中对连接点匹配的主要更新,而把相对次要的(例如处理自动装箱和协变返回类型的新方式)留给您自己去发现。其他没有在这里介绍,但是值得研究的特性包括:
新的pertypewithin方面实例化模型、在运行时询问方面类型的反射 API、对declare soft处理运行时异常的方式的修订、兼容性、织入性能的提高,等等。
读到这里,我可以肯定,您可以猜测得到从哪继续学习这些(及更多)新特性。您猜对了,就是AspectJ 5 Developers Guide。
转自http://www.ibm.com/developerworks/cn/java/j-aopwork8/
导入论坛 引用链接 收藏 分享给好友 推荐到圈子 管理 举报
TAG: 元注释


