AspectJ介绍

AspectJ是Java中流行的AOP(Aspect-oriented Programming)编程扩展框架,是Eclipse托管给Apache基金会的一个开源项目。俗话说得好,要学编程先写个HelloWorld,下面我们来通过一个简单的例子来了解AspectJ。

在动手前先准备下环境,目前国内的互联网公司的开发环境标配为:

  • JDK(最好是1.8)
  • Maven
  • IntelliJ Idea

我们的实验环境也是如此。好,现在来进入实验流程。

假设有一个Boy类,它的定义如下:

public class Boy {

    public void watchBasketball() {
        System.out.println("Watching basketball!");
    }

}

假如我们想在调用Boy.watchBasketball前后打印日志,应该怎么办?最简单的办法是修改Boy的代码,在方法前后加入打印日志的代码。但是如果有上百个方法的话,这种办法效率太低。下面我们用AspectJ来实现这个功能。

首先,我们需要通过Maven来引入AspectJ的两个包:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.9</version>
</dependency>

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.9</version>
</dependency>

然后在Maven的Build过程增加编译期的AspectJ相关插件:

<build>
    <plugins>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>aspectj-maven-plugin</artifactId>
            <version>1.7</version>
            <configuration>
                <complianceLevel>1.8</complianceLevel>
                <source>1.8</source>
                <target>1.8</target>
                <showWeaveInfo>true</showWeaveInfo>
                <verbose>true</verbose>
                <Xlint>ignore</Xlint>
                <encoding>UTF-8 </encoding>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <!-- use this goal to weave all your main classes -->
                        <goal>compile</goal>
                        <!-- use this goal to weave all your test classes -->
                        <goal>test-compile</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

最后,增加下面这个aspect:

public aspect BoyAspect {
    // 指定执行 Boy.watchBasketball() 方法时执行下面代码块
    void around():call(void Boy.watchBasketball()){
        System.out.println("Begin watching basketball!");
        proceed();
        System.out.println("End watching basketball!");
    }
}

大功告成!我们来测试下:

public class Main {
    public static void main(String[] args) throws Exception {
        Boy boy = new Boy();
        boy.watchBasketball();
    }
}

输出结果如下所示:

Begin watching basketball!
Watching basketball!
End watching basketball!

可以看到,虽然我们还是调用Boy.watchBasketball()方法,但是前后已经加上了打印日志,而且源代码没有任何改动!这简直太神奇了!

AspectJ是怎么做到的呢?我们可以通过对Main方法进行字节码反编译一探究竟。Java字节码反编译成源代码可以使用http://www.javadecompilers.com/这个在线反编译网站,我们将Main方法的class文件反编译,得到如下源代码:

public class Main
{
  private static final void watchBasketball_aroundBody1$advice(Boy target, BoyAspect ajc$aspectInstance, AroundClosure ajc$aroundClosure)
  {
    System.out.println("Begin watching basketball!");
    AroundClosure localAroundClosure = ajc$aroundClosure;watchBasketball_aroundBody0(target);
    System.out.println("End watching basketball!");
  }

  private static final void watchBasketball_aroundBody0(Boy paramBoy) { paramBoy.watchBasketball(); }
  
  public static void main(String[] args) throws Exception
  {
    Boy boy = new Boy();
    Boy localBoy1 = boy;
    watchBasketball_aroundBody1$advice(localBoy1, BoyAspect.aspectOf(), null);
  }
  
  public Main() {}
}

咦?这不是我写的代码啊,怎么多了些奇奇怪怪的方法和代码?没错,这些代码是AspectJ框架生成的代码。在代码编译时,AspectJ根据我们定义的aspect信息,使用字节码修改技术进行了代码增强。可以看到,在AspectJ修改后,在调用原始方法前后加入了打印日志的代码。

在AOP编程中,有如下概念:

  • JoinPoint:表示代码执行过程中的一个点,例如方法调用或者属性访问。
  • Pointcut:用来匹配多个JoinPoint。
  • Advice:将Pointcut与功能增强代码联系起来,使得在程序执行过程中到达特定JoinPoint时执行相应的功能增强代码(例如打印日志)。
  • Aspect:将JoinPoint、Pointcut与Advice包装起来。

