动态代理种类及原理,你知道多少?

前言

提到动态代理,很多人都会对 JDK 动态代理、CGLib,或者 Proxy、InvocationHandler 等类感到熟悉,甚至有些人会直接提到 Spring AOP。的确动态代理的实现有时会给我们带来意想不到的优势,比如常见的业务解耦、无侵入式的代码扩展等。这篇文章就主要来探讨如下几种实现动态代理的常见方式及其原理:

  • JDK 动态代理
  • CGLib 动态代理
  • javassist 动态代理
  • javassist 字节码
  • ASM 字节码

静态代理

为了下文叙述的方便,先来回顾一下静态代理。生活中身边不乏做微商的朋友,其实就是我们常说的微商代理,目的就是在朋友圈之类的为厂家宣传产品,厂家委托微商为其引流或者销售商品。将这个场景进行抽象,我们可以把微商代理看成“代理类”,厂家看成“委托类”或者“被代理类”等。

那什么是静态代理呐?若代理类在程序运行前就已经存在,那么这种代理方式就是静态代理。因此在程序运行前,我们都会在程序中定义好代理类。同时,静态代理中的代理类和委托类都会实现同一接口或者派生自相同的父类。接下来,我们将会用一段代码进行演示,Factory 代表厂家,即委托类,BusinessAgent 代表微商,即代理类。代理类和委托类都实现 Operator 接口:

public interface Operator {
    // 宣传,商品销售
    void sale();
    // 引流,业务扩张
    void expand();
}

Factory 类定义如下:

public class Factory implements Operator {

    @Override
    public void sale() {
        System.out.println("sale .... ");
    }

    @Override
    public void expand() {
        System.out.println("expand .... ");
    }
}

BusinessAgent 类定义如下:

public class BusinessAgent implements Operator {

    private Factory factory;

    public BusinessAgent(Factory factory){
        this.factory = factory;
    }

    @Override
    public void sale() {
        factory.sale();
    }

    @Override
    public void expand() {
        factory.expand();
    }
}

从 BusinessAgent 类的类结构定义可以看得出来,静态代理主要是通过聚合的方式,来让代理类持有一个委托类的引用,同时我们可以想象,如果我们需要为委托类中的方法做统一处理,比如记录运行时间,那么我们是不是得在代理类中每个方法都单独去处理一遍?

动态代理

在前文,我们对什么是代理,什么是静态代理有了简单回顾。而动态代理跟静态代理的区别在于,代理类是在程序运行时创建,而动态代理的优势在于可以很方便的对代理类的方法进行统一处理。比如记录委托类中每个方法的运行时间。接下来,我们将逐个讲解动态代理的实现方式及其原理。

JDK 动态原理

实例演示

JDK 动态代理的实现主要是借助 InvocationHandler 接口、Proxy 类实现的。在使用时,我们得定义一个位于代理类与委托类之间的中介类,就像传统的微商代理,其实并不是直接跟厂家接触,他们之间可能还会存在一层中介。而这个中介类,需要实现 InvocationHandler 接口:

public interface InvocationHandler { 
  Object invoke(Object proxy, Method method, Object[] args);
}
  • proxy:表示程序运行期间生成的代理类对象,后面可以看见使用 Proxy.newProxyInstance()生成
  • method:表示代理对象被调用的方法
  • args:表示代理对象被调用的方法的参数

调用代理对象的每个方法实际最终都是调用 InvocationHandler 的 invoke 方法。后面我们将论证这个结论。

这里我们使用 AgencyHandler 表示中介类,中介类定义为:

public class AgencyHandler implements InvocationHandler {

    // 委托类对象
    private Object target;

    public AgencyHandler(){}

    public AgencyHandler(Object target){
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
        long startTime = System.currentTimeMillis();
        // 使用反射执行委托类对象具体方法
        Object result = method.invoke(target, args);
        System.out.println(method.getName() + " cost time is:" + (System.currentTimeMillis() - startTime));
        return result;
    }
}

通过 Proxy 的静态方法 newProxyInstance 生成代理对象:

public class Main {

