手写实现一个动态代理框架
- 什么是代理模式
- 什么是动态代理
- 动态代理中的编译、类加载与对象实例化
- 手写实现一个动态代理框架
- 实现细节
- DynamicProxyHandler
- Proxy
- 生成代码
- 写入代码到磁盘文件
- 调用编译器进行编译
- 调用类加载器进行类加载
- 反射实例化
- 删除前面生成的java文件和class文件
- Coder
- 来玩一把
- JDK动态代理和我们的区别
最近写了一个动态代理框架,在这里分享一下,顺便分析一下动态代理的原理,当做复习。
什么是代理模式
首先我们来了解一下代理模式,代理模式是二十三种设计模式中的其中一种,用于对目标对象进行代理增强。
代理类通常和目标类实现同一个接口,这样我们就可以用一个接口类型变量去引用一个代理类对象,然后又可以当成目标对象去使用。
public interface Handler {void handle();
}
public class Proxy implements Handler {private Handler target;public Proxy(Handler target) {this.target = target;}...
}
public class Target implements Handler {...
}
public class Main {public static void main(String[] args) {Target target = new Target();Handler handler = new Proxy(target);...}
}
当我们对目标对象做了代理之后,就可以在调用目标对象方法的前后添加一些增强的处理逻辑。
public class Proxy implements Handler {private Handler target;public Proxy(Handler target) {this.target = target;}@Overridepublic void handle() {pre(); // 前置增强逻辑target.handle();post(); // 后置增强逻辑}
}public class Target implements Handler {@Overridepublic void handle() {...}
}
比如我们要在目标方法的前后添加日志打印,那么我们可以通过代理模式实现。
public class Proxy implements Handler {private Handler target;@Overridepublic void handle() {log.info(...);target.handle();log info(...);}}
这样做的好处有两点:
- 我们可以为目标类添加额外的处理逻辑,而不需要改动目标类原有的代码。
- 我们可以更换目标对象,但是代理类中的增强处理逻辑不用变,更换后的目标对象依然可以复用原先的增强逻辑。
什么是动态代理
动态代理是对普通代理模式的优化,普通代理模式有一个缺点,那就是我们需要手动编写代理逻辑。
比如我们的代理逻辑就是在目标方法的前后添加日志打印。如果只有几个接口需要代理,那么我们可以手动编写这几个接口的代理实现类,那是没有问题的。
public interface HandlerA {void method1();void method2();void method3();}
public interface HandlerB {void method1();void method2();
}
public interface HandlerC {void method1();void method2();void method3();void method4();
}public class ProxyA implements HandlerA {private HandlerA target;@Overridepublic void method1() {log.info(...);target.method1();log info(...);}@Overridepublic void method2() {log.info(...);target.method2();log info(...);}@Overridepublic void method3() {log.info(...);target.method3();log info(...);}}public class ProxyB implements HandlerB {private HandlerB target;@Overridepublic void method1() {log.info(...);target.method1();log info(...);}@Overridepublic void method2() {log.info(...);target.method2();log info(...);}}
public class ProxyC implements HandlerC {...
}
但是我们发现,虽然我们编写的代理类实现了不同的接口,重写了不同的接口方法,但是前后添加的增强逻辑其实是一模一样的。而且如果接口比较多的话,我们一个个手写代理实现类也挺麻烦的。
于是就有了动态代理,动态代理的主要作用就是动态生成代理实现类,不需要我们手动编写。我们只需要定义好增强逻辑,以及需要代理的接口和接口方法,我们就可以通过动态代理框架生成代理类并实例化代理对象。
比如JDK的动态代理,我们定义好我们的接口比如Handler,然后实现JDK动态代理提供的接口InvocationHandler在里面编写增强逻辑,就可以作为输入参数调用JDK动态代理提供的工具类Proxy生成代理实现类并反射实例化代理对象。
这样就不需要我们自己编写那么多的代理实现类了,省掉了一大波繁琐的工作,一片岁月静好。
动态代理中的编译、类加载与对象实例化
我们自己手动编写代理类时,从代理类代码的编写到代理对象的创建,整个流程是这样子:
代码编写和编译器编译是静态的,因为是在JVM启动之前提前做好的。
动态代理相当于是就是在JVM运行的时候,由动态代理框架根据我们给定的接口和代理逻辑动态编写代理实现类,然后调用编译器进行动态编译,通过类加载器加载到内存中,然后反射实例化。
在动态代理的情况下,代码是在JVM运行时动态生成的,比如用StringBuilder拼接(也可以是别的方式生成),然后通过调用JDK提供的API进行运行时编译,类加载器加载也是通过调用JDK提供的API进行动态的加载。也就是说从代码生成、编译、类加载、到反射实例化的这一切,都是在JVM已经运行的情况下,通过Java代码动态实现的,因此称为动态代理。
手写实现一个动态代理框架
那么,我们就来实现一个我们自己的动态代理框架,非常简单,三个类:Coder、DynamicProxyHandler、Proxy
- Coder:代理类代码的生成器类,接收一个Class<?>类型的参数interfaceClass,表示代理类要实现interfaceClass这个接口,Coder根据这个接口生成代理类的代码
- DynamicProxyHandler:留给用户实现的处理器接口,用户需要实现DynamicProxyHandler接口并重新invoke以声明动态代理的增强逻辑
- Proxy:生成代理对象的工具类,调用Coder生成代理类的代码, 通过编译器动态编译,然后通过类加载器动态加载编译出来的代理类class,最后通过反射创建代理对象返回给用户
实现细节
DynamicProxyHandler
DynamicProxyHandler就是预留给用户实现的用于编写增强逻辑的接口,用户需要实现DynamicProxyHandler接口,并重写invoke方法,在invoke方法中编写增加逻辑。
/*** @author huangjunyi* @date 2023/11/28 19:00* @desc*/
public interface DynamicProxyHandler {Object invoke(Object proxy, Method method, Object[] args);}
invoke方法接收三个参数,proxy是代理对象本身,method是当前正在调用的方法的Method对象,args是当前方法接收的参数。
Proxy
Proxy中的代码稍稍有点复杂,不用马上全部看完,先有个大概印象,下面一步步分析里面干了些啥。
/*** @author huangjunyi* @date 2023/11/28 19:00* @desc*/
public class Proxy {private static boolean enablePrintCode = false;public static void printCode() {enablePrintCode = true;}public static <T> T newInstance(ClassLoader classLoader, Class<?> interfaceClass, DynamicProxyHandler handler) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {String code = Coder.generateCode(interfaceClass);if (enablePrintCode) {System.out.println(code);}String path = classLoader.getResource("").getPath() + interfaceClass.getPackage().toString().substring("package ".length()).replaceAll("\\.", "/") + "/" + interfaceClass.getSimpleName() + "$$.java";path = path.replace('\\', '/');File file = new File(path);if (file.getParentFile().exists()) {file.getParentFile().mkdir();}FileWriter fileWriter = new FileWriter(file);fileWriter.write(code);fileWriter.close();JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();int result = compiler.run(null, null, null, path);String parentPath = path.substring(0, path.lastIndexOf("/"));URL[] urls = new URL[] {new URL("file:/" + parentPath + "/")};URLClassLoader loader = new URLClassLoader(urls);Class c = loader.loadClass(interfaceClass.getPackage().toString().substring("package ".length()) + "." + interfaceClass.getSimpleName() + "$$");Constructor constructor = c.getConstructor(DynamicProxyHandler.class);Object o = constructor.newInstance(handler);new File(path.substring(0, path.lastIndexOf(".") + 1) + "class").delete();file.delete();return (T) o;}}
生成代码
首先调用Coder生成代理类的代码,接收一个接口类型参数interfaceClass,返回生成的代码字符串。
String code = Coder.generateCode(interfaceClass);
Proxy里面有个boolean类型的enablePrintCode属性,把它设置为true,会打印动态生成的代理类的代码。
if (enablePrintCode) {System.out.println(code);}
写入代码到磁盘文件
然后把生成的代码写入到磁盘文件
String path = classLoader.getResource("").getPath() + interfaceClass.getPackage().toString().substring("package ".length()).replaceAll("\\.", "/") + "/" + interfaceClass.getSimpleName() + "$$.java";path = path.replace('\\', '/');File file = new File(path);if (file.getParentFile().exists()) {file.getParentFile().mkdir();}FileWriter fileWriter = new FileWriter(file);fileWriter.write(code);fileWriter.close();
磁盘文件的路径必须在类加载器加载类的目录下,通过 classLoader.getResource(“”).getPath() 取得类加载器加载类的对应目录。
然后把类的包名中的“.”替换成“/”,创建目录。
最后创建文件,通过FileWriter把代码写入到文件中。
调用编译器进行编译
代码写到文件后,就要调用编译器把这个java文件编译成class文件。
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();int result = compiler.run(null, null, null, path);
JavaCompiler就是编译器对象,调用JavaCompiler的run方法进行编译,path参数是需要编译的java文件的路径。
调用类加载器进行类加载
编译出class文件后,就要调用类加载器把这个class文件加载到内存中,生成一个Class对象。
String parentPath = path.substring(0, path.lastIndexOf("/"));URL[] urls = new URL[] {new URL("file:/" + parentPath + "/")};URLClassLoader loader = new URLClassLoader(urls);Class c = loader.loadClass(interfaceClass.getPackage().toString().substring("package ".length()) + "." + interfaceClass.getSimpleName() + "$$");
String parentPath = path.substring(0, path.lastIndexOf(“/”)); 这一行是取得java文件所在目录的路径,编译出来的class文件与java文件是同处一个目录下的,这样我们就可以找到class文件。
然后就是创建URLClassLoader对象,调用URLClassLoader的loadClass方法进行类加载,获得代理类的Class对象。
反射实例化
获得代理类的Class对象后,就要反射进行实例化。
Constructor constructor = c.getConstructor(DynamicProxyHandler.class);Object o = constructor.newInstance(handler);
获得构造器对象constructor,调用构造器的newInstance方法进行实例化。
删除前面生成的java文件和class文件
最后要把生成的文件删除掉,以免留下垃圾。
new File(path.substring(0, path.lastIndexOf(".") + 1) + "class").delete();file.delete();
第一行是删除class文件,第二行是删除java文件。
Coder
接下来看一下Coder类。这个Coder类不必细看,代码虽然多,但是都是使用StringBuilder进行字符串拼接,根据给定的接口拼出一个代理类,没什么技术含量。
/*** @author huangjunyi* @date 2023/11/28 19:00* @desc*/
public class Coder {private static Map<String, String> codeCache = new HashMap<>();public static String generateCode(Class<?> interfaceClass) {String interfaceClassName = interfaceClass.getName();if (codeCache.containsKey(interfaceClassName)) {return codeCache.get(interfaceClassName);}StringBuilder codeBuilder = new StringBuilder();// packagePackage aPackage = interfaceClass.getPackage();String packageName = aPackage.getName();codeBuilder.append("package ").append(packageName).append(";").append("\n");codeBuilder.append("\n");// importMethod[] methods = interfaceClass.getDeclaredMethods();Set<String> importTypeSet = new HashSet<>();for (Method method : methods) {Class<?> returnType = method.getReturnType();importTypeSet.add(returnType.getName());Class<?>[] parameterTypes = method.getParameterTypes();for (Class<?> parameterType : parameterTypes) {importTypeSet.add(parameterType.getName());}}for (String importType : importTypeSet) {if ("void".equals(importType) ||"long".equals(importType) ||"int".equals(importType) ||"short".equals(importType) ||"byte".equals(importType) ||"char".equals(importType) ||"float".equals(importType) ||"double".equals(importType) ||"boolean".equals(importType)) {continue;}codeBuilder.append("import ").append(importType).append(";").append("\n");}codeBuilder.append("import java.lang.reflect.Method;");codeBuilder.append("import com.huangjunyi1993.simple.dynamic.proxy.DynamicProxyHandler;");// classcodeBuilder.append("\n\npublic class ").append(interfaceClass.getSimpleName()).append("$$").append(" implements").append(" ").append(interfaceClass.getSimpleName()).append(" {").append("\n\n");// fieldcodeBuilder.append("\tprivate DynamicProxyHandler dynamicProxyHandler;\n\n");// ConstructorcodeBuilder.append("\tpublic ") .append(interfaceClass.getSimpleName()).append("$$").append("(").append("DynamicProxyHandler dynamicProxyHandler) {\n").append("\t\tthis.dynamicProxyHandler = dynamicProxyHandler;").append("\n").append("\t}\n\n");// methodfor (Method method : methods) {Class<?> returnType = method.getReturnType();importTypeSet.add(returnType.getName());codeBuilder.append("\t@Override\n");codeBuilder.append("\t").append("public ").append(returnType.getSimpleName()).append(" ").append(method.getName()).append("(");// method parameterClass<?>[] parameterTypes = method.getParameterTypes();for (int i = 0; i < parameterTypes.length; i++) {Class<?> parameterType = parameterTypes[i];codeBuilder.append(parameterType.getSimpleName()).append(" ").append("var").append(i);if (i < parameterTypes.length - 1) {codeBuilder.append(", ");}}codeBuilder.append(") ").append(" {").append("\n");// method bodycodeBuilder.append("\n\t\tMethod method = null;");codeBuilder.append("\n\t\ttry {");codeBuilder.append("\n\t\t\tmethod = Class.forName(").append("\"").append(interfaceClass.getName()).append("\"").append(")").append(".getDeclaredMethod(").append("\"").append(method.getName()).append("\"");if (parameterTypes.length != 0) {codeBuilder.append(", ");for (int i = 0; i < parameterTypes.length; i++) {Class<?> parameterType = parameterTypes[i];codeBuilder.append(parameterType.getSimpleName()).append(".").append("class");if (i < parameterTypes.length - 1) {codeBuilder.append(", ");}}}codeBuilder.append(");");codeBuilder.append("\n\t\t\tmethod.setAccessible(true);");codeBuilder.append("\n\t\t} catch (ClassNotFoundException|NoSuchMethodException e) {");codeBuilder.append("\n\t\t\tthrow new RuntimeException(\"error when generate code\");");codeBuilder.append("\n\t\t}");if (!"void".equals(returnType.getSimpleName())) {codeBuilder.append("\n\t\treturn (").append(returnType.getSimpleName()).append(") ").append("dynamicProxyHandler.invoke(this, method, new Object[]{");} else {codeBuilder.append("dynamicProxyHandler.invoke(this, method, new Object[]{");}for (int i = 0; i < parameterTypes.length; i++) {codeBuilder.append("var").append(i);if (i < parameterTypes.length - 1) {codeBuilder.append(", ");}}codeBuilder.append("});");codeBuilder.append("\n").append("\t").append("}");codeBuilder.append("\n\n");}codeBuilder.append("}");String code = codeBuilder.toString();codeCache.put(interfaceClassName, code);return code;}}
Coder 有一个Map<String, String>类型的缓存codeCache,用于缓存曾经生成过的代码,如果下一次再调用Coder的generateCode,传入的参数interfaceClass是同一个接口类型,那么直接取缓存中的结果返回,不再重复生成。
String interfaceClassName = interfaceClass.getName();if (codeCache.containsKey(interfaceClassName)) {return codeCache.get(interfaceClassName);}
如果缓存中没有,就要通过StringBuilder动态拼接代理类的代码了。
StringBuilder codeBuilder = new StringBuilder();...
这个拼接的逻辑就不用细看了,只是繁琐的工作,只要知道怎么通过Class对象获取到类信息和方法信息,就可以根据这些信息拼出一个代理实现类。
- Package aPackage = interfaceClass.getPackage(); 获取包路径。
- Method[] methods = interfaceClass.getDeclaredMethods(); 获取接口定义的方法。
- Class<?> returnType = method.getReturnType(); 获取方法返回值类型。
- Class<?>[] parameterTypes = method.getParameterTypes(); 获取方法参数类型。
我们看看生成的代理类是怎么样的。比如我们有一个Hello接口:
/*** @author huangjunyi* @date 2023/11/28 19:58* @desc*/
public interface Hello {String sayHi(int count);}
生成的代理类就是这样:
package com.huangjunyi1993.simple.dynamic.proxy.test;import java.lang.String;
import java.lang.reflect.Method;import com.huangjunyi1993.simple.dynamic.proxy.DynamicProxyHandler;public class Hello$$ implements Hello {private DynamicProxyHandler dynamicProxyHandler;public Hello$$(DynamicProxyHandler dynamicProxyHandler) {this.dynamicProxyHandler = dynamicProxyHandler;}@Overridepublic String sayHi(int var0) {Method method = null;try {method = Class.forName("com.huangjunyi1993.simple.dynamic.proxy.test.Hello").getDeclaredMethod("sayHi", int.class);method.setAccessible(true);} catch (ClassNotFoundException|NoSuchMethodException e) {throw new RuntimeException("error when generate code");}return (String) dynamicProxyHandler.invoke(this, method, new Object[]{var0});}}
生成的代理类的类名是接口名后加两个“$”,比如这里的接口是Hello,那么生成的代理类就是Hello$$。
public class Hello$$ implements Hello
生成的代理类重写了给定接口定义的所有方法,方法里面的逻辑都是反射获取当前方法的Method对象,然后调用DynamicProxyHandler的invoke方法,把当前代理对象this、当前方法的Method对象method、当前方法参数作为invoke方法的入参。
来玩一把
定义一个Hello接口,就是前面的Hello接口:
/*** @author huangjunyi* @date 2023/11/28 19:58* @desc*/
public interface Hello {String sayHi(int count);}
Hello接口实现类HelloImpl,目标类(被代理类):
/*** @author huangjunyi* @date 2023/11/28 20:00* @desc*/
public class HelloImpl implements Hello {@Overridepublic String sayHi(int count) {String result = "";for (int i = 0; i < count; i++) {result += "hi ";}return result;}
}
实现DynamicProxyHandler接口,编写增强逻辑:
/*** @author huangjunyi* @date 2023/11/28 20:02* @desc*/
public class HelloHandler implements DynamicProxyHandler {private Hello target;public HelloHandler(Hello target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) {System.out.println("HelloHandler invoke");try {Object invoke = method.invoke(target, args);System.out.println(invoke);return invoke;} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}return null;}
}
测试类:
/*** @author huangjunyi* @date 2023/11/24 10:05* @desc*/
public class A {@Testpublic void test() throws Exception {HelloImpl hello = new HelloImpl();HelloHandler helloHandler = new HelloHandler(hello);// 调用这个方法,可以打印生成的代理类的代码// Proxy.printCode();Hello o = Proxy.newInstance(hello.getClass().getClassLoader(), Hello.class, helloHandler);o.sayHi(5);}}
运行测试类,控制台输出:
HelloHandler invoke
hi hi hi hi hi
JDK动态代理和我们的区别
JDK动态代理大体流程和我们的思路是相同的,我们看看JDK的Proxy类的newProxyInstance方法:
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)throws IllegalArgumentException{final Class<?>[] intfs = interfaces.clone();// 生成代理类的Class对象Class<?> cl = getProxyClass0(loader, intfs);try {// 获取构造器对象final Constructor<?> cons = cl.getConstructor(constructorParams);final InvocationHandler ih = h;cons.setAccessible(true);// 构造器反射实例化return cons.newInstance(new Object[]{h});} catch (...) {...}}
里面我省略了许多杂七杂八的代码,只留下核心流程,可以看到也是生成一个代理类的Class对象,然后反射调用它的构造器生成代理对象。
getProxyClass0(loader, intfs)最终会调到ProxyClassFactory的apply方法:
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {// 。。。。。。// 生成的不是字符串,而是二进制byte数组,class字节码文件里面的内容byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);try {// 调用native方法直接进行类的初始化,生成Class对象// 没有写盘、编译、类加载的这几步动作return defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);} catch (ClassFormatError e) {...}}
这里就是和我们区别最大的地方,JDK动态代理不是通过字符串拼接生成的代理类代码,而是生成的byte[]类型,里面的内容也不是java文件里面的代码,而是class字节码文件里面的内容。
然后直接调用defineClass0方法(defineClass0方法是native方法)进行类的初始化,省略了写入磁盘(我们也可以设置要保存生成的class文件,这样会把class文件写到磁盘)、编译、调用类加载器进行类加载的这几步。
全文完。