JDK 与 Cglib 的使用和对比

Spring AOP 依靠 JDK 和 CGLib 进行动态代理实现。在此对两种实现方式的一些知识进行整理。

JDK

使用示例

/**
 * 需要被代理的接口
 */
interface Iinterface {

    String proxyMethod(String gift);
}


/**
 * 实现 InvocationHandler 接口,对 invoke 方法进行重写
 */
class MyHandler implements  {

    /**
     * @param proxy 生成的代理实例
     * @param method 被代理类的方法
     * @param args  传入的参数列表
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        // 对应被代理的方法
        if (method.getName().equals("proxyMethod")) {
            System.out.println("接受到参数:" + args[0]);
            return "返回值";
        }
        return null;
    }
}

public class Main {
    public static void main(String[] args) {
        Iinterface proxy = (Iinterface) Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class[]{Iinterface.class},
                new MyHandler()
        );
        String res = proxy.proxyMethod("传入的参数");
        System.out.println(res);
    }
}

JDK 与 Cglib 的使用和对比

可见代理成功。

概括一下,动态代理的方式一般为:

  • 继承 InvocationHandler ,重写方法 invoke

  • 执行 Proxy.newProxyInstance 生成动态代理类

invoke 方法

由上可以看出,proxy 成功对 Iinterface 接口进行代理,但是在使用时,我们并未见到 InvocationHandler 中 invoke 方法的调用,动态代理是如何执行 invoke 的呢?

采用其他资料生成的案例,以下是代理类的反编译代码。

参考链接:链接1 链接2

首先来看看代理类的继承关系:

JDK 与 Cglib 的使用和对比

可以看到代理类继承了 Proxy ,再来看看代理类中的方法调用,其中 teach() 是被代理接口的方法声明,内部知识简单地调用了父类即 Proxy 的 h 属性的 invoke 方法

JDK 与 Cglib 的使用和对比

再回看 Proxy 类的属性

JDK 与 Cglib 的使用和对比

可以猜测,对被代理方法进行调用时,会转而由被代理类中继承自 Proxy 的 InvocationHandler 执行,从而 invoke方法被调用。再来看看调用的 newProxyInstance 的逻辑,代码进行了省略,添加了简单注释

public static Object newProxyInstance(ClassLoader loader,
                                          Class<!--?-->[] interfaces,
                                          InvocationHandler h){
    	// 创建代理类$Proxy0,实现了传入的 intfs 接口,并继承了 Proxy
        Class<!--?--> cl = getProxyClass0(loader, intfs);
		//...
    	// 获取构造方法
        final Constructor<!--?--> cons = cl.getConstructor(constructorParams);
        //...
    	//调用具有 InvocationHandler 的构造方法,创建代理类并返回
        return cons.newInstance(new Object[]{h});
        // ...
    }

目前还有一个问题没搞明白:调用 invoke 时,参数中的 method 能够争取匹配到调用的方法,该匹配是如何实现的?

JDK 与 Cglib 的使用和对比

CGLib

使用示例

class Hello {

    public String hello(String arg) {
        System.out.println("获取到参数: " + arg);
        return "返回参数";
    }

    /**
     * 通过 final 修饰,该方法不能被子类覆盖,因此 CGLib 无法代理
     */
    final public String helloFinal() {
        System.out.println("helloFinal");
        return null;
    }
}

class MyInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("代理执行前");
        String res = (String) methodProxy.invokeSuper(o, objects);
        System.out.println(res);
        System.out.println("代理执行后");
        return null;
    }
}

public class Main {
    public static void main(String[] args) {
        // 通过CGLIB动态代理获取代理对象的过程
        Enhancer enhancer = new Enhancer();
        // 设置enhancer对象的父类
        enhancer.setSuperclass(Hello.class);
        // 设置enhancer的回调对象
        enhancer.setCallback(new MyInterceptor());
        // 创建代理对象
        Hello proxy= (Hello)enhancer.create();
        // 通过代理对象调用目标方法
        proxy.hello("参数");
    }
}

JDK 与 Cglib 的使用和对比

概括一下,CGLib 动态代理的方式一般为:

  • 构造 Enhancer
  • 设置代理对象(作为父类)
  • 设置代理策略
  • 创建代理对象

Callback

参考链接:https://blog.csdn.net/zhang6622056/article/details/87286498

Callback 可以理解成生成的代理类的方法被调用时会执行的逻辑,具有以下六种方式:

  • NoOp:不做任何操作

  • FixedValue:要求实现接口的 loadObjecd 方法,重写了被代理类的响应方法,同时,要求返回值和方法返回值相同,否则会抛出类型转换异常。此方式看不到人喝原方法的信息,也无法调用原方法。

  • MethodInterceptor:类似 AOP 的环绕增强,代理类的方法调用都会转入执行该接口的 intercept 方法。需要执行原方法可以使用参数 method 进行反射调用,或者使用参数 proxy(proxy会快一些)

  • InvocationHandler:类似 MethodInterceptor,若自定义该接口的 invoke 方法,需要注意参数 methodinvoke 方法,会无限循环调用

  • LazyLoader:调用时,返回一个代理对象并存储负责所有的该代理类调用,类似 Spring 的 singleton

  • Dispatcher:每次调用都会返回一个新的代理类,类似 Spring 的 prototye

JDK 与 Cglib 的对比

JDK Cglib
代理目标 接口 类(被代理类会作为父类,无法处理 final)
实现 反射机制 运行过程中修改被代理类的 class 文件字节码
Spring采用策略 目标对象实现接口时采用 目标对象未实现接口时采用
  • 默认为 JDK ,使用时可以进行指定
上一篇:这才是Springboot事务创建流程的正确打开方式(附源码分析)


下一篇:代理模式详解(JDK/Cglib实现)