相信很多人在跟黑马程序员学动态代理的时候都会产生很多问题:传入的Class是什么?为什么要用接口?代理对象是什么?为什么教学时只有一个接口传入的却是一个接口Class数组?
当初我看了两遍网课都没想明白,今天上高数课上突然脑子一热又研究了一下,突然想明白了其中的大致逻辑
先看这么一个示例,为了结果有普遍性,这次我让被代理对象实现了两个接口
public interface CanDance {
String dance(String name);
}
public interface CanSing {
void sing(String name);
}
public class RealStar implements CanDance,CanSing{
@Override
public String dance(String name) {
System.out.println(name);
return "OK";
}
@Override
public void sing(Strling name) {
System.out.println(name);
}
}
以上是两个代理用的接口和被代理对象,被代理对象实现了这两个接口
@SpringBootTest
public class test {
@Test
public void test() throws IOException,
ClassNotFoundException, NoSuchMethodException,
InvocationTargetException, IllegalAccessException {
// --- 核心步骤:开启保存代理类文件的开关 ---
// 如果你用的是 JDK 8:
System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// 如果你用的是 JDK 9 及以上版本(如 JDK 17/21):
System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
RealStar jay = new RealStar();
// 生成代理对象
// 注意:这里传了两个接口 CanSing.class, CanDance.class
Object proxyInstance = Proxy.newProxyInstance(
jay.getClass().getClassLoader(),
new Class[]{CanSing.class, CanDance.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("[经纪人] 谈合同,收定金...");
Object result = method.invoke(jay, args); // 执行真实对象的方法
System.out.println("[经纪人] 演出结束,结清尾款。");
return result;
}
}
);
}
}
不用管Springboot的注解,我懒得开新项目了
可以看到,我们生成了被代理对象后
RealStar jay = new R
就调用Proxy的工厂方法newProxyInstance生成了一份代理对象
而工厂方法的这三个参数则是理解代理对象的关键
Class数组
我们先看第二个参数:Class[],该参数传入了所有接口的Class对象
在 Java 里,.class 文件存在磁盘上,而 Class 对象存在内存里。你可以把它理解为一份详尽的说明书(Blueprint)。(是底层C++的Klass对象在Java的映射)
当你传 new Class[]{CanSing.class} 给代理工具时,你其实是在告诉它:
-
方法列表:这个代理类需要有哪些方法(比如
sing())。 -
参数与返回值:这些方法接收什么参数,返回什么类型。
-
访问权限:哪些是 public 的。
类加载器
这是我目前仍然很懵的地方,因为届时我还没学JVM,不过,我们依然可以用
最简单的理论讲述他
类加载器,顾名思义是加载类对象的,我们传入的这么多接口类都需要用这个类加载器来加载才可以被使用。我们不难怀疑为什么明明传入的是接口的类对象却使用实现类类加载器。相信大部分人都听过双亲委派模型,但是初学者不用担心我讲的多复杂,因为我也不会,我现查的。
这个问题的核心在于Java类加载器的可见性机制(双亲委派模型)以及动态生成类的运行环境。
Java的类加载器遵循双亲委派模型:子类加载器可以看见父类加载器加载的类,但父类加载器看不见子类加载器加载的类。
-
接口通常位于更上层的类加载器: 在很多架构中(例如Tomcat、Spring、OSGi等),接口通常定义在公共包中,由父类加载器加载;而实现类定义在具体的应用包中,由子类加载器加载。
-
如果使用接口的类加载器: 它可能无法“看到”当前应用环境中的其他类。
-
如果使用实现类的类加载器: 因为实现类能正常运行,说明它的类加载器既能看到自己,也能(通过双亲委派)看到父类加载器加载的接口类。它是当前上下文中“权限最大”、“视野最广”的类加载器。
通俗的说,接口的类加载器等级高,可能不认识实现类这个低级加载器,但是低级加载器不可能不认识自己的上级
invoke方法
很多人会疑惑:奇怪啊?我明明最后才返回的一个proxy代理对象,他怎么还能当作参数传进invoke方法去的?
invoke方法是怎么发挥作用的?
这时候就不得不品一品上面这个代理类反编译后的结果了(简化版)
public final class $Proxy0 extends Proxy implements CanDance, CanSing {
// 静态变量缓存方法对象
private static Method m1; // equals
private static Method m2; // toString
private static Method m0; // hashCode
private static Method m3; // dance方法
private static Method m4; // sing方法
// 构造函数,依然是把 InvocationHandler 传给父类 Proxy
public $Proxy0(InvocationHandler h) {
super(h);
}
// ========================================================
// 重点 1:dance 方法(有参数、有返回值)
// ========================================================
public final String dance(String var1) {
try {
// 【注意这里】:
// 1. 参数 var1 被包装成了 new Object[]{var1} 传给 invoke
// 2. invoke 方法返回的是 Object,所以前面必须强转成 (String) 并 return
return (String) super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error e) {
throw e; // 运行时异常和Error直接抛出
} catch (Throwable t) {
throw new UndeclaredThrowableException(t); // 其他受检异常包装后抛出
}
}
// ========================================================
// 重点 2:sing 方法(有参数、无返回值)
// ========================================================
public final void sing(String var1) {
try {
// 【注意这里】:
// 1. 同样把参数包装成数组 new Object[]{var1}
// 2. 因为原方法返回 void,所以前面没有 return,也不需要强转
super.h.invoke(this, m4, new Object[]{var1});
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable t) {
throw new UndeclaredThrowableException(t);
}
}
// ========================================================
// 静态代码块:反射获取方法对象
// ========================================================
static {
try {
// 获取 Object 的基础方法 (省略 m0, m1, m2 的详细代码)
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
// ...
// 【注意这里】:
// 获取方法时,除了传方法名,还要传参数类型的 Class 对象:Class.forName("java.lang.String")
m3 = Class.forName("com.lzh.server.proxytest.CanDance")
.getMethod("dance", Class.forName("java.lang.String"));
m4 = Class.forName("com.lzh.server.proxytest.CanSing")
.getMethod("sing", Class.forName("java.lang.String"));
} catch (NoSuchMethodException e) {
throw new NoSuchMethodError(e.getMessage());
} catch (ClassNotFoundException e) {
throw new NoClassDefFoundError(e.getMessage());
}
}
// (省略 equals, toString, hashCode 方法的代码,它们的逻辑和 sing/dance 是一样的)
}
可以看到代理类继承了Proxy(这也是代理为什么是实现而非继承,Java的单继承机制使得其没有办法在继承要代理的对象),并且实现了传入的两个接口,构造时直接使用父类构造方法
并且通过静态代码块获取了被代理对象的各个方法,而调用对应方法时则是调用了invoke方方法并把自己和该方法传了进去
因此这个invoke是用来被代理对象执行的,每次调用方法代理对象实际都会调用这个invoke,而在这个invoke里面method自己也调用了invoke,这个则是Java的反射机制,二者不是一个invoke
总结
所以完整流程实际上就是
我们想要实现动态代理,就给这个类写了了对应接口并让被代理类实现它
而构造代理对象则是Proxy类通过工厂方法,用被代理对象的低级(子)类加载器,加载了高级的类(接口)
获得对应方法后在代理对象反射出来,作为参数传入了只有invoke方法的函数式接口InvocatHandler实现类(这里用lambda表达式构造的实现类)
该实现类又作为参数传入工厂被代理对象获得,并使代理对象自动生成的同名方法调用这个实现类的invoke。
至此,我们就获得了一个代理对象了。
而代理对象方法的执行可以理解为下图
代理对象在同名方法中调用了invoke(函数式接口的方法),这个invoke不仅包含我们想增加的功能,还通过反射机制invoke调用了原方法
感谢观看 (人生第一次发博客,写的不好请见谅,这是我的博客地址myblog-zeta-kohl.vercel.app/ )