AspectJ 初探

debian 里面 aspectj 有这么几个 package:

  • aspectj 包括了一些命令行工具,主要是启动 aspectj 的脚本
  • libaspectj-java 主要包括了三个 jar,aspectjweaver、aspectjrt 和 aspectjtools,weaver 自然包括的是 load-time weaving(也能做 runtime weaving),rt 是运行时的 package(如果做 compile-time weaving 运行时就只需要这个了),最后的 tools 提供了对 ant 等工具的支持
  • libaspectj-maven-plugin-java 自然是 maven 的插件了

我们参看这篇 blog 的例子,先是一个很简单的类

public class TestTarget {
    public static void main (String[] args) {
        System.out.println(">--------- Start test -----------<");
        new TestTarget ().test () ;
        System.out.println(">---------- End test -----------<");
    }

    public void test () {
        System.out.println ("TestTarget.test()") ;
    }
}

然后是一个 aspect 的定义

import org.aspectj.lang.annotation.Aspect ;
import org.aspectj.lang.annotation.Before ;
import org.aspectj.lang.annotation.After ;
import org.aspectj.lang.JoinPoint ;

@Aspect
public class TestAspect {
    @Before ("execution (* TestTarget.test*(..))")
    public void beforeAdvice (JoinPoint joinPoint) {
        System.out.printf ("TestAspect.advice() called on '%s'%n", joinPoint) ;
    }

    @After ("execution (* TestTarget.test*(..))")
    public void afterAdvice (JoinPoint joinPoint) {
        System.out.printf ("TestAspect.advice() call ended%n") ;
    }
}

意思基本一看就明白,现在我们用 aspectj 做 compile-time weaving,这时我们需要用 ajc 来编译(使用了 weaver 里面对应类实现的编译),

$ ajc -1.5 TestTarget.java TestAspect.java

这样获得的代码将会有什么不一样吗?请看

$ javap -p TestTarget.class 
Compiled from "TestTarget.java"
public class TestTarget {
  private static final org.aspectj.lang.JoinPoint$StaticPart ajc$tjp_0;
  public TestTarget();
  public static void main(java.lang.String[]);
  public void test();
  static {};
  private static void ajc$preClinit();
}

而且直接要运行明显是不行的

$ java TestTarget
Exception in thread "main" java.lang.NoClassDefFoundError: org/aspectj/lang/Signature
	at java.lang.Class.getDeclaredMethods0(Native Method)
	at java.lang.Class.privateGetDeclaredMethods(Class.java:2521)
	at java.lang.Class.getMethod0(Class.java:2764)
	at java.lang.Class.getMethod(Class.java:1653)
	at sun.launcher.LauncherHelper.getMainMethod(LauncherHelper.java:494)
	at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:486)
Caused by: java.lang.ClassNotFoundException: org.aspectj.lang.Signature
	at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 6 more
$ java -cp /usr/share/java/aspectjrt.jar:. TestTarget
>--------- Start test -----------<
TestAspect.advice() called on 'execution(void TestTarget.test())'
TestTarget.test()
TestAspect.advice() call ended
>---------- End test -----------<

很明显的是 aspectjrt 不知道怎样将 TestTarget 的 test 方法弄成前后会调用另一个类里面的方法了。看看 bytecode 吧

  public void test();
    Code:
       0: getstatic     #47                 // Field ajc$tjp_0:Lorg/aspectj/lang/JoinPoint$StaticPart;
       3: aload_0       
       4: aload_0       
       5: invokestatic  #53                 // Method org/aspectj/runtime/reflect/Factory.makeJP:(Lorg/aspectj/lang/JoinPoint$StaticPart;Ljava/lang/Object;Ljava/lang/Object;)Lorg/aspectj/lang/JoinPoint;
       8: astore_1      
       9: invokestatic  #59                 // Method TestAspect.aspectOf:()LTestAspect;
      12: aload_1       
      13: invokevirtual #63                 // Method TestAspect.beforeAdvice:(Lorg/aspectj/lang/JoinPoint;)V
      16: getstatic     #17                 // Field java/lang/System.out:Ljava/io/PrintStream;
      19: ldc           #39                 // String TestTarget.test()
      21: invokevirtual #25                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      24: goto          37
      27: astore_2      
      28: invokestatic  #59                 // Method TestAspect.aspectOf:()LTestAspect;
      31: aload_1       
      32: invokevirtual #66                 // Method TestAspect.afterAdvice:(Lorg/aspectj/lang/JoinPoint;)V
      35: aload_2       
      36: athrow        
      37: invokestatic  #59                 // Method TestAspect.aspectOf:()LTestAspect;
      40: aload_1       
      41: invokevirtual #66                 // Method TestAspect.afterAdvice:(Lorg/aspectj/lang/JoinPoint;)V
      44: return

