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 方法。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论