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 编译 Java 5 应用程序

AspectJ 编译器 (ajc)支持在版本1.3(及以前版本)、1.45.0的兼容级别上编译 Java 源代码,并生成针对 1.1 版以上 VM 的字节码。像 javac 一样,ajc 有一些限制:在 1.4 版本兼容级别上编译源代码只支持 1.4 及以上版本的目标 VM,在 5.0 版本兼容级别上编译源代码只支持 5.0 版本的目标 VM。

关于本系列

AOP@Work系列是为具有一定面向方面的编程背景、并准备扩展或者加深他们的知识的开发人员准备的。与大多数 developerWorks 文章一样,本系列具有很高实用性:从每一篇文章中学到的知识立刻就能使用得上。

所挑选的为这个系列撰稿的每一位作者,都在面向方面编程方面处于领导地位或者拥有这方面的专业知识。许多作者都是本系列中讨论的项目或者工具的开发人员。每篇文章都经过仔细审查,以确保所表达的观点的公平和准确。

关于文章的意见和问题,请直接与文章的作者联系。如果对整个系列有意见,可以与本系列的组织者Nicholas Lesiecki联系。更多关于 AOP 的背景知识,请参阅参考资料

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 和 Eclipse 编译

如果正在用 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 兼容级别
在 Eclipse 中指定 5.0 兼容级别





在方面中使用 Java 5 的特性

有了这些基础知识,现在可以把注意力转到在 AspectJ 中使用 Java 5 特性了。我选择了一个示例方面,它支持一组基本的生命周期操作,可以用于任何被注释为ManagedComponent的类型 。ManagedComponent是一个简单的标记注释,如下所示:

public @interface ManagedComponent {}

方面本身被设计成可以表现许多 Java 5 和 AspectJ 5 语言的特性,包括枚举、注释、新风格的for循环以及泛型。LifecycleManager方面的第一部分仅定义了enum,表示托管组件可能存在的状态,还定义了托管组件将会支持的Lifecycle接口,如清单 1 所示:


