Spring AOP属于第二代AOP,通过动态代理将切面逻辑(advise)织入到目标类方法中, AOP的拦截功能是由java中的动态代理来实现的。说白了,就是在目标类的基础上增加切面逻辑,生成增强的目标类(该切面逻辑或者在目标类函数执行之前,或者目标类函数执行之后,或者在目标类函数抛出异常时候执行。不同的切入时机对应不同的Interceptor的种类,如BeforeAdviseInterceptor,AfterAdviseInterceptor以及ThrowsAdviseInterceptor等). 为了深入理解AOP, 有必要对动态代理学习一下.

Java动态代理是利用反射机制生成一个实现代理接口匿名类, 在调用具体方法前用InvokeHandler处理. CGLib动态代理是利用ASM开源包, 对代理对象的class文件修改生成子类来处理.

  • 如果目标对象实现了接口, 默认情况下使用JDK的动态代理实现AOP.
  • 如果目标对象实现了接口, 可以强制使用CGLib实现AOP.
  • 如果目标对象没有实现接口, 必须使用CGLib库.

啥事动态/静态代理呢? 这个需要写个demo来对比理解. 为啥JDK动态代理必须要实现接口呢? 这个问题又引出了reflect包中源码的阅读.

静态代理

代理模式包括 代理接口Subject, 代理类ProxySubject, 委托类RealSubject. 为什么需要接口呢? 接口保证了代理类和委托类具有相同的规范.

public interface Subject {
    public void doTask();
}

委托类

public class RealSubject implements Subject{
    @Override
    public void doTask() {
        System.out.println("do something...");
    }
}

代理类

public class ProxySubject implements Subject{
    private Subject delegate;

    public ProxySubject(Subject delegate) {
        this.delegate = delegate;
    }

    @Override
    public void doTask() {
        delegate.doTask();
    }
}

缺点:

  1. 代理对象的一个接口只服务于一种类型的对象, 如果要代理的方法很多, 就无法胜任了.
  2. 如果接口增加一个方法, 除了所有实现类要实现这个方法, 所有的代理类也要实现.

动态代理

动态代理类的源码是在程序运行期间由JVM根据反射机制动态生成的, 不存在代理类的字节码文件, 代理类和委托类的关系是在程序运行时确定的.这里讲的很透彻.

Proxy类的静态方法:

// 方法 1: 该方法用于获取指定代理对象所关联的调用处理器  
static InvocationHandler getInvocationHandler(Object proxy)   
  
// 方法 2:该方法用于获取关联于指定类装载器和一组接口的动态代理类的类对象  
static Class getProxyClass(ClassLoader loader, Class[] interfaces)   
  
// 方法 3:该方法用于判断指定类对象是否是一个动态代理类  
static boolean isProxyClass(Class cl)   
  
// 方法 4:该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例  
static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)   

InvocationHandler的核心方法

Object invoke(Object proxy, Method method, Object[] args) 

ClassLoader

将类的字节码装载到JVM中并为其定义类对象, 然后使用该类. 运行时动态生成代理类.

动态代理实现步骤(这四个步骤是动态代理的精髓):

  1. 实现InvocationHandler接口创建自己的调用处理器
  2. 给Proxy类提供ClassLoader和代理接口类型数组创建动态代理类
  3. 以调用处理器类型为参数,利用反射机制得到动态代理类的构造函数
  4. 以调用处理器对象为参数,利用动态代理类的构造函数创建动态代理类对象 ```java // InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发
    // 其内部通常包含指向委托类实例的引用,用于真正执行分派转发过来的方法调用
    InvocationHandler handler = new InvocationHandlerImpl(..);

// 通过 Proxy 为包括 Interface 接口在内的一组接口动态创建代理类的类对象
Class clazz = Proxy.getProxyClass(classLoader, new Class[] { Interface.class, … });

// 通过反射从生成的类对象获得构造函数对象
Constructor constructor = clazz.getConstructor(new Class[] { InvocationHandler.class });

// 通过构造函数对象创建动态代理类实例
Interface Proxy = (Interface)constructor.newInstance(new Object[] { handler });

Proxy的静态方法newProxyInstance对上面步骤的后三步做了封装.
```java
InvocationHandler handler = new InvocationHandlerImpl(..);   

