AOP@Work: 介绍 AspectJ 5

 
目前,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.
 */
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();
        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 的织入器解释。例如,在代码风格中,可以这样编写:

public aspect TransactionManager {
  ...

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

@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 options="-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



参考资料

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值