清单 1. 有 State 和 Lifecycle 声明的 LifecycleManager 方面
/**
* This aspect provides default lifecycle management for all
* types with the @ManagedComponent annotation.
*/
publicaspectLifecycleManager {
/**
* The defined states that a managed component can be in.
*/
public enum State {
INITIAL,
INITIALIZING,INITIALIZED,
STARTING,STARTED,
STOPPING,
TERMINATING,TERMINATED,
BROKEN;
}

/**
* The lifecycle interface supported by managed components.
*/
public interface Lifecycle {
void initialize();
void start();
void stop();
void terminate();
boolean isBroken();
State getState();
void addObserver(LifecycleObserver observer);
void removeObserver(LifecycleObserver observer);
}

...

基于注释的类型匹配

方面的下一部分使用了一些新的 AspectJ 5 支持,以进行基于注释的类型匹配。这说明任何具有ManagedComponent注释的类型都要实现Lifecycle接口(并且因此稍后在方面中将会获得为此类组件定义的全部行为)。类型模式“@ManagedComponent *”匹配具有ManagedComponent注释、名称任意的类型,如清单 2 所示:


清单 2. 用基于注释的类型匹配声明双亲
/**
* Any type with an @ManagedComponent annotation implements
* the Lifecycle interface (and acquires the default implementation
* defined in this aspect if none is provided by the type).
*/
declare parents : @ManagedComponent * implements Lifecycle;

LifeCycleObserver 接口

清单 3 显示了Lifecycle中的添加/删除观察者操作中引用的LifecycleObserver接口的定义:


清单 3. LifecycleObserver 接口
/**
* Interface to be implemented by any type needing to
* observe the lifecycle events of managed components.
*/
public interface LifecycleObserver {
void componentInitialized(Lifecycle component);
void componentStarted(Lifecycle component);
void componentStopped(Lifecycle component);
void componentTerminated(Lifecycle component);
void componentBroken(Lifecycle component);
}

对于没有提供自己的定义的所有实现者,方面提供了Lifecycle操作的默认实现。它还为所有实现者声明了私有的stateobservers字段。注意state字段是枚举类型,而observers字段使用参数化类型,如清单 4 所示:


清单 4. Lifecycle 接口的默认实现
// default implementations for the state-based lifecycle events
private State Lifecycle.state = State.INITIAL;
public void Lifecycle.initialize() {}
public void Lifecycle.start() {}
public void Lifecycle.stop() {}
public void Lifecycle.terminate() {}
public boolean Lifecycle.isBroken() { return state == State.BROKEN; }
public State Lifecycle.getState() { return state; }

// default implementation of the add/remove observer lifecycle operations
private List<LifecycleObserver> Lifecycle.observers = new ArrayList<LifecycleObserver>();
public void Lifecycle.addObserver(LifecycleObserver observer) {
observers.add(observer);
}
public void Lifecycle.removeObserver(LifecycleObserver observer) {
observers.remove(observer);
}

状态管理和事件处理

因为我想在这篇文章中介绍许多基础知识,所以从剩下的方面实现中我只摘录几个。对于每个生命周期事件,方面都提供beforeafter returning建议,以验证托管组件处于有效状态,从而执行操作并把变化通知给已注册的观察者,如下所示:


清单 5. 状态管理和通知
// these pointcuts capture the lifecycle events of managed components
pointcut initializing(Lifecycle l) : execution(* Lifecycle.initialize(..)) && this(l);
pointcut starting(Lifecycle l) : execution(* Lifecycle.starting(..)) && this(l);
pointcut stopping(Lifecycle l) : execution(* Lifecycle.stopping(..)) && this(l);
pointcut terminating(Lifecycle l): execution(* Lifecycle.terminating(..)) && this(l);

/**
* Ensure we are in the initial state before initializing.
*/
before(Lifecycle managedComponent) : initializing(managedComponent) {
if (managedComponent.state != State.INITIAL)
throw new IllegalStateException("Can only initialize from INITIAL state");
managedComponent.state = State.INITIALIZING;
}

/**
* If we successfully initialized the component, update the state and
* notify all observers.
*/
after(Lifecycle managedComponent) returning : initializing(managedComponent) {
managedComponent.state = State.INITIALIZED;
for (LifecycleObserver observer: managedComponent.observers)
observer.componentInitialized(managedComponent);
}

注意建议体中使用了新风格的for循环,对所有已注册的观察者进行迭代。如果生命周期操作正常返回,就执行after returning建议。如果生命周期操作通过运行时异常而退出,那么后面的建议(清单 6)就把组件转变成BROKEN状态。可以想像,方面中会有进一步的建议,防止对状态是BROKEN的托管组件执行任何操作,但是这个讨论超出了这篇文章的范围:


清单 6. 故障检测和到 BROKEN 状态的转变
/**
* If any operation on a managed component fails with a runtime exception
* then move to the broken state and notify any observers.
*/
after(Lifecycle managedComponent) throwing(RuntimeException ex) :
execution(* *(..))
&& this(managedComponent) {
managedComponent.state = State.BROKEN;
for (LifecycleObserver observer: managedComponent.observers)
observer.componentBroken(managedComponent);
}

示例方面已经表明,在方面中使用 Java 5 特性,就像在类中使用它们一样简单。而且从匹配的角度来看,根据注释的存在与否(在declare parents语句中),示例方面也给出了 AspectJ 5 能做什么的提示。但是在 AspectJ 5 中除了注释匹配之外还有许多东西,在下一节中会看到。






连接点匹配和注释

AOP@Work系列以前的文章中,介绍了注释、元数据和面向方面编程之间的关系 (请参阅参考资料一节中的 “AOP and metadata”),所以这里不再赘述,直接介绍 AspectJ 5 能做的一些事情。

出于示例的原因,我将采用 EJB 3.0 规范中的一些注释(请参阅参考资料)。 对于具有相关事务策略的方法,可以用@Tx注释进行注释。例如:

@Tx(TxType.REQUIRED)
void credit(Money amount) {...}

如果想编写TransactionManager方面,那么可能会对带有@Tx注释的方法的执行感兴趣。编写与它们匹配的切入点很简单,如清单 7 所示:


清单 7. 匹配事务性方法的执行
public aspect TransactionManager {
/**
* The execution of any method that has the @Tx
* annotation
*/
pointcut transactionalMethodExecution() :
execution(@Tx * *(..));

/**
* Placeholder for implementing tx policies
*/
Object around() : transactionalMethodExecution() {
return proceed();
}

}

匹配注释方法的调用和执行

execution(@Tx * *(..))切入点表达式匹配任何方法的执行,可以使用任何名称、任何类型、任何参数,只要方法用@Tx注释。如果需要,也可以缩小范围。到事务性方法的匹配调用同样简单,只需编写“call(@Tx * *(..))”即可。

在这种情况下,实现事务策略的建议需要知道执行方法上的@Tx注释的value。使用 AspectJ,可以把连接点的上下文值绑定到切入点表达式,然后向建议公布上下文。在 AspectJ 5 中,用新的切入点指示符@annotation把这个能力扩展到了注释上。像 AspectJ 中所有的其他上下文绑定切入点指示符一样,@annotation扮演着双重角色:既把连接点匹配限制到主题(方法、字段、构造函数等)具有指定类型注释的连接点上,公开那个值。可以很容易地重新定义TransactionManager方面,让它利用这一优点,如下所示:


清单 8. 公开注释值
public aspect TransactionManager {
/**
* The execution of any method that has the @Tx
* annotation
*/
pointcut transactionalMethodExecution(Tx tx) :
execution(* *(..))&& @annotation(tx);

/**
* Placeholder for implementing tx policies
*/
Object around(Tx tx) : transactionalMethodExecution(tx) {
TxType transactionType = tx.value();
// do before processing
Object ret = proceed(tx);
// do after processing
return ret;
}

}

运行时持久性

在使用@annotation来匹配注释时,注释类型必须拥有运行时持久性(否则 AspectJ 就不能在运行时公开注释值)。就像前面看到的,匹配只使用execution就能处理只具有类文件持久性的注释。

注释和持久性策略

注释类型可以使用三种不同的持久性策略:SOURCECLASSRUNTIMECLASS文件持久性是默认选择。

AspectJ 5 不支持对带有SOURCE持久性的注释的连接点匹配,因为在进行二进制织入的时候不支持这项功能(织入时用 .class 文件作为输入)。支持把注释公开为上下文(@this、@target、@args@annotation)的 AspectJ 5 切入点指示符只能用在拥有RUNTIME持久性的注释上。

到目前为止所展示的技术,处理的都是基于字段的连接点。假设有个字段用@ClassifiedData注释,那么可以编写清单 9 所示的两个切入点中的一个,具体取决于是否需要公开实际的注释值:


清单 9. 带注释的字段
/**
* Any access or update to classified data
*/
pointcut classifiedAction() :
get(@ClassifiedData * *) || set(@ClassifiedData * *);

/**
* Alternative declaration:
* Any access or update to classified data,
* exposes the annotation to provide access to
* classification level attribute
*/
pointcut classifiedAction(ClassifiedData classification) :
(get(* *) || set(* *)) && @annotation(classification);

匹配带注释的类型

在结束关于注释的讨论之前,先深入研究一下 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,那么可以像下面这样重新定义清单 8transactionalMethodExecution切入点。

pointcut transactionalMethodExecution(Tx tx) :
execution(* *(..)) && @annotation(tx)
&& within(@(Stateless || Stateful) *);

可以把这段代码读作“匹配在具有StatelessStateful注释的类型中带有Tx注释的任何方法的执行”。另一种编写它的方法是直接在切入点表达式中表达这个类型模式:execution(* (@(Stateless || Stateful) *).*(..)),但是我认为前者更清楚。(注意,如果使用call则不是execution,那么两者之间会有显著差异:前者匹配从会话 bean 中发出的对事务方法的调用,而后者匹配对在会话 bean 中定义的事务方法的调用。)

基于注释值的连接点匹配

可以编写 AspectJ 5 切入点表达式,不仅根据注释的存在与否进行匹配,还能根据注释的值 (例如TxType)进行匹配。

只要在切入点表达式中使用if测试即可:

pointcut transactionRequired(Tx tx) :
execution(* *(..)) &&
@annotation(tx) &&
if(tx.value() == TxType.REQUIRED);

更多切入点指示符

AspectJ 为匹配和公开注释定义了更多切入点指示符:

@withincode
匹配的连接点,由拥有指定注释的成员(方法、构造函数、建议)代码的执行产生。
@within
匹配的连接点在拥有指定注释的类型内部。
@this
在匹配的连接点中,对象目前绑定到有指定注释的this
@target
匹配的连接点的目标有指定注释。
@args
匹配的连接点的参数拥有指定注释。

请参阅AspectJ 5 Developers Guide获取连接点匹配和注释的更多信息。






AspectJ 5 中的泛型

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> {

private T data;

public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}

}
aspect DataHistory {

private E DataBucket<E>.previousDataValue;

private E DataBucket<E>.getPreviousDateValue() {
return previousDataValue;
}

}

