手写动态代理
一、为什么需要动态代理?
传统静态实现的痛点:
假设需求是”让每个方法都打印自己的名字和名字长度”,传统做法是手动编写每个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class NameAndLengthImpl implements MyInterface { @Override public void func1() { String methodName = "func1"; System.out.println(methodName); System.out.println(methodName.length()); }
@Override public void func2() { String methodName = "func2"; System.out.println(methodName); System.out.println(methodName.length()); } }
|
问题:
- 代码重复 - 每个方法都重复了获取方法名的逻辑
- 修改困难 - 如果要修改打印格式,需要修改所有方法
- 扩展困难 - 新增方法时需要重复同样的模板代码
动态代理的解决方案: 通过动态生成代码,只写一次逻辑,应用到所有方法上!
二、动态代理核心流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| ┌─────────────────┐ │ 1. 生成类名 │ 使用原子计数器确保类名唯一 └────────┬────────┘ ▼ ┌─────────────────┐ │ 2. 生成源码 │ 根据 Handler 提供的方法体,组装成完整 Java 类 └────────┬────────┘ ▼ ┌─────────────────┐ │ 3. 写入文件 │ 将源码写入 .java 文件 └────────┬────────┘ ▼ ┌─────────────────┐ │ 4. 编译 │ 调用 JavaCompiler 编译为 .class └────────┬────────┘ ▼ ┌─────────────────┐ │ 5. 加载类 │ 使用 URLClassLoader 加载 └────────┬────────┘ ▼ ┌─────────────────┐ │ 6. 创建实例 │ 反射调用构造器,返回代理对象 └─────────────────┘
|
三、手写代码
3.1 目标接口
1 2 3 4 5 6 7
| package com.sap.proxy;
public interface MyInterface { void func1(); void func2(); void func3(); }
|
3.2 方法体生成器接口(核心抽象)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package com.sap.proxy;
public interface MyHandler {
String functionBody(String methodName);
default void setProxy(MyInterface proxy) throws Exception { } }
|
3.3 动态代理工厂(核心实现)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| package com.sap.proxy;
import java.io.File; import java.lang.reflect.Constructor; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; import java.util.concurrent.atomic.AtomicInteger;
public class MyInterfaceFactory {
private static final AtomicInteger count = new AtomicInteger();
public static MyInterface createProxyObject(MyHandler myHandler) throws Exception { String className = getClassName(); File javaFile = createJavaFile(className, myHandler); Compiler.compile(javaFile); return newInstance(className, myHandler); }
private static String getClassName() { return "MyInterface$proxy" + count.incrementAndGet(); }
private static File createJavaFile(String className, MyHandler myHandler) throws Exception { String func1Body = myHandler.functionBody("func1"); String func2Body = myHandler.functionBody("func2"); String func3Body = myHandler.functionBody("func3");
String sourceCode = "package com.sap.proxy;\n" + "\n" + "public class " + className + " implements MyInterface {\n" + " MyInterface target;\n" + "\n" + " @Override\n" + " public void func1() {\n" + " " + func1Body + "\n" + " }\n" + "\n" + " @Override\n" + " public void func2() {\n" + " " + func2Body + "\n" + " }\n" + "\n" + " @Override\n" + " public void func3() {\n" + " " + func3Body + "\n" + " }\n" + "}\n";
Path sourceDir = Path.of("src/main/java/com/sap/proxy"); if (!Files.exists(sourceDir)) { Files.createDirectories(sourceDir); } File javaFile = sourceDir.resolve(className + ".java").toFile(); Files.writeString(javaFile.toPath(), sourceCode); return javaFile; }
private static MyInterface newInstance(String className, MyHandler handler) throws Exception { File classesDir = new File("./target/classes"); URLClassLoader classLoader = new URLClassLoader( new URL[]{classesDir.toURI().toURL()}, MyInterfaceFactory.class.getClassLoader() );
Class<?> clazz = classLoader.loadClass("com.sap.proxy." + className); Constructor<?> constructor = clazz.getConstructor(); MyInterface proxy = (MyInterface) constructor.newInstance(); handler.setProxy(proxy); return proxy; } }
|
3.4 Java 编译器工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| package com.sap.proxy;
import javax.tools.*; import java.io.File; import java.util.Collections;
public class Compiler {
public static void compile(File javaFile) { if (javaFile == null || !javaFile.exists()) { throw new IllegalArgumentException("Java file does not exist"); }
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); if (compiler == null) { throw new RuntimeException("System Java compiler not available. " + "Please run with JDK (not JRE)"); }
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager( diagnostics, null, null)) {
File outputDir = new File("./target/classes"); if (!outputDir.exists()) { outputDir.mkdirs(); }
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(Collections.singletonList(javaFile));
Iterable<String> options = java.util.Arrays.asList( "-d", outputDir.getAbsolutePath(), "-cp", outputDir.getAbsolutePath() );
JavaCompiler.CompilationTask task = compiler.getTask( null, fileManager, diagnostics, options, null, compilationUnits );
boolean success = task.call();
if (!success) { StringBuilder errorMsg = new StringBuilder("Compilation failed:\n"); diagnostics.getDiagnostics().forEach(diagnostic -> errorMsg.append(diagnostic.getMessage(null)).append("\n") ); throw new RuntimeException(errorMsg.toString()); }
} catch (Exception e) { throw new RuntimeException("Compilation error: " + e.getMessage(), e); } } }
|
3.5 使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| package com.sap.proxy;
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) throws Exception { System.out.println("=== Demo 1: 简单代理 ==="); MyInterface proxy1 = MyInterfaceFactory.createProxyObject( new MyHandler() { @Override public String functionBody(String methodName) { return "System.out.println(\"" + methodName + "\");"; } } ); proxy1.func1(); proxy1.func2();
System.out.println("=== Demo 2: 嵌套代理(AOP思想)==="); MyInterface innerProxy = MyInterfaceFactory.createProxyObject(new PrintHandler()); MyInterface proxy2 = MyInterfaceFactory.createProxyObject(new LogHandler(innerProxy)); proxy2.func1(); }
static class PrintHandler implements MyHandler { @Override public String functionBody(String methodName) { return "System.out.println(\"" + methodName + "\");"; } }
static class LogHandler implements MyHandler { private final MyInterface target; public LogHandler(MyInterface target) { this.target = target; }
@Override public String functionBody(String methodName) { return "System.out.println(\"before\");\n" + " target." + methodName + "();\n" + " System.out.println(\"after\");"; }
@Override public void setProxy(MyInterface proxy) { try { Field targetField = proxy.getClass().getDeclaredField("target"); targetField.setAccessible(true); targetField.set(proxy, this.target); } catch (Exception e) { throw new RuntimeException(e); } } } }
|
四、面试常考点
4.1 JDK 动态代理 vs CGLIB 动态代理
| 特性 |
JDK 动态代理 |
CGLIB 动态代理 |
| 实现方式 |
基于接口 |
基于继承(生成子类) |
| 要求 |
目标类必须实现接口 |
目标类不能是 final |
| 性能 |
稍慢(反射调用) |
较快(直接调用) |
| 依赖 |
JDK 内置 |
需要引入 cglib 库 |
| 使用场景 |
Spring 默认使用 |
目标无接口时使用 |
4.2 Spring 中如何选择代理方式?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ┌─────────────────────────────────────┐ │ 目标类是否实现接口? │ └──────────────┬──────────────────────┘ │ ┌───────┴────────┐ 是 否 │ │ ▼ ▼ ┌──────────────┐ ┌──────────────────────┐ │ 使用 JDK 代理 │ │ proxyTargetClass=true?│ │ (可强制关闭)│ └───────────┬───────────┘ └──────────────┘ │ ┌────────┴────────┐ 是 否 │ │ ▼ ▼ ┌────────────┐ ┌──────────────┐ │ 使用 CGLIB │ │ 尝试 JDK 代理 │ │ 代理 │ │ (可能报错) │ └────────────┘ └──────────────┘
|
4.3 动态代理的应用场景
- AOP(面向切面编程) - Spring AOP 的核心机制
- 事务管理 - 在方法前后自动开启/提交事务
- 日志记录 - 统一记录方法调用日志
- 权限检查 - 方法调用前检查用户权限
- RPC 远程调用 - Dubbo 等框架的代理实现
- MyBatis Mapper - 接口只有定义,无实现类
五、易错点记录
- 类加载器问题 - 必须使用 URLClassLoader 从 target/classes 加载,默认 ClassLoader 找不到动态编译的类
- 包名问题 - 生成的源码必须放在正确的包目录下,否则编译器找不到依赖
- JDK vs JRE - 需要 JDK 环境才能使用 ToolProvider.getSystemJavaCompiler()
- setProxy 时机 - 嵌套代理时,必须在创建实例后注入 target 字段
- 字段名匹配 - setProxy 中反射获取的字段名必须与生成源码中的字段名一致
六、与 JDK Proxy 的对比
| 我们的实现 |
JDK Proxy (java.lang.reflect.Proxy) |
| 生成 .java 文件并编译 |
直接生成字节码(不经过源码阶段) |
| 需要文件系统操作 |
纯内存操作 |
| 易于理解原理 |
性能更好 |
| 学习用 |
生产用 |
JDK Proxy 核心 API:
1 2 3 4 5 6 7 8 9 10 11 12 13
| MyInterface proxy = (MyInterface) Proxy.newProxyInstance( MyInterface.class.getClassLoader(), new Class[]{MyInterface.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) { System.out.println("before"); Object result = method.invoke(target, args); System.out.println("after"); return result; } } );
|