在上面的例子中,我们定义了BoyAspect这个aspect:

public aspect BoyAspect {
    // 指定执行 Boy.watchBasketball() 方法时执行下面代码块
    void around():call(void Boy.watchBasketball()){
        System.out.println("Begin watching basketball!");
        proceed();
        System.out.println("End watching basketball!");
    }
}
  • Pointcut:call(void Boy.watchBasketball())定义了一个Pointcut,它匹配了代码执行过程中Boy类执行watchBasketball方法的这个JoinPoint。AspectJ的Pointcut定义语法非常强大,我们可以正则表达式来匹配多个JoinPoint,例如call(void Boy.watch*())匹配了Boy类所有以watch开头的方法执行。
  • Advice:上面代码中使用了aroundAdvice,也就是在对原始方法调用前后都加上了代码增强;除此之外,我们还可以用before()、after()等这些Advice,分别对应于只在原始调用前或者原始调用后进行代码增强。

通过使用AspectJ,我们可以非常方便的对原始代码进行切面式的代码增强,例如对于所有类的方法调用前后都打印日志。使用AspectJ AOP的好处是,我们不用一个个的修改原始代码类,只用写一个aspect,使用Pointcut来匹配多个类的方法执行点,再实现代码增强即可。

通过使用AspectJ,我们还可以动态的增加原始类的方法或者字段。在下面代码中,我们对原始的Boy类增加了observers字段,并且增加了addObserver(observer)和removeObserver(observer)方法,使用advice来在调用Boy.setxx方法时通知观察者:

public aspect BoyAspect {
    // 对Boy类增加observers字段
    private Vector<Observer> Boy.observers = new Vector();

    // 对Boy类增加addObserver方法
    public void Boy.addObserver(Observer observer) {
        observers.add(observer);
    }

    // 对Boy类增加removeObserver方法
    public void Boy.removeObserver(Observer observer) {
        observers.remove(observer);
    }

    // 使用after Advice, 指定执行Boy.setxx方法返回时, 通知Boy中的所有观察者
    after(Boy boy): target(boy) && call(void Boy.set*(int)) {
        Iterator<Observer> iterator = boy.observers.iterator();
        while (iterator.hasNext()) {
            Observer observer = iterator.next();
            observer.observe(boy);
        }
    }
}

为了匹配Boy.setxx方法,我们在Boy类增加一个age字段,并且增加set/get方法:

public class Boy {
    private int age;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

}

现在来试试效果如何:

public class Main {

    public static void main(String[] args) throws Exception {
        Boy boy = new Boy();
        boy.addObserver(new Observer());
        boy.setAge(10);
    }
}

输出结果如下所示:

Boy is changing!

鹅妹子嘤!我们并没有对原始的Boy类做代码修改,但是居然对于Boy增加了字段修改的观察者并且实现了通知!

我们对Boy.class进行反编译看下代码:

public class Boy { 
  public static Vector ajc$get$observers(Boy paramBoy) 
  { 
    return observers; 
  } 

  public static void ajc$set$observers(Boy paramBoy, Vector paramVector) 
  { 
    observers = paramVector; 
  } 

  public void addObserver(Observer paramObserver) 
  { 
    BoyAspect.ajc$interMethod$AOP_BoyAspect$AOP_Boy$addObserver(this, paramObserver); 
  } 

  public void removeObserver(Observer paramObserver) 
  { 
    BoyAspect.ajc$interMethod$AOP_BoyAspect$AOP_Boy$removeObserver(this, paramObserver); 
  }
  
  private int age;
  private Vector<Observer> observers;
  public Boy() {
    BoyAspect.ajc$interFieldInit$AOP_BoyAspect$AOP_Boy$observers(this);
  }
  
  public int getAge() {
    return age;
  }
  
  public void setAge(int age) {
    this.age = age;
  }
}

可以看到,字节码文件中的确增加了相应的observers字段以及addObserver、removeObserver方法。

Written on October 19, 2017