如何理解动态代理

张开发
2026/5/4 6:15:20 15 分钟阅读
如何理解动态代理
Java 动态代理可以理解成一种【织入手段】在运行期动态生成代理类把接口方法调用统一导向一个统一的【方法处理器】从而在不改业务代码的前提下【无侵入】地织入日志、权限、事务、RPC等【横切逻辑】。你就先简单有一个大概的理解记住一些关键词就可以了。下面我用2个篇章来帮助你真正理解JAVA的动态代理。先手写一个 mini 动态代理并运行起来让你看一下【动态生成代理类】这件事的完整过程简单分析一下JDK 17的源码对比一下工业级实现和我自己手写的有啥区别。一、只用JDK 17的基础类手写一个mini版的动态代理一般来说你要先有设计的思路然后才可以一步一步按照设计来实现。这块的整体的思路可以拆成四步拼接出代理类的Java源代码字符串生成一个实现了接口的代理类比如$MiniProxy1 implements UserService这个类的每个方法内部都不写业务逻辑只是把调用转发给一个”方法处理器”用JavaCompiler在内存里编译源代码把这段字符串源码编译成.class字节码用自定义ClassLoader把字节码加载进 JVM得到真正可以new的代理类通过构造器创建代理实例传入我们自己的MiniInvocationHandler让所有接口方法都走这一个统一的处理器。如下图第一步MiniInvocationHandler,定义统一的调用入口import java.lang.reflect.Method; FunctionalInterface public interface MiniInvocationHandler { Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }参数proxy当前的代理对象本身后面生成的$MiniProxyX实例。参数method被调用的接口方法。参数args调用参数。后面所有代理方法最终都会汇总到这里形成一个统一的拦截点。举个简单的日志版实现MiniInvocationHandler handler (proxy, method, methodArgs) - { System.out.println([MiniProxy] before method.getName()); //反射调真实对象 Object result method.invoke(target, methodArgs); System.out.println([MiniProxy] after method.getName()); return result; };到这一步你只需要记住一件事后面生成的代理类所有方法最终都会走到这个invoke里。第二步JavaSourceFromString,把字符串伪装成Java源代码文件为什么得这么干? 因为JavaCompiler不认字符串的它只认JavaFileObject这种类型。所以你得把源码字符串包装成一个假的java文件骗过编译器。这里我需要借助JDK自带的SimpleJavaFileObject这个抽象类来实现。它是javax.tools包里的工具类专门用来表示一个Java源文件或class文件你可以继承它来自定义文件内容的来源比如从字符串、网络、数据库等。请记住这一步非常关键。具体的代码如下:import javax.tools.SimpleJavaFileObject; import java.net.URI; /** * 把字符串形式的源码包装成 JavaCompiler 可识别的 JavaFileObject */ public class JavaSourceFromString extends SimpleJavaFileObject { private final String source; // className: 完整类名比如 com.example.miniproxy.$MiniProxy1 // source: 这个类的完整Java源代码字符串 public JavaSourceFromString(String className, String source) { // 把类名转成 URI 格式比如 string:///com/example/miniproxy/$MiniProxy1.java // 这样编译器就以为你在编译一个真实的 .java 文件 super(URI.create(string:/// className.replace(., /) Kind.SOURCE.extension), Kind.SOURCE); this.source source; } Override public CharSequence getCharContent(boolean ignoreEncodingErrors) { return source; } }构造时传入完整类名 源码字符串getCharContent直接把源码字符串交给编译器。就是说JavaSourceFromString就是一个假的.java 文件让编译器以为它在编译真实源码文件。第三步MemoryJavaFileManager把.class字节码拦截到内存里由于我选择的是用 JavaCompiler 编译源码字符串而 JavaCompiler 默认会把编译出来的.class写到磁盘上。这一步是我年轻时踩过的坑当年手写动态代理老是失败其中一个卡壳的地方就是这里编译是成功了.class也被写到磁盘上了但我不知道怎么让 ClassLoader 去加载它。而且就算搞定了加载还得处理临时文件清理、并发安全等一堆事。后来查资料才知道正确思路是拦截编译输出把字节码直接留在内存里根本不写文件这样既快又干净也和JDK动态代理的思路一致。具体的办法就是写一个JavaFileManager代理拦截编译输出把字节码塞进一个MapString, byte[]里去。这里需要借助两个 JDK 自带的类JavaFileManager管理编译器的输入输出文件它负责告诉编译器”从哪读源码、往哪写 class”ForwardingJavaFileManager它是JavaFileManager的一个包装类可以继承它来做”选择性拦截”只改你关心的那几个方法其他方法都转发给原始的 FileManager。因此就可以继承ForwardingJavaFileManager只拦截getJavaFileForOutput这一个方法编译器准备写class的时候会调它其他什么都不动。import javax.tools.*; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.URI; import java.util.Collections; import java.util.HashMap; import java.util.Map; /** * 把JavaCompiler输出的.class字节码存到内存里 */ public class MemoryJavaFileManager extends ForwardingJavaFileManagerJavaFileManager { private final MapString, byte[] classBytes new HashMap(); public MemoryJavaFileManager(JavaFileManager fileManager) { super(fileManager); } public MapString, byte[] getClassBytes() { return Collections.unmodifiableMap(classBytes); } Override public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { return new SimpleJavaFileObject( URI.create(mem:/// className.replace(., /) kind.extension), kind ) { private final ByteArrayOutputStream bos new ByteArrayOutputStream(); Override public OutputStream openOutputStream() { return new OutputStream() { Override public void write(int b) throws IOException { bos.write(b); } Override public void write(byte[] b, int off, int len) throws IOException { bos.write(b, off, len); } Override public void flush() throws IOException { bos.flush(); } Override public void close() throws IOException { super.close(); classBytes.put(className, bos.toByteArray()); } }; } }; } }getJavaFileForOutput被调用时说明编译器准备往某个文件写class返回一个我们上面提到的SimpleJavaFileObject它的openOutputStream提供的是自定义OutputStreamOutputStream.close()时把字节写进classBytes这张表里。MemoryJavaFileManager就是个拦截器把编译产生的.class文件截胡放进内存 Map而不是落盘。第四步MemoryClassLoader把Map里的byte[]变成真正的Class对象现在我们已经拿到了MapString, byte[] classBytes;接下来就需要自定义ClassLoaderimport java.util.HashMap; import java.util.Map; /** * 把内存中的byte[]字节码喂给JVM变成可以new的Class */ public class MemoryClassLoader extends ClassLoader { private final MapString, byte[] classBytes; public MemoryClassLoader(ClassLoader parent, MapString, byte[] classBytes) { super(parent); this.classBytes new HashMap(classBytes); } Override protected Class? findClass(String name) throws ClassNotFoundException { byte[] buf classBytes.get(name); if (buf null) { return super.findClass(name); } return defineClass(name, buf, 0, buf.length); } }查找Map有就defineClass没有就交给父加载器到了这里就是把内存里的字节码真正喂给JVM了。MemoryClassLoader的职责就是从Map里捞出字节码喂给JVM变成Class对象。5. 第五步核心步骤MiniProxy拼源码 编译 加载 new出代理实例前面的步骤都是打基础的真正把它们串成一条动态代理流水线的是MiniProxy这个核心类/** * mini版动态代理实现 */ public class MiniProxy { private static final AtomicInteger COUNTER new AtomicInteger(0); public static T T newProxyInstance(ClassLoader loader, ClassT interfaceType, MiniInvocationHandler handler) { // 1. 生成类名放在接口同一个包下名字形如 $MiniProxy1 String pkg interfaceType.getPackage() null ? : interfaceType.getPackage().getName(); String proxySimpleName $MiniProxy COUNTER.incrementAndGet(); String className pkg.isEmpty() ? proxySimpleName : pkg . proxySimpleName; // 2. 根据接口方法签名拼出代理类源码 String sourceCode generateSourceCode(className, interfaceType); // 3. 编译 加载 Class? proxyClass compileAndLoad(loader, className, sourceCode); // 4. 通过构造器 new 出实例 try { SuppressWarnings(unchecked) ConstructorT ctor (ConstructorT) proxyClass .getConstructor(MiniInvocationHandler.class); return ctor.newInstance(handler); } catch (ReflectiveOperationException e) { //忽略 } } }这里做的事情和JDKProxy.newProxyInstance差不多generateSourceCode对应ProxyGenerator.generateProxyClass只是人家拼字节码我们拼Java源码compileAndLoad对应defineClass最后通过构造器把MiniInvocationHandler注入进去。5.1generateSourceCode按接口的定义拼出一个代理类源码注意下面的代码非常的长但是你只要记住下面的代码跑完后就是为了生成如下类似的代码。/** * public class $MiniProxy1 implements com.example.miniproxy.UserService { * private final com.example.miniproxy.MiniInvocationHandler handler; * * public $MiniProxy1(com.example.miniproxy.MiniInvocationHandler handler) { * this.handler handler; * } * * Override * public java.lang.String findUser(java.lang.String id) { * try { * java.lang.reflect.Method m * com.example.miniproxy.UserService.class * .getMethod(findUser, java.lang.String.class); * Object[] args new Object[] { id }; * Object result handler.invoke(this, m, args); * return (java.lang.String) result; * } catch (Throwable t) { * throw new RuntimeException(t); * } * } * } */下面的代码一般很难独立写得出来的肯定是要谷歌一把再参考一下jdk的实现才能慢慢凑出来但是最关键的是还是那句话你要先有思路然后带着问题去找资料。private static String generateSourceCode(String className,Class? interfaceType) { // 1. 先算出包名和简单类名 String pkg interfaceType.getPackage() ! null ? interfaceType.getPackage().getName() : ; String simpleName className.substring( className.lastIndexOf(.) 1); // 2. 如果有包名就先拼 package 语句 StringBuilder sb new StringBuilder(); sb.append(package ).append(pkg).append(;\n\n); } // 3. 拿到 MiniInvocationHandler 和接口的全限定类名方便后面直接拼 String handlerType MiniInvocationHandler.class.getCanonicalName(); String interfaceTypeName interfaceType.getCanonicalName(); // 4. 生成类声明public class $MiniProxyX implements 接口 sb.append(public class ).append(simpleName) .append( implements ).append(interfaceTypeName).append( {\n\n); // 5. 生成一个字段用来保存方法处理器 handler sb.append( private final ).append(handlerType).append( handler;\n\n); // 6. 生成构造器把 handler 传进来 sb.append( public ).append(simpleName) .append(().append(handlerType).append( handler) {\n) .append( this.handler handler;\n) .append( }\n\n); // 7. 为接口里的每个非 static 方法生成一份代理实现 for (Method method : interfaceType.getMethods()) { int mod method.getModifiers(); if (Modifier.isStatic(mod)) { continue; } sb.append(generateMethodCode(interfaceType, method)); sb.append(\n); } sb.append(}\n); return sb.toString(); } private static String generateMethodCode(Class? interfaceType, Method method) { StringBuilder sb new StringBuilder(); Class? returnType method.getReturnType(); Class?[] paramTypes method.getParameterTypes(); // 1. 方法签名部分 sb.append( Override\n); sb.append( public ) .append(typeToString(returnType)) .append( ) .append(method.getName()) .append((); // 2. 拼形参列表 为后面准备两个辅助字符串 // - argsArrayBuilder实际传给 handler 的实参数组 // - paramTypeClassList反射拿 Method 时用到的参数类型列表 StringBuilder argsArrayBuilder new StringBuilder(); StringBuilder paramTypeClassList new StringBuilder(); for (int i 0; i paramTypes.length; i) { if (i 0) { sb.append(, ); argsArrayBuilder.append(, ); paramTypeClassList.append(, ); } Class? pType paramTypes[i]; String pName arg i; // 形参类型 名字 sb.append(typeToString(pType)).append( ).append(pName); // 实参数组里的元素名 argsArrayBuilder.append(pName); // 反射获取 Method 时的参数类型xxx.class .append(typeToString(pType)) .append(.class); } sb.append() {\n); sb.append( try {\n); // 3. 用反射拿到接口上的 Method 对象 sb.append( java.lang.reflect.Method m ) .append(interfaceType.getCanonicalName()) .append(.class.getMethod(\) .append(method.getName()) .append(\); if (paramTypes.length 0) { sb.append(, ).append(paramTypeClassList); } sb.append();\n); // 4. 准备 Object[] args传给 handler if (paramTypes.length 0) { sb.append( Object[] args new Object[]{ ) .append(argsArrayBuilder) .append( };\n); } else { sb.append( Object[] args new Object[0];\n); } // 5. 把调用交给 handler让它决定怎么处理 sb.append( Object result this.handler.invoke(this, m, args);\n); // 6. 处理返回值有返回值就转型返回没有就直接 return if (!returnType.equals(void.class)) { sb.append( return ) .append(castReturn(result, returnType)) .append(;\n); } else { sb.append( return;\n); } sb.append( } catch (Throwable t) {\n); sb.append( throw new RuntimeException(t);\n); sb.append( }\n); sb.append( }\n); return sb.toString(); } // 支持数组类型比如 String[]、int[][] 这种 private static String typeToString(Class? type) { if (type.isArray()) { return typeToString(type.getComponentType()) []; } return type.getCanonicalName(); } // 1. 基本类型需要做拆箱比如 (Integer) result - result.intValue() private static String castReturn(String expr, Class? returnType) { if (returnType.isPrimitive()) { if (returnType int.class) { return ((java.lang.Integer) expr ).intValue(); } else if (returnType long.class) { return ((java.lang.Long) expr ).longValue(); } else if (returnType boolean.class) { return ((java.lang.Boolean) expr ).booleanValue(); } else if (returnType byte.class) { return ((java.lang.Byte) expr ).byteValue(); } else if (returnType short.class) { return ((java.lang.Short) expr ).shortValue(); } else if (returnType char.class) { return ((java.lang.Character) expr ).charValue(); } else if (returnType float.class) { return ((java.lang.Float) expr ).floatValue(); } else if (returnType double.class) { return ((java.lang.Double) expr ).doubleValue(); } else { // 不太可能走到这里 throw new IllegalArgumentException(Unsupported primitive type: returnType); } } else { return ( returnType.getCanonicalName() ) expr; } }上面的代码虽然多但是你只需要记住如下几个点就可以了它要implements interfaceType里面有一个字段private final MiniInvocationHandler handler;构造器把 handler 接进来顺手存到字段里接口里的每个非 static 方法都按同一个套路去实现 1.先用反射拿到这个方法对应的Method对象 2.把入参打包成Object[] args 3.调handler.invoke(this, m, args)把调用转交出去 4.有返回值就按类型强转/拆箱再返回没有就直接return;。5.2compileAndLoad把前面的几个基础步骤给串起来private static Class? compileAndLoad(ClassLoader loader, String className, String sourceCode) { // 1. 拿到系统的 JavaCompiler JavaCompiler compiler ToolProvider.getSystemJavaCompiler(); // 2. 准备标准的 FileManager然后用我们的 MemoryJavaFileManager 包一层 StandardJavaFileManager standardFileManager compiler.getStandardFileManager(null, null, null); MemoryJavaFileManager fileManager new MemoryJavaFileManager(standardFileManager); // 3. 把源码字符串包装成 JavaFileObject JavaFileObject sourceFile new JavaSourceFromString(className, sourceCode); // 4. 创建编译任务并执行 JavaCompiler.CompilationTask task compiler.getTask(null, fileManager, null, List.of(), null, List.of(sourceFile)); Boolean result task.call(); // 5. 从 MemoryJavaFileManager 里拿到编译出来的字节码 MapString, byte[] classBytes fileManager.getClassBytes(); // 6. 用 MemoryClassLoader 把字节码加载成Class MemoryClassLoader memoryClassLoader new MemoryClassLoader(loader, classBytes); try { return memoryClassLoader.loadClass(className); } catch (ClassNotFoundException e) { //忽略 } }第六步本地跑一下这个mini版的动态代理有了MiniProxy之后就可以开始在本地运行了public interface UserService { String findUser(String id); } public class UserServiceImpl implements UserService { Override public String findUser(String id) { System.out.println([UserServiceImpl] 查询用户 id id); return User- id; } } public class MiniProxyDemo { public static void main(String[] args) { UserService target new UserServiceImpl(); MiniInvocationHandler handler (proxy, method, methodArgs) - { System.out.println([MiniProxy] before method.getName()); Object result method.invoke(target, methodArgs); System.out.println([MiniProxy] after method.getName()); return result; }; UserService proxy MiniProxy.newProxyInstance( UserService.class.getClassLoader(), UserService.class, handler ); String user proxy.findUser(42); System.out.println(result user); } }运行输出的结果是这串输出说明你调用的是proxy.findUser(42)JVM 实际调用的是我们运行期生成的$MiniProxyX.findUser这个方法内部什么业务都没干只是把调用参数打包后丢给MiniInvocationHandler.invoke真正的业务逻辑仍然在UserServiceImpl里通过反射被调起。也就是说我们已经完全用 JDK 基础设施自己实现了一条“源码 → class → ClassLoader → 代理实例的动态代理流水线。当你拥有上面的代码后你就可以不断通过debug的方式慢慢的完全理解动态代理。上面的代码我本地运行过的没问题你也可以试试看。好下面我们可以简单的看看jdk 17本身是如何实现动态代理的。二、简单分析一下JDK 17的源码核心类主要有三个[Proxy]对外提供newProxyInstance静态方法。[InvocationHandler]方法处理器接口。[ProxyGenerator]用 ASM 生成代理类字节码。1. JDK 动态代理的大致流程可以简单压缩成三步来理解入口方法Proxy.newProxyInstance找到/生成代理类拿到带InvocationHandler参数的构造器并 new 出实例。生成/加载代理类getProxyConstructor→ProxyGenerator.generateProxyClass在内存里生成$ProxyX字节码并用defineClass加载不落盘。统一入口InvocationHandler.invoke所有代理方法最终都转发到invoke(proxy, method, args)这一处集中处理。用一句话来讲就是按接口列表在内存里生成一个$ProxyX类不落盘让它实现所有接口方法但每个方法内部什么业务都不干只负责把调用转发到InvocationHandler.invoke。2. 和上面的 mini 动态代理思路有什么不同放在一起对比会更容易看清楚两者的关系相同点核心思想是一致的都是“基于接口”的代理只能代理接口类型都是运行期动态生成一个新的类我们的是$MiniProxyXJDK 是$ProxyX不在磁盘落盘生成出来的类方法体里都不干正事只负责打包参数然后把调用转交给一个统一入口我们的MiniInvocationHandler.invokeJDK 的InvocationHandler.invoke。不同点工程化程度不同我们的 mini 版是教学版的实现只支持单接口、单类加载器、最小必要功能走的是先拼 Java 源码→再用JavaCompiler编译→用ClassLoader加载的路线不关心模块系统、接口可见性边界、多接口组合、equals/hashCode/toString特殊处理等细节。JDK 的正式实现支持多个接口一起代理还要在模块系统下选对包和 Module保证所有类型对这个代理类是可见的直接用 ASM 在内存里拼字节码比我们拼字符串编译更高效、更可控对Object的基础方法、异常包装、default 方法调用等都做了非常细致的规范处理还有类名缓存等一整套工程化细节。从学习角度看你可以把这两套实现理解成我们手写的是白盒教学版用最少的代码把动态代理的本质暴露出来。而JDK自带的是工业级正式版在这个骨架上加了一大堆安全性、兼容性和性能优化。

更多文章