AOP 与 AspectJ5

eclipse的aspectj url:  http://www.eclipse.org/aspectj/  作者: Jonas Bonér, Alexandre Vasseur,Joakim Dahlstedt

      面向方面编程(AOP) 在软件社区和企业级应用获得普遍认同。90年代 Xerox引入AOP以来,AOP通过在研究团体、开源社区、企业级应用领域的几次创新,变得越来越成熟。在过去两年,开源技术在Java领域起了重大的推动作用,促成了最近AspectJ和AspectWerkz的合并——合并成归于Eclipse基金下的AspectJ5项目。AspectJ由BEA和IBM公司赞助,可以认为是Java AOP事实上的标准。
随着AOP的流行和研究团体的推动,相关词汇、概念和实现已经取得一致性,为更好的工具支持和开发人员经验累计(使用诸如AspectJ的Eclipse插件AJDT)打下基础。

AOP经历了多种实现技术,从源代码处理,到Java中广泛采用(特别是在Java5 JVMTI 出现之后)的字节码处理技术。现在这项技术已经被一些“应用程序管理和监控”领域的、应用AOP的、企业级的产品采用,新近跟随基于POJO的中间件和透明集簇技术而变得更加流行。

因此,无论在何种承诺下,字节码处理都将越来越可能成为你最终必须了解的事物。你不得不回答的问题包括:到什么时候字节码处理技术能变得管理简便、透明和高效?是否存在这样的风险——依赖于二进制处理技术的AOP实现最终到达一个无法进行效率革新的终点?JVM级的AOP支持能否解决这些问题,需要进行什么扩展?本系列的文章将揭开JRockit JVM的AOP支持(以及相关的激烈争论)的神秘面纱,并企此具体回答以上问题。

第一篇文章讲介绍AOP的概念,简要描述了为什么许多AOP实现,比如说AspectJ,都建立在字节码处理的基础上,解释了字节码处理技术的一些局限,以及为什么最终会影响到可量测性和可用性。最后一段介绍JRockit JVM对AOP的支持,支持的目的在于:解决上述局限性,并为AOP和其他的截取机制提供一个高效的基础。
第二篇文章将通过一些具体的API细节和离子,来阐述这个支持能走多远。

什么是AOP?
面向对象分析和设计通过引入继承、抽象和多态等概念,给我们提供了减少软件复杂度的工具。但是,开发人员每天都要面对一些难以用面向对象软件开发技术解决的问题。其中一个问题就是:如何处理应用中的横切关注点(Cross-cutting concerns)?
横切关注点
一个关注点就是这样一个特殊概念:一块我们感兴趣的的区域。例如,在一个订购系统中,核心关注点(Core concerns)就是订单处理和生产,同时,系统关注点(system concerns)就是事务处理和安全管理。

一个横切关注点是一个影响多个类或模块的关注点,一个无法很好本地化和模块化的关注点。
横切关注点的缺点:
·代码混乱:当一个模块或一个代码段同时管理着多个关注点,代码显得混乱。
·代码分散:当一个关注点分散在多个模块中,没有很好地本地化和模块化时,代码显得很分散。
这些缺点会通过许多途径影响软件。比如,它们使得软件代码难以编写、晦涩难懂,软件难于维护和重用。

关注点分离
AOP试图通过引进关注点分离(Separation of concerns)的概念来解决这个问题。在关注点分离的概念里,关注点可以用一种非常模块化和本地化的方式来实现。AOP在软件设计的空间中加上额外的一维,用户可以定义横切关注点,并把它们挑出来放在新的那一维空间中,用一种模块化的方式将这些关注点“打包”。
[译注]:这一段作者写的非常抽象,翻译得也不好。如有疑义请参考英文原文。

