聊聊JDK动态代理原理
什么是代理
为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。——来自百度百科
通俗地讲,就是我们生活中的中介
什么是动态代理
动态代理是在程序运行时通过反射机制操作字节码,从而动态的创建 字节码文件,进而创建代理对象。
不多说了,先上代码
- 创建接口:
public interface OutInfo {
void printInfo();
}
- 创建实现类:
public class Person implements OutInfo {
private String name;
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
@Override
public void printInfo() {
System.out.println(name + "..." + age);
}
}
- 编写动态代理demo
import java.lang.reflect.Proxy;
public class DynamicProxy {
public static void main(String[] args) {
Person person = new Person("张三", 23);
OutInfo oi = (OutInfo) Proxy.newProxyInstance(
Person.class.getClassLoader(),
Person.class.getInterfaces(),
/*
* 此代码基于JDK8编写,所以用到了Lambda表达式
* proxy指代理对象, method指当前方法, arg指方法传入的参数
*/
(proxy, method, arg) -> {
System.out.println("方法前处理");
/*
* 注意:invoke()的第一个参数只能写被代理对象的实例
* 不能写参数proxy,否则会栈溢出,原因后面讲解
*/
Object ret = method.invoke(person, arg);
System.out.println("方法后处理");
return ret;
}
);
oi.printInfo();
}
}
看运行结果:
从运行结果看,代理结果是没问题的。那么,代理的过程是什么样的呢?
代理过程分析
之前说过动态代理是通过操作字节码来动态的创建代理对象的,那我们先看一下它生成的字节码文件里面有些什么东西:
通过以下代码可以获取到动态代理生成的字节码文件,但是需要独立的JRE才可以,嗯!推荐用JDK8!!!
public void saveProxyClassFile(){
// 参数:(生成的字节吗对象的名字,被代理对象实现的接口数组)
byte[] bytes = ProxyGenerator.generateProxyClass("$Proxy", new Class[]{OutInfo.class});
try(
FileOutputStream fos =new FileOutputStream(new File("$Proxy.class"))
){
fos.write(bytes);
fos.flush();
}catch (Exception e) {
e.printStackTrace();
}
}
这是通过idea自带的反编译工具翻译出来的内容:
public final class $Proxy extends Proxy implements OutInfo {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
public $Proxy(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 printInfo() throws {
try {
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);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("ann.study.OutInfo").getMethod("printInfo");
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());
}
}
}
通过阅读翻译出来的代码,我们可以知道变量m3保存的是接口的方法对象,而生成的代理方法printInfo()
中有这么一行代码:
super.h.invoke(this, m3, (Object[])null);
其中,h就是我们用Lambda表达式传入的匿名对象,而这个方法的作用就是调用我们重写的invoke()
方法。
其实这个方法是这样子的:
public Object invoke(Object proxy, Method method, Object[] args)
把参数对应起来后,我们就会知道参数proxy的作用,以及动态代理demo中method.invoke(person, arg)
中写proxy会引起栈溢出的原因:
> 递归调用引起了死循环,程序无法结束循环,所以会报栈溢出异常
通过这种机制,JDK便完成了动态代理
总结一下
- 动态代理是通过操作字节码文件来生成代理对象的,这样就解决了静态代理生成的代理类很多以及维护困难的问题
- 动态代理中有一些坑,比如说:
- JDK的动态代理要求被代理类必须实现接口,返回的代理对象也是该接口的实现类;如果想不实现接口,可以使用cglib
- 在实现InvocationHandler接口,重写
incvoke()
方法时,要注意method.invoke()
的第一个参数要传被代理对象,不能传重写的invoke()
方法的第一个参数,因为那个参数是代理对象。
这是博主第一次写博文,内容中错误的地方还请评论指正,同时也希望大家多多包涵。最后,感谢大家阅读我的博文。