什么是面向切面编程(Aop)

面向切面编程(AOP,Aspect-Oriented Programming)是一种编程范式,它允许开发者通过切面(Aspect)来模块化横切关注点,并将它们封装成可重用的组件。横切关注点是指那些存在于应用程序中多个模块中的功能,它们通常与应用程序的核心业务逻辑无关,例如日志记录、事务管理、安全性等。

在面向切面编程中,切面是一个模块化的单元,它横跨一个或多个对象,提供了在应用程序中的多个位置进行代码重用的机制。切面可以定义一组横切关注点,并将它们与应用程序的核心业务逻辑分离开来。这样,当需要修改或者添加新的横切关注点时,只需要修改或者添加对应的切面,而不需要修改核心业务逻辑代码。

面向切面编程的核心概念是切面、连接点、通知、切点和增强。其中,切面是关注点的模块化单元,连接点是在应用程序中可以插入切面的点,通知是切面在连接点处执行的代码,切点是一组连接点的集合,而增强是在切点处执行的通知的行为。

总的来说,面向切面编程通过将横切关注点与核心业务逻辑进行解耦,提高了代码的模块化性、可维护性和可重用性,使得应用程序更易于理解、扩展和维护。

主流AOP技术分类

首先,从织入的时机的角度看,可以分为源码阶段、class阶段、dex阶段、运行时织入。

对于前三项源码阶段、class阶段、dex织入,由于他们都发生在class加载到虚拟机前,我们统称为静态织入, 而在运行阶段发生的改动,我们统称为动态织入。

静态织入:

  • APT
  • AspectJ
  • ASM
  • Javassit

动态织入:

  • java动态代理
  • cglib
  • Javassit

静态织入发生在编译器,因此几乎不会对运行时的效率产生影响;动态织入发生在运行期,可直接将字节码写入内存,并通过反射完成类的加载,所以效率相对较低,但更灵活。

其次,我们需要关注这些框架具备哪切面编程的能力,这有助于帮助我做技术选型,由于AspectJ、ASM 、Javassit是相对比较完善的AOP框架,因此只对三者进行比较。

能力 AspectJ ASM Javassit
切面抽象
切点抽象
通知类型抽象

其中:

  • 切面抽象:具备筛选过滤class的能力,比如我们想为Activity的所有生命周期织入代码,那你是不是首先需要具备过滤Activity及其子类的能力。
  • 切点抽象:具体到某个class,是否具备方法、字段、注解访问的能力。
  • 通知类型抽象:是否直接支持在方法前、后、中直接织入代码。

当然不具备能力不代表不能做AOP编程,可以通过其他方法解决,只是易用性的问题。

APT

APT 是 Annotation Processing Tool 的缩写,即注解处理工具。APT 是一种工具,用于在编译时处理 Java 源代码中的注解,生成额外的 Java 代码或者其他文件,以帮助开发者完成一些重复性的工作或者自动生成一些代码。

通过 APT,开发者可以定义自己的注解,并编写相应的处理器来处理这些注解。在编译时,APT 会扫描源代码中的注解,并调用相应的处理器来生成额外的代码或者其他文件。这些生成的代码可以用于实现一些功能,如自动实现 Parcelable 接口、生成单例模式的代码、自动生成代理类等。

APT 在 Android 开发中有着广泛的应用,例如 ButterKnife 库就是利用 APT 实现的,它通过在 View 属性上添加注解,然后在编译时生成相应的 View 绑定代码,从而简化了代码编写。另外,ButterKnife、Dagger等一些常用的 Android 库也使用了 APT 来生成代码。

总的来说,APT 是一种强大的工具,可以在编译时自动化处理一些重复性的工作,提高了开发效率,减少了开发者的工作量。

AspectJ

AspectJ 是一个基于 Java 语言的面向切面编程(AOP)的扩展框架,它提供了一种简洁而强大的方式来实现横切关注点的模块化和重用。

AspectJ 扩展了 Java 语言,引入了一组新的关键字和语法规则,使得开发者可以更方便地定义切面和通知,并将它们应用到代码中的特定连接点上。AspectJ 通过编译时的静态代码织入技术,将切面的行为嵌入到应用程序的字节码中,从而实现了对横切关注点的有效管理和控制。

使用 AspectJ,开发者可以通过注解或者 XML 配置文件来定义切面,并使用 Pointcut 表达式来指定切入点(即横切关注点所在的位置)。然后,开发者可以编写 Advice(通知)来定义切面在连接点处执行的代码逻辑,例如在方法调用之前或之后执行的操作。