很明显的是这里在 test 方法里面加入了调用 aspect 的部分,那么如果是一个 aspect 不能匹配的方法,会有什么结果呢?悄悄对应的 main 函数,发现似乎没有任何的变化。看样子 ajc 的确在编译时做了些什么!很重要的一点在这里就是如果 ajc 编译时没有传递 TestAspect.java 结果和 javac 编译的是一样的,看来这个实现有个致命的问题:你不能写一个 aspect 弄成 package 给人用,你只能让人家把你的 aspect 拿去。

于是第二种策略就用上了,咱能 post-compile weaving 不?这个 idea 是说,那我们拿到了别人的 bytecode,运行之前我们先做点手脚,这个时候我们还是可以用 ajc 的,同时我们的 aspect 也有编译好的版本,那 ajc 是不是能“处理”一下 jar 里面的 class,匹配上我们的 pointcut 就来一下呢?

$ ajc -inpath . -aspectpath . -outjar test.jar
$ jar tvf test.jar 
    55 Thu Aug 29 23:40:42 PDT 2013 META-INF/MANIFEST.MF
  2718 Thu Aug 29 23:40:44 PDT 2013 TestAspect.class
  2223 Thu Aug 29 23:40:44 PDT 2013 TestTarget.class

看样子这也是一个很不错的解决方案,因此你需要一个 task 安插在编译(或者获得 jar)之后,这时利用 ajc 将 aspect 嵌入进去,为此你可以使用 aspectjtools 提供的 ant task 集成到你的 ant 里面去。这可能是 aspectj 最佳的策略了。

假使我们不能修改 jar 呢?hmm,我们还有 load-time weaving 嘛!也就是说加载类 TestTarget 的时候能 wire in 我们怎么 hack 一下,让我们的 aspect 进去。这就需要特别的 class loader 了,要做 LTW 主要需要一个 aop.xml,它描述了 load-time weaving 的目标 package(减少不必要的 weaving 匹配搜索范围),使用的 aspect。比如以上例子

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
  <weaver options="-verbose -showWeaveInfo">
    <include within="*"/>
  </weaver>
  <aspects>
    <aspect name="TestAspect"/>
  </aspects>
</aspectj>

我们把这个 aop.xml 放置在 jar 的 META-INF 里面。这样执行

$ aj -javaagent:/path/to/aspectjweaver-1.7.3.jar -cp /path/to/aspectjrt-1.7.3.jar:/path/to/aspectjweaver-1.7.3.jar:test.jar TestTarget
[AppClassLoader@417470d0] info AspectJ Weaver Version 1.7.3 built on Thursday Jun 13, 2013 at 19:41:31 GMT
[AppClassLoader@417470d0] info register classloader sun.misc.Launcher$AppClassLoader@417470d0
[AppClassLoader@417470d0] info using configuration file:/home/heli/code/test/aspectj/test.jar!/META-INF/aop.xml
[AppClassLoader@417470d0] info register aspect TestAspect
[AppClassLoader@417470d0] weaveinfo Join point 'method-execution(void TestTarget.test())' in Type 'TestTarget' (TestTarget.java:9) advised by before advice from 'TestAspect' (TestAspect.java)
[AppClassLoader@417470d0] weaveinfo Join point 'method-execution(void TestTarget.test())' in Type 'TestTarget' (TestTarget.java:9) advised by after advice from 'TestAspect' (TestAspect.java)
>--------- Start test -----------<
TestAspect.advice() called on 'execution(void TestTarget.test())'
TestTarget.test()
TestAspect.advice() call ended
>---------- End test -----------<