// 通过 Proxy 直接创建动态代理类实例  
Interface proxy = (Interface)Proxy.newProxyInstance( classLoader,   
     new Class[] { Interface.class },  handler ); 

插句题外话, 看Proxy的源码时, 发现Objects.requireNonNull(h);方法. 有趣的是, Objects并不是Object, 而是JDK1.7引入的关于Object的工具类, 其中的方法均为静态方法.

  • This class consists of {@code static} utility methods for operating on objects. These utilities include {@code null}-safe or {@code null}-tolerant methods for computing the hash code of an object, returning a string for an object, and comparing two objects. 从代码实践上来看, 这样能简化判断, 使结构更清晰, 简洁.

再插一句题外话, 代理类中经常出现@CallerSensitive注解, 这个注解是为了堵住漏洞用的。曾经有黑客通过构造双重反射来提升权限,原理是当时反射只检查固定深度的调用者的类,看它有没有特权,例如固定看两层的调用者(getCallerClass(2))。如果我的类本来没足够权限群访问某些信息,那我就可以通过双重反射去达到目的:反射相关的类是有很高权限的,而在 我->反射1->反射2 这样的调用链上,反射2检查权限时看到的是反射1的类,这就被欺骗了,导致安全漏洞。使用CallerSensitive后,getCallerClass不再用固定深度去寻找actual caller(“我”),而是把所有跟反射相关的接口方法都标注上CallerSensitive,搜索时凡看到该注解都直接跳过,这样就有效解决了前面举例的问题

通过反编译被生成的$Proxy0.class文件发现: class类定义为:

public final class $Proxy0 extends Proxy implements Interface {
        public $Proxy0(InvocationHandler paramInvocationHandler) {
        super(paramInvocationHandler);
    }
}

由于java的单继承,动态生成的代理类已经继承了Proxy类的,就不能再继承其他的类,所以只能靠实现被代理类的接口的形式,故JDK的动态代理必须有接口。

另外,为何调用代理类的方法就会自动进入InvocationHandler 的 invoke()方法呢?

其实是因为在动态代理类的定义中,构造函数是含参的构造,参数就是我们invocationHandler 实例,而每一个被代理接口的方法都会在代理类中生成一个对应的实现方法,并在实现方法中最终调用invocationHandler 的invoke方法,这就解释了为何执行代理类的方法会自动进入到我们自定义的invocationHandler的invoke方法中,然后在我们的invoke方法中再利用jdk反射的方式去调用真正的被代理类的业务方法,而且还可以在方法的前后去加一些我们自定义的逻辑。比如切面编程AOP等。

CGLib动态代理


public class CGLibProxy implements MethodInterceptor {
    public Object getProxy(Object o) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(o.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("before");
        Object res = methodProxy.invokeSuper(o,objects);
        System.out.println("after");
        return res;
    }

    @Test
    public void test(){
        CGLibProxy proxy = new CGLibProxy();
        SleepDaoImpl s = new SleepDaoImpl();
        SleepDaoImpl sl = (SleepDaoImpl)proxy.getProxy(s);
        sl.sleep();
    }
}

速度比较

速度比较 //测试结果 //创建代理的速度 Create JDK Proxy: 13 ms   Create CGLIB Proxy: 217 ms  //较慢 Create JAVAASSIST Proxy: 99 ms   Create JAVAASSIST Bytecode Proxy: 168 ms   //较慢 Create ASM Proxy: 3 ms  //最快

================   Run JDK Proxy: 2224 ms, 634,022 t/s  //很慢,jdk生成的字节码考虑了太多的条件,所以字节码非常多,大多都是用不到的 Run CGLIB Proxy: 1123 ms, 1,255,623 t/s  //较慢,也是因为字节码稍微多了点 Run JAVAASSIST Proxy: 3212 ms, 438,999 t/s   //非常慢,完全不建议使用javassist的代理类来实现动态代理 Run JAVAASSIST Bytecode Proxy: 206 ms, 6,844,977 t/s  //和asm差不多的执行速度,因为他们都是只生成简单纯粹的执行字节码,非常少(所以直接用javassist生成代理类的方式最值得推荐[从速度上讲]) Run ASM Bytecode Proxy: 209 ms, 6,746,724 t/s   //asm什么都是最快的,毕竟是只和最底层打交道