AOP 面向切面编程,就是把程序执行过程中的一些 “面” (比如 某个函数执行前) 切开,在其中插入代码执行。
本文主要分析 AOP 的各种实现方式。
AOP之编译时插入 (静态编码 , AspectJ , 注解处理器)
在编译时插入一般来说就是在类编译成 class 文件时做一些手脚,让编译完成的 class 文件中包含我们想要插入的横切逻辑
静态编码
自己写一个代理类,把该切入的逻辑加进去,这种性能肯定最好,但是一般都有很多需要切入的地方要放相同的逻辑,把它们集中管理才是AOP的意义
class SayHello {
public void sayH() {
System.out.println("H");
}
}
class SayHelloProxy extends SayHello{
public void sayH() {
System.out.println("----before----");
super.SayH();
System.out.println("----after----");
}
public static void main(String[] args) {
SayHello sayHello = new SayHelloProxy();
sayHello.sayH();
}
}
AspectJ
AspectJ是一个面向切面的框架,它扩展了Java语言。
AspectJ 直接不使用 javac 编译器,在用它的时候需要用 Ajc 编译器编译 java 文件,它用起来的特殊之处在于,在同一个类中的方法互相调用也可以执行横切逻辑(运行时动态代理就做不到)代码如下:
public class SayHello {
public void SayH() {
System.out.println("H");
}
public void Saye() {
System.out.println("e");
SayH();
}
}
//这个类定义了切入点和切入逻辑
public aspect HelloAspect {
pointcut HelloPointCut() : execution(public void SayHello.SayH());
//在 SayHello.SayH() 之前要执行的逻辑
before() : HelloPointCut(){
System.out.println("----before----");
}
//在 SayHello.SayH() 之后要执行的逻辑
after() : HelloPointCut(){
System.out.println("----after----");
}
}
//调用函数
public class Main {
public static void main(String[] args) {
SayHello sayHello = new SayHello();
sayHello.SayH();
sayHello.Saye();
}
}
输出:
----before----
H
----after----
e
----before----
H
----after----
如果是动态代理输出将是这样的:
----before----
H
----after----
e
H
注解处理器
这个注解处理器则需要在 javac 编译时加额外参数,通过继承 AbstractProcessor 编写注解处理器,把注解读进来可以对这个类进行各种操作,当然也可以实现AOP,不过这个就属于造轮子了,实现起来比较复杂。
AOP之运行时插入 (JDK动态代理 , cglib , 类加载时改字节码)
JDK动态代理
动态代理是在运行时动态的生成一个代理类,在其中插入横切逻辑,在调用方法时不调用本来类的方法,转而调用代理类的方法。性能比编译时插入差一些,但是不需要特殊的编译器,实现起来是这样的:
//被代理的类
public class SayHelloImpl implements SayHello {
@Override
public void sayH() {
System.out.println("H");
}
@Override
public void saye() {
System.out.println("e");
SayH();
}
}
//切入逻辑
public class ProxyAbout implements InvocationHandler {
private SayHelloImpl o = new SayHelloImpl();
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("----");
method.invoke(o, args);
System.out.println("----");
return null;
}
}
public class Main {
public static void main(String[] args) {
//这个就是生成的代理类
SayHello sayHello = (SayHello) Proxy.newProxyInstance(SayHello.class.getClassLoader(),
new Class[]{SayHello.class}, new ProxyAbout());
//这里调用的方法就是代理类生成的方法
sayHello.saye();
sayHello.sayH();
}
}
代理的类是这样的 $Proxy0
public final class $Proxy0 extends Proxy implements SayHello {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m4;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
//省略部分代码
public final void sayH() throws {
try {
//这里的 h 就是 var1 这里调用的就是 ProxyAbout 的 invoke 方法
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final void saye() throws {
try {
super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
}
可以看到,当你调用 SayH() 时它的内部会调用 ProxyAbout 的 invoke() 方法,这个方法即会执行切入的逻辑和原本的方法,所有在调用 saye() 时是调用不到 sayH() 加入的横切逻辑的。
cglib
CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。
cglib 代理原理和上面的 jdk动态代理原理相似,也是运行时动态的生成一个代理类,然后去和代理类打交道,区别主要是当用 jdk动态代理时必须让被代理的类生成一个接口,而 cglib 不需要,因为 cglib 的代理类是继承了被代理类,而 jdk动态代理是和被代理类继承了同一个接口,这也导致 cglib 的代理类无法代理被 final static 修饰的方法。
还有一点是 cglib 有一个 FASTCLASS 机制,可以绕开反射调用,比jdk动态代理的反射调用性能要好一些。
类加载时改字节码
这也是不到万不得以肯定不用的方法了,让那些需要加入横切逻辑的类经过一个你自定义的类加载器加载到虚拟机中,在加载之前对 Class 文件进行修改,可以用 ASM ,Javassist 这样的工具。
Spring 对 AOP 的处理办法
spring 在一类实现了一个接口的情况下使用 jdk 动态代理,在没有实现接口时使用 cglib