AOP引入的新概念
AOP引入一些新概念。Join Point指程序执行流程的一个点(比如一个方法被调用的地方,或者一个异常被抛出的地方)。Pointcut则是指一些满足某些特定条件的Join Point的集合。Advice(是指将要在所有满足条件的Join points所在位置上执行的代码。Introduction则让我们可以添加一些额外的代码到已存在的类中,比如:添加方法、属性或者接口到一个已存在的类。最后,AOP引入了aspect(方面)结构,它是AOP的基本模块单元,其定义基于Join Point、Pointcut、Advice、Introduction(也称作Inter-type declaration,类型内声明)。

AspectJ的AOP例子
以下是一些AspectJ5的简单例子,能部分地说明怎么在代码中实现上面提到的那些概念。关于该AOP语言的细节,请参考AspectJ的文档。
// using a dedicated syntax
// that compliments the Java language
public aspect Foo {

  pointcut someListOperation() : call(* List+.add(..));

  pointcut userScope() : within(com.biz..*);

  before() : someListOperation() && userScope() {
    System.out.println("called: "
        + thisJoinPoint.getSignature()
    );
  }
}
上面的代码使用了AspectJ专用的语法,你可以用Java Annotations写出等效的代码,如下:
// the Java 5 annotations approach
@Aspect
public class Foo {

  @Pointcut("call(* java.util.List+.add(..))")
  public void someListOperation() {}

  @Pointcut("within(com.biz..*)")
  public void userScope() {}

  @Before("someListOperation() && userScope()")
  public void before(JoinPoint thisJoinPoint) {
    System.out.println("called: "
        + thisJoinPoint.getSignature()
    );
  }
}


第一段代码定义了一个叫Foo的aspect,这个aspect拥有两个pointcut分别叫做someListOperation()和 userScope()。这些Pointcut将会在应用(application)中选取一系列Join points。它们用类似boolean表达式的方式结合在一起,所以那个before advice将在每一次调用List类的子类的叫做add的方法之前都会执行——倘若这个“调用”发生在com.biz包或者其子包之类。那个before advice将会打印出那些Join Points上调用的方法签名。第二段代码演示了如何使用可选的依赖于Java5 annotations的语法来定义一个相同的aspect。

什么是植入(Weaving)
如前文所描述和前面的代码所演示,aspects可以横切整个应用。“植入”就是指把aspects和正规的面向对象应用“编织”到一个独立的单元,一个独立的应用中去的过程。
植入可以发生在不同的时间段内:
·编译时植入:在编译时对代码进行再处理,例如,在部署(deployment)之前(当然也在运行时之前)(例如AspectJ 1.X中)
·加载时植入:植入是在类文件被加载时完成的——也就是说,在部署时完成(例如在AspectWerkz2.0中)。
·运行时植入:植入可以发生在整个应用生命周期的任何时刻(例如在JRockit和StreamLoom项目中)。

这个过程也通过多种不同方式来完成:
·源代码植入: 输入是开发的源代码,输出是修改了的已经植入了那些aspects的源代码(例如AspectJ 1.X中)。
·字节码植入: 输入是已编译的应用的类字节码,而输入是已经被修改了的字节码。(例如AspectWerkz2.0和AspectJ1.1及更新版本)
源代码植入用途有限,因为只有当所有源代码可用时才能进行植入。例如,这使得不可能实现通用监控服务。

编译时植入同样有这个问题:所有将要部署的字节码,必须在部署之前的“后编译时”阶段准备好。
本系列文章将涵盖字节码植入 vs. JVM植入的内容,并在接下来的章节中讨论。
此外,动态代理(一种受限形式的植入)在JVM中早已可用。该API从JDK1.3起就成为JDK的一部分,它让你能够为一个接口(和/或一系列接口)创建一个动态的虚拟的代理,这给予你截获每一个对该代理的调用并将之重定向到任何你希望的地方去的可能性。依据定义,这不算真正的“植入”,但它像“植入”一样提供了一种进行方法截取的简单手段。它被许多框架用来做简单的AOP,比如Spring框架。

基于字节码处理的植入存在的问题
值得强调的是,下面描述的问题是因字节码处理产生,目前的AOP实现诸如AspectJ都经受其害。大体而言,这些问题影响到所有基于字节码处理的产品,比如应用监控解决方案、统计信息工具以及其它应用AOP的解决方案。

字节码处理效率低
植入操作的实际处理部分通常大量占用CPU,并且有时会耗用一块可观的内存。这会影响启动时间。比如说截取所有对toString()方法的调用或者对某一个属性的访问,你需要一个接一个地分析所有的类中的每一个字节码指令。这也意味着,为了通过一种便于使用的方式暴露字节码,字节码处理框架需要建立许多中间结构。深一层的意义是,植入者需要分析整个应用(包括第三方库等等)的所有类中的所有字节码指令。在最坏情况下,类的总数能达到10000个。
如果使用了多个植入者,消耗也同样倍增。

重复记录:为一个植入者建立类数据库的代价是昂贵的
为了知道一个类/方法/属性是否应该进行植入,植入者需要对一个类或成员进行匹配检测。大多数AOP框架和应用AOP的产品都有某种高级表达式语言(pointcut表达式)用来定义应该把一个代码块植入到哪里(把一个advice植入到那里)。比如说,这种表达式语言能让你挑出所有这样的方法——它的返回类型是一个实现了接口T的类。对一个特定的方法的调用信息,在字节码指令中是不可用的。确定一个特定的方法methodM 是否应该进行植入的唯一途径是为植入者建立某种类数据库,查询该方法的返回类型,并检查其返回类型是否实现了给定的接口T。

你也许在想:为什么不仅仅使用java.lang.reflect.* API?在这里使用反射机制的问题是:若要对某个类进行反射查询,就必须触发对该类的加载,这也将在我们知道足够多的进行植入的信息之前,触发对该类的植入(在加载时植入的基础上)。简单而言:这是一个经典的鸡和蛋的问题。

因而,植入者需要一个类数据库(通常建立在内存中,来自于磁盘上的初始字节码)来执行所要求的查询,如果获取实际的Join points的时候需要这些查询的话。有时候可通过限制表达式语言的表示来避免这个问题,但是通常这种“限制”同样也会限制产品的可用性。

这种内存中的类数据库在植入操作完成后就是多余的。JVM在自己的数据库中已经有了所有这些信息,并已很好地优化了(例如,它为java.lang.reflect API服务)。所以我们要结束这种对类结构(对象模型)的重复记录,这种重复记录消耗了大量不必要的内存,也增加了建立类数据库并在发生改变时维护之的启动消耗。
如果使用了多个植入者,消耗也同样倍增。

HotSwap(热替换):运行时修改字节码增加了复杂性
Java5中加入了Hotswap API,作为JVMTI规范的一部分。在Java5之前,这个API只有运行在debug模式下才可用,而且仅仅对本地C/C++ JVM扩展有效。该API使得可以在运行时改变字节码——也就是重定义一个类。它被一些AOP框架和一些应用AOP的产品用来支持运行时植入的能力。
无论是否非常强大,该API通过三种方式局限了可用性和可量测性:
·效率低。因为字节码是运行时改变的,字节码处理带来了运行时的性能损耗(CPU和内存)。同样,如果需要在许多地方改变字节码,意味着要重定义许多类。JVM不得不重做所有的优化和内嵌,而这些都是之前已经做过了的。
·非常局限。该API没有指明当前运行的字节码在何处可以安全地改变。一个植入者因此需要假定字节码就是在磁盘上,或者需要对字节码保持跟踪。下一段解释了当使用了多个植入者时,以上所述问题会成为一个主要的问题。

此外,目前还没有任何一个HotSwap API的实现支持schema change,schema change在规范中声明为可选。这意味着不可能在运行时改变一个类的schema,比如,不能为基本处理模型添加方法/属性/接口。这使得某些类型的运行时植入变得不可能,因此要求用户预先“准备”好那些类。

多agent是个问题
当多个产品使用字节码处理时,可能发生不希望的问题。这些问题可能关系到了优先级、改变的通知、改变的撤销等等。也许现在这还不是大问题,但在将来一定是一个严重的问题。一个植入者可以视为一个agent(同JVMTI规范中提到的agent),这些agent在加载时或者运行时进行字节码处理。在多agent的情况下会有一个极大的风险:每一个agent都会干扰到其它agent,它通过某种方式改变了字节码,而另一个agent则假设自己是唯一的agent,那前者对字节码的改变也许是后者所不希望看到的。

这里是一个例子,当两个agent不知道对方的存在,那么可能会出现问题。当一个应用使用了两个agent(一个AOP植入者和一些应用程序性能工具),它们同样都在加载时进行字节码处理,那么植入的代码可能成为被测量的代码的一部分,也可能不是,这取决于配置,如下面的代码所示:
// say this is the original user code
void businessMethod() {
  userCode.do();
}

//---- Case 1
// say the AOP weaver was applied BEFORE the
// performance management weaver
// the woven code will behave like:
void businessMethod() {
  try {
    performanceEnter();
    aopBeforeExecuting();//hypothetical advice
    userCode.do()
  } finally {
    performanceExit();
  }
}
// ie the AOP code affect the measure


//---- Case 2
// say the AOP weaver was applied AFTER the
// performance management weaver
// the woven code will behave like:
void businessMethod() {
  aopBeforeExecuting();//hypothetical advice
  try {
    performanceEnter();
    userCode.do()
  } finally {
    performanceExit();
  }
}
// ie the AOP code will NOT affect the measure


问题在于agent之间 的优先级——这里没有一个细致的配置,用以在Join Point(或Pointcut)级别上控制顺序。
还有一些情况可能导致不可预知的结果。比如,当对一个属性的访问被截取,一般意味着获取该属性的字节码指令被移到新加的一个方法内,然后这个新方法被调用。因而,下一个植入者将从另外的地方(新加的方法内)发现对该属性的访问,而这个新的Join Point可能已经不符合它自己的匹配机制和配置。

总结一下,主要的问题有:
·Agent看到的哪一份字节码?问题在于,被植入的代码通常通过类装载这个途径获取,而被依赖的用以建立类数据库的字节码是从磁盘上读取的。当涉及多agent,执行的字节码已经不再是磁盘上的哪一份,因为某些agent可能已经改变了字节码。使用HotSwap API 也会遇到同样的问题。
·当agent A撤销或者更改它的植入操作时也存在问题。如果agent B在agent A的植入操作之后对修改,那么agent B可能重构了字节码,使得看起来和之前完全不同(尽管表现得一样),然后agent A就不知道该怎么做了。

无法截取反射调用
目前的植入方式仅仅能(至少部分能)处理能静态确定的执行流程。
考虑下面的代码例子,通过反射调用接口foo的void doA()方法:
public void invokeA(Object foo) throws Throwable {
  Method mA = foo.getClass().getDeclaredMethod("doA", new Class[0]);
  mA.invoke(foo, new Object[0]);
}


这种反射访问机制在现在的类库中常常用来创建实例、调用方法、访问属性。
从字节码的角度看,对void doA()方法的调用是不可见的,植入者只能看到对java.lang.reflect API的调用。对于植入使用了反射的方法调用,还没有一个简单高效的办法。这是目前在植入操作如何执行、AOP如何实现方面的一个严重局限性。

其它问题
对于字节码处理,尤其是加载时/运行时的处理,有些人仍抱怀疑态度。但毋庸置疑的是,自由改写字节码绝对不容小觑,尤其是当它和一项改变了思维方式的革命性新技术(诸如AOP或服务的透明注入)一起出现时。而多agent下可能发生的冲突增强了怀疑论。
另一潜在的问题是,JAVA规范中指明了class文件的64Kb的最大值。方法体被限制在总共64Kb大小的二进制指令之下。当对一个已经很大的class文件(比如,把jsp编译成servlet之后得到的class文件)进行植入操作时,可能导致突破64Kb的限制,从而导致一个运行时的错误。

提议的解决方案
JVM 植入自然成了上面讨论的大部分问题的答案。要知道为什么,我们先看两个例子,这些例子表明JVM已经做了进行植入所需要的大部分的工作:当一个类被加载,JVM会读入该类的字节码,构造java.lang.reflect.* API所需要的数据。另外一个例子是方法的分发(dispatching)。高级的JVM会将方法或者代码块的字节码编译成更高级、更高效的结构和执行流程(在适当的地方进行代码内嵌(inline))。基于HotSwap(热替换) API的需求,JRockit JVM(也许还有其他的JVM)保存着方法调用的纪录,从而使得如果一个方法体(method body)所属的类被重定义了,那么这个方法体可以在任何期望的地方被HotSwap,无论它是不是进行了内嵌。

因而,与通过改变字节码来把advice weave进去——比如说在某个方法调用之前——不同的是,JVM知道足够多的信息,轻易就能在实际的方法分发之前把advice分派到任何符合条件的Join Point上。

因字节码没有改变,我们可以期望一些直接的优势,比如:
·没有因字节码处理带来的启动时损耗。
·完全支持运行时增删advices,无论何时何地,并且只会带来线性损耗。
·潜在的对反射调用的支持
·不存在因为把class原型复制到某些框架特有结构中去而带来的额外内存消耗。

关于未来在JRockit JVM中加入对AOP的支持的详细描述,将在本系列的第二篇文章中出现。
以下代码就作为结尾说明。其中就是在方法sayHello()调用之前的地方分排一个静态方法advice():
public class Hello {

  // -- the sample method to intercept
  public void sayHello() {
    System.out.println("Hello World");
  }

  // -- using the JRockit JVM support for AOP
  static void weave() throws Throwable {
    // match on method name
    StringFilter methodName = new StringFilter(
        "sayHello",
        StringFilter.Type.EXACT
    );

    // match on callee type
    ClassFilter klass = new ClassFilter(
        Hello.class,
        false,
        null
    );

    // advice is a regular method dispatch
    Method advice = Aspect.class.getDeclaredMethod(
        "advice",
        new Class[0]
    );

    // get a JRockit weaver and subscribe the
    // advice to the join point picked out by the filter
    Weaver w = WeaverFactory.createWeaver();

    w.addSubscription(new MethodSubscription(
        new MethodFilter(
            0,
            null,
            klass,
            methodName,
            null,
            null
        ),
        MethodSubscription.InsertionType.BEFORE,
        advice
    ));
  }

  // -- sample code

  static void test() {
    new Hello().sayHello();
  }

  public static void main(String a[])
  throws Throwable {
    weave();
    test();
  }

  // -- the sample aspect

  public static class Aspect {

    public static void advice() {
        System.out.println("About to say:");
    }
  }
}


结语
在JAVA社区,为了实现一些高级技术诸如AOP、中间件领域的透明服务添加,字节码处理(Bytecode instrumentation)变得很流行。但是,拜它的几个关键局限性所赐,它的广泛应用会导致一些进一步的问题,影响到可量测性和可用性。
字节码处理已经或多或少成了AOP中植入的标准方式,将来一定会深受本文中指出的局限和问题所害——假设现在还没有受害的话。
我们相信JVM支持AOP是这些问题的天生的解决方案。我们正在计划一个订购式的、和JVM的方法分派组件紧密结合的API。本系列的下一篇文章将详细说明这个API,并解释每个问题是如何解决的。

附加阅读
·JRockit 技术中心 —— 第一个支持AOP的企业级JVM的主页。
·新闻组:: jrockit.developer.interest.aop
·AspectWerkz——简单的JAVA AOP 框架
·AspectJ——事实上的JAVA AOP 标准
·AOSD—— 面向方面软件开发
·Quick Start Guide to Enterprise AOP with Aspectwerkz 2.0 - David Teare的一篇文章 (dev2dev, 2005年5月)

[译注]术语
以下术语仅供参考
AOP: 面向方面编程,面向切面编程
JVM: JAVA虚拟机
Concern: 关注点
Cross-cutting concern:横切关注点
Separation of concerns:关注点分离
Join Point:切入点
Pointcut:一系列符合特定条件的Join Point的集合。
Advice:切入的方面,即被植入到Pointcut所在位置去的代码
before advice:插入到Pointcut之前的advice
after advice:插入到Pointcut之后的advice
Aspect:方面
Weave/weaving:植入
Inline:代码内嵌

作者简介
Jonas Bonér是BEA系统公司JRockit组的高级软件工程师,动态AOP、VM(虚拟机)植入、传播AOP是目前的工作。他是AspectWerkz AOP框架的创始人,和AspectJ5项目的委员。
Joakim Dahlstedt 是BEA系统公司Java Runtime 产品团队的CTO, 他在组内负责未来JVM的未来发展方向。Alexandre Vasseur 是BEA系统公司Java Runtime 产品团队的软件工程师, 主要关注面向方面的技术.他是AspectWerkz AOP 框架的合作创始人和Eclipse AspectJ项目的委员。

你可能感兴趣的:(AOP 与 AspectJ5)