手写动态代理 - 深入理解 JDK 动态代理原理

手写动态代理


一、为什么需要动态代理?

传统静态实现的痛点:

假设需求是”让每个方法都打印自己的名字和名字长度”,传统做法是手动编写每个方法:

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());
}
// ... func3 同样重复
}

问题:

  1. 代码重复 - 每个方法都重复了获取方法名的逻辑
  2. 修改困难 - 如果要修改打印格式,需要修改所有方法
  3. 扩展困难 - 新增方法时需要重复同样的模板代码

动态代理的解决方案: 通过动态生成代码,只写一次逻辑,应用到所有方法上!


二、动态代理核心流程

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 {
// 步骤 1:生成唯一类名
String className = getClassName();

// 步骤 2 & 3:生成源码并写入文件
File javaFile = createJavaFile(className, myHandler);

// 步骤 4:编译 Java 文件
Compiler.compile(javaFile);

// 步骤 5 & 6:加载类并创建实例
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");

// 组装完整的 Java 类源码
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 {
// 使用 URLClassLoader 从 target/classes 加载类
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");
}

// 获取系统 Java 编译器(需要 JDK 环境)
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 {
// ========== Demo 1: 简单代理(打印方法名)==========
System.out.println("=== Demo 1: 简单代理 ===");
MyInterface proxy1 = MyInterfaceFactory.createProxyObject(
new MyHandler() {
@Override
public String functionBody(String methodName) {
return "System.out.println(\"" + methodName + "\");";
}
}
);
proxy1.func1(); // 输出: func1
proxy1.func2(); // 输出: func2

// ========== Demo 2: 嵌套代理(添加 before/after 日志)==========
System.out.println("=== Demo 2: 嵌套代理(AOP思想)===");

// 先创建内部代理
MyInterface innerProxy = MyInterfaceFactory.createProxyObject(new PrintHandler());

// 再用 LogHandler 包装,添加日志增强
MyInterface proxy2 = MyInterfaceFactory.createProxyObject(new LogHandler(innerProxy));
proxy2.func1(); // 输出: before -> func1 -> after
}

// 简单处理器:打印方法名
static class PrintHandler implements MyHandler {
@Override
public String functionBody(String methodName) {
return "System.out.println(\"" + methodName + "\");";
}
}

// 包装处理器:添加 before/after 日志(AOP核心思想)
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 动态代理的应用场景

  1. AOP(面向切面编程) - Spring AOP 的核心机制
  2. 事务管理 - 在方法前后自动开启/提交事务
  3. 日志记录 - 统一记录方法调用日志
  4. 权限检查 - 方法调用前检查用户权限
  5. RPC 远程调用 - Dubbo 等框架的代理实现
  6. MyBatis Mapper - 接口只有定义,无实现类

五、易错点记录

  1. 类加载器问题 - 必须使用 URLClassLoader 从 target/classes 加载,默认 ClassLoader 找不到动态编译的类
  2. 包名问题 - 生成的源码必须放在正确的包目录下,否则编译器找不到依赖
  3. JDK vs JRE - 需要 JDK 环境才能使用 ToolProvider.getSystemJavaCompiler()
  4. setProxy 时机 - 嵌套代理时,必须在创建实例后注入 target 字段
  5. 字段名匹配 - 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;
}
}
);