这就是所谓的 LTW 了,注意 javaagent 选项和 aj 脚本里面设置的变量,本质上如果你启动脚本是 java trigger 的你需要这些东西确保 LTW 能正常运作。

Spring 提供的 AOP 支持现在可以跟 aspectj 接轨,但是具体实现的机理却很不一样,spring 是通过 java 的 dynamic proxy 来实现的,这意味着:

  • 仅仅 bean 才能享受到 AOP 待遇,比如你有类 A,你需要对其 foo 方法做 aspect,那么当你在 spring 里面产生它才行,如果类 B 自己调用 A 的 constructor,那调用这些对象的 foo 就没用了
  • 不能解决“内部调用”,如果 A 的 foo 调用了 bar,然后 bar 都有 spring 的 aspect,你会发现 foo 调用 bar 和直接通过 A 对象调用 bar 行为不同,前者并不使用对应的 aspect
  • 不能解决 private/protected 方法

当然纯粹使用 call/execution/throw 之类的定位 pointcut 是非常麻烦的:不能将定位 pointcut 的逻辑从 aspect lib 里面分离出去,这导致 aspect 似乎不可重用;好处自然是你可以为某些事物写好 aspect,比如你希望增加调试信息,写了一个 aspect 输出调用的输入输出,就可以应用到你自己的 application package 上。那么对更精细的定位就需要 annotation 的支持了。

使用 annotation 的 aspect 有一个典型的用法,那就是 spring 的 contextual session management,通过 @Transactional 标定使用 session 的 beginTransaction 的范围,有很多参数决定 nested transaction 是如何协作的,那么典型的问题是多线程调用的情况下你如何避免这其中的状态互相不冲突。一个典型的 pattern 就是将可能冲突的状态(来自 annotation)放在 holder 类,这个类使用 ThreadLocal 存放这些状态,所写的 aspect 使用 holder 类获得(上层)状态,使用 reflection 获得当前的状态,这样就能实现切换。

比如我们的数据库有一个 master 一个 replica,我们应用有一部分做 reporting 的功能,这些 query 一般比较 heavy,我们不希望它们 hit master 干扰线上的 query,那么一个想法就是将这些 reporting 的 API 选择出来,让它们的 data source 能够 route 到另一个 endpoint。看起来很简单的想法,但是你使用 AOP 实现起来就不那么容易了(hack 的方法自然就是将那些类使用的 persistent layer interface 使用显式的 override):

  • 提供一个 runtime available 的 annotation 类,它记录使用的 datasource(如 string)
  • 使用一个 routing data source(spring 里面有现成的)
  • 实现一个 holder 记录 datasource
  • 实现 aspect 匹配 annotation,根据 holder、当前的 annotation 切换状态

类似的 aspectj 的应用很多

  • monitoring,对特定的 API 做 annotation,一旦有异常就可以输出特定的信息到 log,这样在 monitor 做 log scan 的时候可以有更好的针对性(一般的做法似乎是搜特定的 exception 类型,由于 exception 类型是 application specific 的,这样 monitoring 的逻辑似乎就跟 application 耦合在一起了)
  • caching 这个很显然的,如果输入的结果 aspect 知道,那么就可以跳过实际计算的逻辑直接返回
  • “重试”,这个使用 AOP 实现有一定的 trick,有的重试概念上是简单的,但实现的时候会有一定的区别,比如做 service call,有时怕网络暂时的问题影响稳定性,那么可以通过 annotation 来标注重试的函数,问题是如果你的 API 是 client.newABCDCall ().call (request),而不是简单的 client.call (request) 的话有可能你就得将这个单独弄成一个 helper function 对其重试(至少 Spring AOP 不能 handle 前者了)
  • 收集 metrics,比如每个 API 运行的时间

——————
And she gave him Bilhah her handmaid to wife: and Jacob went in unto her.

Advertisements
AspectJ 初探

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s