目录
写在之前
Java面试必知必会.Java基础.05.动态代理(JDK/CGLIB)
我在学习的时候光看课本是根本看不明白的,所以去万能的B站找找解答,上面是我推荐看的一个视频,应该是我认为讲的最让人理解而且时间最短的,可以先听听 up主是怎么讲的。
静态与动态代理的区别
静态代理实现了刚刚的保存日志的需求,但是必须要再新手动创建一个代理类,这样不易管理,也很麻烦,而动态代理就解决了这一问题,他可以动态的创建代理类,并动态的处理代理方法的调用,只需要我们简单的制定一组接口以委托对象。
还是回到我们在静态代理中提到的需求:
- 请为 Person 类创建代理类 PersonProxy,PersonProxy的在代理Person类的所有setter方法时,把方法的调用时间、方法名称写入到文本文件中,每一行日志的格式为 时间:2023-03-27 23:34:24; 方法名称:setName; 参数:cqy
这次我们不去自己手动创建 PersonProxy 类了,而是编写一个实现 InvocationHandler 接口的 DynamicProxy 调用器类
package test3;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DynamicProxy implements InvocationHandler {
private Object target;
static String file = "./src/test3/log/log4j";
public DynamicProxy(Object target) {
this.target = target;
}
/**
* 处理对委托对象的访问
* @param proxy 代理对象
* @param method 被执行的委托方法
* @param args 所需要的参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 只处理setter方法
if (method.getName().startsWith("set")) {
// 获取当前时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time = sdf.format(new Date());
// 组装日志信息
String log = "时间:" + time + ";方法名称:" + method.getName() + ";参数:" + args[0];
// 写入到文本文件
System.out.println(log);
writeToFile(file, log, true);
}
// 调用原方法
return method.invoke(target, args);
}
/**
* 将文件写入到日志文件中保存
*
* @param filePath 保存文件夹的路径
* @param content 内容
* @param append 是否追加
*/
public static void writeToFile(String filePath, String content, boolean append) {
FileWriter fileWriter;
try {
fileWriter = new FileWriter(filePath, append);
fileWriter.write(content + "\n");
fileWriter.flush();
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
相比于静态代理,在直观上我们省去了手动重写所有 Setter 方法的步骤,改成了重写 InvocationHandler 接口中的 invoke 方法,不仅如此,如果我们要给其他的比如 Student 类,teacher 类实现保存日志的功能,也不需要再编写代理类了。
先看测试类
/**
* 测试
*
* @author cqy
* @version 1.0
*/
public class test {
public static void main(String[] args) throws InterruptedException {
//System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// 创建被代理的Person对象
Person person = new Person();
// 创建代理对象
DynamicProxy proxy = new DynamicProxy(person);
Human human = (Human) Proxy.newProxyInstance(Human.class.getClassLoader(), new Class[]{Human.class}, proxy);
// 调用setter方法
human.setName("cqy");
//Thread.sleep(2000);
human.setAge(20);
human.setIdNo("121xxxxxx");
//Thread.sleep(2000);
human.setSex("男");
human.setMarried(false);
}
}
以上是动态代理的明面的过程,背后具体从源码上是如何做到的呢? 我们从测试类的 newProxyInstance 方法开始
newProxyInstance 方法
按住Ctrl键,左键单击测试类的 newProxyInstance 方法就可以进入源码,它位于源码的 Proxy 类中。
在看源码的时候最重要的就是先弄懂全局的流程,所以我们一开始可以只关注每个方法的输入参数和返回值以及其中最重要的几步方法。
在 newProxyInstance 方法中传入了三个参数:代理类的加载器、代理类实现的接口列表、实现了InvocationHandler的调用处理器类
在写有注释的地方一般都是重要的地方,所以我们看看第一步
/*
* Look up or generate the designated proxy class.
*/
Class<?> cl = getProxyClass0(loader, intfs);
这里注释说查询或生成一个代理类,具体怎么生成的先不急着点进去看,先往下看看
/*
* Invoke its constructor with the designated invocation handler.
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
//异常处理的部分略。。。
最重要的就是这两行代码,第一句去获取了代理类的构造器,第二句通过构造器的 newInstance 方法去实例化一个代理类的对象。
final Constructor<?> cons = cl.getConstructor(constructorParams);
return cons.newInstance(new Object[]{h});
那么目前大致过程我们明白了,三个步骤分别是:
1. getProxyClass0 创建代理类 2. cl.getConstructor 获取代理类的构造器 3. cons.newInstance 实例化代理类对象
这三个步骤一定要牢记!!! 我们重点看第一步 getProxyClass0 创建代理类
getProxyClass0 方法
private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
return proxyClassCache.get(loader, interfaces);
}
传入的参数是加载器和接口列表,列表的数目不能超过65535,这是 JVM 的规定,三行注释的意思就是所需要的代理类如果已经创建,那么直接返回,没有创建,就会通过 ProxyClassFactory 工厂去创建。那么我们直接去找找这个工厂类,先点击 proxyClassCache 。
/**
* a cache of proxy classes
*/
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
再点击 ProxyClassFactory ,就找到了这个工厂。
ProxyClassFactory 类
这是位于 Proxy 类中的一个内部类
// prefix for all proxy class names
private static final String proxyClassNamePrefix = "$Proxy";
映入眼帘的第一行代码规定了动态代理类的类名前缀,之后便是重写的 apply 方法,前面注释带有 Verify 的都是一些验证参数的代码,可以不用管,往下走直到看到一段三行的注释。
/*
* Record the package of a non-public proxy interface so that the
* proxy class will be defined in the same package. Verify that
* all non-public proxy interfaces are in the same package.
*/
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}
if (proxyPkg == null) {
// if no non-public proxy interfaces, use com.sun.proxy package
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
/*
* Choose a name for the proxy class to generate.
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
这里说明了动态生成的代理类都会放在 com.sun.proxy 这个包下,并且在 $Proxy 后加上一个数字,其中 N 是一个逐一递增的阿拉伯数字,代表 Proxy类第N次生成的动态代理类。值得注意的是,并不是每次调用 Proxy 的静态方法创建动态代理类都会使得 N 值增加,原因是如果对同一组接口 (包括接口排列的顺序相同) 试图重复创建动态代理类,它会返回先前已经创建好的代理类的类对象,而不会再尝试去创建一个全新的代理类,这样可以节省不必要的代码重复生成,提高了代理类的创建效率。
接下来到了很重要的创建字节码文件的地方,它由一个 byte 数组接收
/*
* Generate the specified proxy class.
*/
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
throw new IllegalArgumentException(e.toString());
}
最后通过 defineClass0 函数返回,我们已经知道了字节码文件可以反射生成Java文件,点击 defineClass0 方法进去看看。
private static native Class<?> defineClass0(ClassLoader loader, String name,
byte[] b, int off, int len);
可以看到该方法有一个 native 关键字,没有实现体,原因是他是由c++语言编写的,视频中up主没有提到这一点,感兴趣可以去搜一下。
回到 byte 数组,它是由 generateProxyClass 方法创建的,我们依然点进去看一下,在方法中有一个 saveGeneratedFiles 参数,如果将该参数设置为true,我们就能显示的看到Java动态生成的代理类了,这时候在测试类的第一行加上,并重新运行。
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
就会发现左边项目结构中出现 com.sun.proxy 的包了,这里截取前面部分。
package com.sun.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import test3.Human;
public final class $Proxy0 extends Proxy implements Human {
private static Method m1;
private static Method m7;
private static Method m5;
private static Method m2;
private static Method m6;
private static Method m3;
private static Method m4;
private static Method m0;
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 setName(String var1) throws {
try {
super.h.invoke(this, m7, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
//。。。。。。
至此动态代理的最最最重要的第一步,创建代理类就完成了。
$Proxy0 类
前面的八个方法 m0 ~m7,在最后的静态代码块中可以看到是通过反射获得的需要代理保存日志的五个 Setter 方法和equals方法、toString方法和 hashCode方法。
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m7 = Class.forName("test3.Human").getMethod("setName", Class.forName("java.lang.String"));
m5 = Class.forName("test3.Human").getMethod("setSex", Class.forName("java.lang.String"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m6 = Class.forName("test3.Human").getMethod("setMarried", Class.forName("java.lang.Boolean"));
m3 = Class.forName("test3.Human").getMethod("setAge", Class.forName("java.lang.Integer"));
m4 = Class.forName("test3.Human").getMethod("setIdNo", Class.forName("java.lang.String"));
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
在动态代理的第二步中,我们获取了代理类的构造器。
final Constructor<?> cons = cl.getConstructor(constructorParams);
第三步中调用了构造器的 newInstance 方法,传入我们实现 InvocationHandler 接口并重写了 invoke 方法的调用器类的实例 h
return cons.newInstance(new Object[]{h});
我们先来看看代理类 $Proxy0 的构造器。
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
出现了super关键字,代表调用父类的构造器,翻到顶部发现 $Proxy0 继承 Proxy 类,那此时我们在 newProxyInstance传入的第三个参数便顺着以上方法传入了 Proxy 类的构造器中,回到 Proxy 类在最上面的属性
/**
* the invocation handler for this proxy instance.
* @serial
*/
protected InvocationHandler h;
/**
* Prohibits instantiation.
*/
private Proxy() {
}
protected Proxy(InvocationHandler h) {
Objects.requireNonNull(h);
this.h = h;
}
此时 Proxy 类中的 h便被赋值为我们实现了 InvocationHandler 接口并重写了 invoke 方法的调用器类的实例了。
Proxy 的子类 $Proxy0 类中的所有 Setter 方法全都调用 super.h.invoke 方法,并依次传入三个参数:动态生成的代理对象(this)、被执行的委托方法(m0~7)、委托方法所需要的参数(new Object[]{var1}),就可以实现我们自己编写的这一段逻辑了。
/**
* 处理对委托对象的访问
* @param proxy 代理对象
* @param method 被执行的委托方法
* @param args 所需要的参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 只处理setter方法
if (method.getName().startsWith("set")) {
// 获取当前时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time = sdf.format(new Date());
// 组装日志信息
String log = "时间:" + time + ";方法名称:" + method.getName() + ";参数:" + args[0];
// 写入到文本文件
System.out.println(log);
writeToFile(file, log, true);
}
// 调用原方法
return method.invoke(target, args);
}
至此,动态代理的全过程就结束了。
写在最后
以上是我对于 Java 动态代理的全部理解,为什么说动态代理很重要,我在学SpringBoot时,老师提到了springAOP(面向切面编程),这是spring 框架的高级技术,旨在管理 bean 对象的过程中,对特定的方法进行编程,其底层原理正是动态代理。
第一次写博客,如有差错,欢迎指正。