    public static void main(String[] args) {
        AgencyHandler agencyHandler = new AgencyHandler(new Factory());
        // 创建代理对象
        Operator operator = (Operator) Proxy.newProxyInstance(Operator.class.getClassLoader(), 
                            new Class[]{Operator.class}, 
                            agencyHandler);
        operator.sale();
        operator.expand();
    }
}
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
  • loader:表示类加载器,将运行期动态生成的代理类加载到内存
  • interfaces:表示委托类的接口,生成代理类需要实现的接口
  • h:InvocationHandler 实现类对象,负责连接代理类和委托类的中介类

正如预期运行结果为:

sale .... 
sale cost time is:1s
expand .... 
expand cost time is:0s

这里我们将委托类对象 new Factory() 作为 AgencyHandler 构造方法入参创建了 agencyHandler 对象,然后通过 Proxy.newProxyInstance(…) 方法创建了一个代理对象,实际代理类就是这个时候动态生成的。我们调用该代理对象的方法就会调用到 agencyHandler 的 invoke 方法(类似于静态代理),而 invoke 方法实现中调用委托类对象 new Factory() 相应的 method(类似于静态代理)。因此,动态代理内部可以看成是由两组静态代理构成

代理类源码分析

其实上面一段话已经对动态代理的原理讲得很清楚了,下面我们从源码的角度来梳理一下。既然 JDK 动态代理的代理对象是运行期生成的,那么它在运行期也会对应一段字节码,可以使用 ProxyGenerator.generateProxyClass 方法进行获取。为了让大家一步到位,这里贴一下这个工具类:

public class ProxyUtils {
    public static boolean saveProxyClass(String path, String proxyClassName, Class[] interfaces) {
        if (proxyClassName == null || path == null) {
            return false;
        }
        byte[] classFile = ProxyGenerator.generateProxyClass(proxyClassName, interfaces);
        FileOutputStream out = null;
        try {
            out = new FileOutputStream(path);
            out.write(classFile);
            out.flush();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }
}

将得到的字节码文件进行反编译就能看到其中的源代码了:

import com.limynl.proxy.Operator;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements Operator {
  // 这 5 个方法分别是 equals、expand、toString、sale、hashCode
  private static Method m1;
  private static Method m3;
  private static Method m2;
  private static Method m4;
  private static Method m0;

  static {
    try {
      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
      m3 = Class.forName("com.limynl.proxy.Operator").getMethod("expand", new Class[0]);
      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
      m4 = Class.forName("com.limynl.proxy.Operator").getMethod("sale", new Class[0]);
      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      return;
    } catch (NoSuchMethodException noSuchMethodException) {
      throw new NoSuchMethodError(noSuchMethodException.getMessage());
    } catch (ClassNotFoundException classNotFoundException) {
      throw new NoClassDefFoundError(classNotFoundException.getMessage());
    } 
  }
  // 构造方法接收一个 InvocationHandler 对象为参数
  public $Proxy0(InvocationHandler paramInvocationHandler) {
    // 传至父类中的 InvocationHandler 类型变量 h
    super(paramInvocationHandler);
  }

  public final boolean equals(Object paramObject) {
    try {
      // this.h.invoke 将会调用实现了 InvocationHandler 接口的类,上面我们传入的是 agencyHandler 对象,
      // 因此会调用 AgencyHandler 的 invoke 方法
      // 同时这里也印证了,invoke 的方法的第一个参数就是代理对象本身。下面其余方法类似
      return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }

  public final void expand() {
    try {
      this.h.invoke(this, m3, null);
      return;
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }

  public final String toString() {
    try {
      return (String)this.h.invoke(this, m2, null);
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }

  public final void sale() {
    try {
      this.h.invoke(this, m4, null);
      return;
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }

  public final int hashCode() {
    try {
      return ((Integer)this.h.invoke(this, m0, null)).intValue();
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }
}
  • 从中我们可以看出动态生成的代理类是以 $Proxy 为类名前缀,继承自 Proxy,并且实现了 Proxy.newProxyInstance(…) 第二个参数传入的所有接口。
  • 代理类的构造方法传入的是 InvocationHandler 对象,即 Proxy.newProxyInstance(…) 第三个参数,同时 sale()、expand() 都交给 h 去处理,最终会传递到 agencyHandler 对象的 invoke 方法里面,该方法里面继续使用反射的方式找到最终需要调用的委托类的方法。从而也论证了开头说的:调用代理对象的每个方法实际最终都是调用 InvocationHandler 的 invoke 方法。
  • 所以 InvocationHandler 的子类 AgencyHandler 连接代理类和委托类的中介类。

到这里我们已经把 JDK 动态代理的原理讲完了,所以大家可以在脑海中回忆一下:JDK 动态代理内部可以看成是由两组静态代理构成,是不是这个意思?

通过这个代理类也将明白(这里需要拿笔圈起来^_^):

  • 为什么在 Proxy.newProxyInstance 过程需要接口:因为生成的代理类需要实现这个接口
  • 为什么 JDK 动态代理只能代理接口:因为 java 是单继承,代理类已经继承了 Proxy,因此没办法在继承另外一个类
  • JDK 动态代理中除使用了反射外,也操作了字节码

CGLib 动态代理

JDK 动态代理的类必须实现一个接口,而且生成的代理类是其接口的实现类,对于不使用接口的类,无法使用 JDK 动态代理。此时就可以使用另外的替代方案,例如 CGLib。

首先 CGLib 是一个强大、高性能代码生成包,底层采用字节码处理框架 ASM。它能够为没有实现接口的类提供代理。在强大的 Hibernate、Spring 等框架中都能够看见它的影子。其原理是:动态生成一个被代理类的子类,子类重写被代理类的所有非 final 方法。

实例演示

首先使用 CGLib,需要添加 CGLib 依赖:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.5</version>
</dependency>

这里我们还是使用上面的厂家委托类,同样使用 CGLib 也需要在代理类和委托类中有一个中介类。这个中介类就是 MethodInterceptor 接口:

public interface MethodInterceptor extends Callback{
    public Object intercept(Object obj, 
                            java.lang.reflect.Method method, 
                            Object[] args,
                            MethodProxy proxy) throws Throwable;
}
  • obj:动态生成的代理类对象
  • method:被代理对象的方法
  • args:需要被执行的方法参数
  • proxy:生成的代理类的方法

等会我们剖析代理类的源码时,对这几个参数便会有清晰的认识。

接着我们创建中介类 AgencyInterceptor:

public class AgencyInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        long startTime = System.currentTimeMillis();
        // 执行父类中的具体方法,即执行委托类中对应方法
        Object result = methodProxy.invokeSuper(o, objects);
        System.out.println(method.getName() + " cost time is:" + (System.currentTimeMillis() - startTime) + "s");
        return result;
    }
}

通过 Enhancer.create 生成代理对象:

public class Main {

    public static void main(String[] args) {
        // 使用 enhancer 创建动态代理对象
        Enhancer enhancer = new Enhancer();
        // 指定需要代理的委托类
        enhancer.setSuperclass(Factory.class);
        // 设置回调,对于代理类上所有方法的调用,都会执行 AgencyInterceptor 中的 intercept 对其拦截
        enhancer.setCallback(new AgencyInterceptor());
        // 获得创建的代理对象
        Factory factoryProxy = (Factory) enhancer.create();
        // 使用代理对象进行代理访问
        factoryProxy.sale();
        factoryProxy.expand();
    }
}

运行结果为:

sale .... 
sale cost time is:2s
expand .... 
expand cost time is:1s

从整体结构上来说,其实跟 JDK 动态代理的实现方式还是比较相似。下面将从源码的方式剖析其中的调用过程。

代理类源码解析

这里我们将得到的代理类 class 文件进行反编译,由于代码过长,我们将抽取主要部分进行梳理,并且这里我们以代理 sale() 为例进行讲解,其他方法执行原理一样。

public class Factory$$EnhancerByCGLIB$$f5927596 extends Factory implements Factory {
  private boolean CGLIB$BOUND;
  private static final Callback[] CGLIB$STATIC_CALLBACKS;

  // 构造 Enhancer 时,传入的拦截器
  private MethodInterceptor CGLIB$CALLBACK_0;
  // 被代理的方法
  private static final Method CGLIB$sale$1$Method;
  // 代理方法
  private static final MethodProxy CGLIB$sale$1$Proxy;

  static void CGLIB$STATICHOOK1() {
    ......
    // 代理类
    Class clazz1 = Class.forName("com.limynl.Factory$$EnhancerByCGLIB$$f5927596");
    // 被代理类
    Class clazz2;
    CGLIB$sale$1$Method = ReflectUtils.findMethods(new String[] { "expand", "()V", "sale", "()V" }, (clazz2 = Class.forName("com.limynl.Factory")).getDeclaredMethods())[1];
    CGLIB$sale$1$Proxy = MethodProxy.create(clazz2, clazz1, "()V", "sale", "CGLIB$sale$1");
    ......
  }
}

从反编译 class 文件可以看见,代理类会继承委托类(注意和接口 Factory 区别,不要混淆),重写父类中的方法。

// 方法一
public final void sale() {
    MethodInterceptor tmp4_1 = this.CGLIB$CALLBACK_0;
    //首先执行 CGLIB$BIND_CALLBACKS
    if (tmp4_1 == null){        
      CGLIB$BIND_CALLBACKS(this);
      tmp4_1 = this.CGLIB$CALLBACK_0;
    }
    // 执行这里
    if (this.CGLIB$CALLBACK_0!= null){
        //调用拦截器,this 就是当前的代理类
        tmp4_1.intercept(this, CGLIB$sale$1$Method, CGLIB$emptyArgs,CGLIB$sale$1$Proxy);
    }
    else{
        super.sale();
    }
}

// 方法二
final void CGLIB$sale$1() {
    super.sale();
}
  • 当通过代理对象执行:factoryProxy.sale() 便会调用上面的方法一 sale(),然后会调用 intercept,从这里我们就能清晰看见该方法各个参数的含义
  • 调用 intercept 拦截器,执行里面的 methodProxy.invokeSuper(o, objects);
  • 执行完 methodProxy.invokeSuper(o, objects) 后,便会调用方法二 CGLIB$sale$1()
  • 调用 super.sale(),就是需要执行的委托类方法

接下里分析如何从 methodProxy.invokeSuper 方法到 CGLIB$sale$1()

在拦截器中,通过调用 MethodProxy 的 invokeSuper 方法来调用代理方法,还记得代理类中如下代码:

// MethodProxy CGLIB$sale$1$Proxy
// 参数从左到右依次为:
// 被代理对象,代理对象,入参类型,被代理方法名,代理方法名(注意一下代理方法名)
CGLIB$sale$1$Proxy = MethodProxy.create(clazz2, clazz1, "()V", "sale", "CGLIB$sale$1");

下面看下 methodProxy.invokeSuper 方法内部:

public Object invokeSuper(Object obj, Object[] args) throws Throwable {
    try {
        init();
        FastClassInfo fci = fastClassInfo;
        // fci.f2.invoke 方法参数依次为:代理方法索引、代理对象、方法参数
        return fci.f2.invoke(fci.i2, obj, args);
    } catch (InvocationTargetException e) {
        throw e.getTargetException();
    }
}

调用了 init() 方法,该方法主要是生成一个 FastClassInfo 对象,该对象中包含两个 FastClass,分别是代理对象和被代理对象的详细信息:

private void init(){
    // 首先执行这个分支,create 时并没有赋值
    if (fastClassInfo == null){
        // 获得对象锁,使用双重校验机制
        synchronized (initLock){
            if (fastClassInfo == null){
                // createInfo 包含了代理类与被代理类的信息
                CreateInfo ci = createInfo;
                // 创建新的 FastClassInfo 对象
                FastClassInfo fci = new FastClassInfo();
                // 获得被代理对象的 FastClass,如果缓存中有就从缓存中取出,没有就生成新的 fastclass
                fci.f1 = helper(ci, ci.c1);
                // 获得代理对象的 FastClass,如果缓存中有就从缓存中取出,没有就生成新的 fastclass
                fci.f2 = helper(ci, ci.c2);
                // 获得被代理对象中被代理方法的索引
                fci.i1 = fci.f1.getIndex(sig1);
                // //获得代理对象中代理方法的索引
                fci.i2 = fci.f2.getIndex(sig2);
                fastClassInfo = fci;
                createInfo = null;
            }
        }
    }
}

然后执行 FastClass 的 invoke 方法,从这里它就会找到代理对象中方法名为 CGLIB$sale$1 的代理方法。

fci.f2.invoke(fci.i2, obj, args);

这里我们准备不再深入了,简单说下 fci.f2.invoke 这个方法的原理。还记得生成的代理字节码文件吗,对于 CGLib 总共会生成 3 个字节码文件,其中有一个:

Factory$$EnhancerByCGLIB$$f5927596$$FastClassByCGLIB$$49bafad3.class

这个文件中主要跟 FastClass 有关,对应的是索引跟代理对象的方法之间的关系,因为每个代理方法,都有一个索引对应。因此 fci.f2.invoke 方法的第一个参数就是传递的方法索引,因此最终能够找到需要执行的代理方法,对于执行 factoryProxy.sale(),便会找到代理类中的 CGLIB$sale$1 方法,所以调用 methodProxy.invokeSuper 方法最终会到 CGLIB$sale$1() 处。对于流程还不太清晰的,可以在纸上画一画。

FastClass 机制原理:为代理类中的每个方法生成一个索引,当调用时直接通过索引调用对应方法,否则使用反射调用将会带来更多的性能损耗。

说到这里可以补充一个常见面试题:如果我们在拦截器 intercept 中直接调用 MethodProxy 的 invoke 方法将会直接出现栈溢出,因为程序出现了死循环。如果感兴趣,可以自己去分析一下原因。

tips:注意 MethodProxy.invoke(…) 第一个参数,代表的是被代理类方法的索引,因此为什么死循环就很明了了。

javassist 字节码

javassist 是一个开源的分析、编辑和创建 Java 字节码的类库。它使程序能够在运行时定义或修改类,并在 JVM 加载时修改类文件。为了方便使用,javassist 提供了两个基本的方式:API 类操作级别和字节码级别。如果使用 API,可以直接编辑类文件而不需要了解 Java 字节码的规范,跟平常写 Java 代码一样。同时还可以以源文本的形式直接操作字节码文件,javassist 将即时编译它。平时使用多是操作 javassist 类库提供的 API,并且多用作:

  • 动态创建类或接口的二进制字节码(如:动态代理生成代理类)
  • 动态扩展已有类或接口的二进制字节码(如:扩展框架中的某些类)

下面我们就通过两个小例子来了解一下 javassist。

动态创建类或接口

比如我们动态创建一个 User 类:

public class Person {
    private String name;
    public Person(String name){
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "name=" + this.name;
    }
}

首先引入依赖

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.21.0-GA</version>
</dependency>

创建代码如下:

public class Main {

    public static void main(String[] args) throws Exception {
        String className = "Person";

        ClassPool classPool = ClassPool.getDefault();
        // 定义一个名为 Person 的新类
        CtClass ctClass = classPool.makeClass(className);

        // 定义成员变量 name,类型为 String
        CtField ctFieldName = new CtField(classPool.get("java.lang.String"), "name", ctClass);
        // 设置成员变量 name 访问修饰符
        ctFieldName.setModifiers(Modifier.PRIVATE);
        // 添加为类 Person 的成员变量
        ctClass.addField(ctFieldName);

        //定义构造函数
        CtClass[] parameters = new CtClass[]{classPool.get("java.lang.String")};
        CtConstructor constructor = new CtConstructor(parameters, ctClass);
        //方法体 $0 表示 this,$1 表示方法的第一个参数
        String body = "{$0.name = $1;}";
        constructor.setBody(body);
        ctClass.addConstructor(constructor);

        // 定义 setName getName 方法
        ctClass.addMethod(CtNewMethod.setter("setName", ctFieldName));
        ctClass.addMethod(CtNewMethod.getter("getName", ctFieldName));

        // 定义 toString 方法
        CtClass returnType = classPool.get("java.lang.String");
        CtMethod toStringMethod = new CtMethod(returnType, "toString", null, ctClass);
        toStringMethod.setModifiers(Modifier.PUBLIC);
        toStringMethod.setBody("{return \"name=\"+$0.name;}");
        ctClass.addMethod(toStringMethod);

        // 生成 Class 对象
        Class<?> c = ctClass.toClass();
        Object person = c.getConstructor(String.class)
                .newInstance("Limynl");

        // 使用反射调用
        Method method = person.getClass().getMethod("toString", null);
        String result = (String) method.invoke(person, null);
        System.out.println(result);
    }
}

通过 javassist 提供的相关 API,我们就可以在程序运行时创建新的类

动态扩展已有类或接口

接下来我们看看如何动态扩展已有类,比如我们想修改已有类 Person 的 toString() 方法,记录该方法运行时间。我们的实现思路为:将原方法命名为 toString\\(1, 然后重新创建一个新方法名为 toSting,在新方法的中调用原方法 toString\\)1,利用这个技巧就对原方法进行了扩展。

public class Main {

    public static void main(String[] args) throws Exception {
        //需要修改的已有的类名和方法名
        String className = "com.limynl.Person";
        String methodName = "toString";

        //修改为原有类的方法名为 toString$1
        CtClass clazz = ClassPool.getDefault().get(className);
        CtMethod method = clazz.getDeclaredMethod(methodName);
        String newname = methodName + "$1";
        method.setName(newname);

        //使用原始方法名,定义一个新方法,在这个方法内部调用 loop$impl
        CtMethod newMethod = CtNewMethod.make("public void " + methodName + "(){" +
                        "long startTime=System.currentTimeMillis();" +
                        "" + newname + "();" +//调用 toString$1
                        "System.out.println(\"耗时:\"+(System.currentTimeMillis()-startTime));" +
                        "}"
                , clazz);
        clazz.addMethod(newMethod);

        //调用修改后的 Person 类的 toString 方法
        Person person = (Person) clazz.toClass().newInstance();
        System.out.println(person.toString());
    }
}

这里我们就把 javassist 操作字节码常见 API 了解了一下,下面我们就看看如何使用 javassist 实现动态代理。

javassist 动态代理

这里我们回顾一下 JDK 动态代理的实现:

  • 动态生成一个代理类
  • 继承 Proxy,提供 InvocationHandler h 实现代理逻辑
  • 实现接口方法,调用 InvocationHandler 的 invoke 方法
  • 在 InvocationHandler 中 invoke 方法使用反射调用被代理类的方法

因此这里最关键的就是生成一个代理类,因此就是 JDK 动态代理中这一步的实现:

Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

有了上面的 javassist 的基本了解,因此我们需要手动实现一个 newProxyInstance 方法来动态生成一个代理类 BusinessProxy,替换 JDK 动态代理中生成代理类的方式即可实现我们的需求。

public class ProxyGenerator {

    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws Throwable {
        ClassPool pool = ClassPool.getDefault();

        // ①创建代理类:public class BusinessProxy
        CtClass proxyCc = pool.makeClass("BusinessProxy");

        // ②给代理类添加字段:private InvocationHandler h;
        CtClass handlerCc = pool.get(InvocationHandler.class.getName());
        CtField handlerField = new CtField(handlerCc, "h", proxyCc);
        handlerField.setModifiers(AccessFlag.PRIVATE);
        proxyCc.addField(handlerField);

        // ③生成构造函数:public BusinessProxy(InvocationHandler h) { this.h = h; }
        CtConstructor ctConstructor = new CtConstructor(new CtClass[]{handlerCc}, proxyCc);
        // $0 代表 this, $1 代表构造函数的第 1 个参数
        ctConstructor.setBody("$0.h = $1;");
        proxyCc.addConstructor(ctConstructor);

        // ④依次为代理类实现相关接口
        for (Class<?> interfaceClass : interfaces) {
            // 为代理类添加相应接口方法及实现
            CtClass interfaceCc = pool.get(interfaceClass.getName());
            // 为代理类添加接口:public class BusinessProxy implements Operator
            proxyCc.addInterface(interfaceCc);
            // 为代理类添加相应方法及实现
            CtMethod[] ctMethods = interfaceCc.getDeclaredMethods();
            for (int i = 0; i < ctMethods.length; i++) {
                // 新的方法名,即需要被代理的方法
                String methodFieldName = "m" + i;
                // 为代理类添加反射方法字段
                // 如:private static Method m1 = Class.forName("com.limynl.Operator").getDeclaredMethod("sale", new Class[0]);
                // 构造反射字段声明及赋值语句
                // 方法的多个参数类型以英文逗号分隔
                String classParamsStr = "new Class[0]";
                // getParameterTypes 获取方法参数类型列表
                if (ctMethods[i].getParameterTypes().length > 0) {
                    for (CtClass clazz : ctMethods[i].getParameterTypes()) {
                        classParamsStr = (("new Class[0]".equals(classParamsStr)) ? clazz.getName() : classParamsStr + "," + clazz.getName()) + ".class";
                    }
                    classParamsStr = "new Class[] {" + classParamsStr + "}";
                }
                String methodFieldTpl = "private static java.lang.reflect.Method %s=Class.forName(\"%s\").getDeclaredMethod(\"%s\", %s);";
                String methodFieldBody = String.format(methodFieldTpl, "m" + i, interfaceClass.getName(), ctMethods[i].getName(), classParamsStr);
                // 为代理类添加反射方法字段. CtField.make(String sourceCodeText, CtClass addToThisClass)
                CtField methodField = CtField.make(methodFieldBody, proxyCc);
                proxyCc.addField(methodField);

                // 为方法添加方法体
                // 构造方法体. this.h.invoke(this, 反射字段名, 方法参数列表);
                String methodBody = "$0.h.invoke($0, " + methodFieldName + ", $args)";
                // 如果方法有返回类型,则需要转换为相应类型后返回,因为 invoke 方法的返回类型为 Object
                if (CtPrimitiveType.voidType != ctMethods[i].getReturnType()) {
                    // 对 8 个基本类型进行转型
                    // 例如:((Integer)this.h.invoke(this, this.m2, new Object[] { paramString, new Boolean(paramBoolean), paramObject })).intValue();
                    if (ctMethods[i].getReturnType() instanceof CtPrimitiveType) {
                        CtPrimitiveType ctPrimitiveType = (CtPrimitiveType) ctMethods[i].getReturnType();
                        methodBody = "return ((" + ctPrimitiveType.getWrapperName() + ") " + methodBody + ")." + ctPrimitiveType.getGetMethodName() + "()";
                    } else {
                        // 对于非基本类型直接转型即可
                        methodBody = "return (" + ctMethods[i].getReturnType().getName() + ") " + methodBody;
                    }
                }
                methodBody += ";";
                // 为代理类添加方法. CtMethod(CtClass returnType, String methodName, CtClass[] parameterTypes, CtClass addToThisClass)
                CtMethod newMethod = new CtMethod(ctMethods[i].getReturnType(), ctMethods[i].getName(),
                        ctMethods[i].getParameterTypes(), proxyCc);
                newMethod.setBody(methodBody);
                proxyCc.addMethod(newMethod);
            }
        }
        // 将代理类字节码文件写到指定目录,方便我们查看源码
        proxyCc.writeFile("D:/");

        // ⑤生成代理实例. 将入参 InvocationHandler h 设置到代理类的 InvocationHandler h 变量
        return proxyCc.toClass().getConstructor(InvocationHandler.class).newInstance(h);
    }
}

然后我们像 JDK 动态代理那样来使用:

public class Main {
    public static void main(String[] args) throws Throwable {
        AgencyHandler agencyHandler = new AgencyHandler(new Factory());
        Operator operator = (Operator) ProxyGenerator
                .newProxyInstance(Operator.class.getClassLoader(), 
                new Class[]{Operator.class}, agencyHandler);
        operator.sale();
    }
}

看到这里是不是跟 JDK 动态代理神似,因为我们思路就是仿照 JDK 的,只不过代理类的生成我们是借助 javassist 实现的。

ASM 字节码

这一节我们不会十分详细的介绍 ASM 原理以及字节码相关知识,就粗略的谈谈对 ASM 的认识,等真正有这方面需求时想起有这么个工具,再去深入了解,想必帮助会更大。

简介

前面介绍 CGLib 时提到过,它底层是采用 ASM 作为字节码处理,生成的代理类就是使用 ASM 实现的。因此 ASM 库是一个基于 Java 字节码层面的代码分析和修改工具,可以直接生产二进制的 class 文件,也可以在类被加载入 JVM 之前动态修改类行为。因此要想实际操作 ASM,对 class 文件格式的十分熟悉。

ASM 中的每个 API 都和 class 文件格式中的特定部分相吻合,同时是采用访问者模式设计的。

ASM 中比较重要的类有:

  • ClassReader:它将字节数组或者 class 文件读入到内存当中,并以树的数据结构表示,树中的一个节点代表着 class 文件中的某个区域。
  • ClassVisitor:ClassReader 对象创建之后,调用 ClassReader#accept() 方法,传入一个 ClassVisitor 对象。在 ClassReader 中遍历树结构的不同节点时会调用 ClassVisitor 对象中不同的 visit()方法,从而实现对字节码的修改。
  • ClassWriter:ClassWriter 是 ClassVisitor 的实现类,它是生成字节码的工具类,它一般是责任链中的最后一个节点,其之前的每一个 ClassVisitor 都是致力于对原始字节码做修改。

动态创建类

比如我们将动态创建的类如下:

public class Person{
    public String name;
}

创建过程如下:

public class Main extends ClassLoader implements Opcodes {

    public static void main(String[] args) throws Exception{
        // 创建一个 ClassWriter, 以生成一个新的类
        ClassWriter cw = new ClassWriter(0);
        // V1_6 是生成的 class 的版本号  ACC_PUBLIC 是类访问修饰符
        cw.visit(V1_6, ACC_PUBLIC, "com/limynl/proxy/asm/Person", null, "java/lang/Object", null);

        // 生成构造方法,因此从这里可以看出,如果类中没有构造方法,系统会给我们一个默认的构造方法
        MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null,
                null);
        mw.visitVarInsn(ALOAD, 0);
        mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
        mw.visitInsn(RETURN);
        mw.visitMaxs(1, 1);
        mw.visitEnd();

        // 添加字段,public 访问类型
        FieldVisitor fv = cw.visitField(ACC_PUBLIC, "name", "Ljava/lang/String;", null, null);
        fv.visitEnd();

        // 转换成 Class 对象
        byte[] code = cw.toByteArray();
        Main loader = new Main();
        Class<?> clazz = loader.defineClass(null, code, 0, code.length);

        // 通过默认构造函数创建对象
        Object beanObj = clazz.getConstructor().newInstance();
        // 为成员变量 name 赋值 Limynl
        clazz.getField("name").set(beanObj, "Limynl");
        String nameString = (String) clazz.getField("name").get(beanObj);
        System.out.println("filed value : " + nameString);
    }
}

从这个小例子我们能够看出,使用 ASM 生成类,还是比较复杂。单从这个例子其实还看不出来。

Tips:这里有个小技巧,如果想学习练习 ASM 这些 API,推荐使用 ASMifier,可以帮助我们生成这些晦涩难懂的 ASM 代码。

总结

这里我们主要讨论了:

  • JDK 动态代理
  • CGLib 动态代理(实质使用 ASM)
  • javassist 动态代理
  • javassist 字节码和 ASM 字节码

总的来说 Java 动态代理实现的原理:在编译期或运行期间操作修改 Java 的字节码。

从实现上来说主要分为两种:

  • 操作字节码,创建新类或者修改已有类,比如 JDK 动态代理
  • 使用 Java 编码方式创建新类或者修改已有类,比如 javassist(也提供直接字节码层面操作)

关于动态代理的性能:在 CGLib 和 JDK 代理对象调用时,使用的是反射,而在 javassist 生成的代理对象调用,是直接调用的。因此使用 CGLib 和 JDK 代理时可能会由于反射性能较慢。但是如果大家感兴趣的话,可以去测试一下,其实 CGLib 跟 javassist 性能持平,而对于 JDK 动态代理在低版本中性能很差,但在 1.8 及以上,已经有了非常大的提升。