注意,类型间声明中使用的类型参数名称不必与DataBucket类本身声明中使用的类型参数名称对应;相反,类型的签名必须匹配(类型参数的数量,以及通过extendssuper子句放在类型参数上的限制)。

这一节的其余部分,我把重点放在切入点表达式中通用签名和类型的匹配上。在随后的讨论中,把切入点指示符分成两种类型是有帮助的:一类是基于静态签名进行匹配的切入点指示符(executioncallgetset,等等),另一类是根据运行时类型信息进行匹配的切入点指示符(thistargetargs)。由于存在叫做擦除(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 模式通配符被用来匹配签名。

基于运行时类型信息的匹配

在根据运行时类型信息进行匹配时,事情变得更有趣了。thistargetargs切入点指示符全都根据运行时类型信息进行匹配。请考虑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(所谓参数的“原始”类型)。

为什么用擦除?

Java 语言规范指出:不让所有泛型具体化(reifiable)(在运行时可用) 的决策是关系到语言的类型系统的最重要、争议最大的设计决策之一。这个决策最根本的原因是平台兼容性(允许在为平台的以前版本编译的模块和为 Java 5 平台编译的模块之间互操作)。泛型系统试图支持迁移兼容性,即允许把现有代码发展到可以利用泛型,而不会在独立开发的软件模块之间加上依赖性。

即使缺少必要的信息进行确定的决策,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> myStringBucket = new DataBucket<String>();
DataBucket<Food> myFoodBucket = new DataBucket<Food>();
...

它的含义就是,DataBucket的内部,没有返回StringIntegerFood实例的getData方法执行这样的东西。相反,只有一个getData方法执行,返回类型参数T的实例。所以可以这样编写,它匹配的方法执行,是在命名类型DataBucket中返回TgetData方法,其中T是类型参数:

execution<T>(T DataBucket<T>.getData())

关于在 AspectJ 5 中对泛型的完整处理,请参阅AspectJ 5 Developers Guide






@AspectJ 注释

介绍了 AspectJ 5 的新发行版中最重要的 Java 5 语言特性之后,我现在把重点转到一些没有显式地捆绑到 Java 5 的新特性上。其中最重要的一个是方面声明的新的基于注释的风格,称作@AspectJ注释。在 AspectJ 5 中,可以用普通的 Java 语法编写方面,然后对声明进行注释,这样,它们就可以由 AspectJ 的织入器解释。例如,在代码风格中,可以这样编写:

publicaspectTransactionManager {
...

在基于注释的风格中,可以这样编写:

@Aspect
public class TransactionManager {
...

随着 AspectJ 与 AspectWerkz 在 2005 年初的合并,@AspectJ 注释被添加到 AspectJ 中。它们使得任何标准的 Java 5 编译器都可以处理 AspectJ 源代码,而实际上任何从 Java 源代码起工作的工具都可以。在使用没有为操作 AspectJ 程序提供集成支持的 IDE 时(当然,这类环境也缺乏显示横切结构的视图),它们也为日常的编辑体验造成显著区别。

需要注意的重点是,AspectJ 5 发行版具有以下内容(虽然有两种开发风格):

  • 一个语言
  • 一个语义
  • 一个织入器

不论选择用什么风格表达方面,它们实际表达的都是同样的东西,而且也用同样的方式发挥作用。这一重要属性使得可以容易地混合和匹配风格(所以用@AspectJ风格开发的方面可以与用代码风格开发的方面结合使用,反之亦然)。但是@AspectJ风格有些限制。例如,在使用常规的 Java 编译器时,就不支持注释风格版本的 AspectJ 构造(例如declare soft),因为这类构造需要编译时支持而不是织入时支持。

现在来看一个使用 @AspectJ 注释的示例。

用 @AspectJ 注释编写方面

我先从清单 11 显示的简化的LifecycleManager方面开始,并用@AspectJ风格重写它:


清单 11. 简化的生命周期管理器方面,代码风格
/**
* This aspect provides default lifecycle management for all
* types with the @ManagedComponent annotation.
*/
public aspect LifecycleManager {
/**
* The defined states that a managed component can be in.
*/
public enum State {
INITIAL,
INITIALIZING,INITIALIZED,
STARTING,STARTED,
STOPPING,
TERMINATING,TERMINATED,
BROKEN;
}

/**
* The lifecycle interface supported by managed components.
*/
public interface Lifecycle {
void initialize();
void start();
void stop();
void terminate();
boolean isBroken();
State getState();
}

/**
* Any type with an @ManagedComponent annotation implements
* the Lifecycle interface (and acquires the default implementation
* defined in this aspect if none is provided by the type).
*/
declare parents : @ManagedComponent * implements Lifecycle;
// default implementations for the state-based lifecycle events
private State Lifecycle.state = State.INITIAL;
public void Lifecycle.initialize() {}
public void Lifecycle.start() {}
public void Lifecycle.stop() {}
public void Lifecycle.terminate() {}
public boolean Lifecycle.isBroken() { return state == State.BROKEN; }
public State Lifecycle.getState() { return state; }

// these pointcuts capture the lifecycle events of managed components
pointcut initializing(Lifecycle l) : execution(* Lifecycle.initialize(..)) && this(l);
pointcut starting(Lifecycle l) : execution(* Lifecycle.starting(..)) && this(l);
pointcut stopping(Lifecycle l) : execution(* Lifecycle.stopping(..)) && this(l);
pointcut terminating(Lifecycle l): execution(* Lifecycle.terminating(..)) && this(l);

/**
* Ensure we are in the initial state before initializing.
*/
before(Lifecycle managedComponent) : initializing(managedComponent) {
if (managedComponent.state != State.INITIAL)
throw new IllegalStateException("Can only initialize from INITIAL state");
managedComponent.state = State.INITIALIZING;
}

/**
* If we successfully initialized the component, update the state and
* notify all observers.
*/
after(Lifecycle managedComponent) returning : initializing(managedComponent) {
managedComponent.state = State.INITIALIZED;
}

...
}

方面声明和内部的类型声明可以容易地转移到新风格,如清单 12 所示:


清单 12. 简化的生命周期管理器方面,注释风格
/**
* This aspect provides default lifecycle management for all
* types with the @ManagedComponent annotation.
*/
@Aspect
public class LifecycleManager {
/**
* The defined states that a managed component can be in.
*/
public enum State {
INITIAL,
INITIALIZING,INITIALIZED,
STARTING,STARTED,
STOPPING,
TERMINATING,TERMINATED,
BROKEN;
}

/**
* The lifecycle interface supported by managed components.
*/
public interface Lifecycle {
void initialize();
void start();
void stop();
void terminate();
boolean isBroken();
State getState();
}

...

与切入点和建议一起使用注释

下面,我们来看看用 @AspectJ 风格重写切入点和建议时发生了什么。切入点是在与切入点具有相同签名的void方法上使用@Pointcut注释而编写的。建议则是在方法上使用@Before、 @Around、@AfterReturning、@AfterThrowing@After注释而编写的,如清单 13 所示:


清单 13. 注释风格的切入点和建议
/**
* This aspect provides default lifecycle management for all
* types with the @ManagedComponent annotation.
*/
@Aspect
public class LifecycleManager {
...

// these pointcuts capture the lifecycle events of managed components
@Pointcut(
"execution(* org.aspectprogrammer.devWorks.LifecycleManager.Lifecycle.initialize(..))
&& this(l)"
)
void initializing(Lifecycle l) {}

@Pointcut(
"execution(* org.aspectprogrammer.devWorks.LifecycleManager.Lifecycle.starting(..))
&& this(l)"
)
void starting(Lifecycle l){}

@Pointcut(
"execution(* org.aspectprogrammer.devWorks.LifecycleManager.Lifecycle.stopping(..))
&& this(l)"
)
void stopping(Lifecycle l) {}

@Pointcut(
"execution(* org.aspectprogrammer.devWorks.LifecycleManager.Lifecycle.terminating(..))
&& this(l)"
)
void terminating(Lifecycle l) {}

/**
* Ensure we are in the initial state before initializing.
*/
@Before("initializing(managedComponent)")
public void moveToInitializingState(Lifecycle managedComponent) {
if (managedComponent.state != State.INITIAL)
throw new IllegalStateException("Can only initialize from INITIAL state");
managedComponent.state = State.INITIALIZING;
}

/**
* If we successfully initialized the component, update the state and
* notify all observers.
*/
@AfterReturning("initializing(managedComponent)")
public void moveToInitializedStated(Lifecycle managedComponent) {
managedComponent.state = State.INITIALIZED;
}

注意,在切入点表达式中,任何引用的类型都必须是全限定的(导入语句只能在源代码条件下存在,在处理注释时,对织入器不可用)。建议方法必须声明成public,并返回void@Around建议除外,它必须返回值)。

与类型间声明一起使用注释

现在剩下的就是把类型间声明也转移到 @AspectJ 风格的方面了。在注释风格中,这些声明像清单 14 所示这样进行:


清单 14. 注释风格的类型间声明
/**
* This aspect provides default lifecycle management for all
* types with the @ManagedComponent annotation.
*/
@Aspect
public class LifecycleManager {
...
// default implementations for the state-based lifecycle events
@DeclareParents("@ManagedComponent *")
class DefaultLifecycleImpl implements Lifecycle {
private State state = State.INITIAL;
public void initialize() {}
public void start() {}
public void stop() {}
public void terminate() {}
public boolean isBroken() { return state == State.BROKEN; }
public State getState() { return state; }
}
...
}

关于注释风格的开发,还有许多其他有趣的问题,例如如何在注释风格的建议方法的方法体内引用thisJoinPointproceedaround建议中是如何被支持的。要获取这些主题的更多信息,请参阅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 文件和它的相关元素。

装入时织入的 XML 规范

aop.xml 文件包含两个主要小节:aspects元素定义用于装入时织入的方面集合,weaver元素指定控制织入器行为的选项(主要是控制应当织入哪个类型)。清单 15 显示了一个示例文件:


清单 15. 示例 aop.xml 文件
<aspectj>

<aspects>
<!-- declare two existing aspects to the weaver -->
<aspect name="com.MyAspect"/>
<aspect name="com.MyAspect.Inner"/>
<!-- define a concrete aspect inline -->
<concrete-aspect name="com.xyz.tracing.MyTracing" extends="tracing.AbstractTracing">
<pointcut name="tracingScope" expression="within(com.xyz..*)"/>
</concrete-aspect>

</aspects>
<weaver ptions="-XlazyTjp">
<include within="com.xyz..*"/>
</weaver>

</aspectj>

aspects元素中,或者通过名称,或者在 aop.xml 文件内部定义,把已知的方面定义到织入器。后一种技术只能用于扩展现有抽象方面(有一个或多个抽象切入点):切入点表达式在 XML 中提供。对于“基础设施”方面,这可以是把配置(切入点表达式)外部化的很好方法。定义了织入器中的方面集合之后,如果需要(上面代码中没显示),可以使 用一个或多个可选的includeexclude元素,控制在织入过程中实际使用哪些方面。默认情况下,织入器使用所有定义的方面。

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: 元注释

 

评分:0

我来说两句

显示全部

:loveliness: :handshake :victory: :funk: :time: :kiss: :call: :hug: :lol :'( :Q :L ;P :$ :P :o :@ :D :( :)

我的栏目

日历

« 2008-01-28  
  12345
6789101112
13141516171819
20212223242526
2728293031  

我的存档

数据统计

  • 访问量: 89
  • 日志数: 14
  • 建立时间: 2008-01-03
  • 更新时间: 2008-01-04

RSS订阅

Open Toolbar