举个简单的例子,假设我们想统计所有view的点击事件,使用AspectJ只需要写一个类即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Aspect
public class MethodAspect {
private static final String TAG = "MethodAspect5";

//切面表达式,声明需要过滤的类和方法
@Pointcut("execution(* android.view.View.OnClickListener+.onClick(..))")
public void callMethod() {
}

//before表示在方法调用前织入
@before("callMethod()")
public void beforeMethodCall(ProceedingJoinPoint joinPoint) {
//编写业务代码
}
}

javassit

Javassist 是一个 Java 字节码操作库,它提供了一种方便的方式来动态修改类的字节码,生成新的类或者修改已有类的行为。Javassist 的全称是 Java Programming Assistant,它允许开发者在运行时动态地创建、修改和操作 Java 类,而无需直接操作字节码。

Javassist 提供了一组简洁而强大的 API,使得开发者可以在运行时对类进行各种操作,例如创建新的类、修改已有类的字段、方法或者构造函数、添加新的方法、修改方法的实现、以及执行类的加载和热替换等。通过 Javassist,开发者可以实现诸如 AOP、动态代理、类加载器、字节码增强等各种功能。

以下是 Javassist 的简单用例,演示了如何使用 Javassist 创建一个新的类,并添加一个新的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import javassist.*;

public class JavassistExample {
public static void main(String[] args) throws Exception {
// 创建类池
ClassPool classPool = ClassPool.getDefault();

// 创建一个新的类
CtClass newClass = classPool.makeClass("ExampleClass");

// 添加一个新的方法
CtMethod newMethod = CtNewMethod.make("public void hello() { System.out.println(\"Hello, Javassist!\"); }", newClass);
newClass.addMethod(newMethod);

// 将新类加载到内存中
Class<?> clazz = newClass.toClass();

// 实例化新类并调用方法
Object obj = clazz.newInstance();
clazz.getMethod("hello").invoke(obj);
}
}

ASM

ASM 是一个 Java 字节码操作和分析框架,它允许开发者在编译时或运行时动态生成或修改 Java 类的字节码。ASM 的全称是 Java Bytecode Engineering Library,它提供了一组简单而强大的 API,使得开发者可以直接操作字节码,从而实现对 Java 类的各种高级操作。

ASM 具有轻量级、高性能和灵活性的特点,它的设计目标是尽可能地接近 Java 字节码的底层结构,以最小的开销实现对字节码的操作。ASM 提供了一种基于访问者模式的 API,开发者可以通过实现不同的访问者接口来遍历、解析和修改类的字节码,从而实现对类的各种操作,如新增、删除、修改字段、方法、注解等。

ASM 可以广泛应用于 Java 字节码的动态生成、增强和优化等领域,例如动态代理、AOP(面向切面编程)、代码生成器、类加载器、代码混淆、性能优化等。由于其高性能和灵活性,ASM 被广泛应用于各种 Java 框架和工具中,如 Spring、Hibernate、Mockito 等。

完整的学习ASM必须了解字节码和JVM相关知识。比如要织入一句简单的日志输出:

1
Log.d("tag", " onCreate");

使用ASM编写是下面这个样子:

1
2
3
4
mv.visitLdcInsn("tag");
mv.visitLdcInsn("onCreate");
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);

可以看出ASM与AspectJ有很大的不同,AspectJ织入的代码就是实际编写的代码,但ASM必须使用其提供的API编写指令。一行java代码可能对应多行ASM API代码,因为一行java代码背后可能隐藏这多个JVM指令。

动态代理

动态代理是代理模式的一种实现,用于在运行时动态增强原始类的行为,实现方式是运行时直接生成class字节码并将其加载进虚拟机。

JDK本身就提供一个Proxy类用于实现动态代理。 我们通常使用下面的API创建代理类。

1
2
3
4
//java.lang.reflect.Proxy
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)

其中在InvocationHandler实现类中定义核心切点代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class InvocationHandlerImpl implements InvocationHandler {

/** 被代理的实例 */
private Object mObj = null;

public InvocationHandlerImpl(Object obj){
this.mObj = obj;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//前切入点
Object result = method.invoke(this.mObj, args);
//后切入点
return result;
}
}

java动态代理最大的问题是只能代理接口,而不能代理普通类或者抽象类,这是因为默认创建的代理类继承Porxy,而java又不支持多继承,这一点极大的限制了动态代理的使用场景,cglib可代理普通类。

AOP框架的特点及优劣势

alt text