动态代理
核心方法为 Proxy.newProxyInstance 方法,该方法需要传入 被代理类的类加载器、被代理类实现的接口 以及一个 InvocationHandler 的实现类实例。
方法会通过字节码技术为我们返回一个代理对象,代理对象会实现被代理对象的所有接口,并在这些接口声明方法前后织入 InvocationHandler 中的代理逻辑。
我们首先定义一个 InvocationHandler 实现类,写入代理逻辑:
public class ProxyHandler implements InvocationHandler { private Object object; ProxyHandler(Object object) { this.object = object; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("before invoke"); method.invoke(object, args); System.out.println("after invoke"); return null; } }
然后使用 Proxy.newProxyInstance 方法获得代理类对象:
public static void main(String[] args) { Dog dog = new Dog(); InvocationHandler handler = new ProxyHandler(dog); Animal dogProxy = (Animal) Proxy.newProxyInstance(Dog.class.getClassLoader(), Dog.class.getInterfaces(), handler); dogProxy.sayHellow(); System.out.println(dogProxy.getClass().getName()); System.out.println(dogProxy.getClass().toString()); }
执行结果,代理对象中织入了代理逻辑:
动态代理的原理
- 通过实现 InvocationHandler 接口创建自己的调用处理器;
- 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;
- 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
- 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
public static void main(String[] args) { System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); Dog dog = new Dog(); InvocationHandler handler = new ProxyHandler(dog); Animal dogProxy = (Animal) Proxy.newProxyInstance(Dog.class.getClassLoader(), Dog.class.getInterfaces(), handler); dogProxy.sayHellow(); }
然后运行,便可以看到新生成的代理类的字节码文件:
我们反编译一下该文件,我将需要注意的点放在注释中:
// 继承了 Proxy 并实现了 Animal
public final class $Proxy0 extends Proxy implements Animal {
//生成字节码文件时,为其设置的 Object 中的方法 private static Method m1; private static Method m3; private static Method m2; private static Method m0; // 构造函数,需要传入一个 InvocationHandler 实现类实例,也就是我们实现的处理器实例 public $Proxy0(InvocationHandler var1) throws { super(var1); } public final boolean equals(Object var1) throws { try { return (Boolean)super.h.invoke(this, m1, new Object[]{var1}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException(var4); } }
//我们要代理的方法 public final void sayHellow() throws { try {
//调用了我们传人的 InvocationHandler 实现类实例的 invoke,所以我们的代理逻辑都是写在 invoke 中的 super.h.invoke(this, m3, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final String toString() throws { try { return (String)super.h.invoke(this, m2, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final int hashCode() throws { try { return (Integer)super.h.invoke(this, m0, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } //生成字节码文件时,为其绑定的 Object 中的方法 static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); m3 = Class.forName("proxy.Animal").getMethod("sayHellow"); m2 = Class.forName("java.lang.Object").getMethod("toString"); m0 = Class.forName("java.lang.Object").getMethod("hashCode"); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); } } }
可能部分童鞋会对 super.h.invoke(this, m3, (Object[])null); 这一句表示疑问,如何调用的被代理类的方法。
我们看生成的静态语句块中:
在代理类被加载时,m3 成员变量便已经被赋值为被代理类中的 sayHellow 方法的 Method 对象,所以 invoke 时只需传入 m3 即可。
我觉着动态代理技术更适合叫动态生成类的字节码技术,因为整个执行过程的核心在于按照上述逻辑生成一个继承 Proxy 并实现了传入接口列表中接口的新类的字节码文件。至于是否对某个对象进行代理使我们在事项 InvocationHandler 时写在 invoke 方法中的。我们完全可以在 invoke 方法中不执行被代理类的方法,固定的做一些其它的事情。比如 Dubbo 框架中生成代理对象,invoke 方法便是进行了一次远程调用,而不是执行传入代理类对象的对应方法。
如果对其它点还有疑问,其实跟着 Proxy.newProxyInstance 方法向下跟代码就可以了,非常清晰。
看到这里对一些经典的问题就可以有自己的体会了,比如 JDK 的动态代理与 CGLIB 的动态代理有什么区别:
1. 代理类需要保证能够在被代理类出现的地方出现,并且拥有被代理类中所有需要被代理的方法。那么我们有两种实现思路,一种是让被代理类实现代理类的所有接口;第二种是让代理类继承被代理类。JDK 动态代理用的是第一种方式,而 CGLIB 用的是第二种。
2. 从反编译代理类的字节码文件可以看出,JDK 的动态代理本质上是通过调用 InvocationHandler 的 invoke 方法间接的调用被代理类之中的方法。这一步在 invoke 中我们是通过反射调用完成的。反射因为规避了常规的权限检查等 JVM 行为,在执行时也并不会被 JVM 优化,因此性能开销会比较大。
而 CGLIB 是直接生成了一份拥有完整方法的代理类字节码,并没有间接的去调用代理类的方法。是将被代理类方法的字节码整合到了代理类中,其调用没有借助反射。所以仅从方法调用上来说,CGLIB 的效率是高于 JDK 动态代理的效率的。