一、💡 一句话理解
核心结论
Java 动态代理会在运行时生成一个代理对象。调用代理对象的方法时,调用会先进入
InvocationHandler.invoke(...),我们就可以在真实方法前后统一增加日志、权限校验、事务等公共逻辑。
可以把它理解成“给真实对象加一个会自动拦截请求的中间人”:调用方只认识接口,不需要知道中间人内部如何记录日志或执行检查。
二、🧭 理论:它是什么
2.1 什么是代理
代理(Proxy)不是业务对象本身,而是一个代替业务对象接收调用的对象。
普通调用是:
调用方 ──────────────> 真实对象加入代理后变成:
调用方 ──> 代理对象 ──> 公共逻辑 ──> 真实对象例如,调用 userService.findUser("小明") 前后,代理可以自动完成:
- 记录方法名和参数;
- 检查当前用户是否有权限;
- 开启、提交或回滚事务;
- 统计方法执行耗时;
- 统一处理异常。
代理的关键点是:调用方仍然按照原来的接口调用,不需要修改业务代码。
2.2 什么是动态代理
静态代理需要我们提前编写代理类的源代码;动态代理则由程序在运行时根据接口生成代理类和代理对象,不需要提前写出具体的代理类文件。
大白话理解
可以把代理想成“代购”。静态代理就是你提前雇好一个代购,还要亲自写好这名代购的工作流程;动态代理则是你只告诉系统“商品接口是什么、统一处理规则是什么”,系统在程序运行时临时生成一个代购。
所以,静态代理是“代理类提前手写好”,动态代理是“只写处理规则,代理类运行时自动生成”。动态代理并不是完全不写代码,而是不需要提前写出具体的
UserServiceProxy类。
例如,静态代理需要自己编写类似下面的代理类:
// 这是理解静态代理的简化示意,不是单独可运行的完整程序。
class UserServiceProxy implements UserService {
private final UserService target;
UserServiceProxy(UserService target) {
this.target = target;
}
@Override
public String findUser(String name) {
// 代理类中手动添加公共逻辑。
System.out.println("记录日志");
// 再手动调用真实对象。
return target.findUser(name);
}
}动态代理则不需要编写 UserServiceProxy 这个类,只需要写统一的处理规则:
// 这是理解动态代理的简化示意;完整可运行代码见 4.3 节。
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("记录日志");
return method.invoke(target, args);
}调用方最终都可以这样调用:
proxy.findUser("小明");调用方式没有变化,区别只在于:静态代理的代理类是我们提前写好的,动态代理的代理类是 Java 在运行时帮我们生成的。
JDK 动态代理的正式定义是:在运行时根据一组接口生成代理类,代理对象上的接口方法调用会被编码后分发给同一个调用处理器。Oracle 官方资料将这个处理器称为 InvocationHandler。Oracle:Dynamic Proxy Classes
2.3 JDK 动态代理的三个核心角色
| 角色 | 白话理解 | 主要职责 |
|---|---|---|
| 业务接口 | 调用方和业务对象之间的约定 | 声明可以调用哪些方法 |
| 真实对象 | 真正完成业务的对象 | 实现接口并执行核心业务 |
InvocationHandler | 代理对象背后的总开关 | 接收方法调用,决定前置逻辑、真实调用和后置逻辑 |
Proxy | JDK 提供的代理工厂 | 根据接口和处理器创建代理对象 |
最容易混淆的一点是:Proxy 负责“生成代理对象”,InvocationHandler 负责“处理代理对象收到的每一次方法调用”。
2.4 JDK 动态代理的限制
JDK 动态代理要求代理目标通过接口暴露方法。也就是说,目标对象通常需要实现一个或多个接口,代理对象也会实现这些接口。
UserService target = new UserServiceImpl();
UserService proxy = ...;调用方只需要持有 UserService 类型的引用,不需要依赖 UserServiceImpl 的具体类型。
如果目标类没有接口,JDK 动态代理不能直接代理这个类;这时通常会考虑基于子类的代理工具,例如 CGLIB 或 Byte Buddy。它们属于其他库,本篇先不展开。
这句话可以这样理解:JDK 动态代理只能代理接口,不能直接代理普通类。
下面这个类没有实现任何接口,所以不能直接使用 JDK 动态代理:
// 这是一个没有接口的普通类。
class UserService {
public void findUser() {
System.out.println("查询用户");
}
}JDK 动态代理的工作方式是“生成一个实现指定接口的代理对象”。因此,目标对象通常需要先实现接口:
// 接口:规定调用方可以使用哪些方法。
interface UserService {
void findUser();
}
// 真实对象:实现接口并完成真正的业务。
class UserServiceImpl implements UserService {
@Override
public void findUser() {
System.out.println("查询用户");
}
}此时就可以让 JDK 动态代理实现 UserService 接口:
UserService 接口
↓
JDK 动态生成一个实现 UserService 的代理对象
↓
调用代理对象的方法
↓
进入 InvocationHandler如果目标类没有接口,CGLIB、Byte Buddy 等工具会采用另一种方式:在运行时生成目标类的子类,并重写可以重写的方法。
注意:下面是另一个独立示例。这里的 PlainUserService 是没有实现接口的普通类,不要和前面示例中的 UserService 接口混淆。
// 这是帮助理解的简化示意,不是 CGLIB 或 Byte Buddy 的实际用法。
// PlainUserService 是没有接口的普通类。
class PlainUserService {
public void findUser() {
System.out.println("查询用户");
}
}
// 代理类通过继承普通类,再重写其中可以重写的方法。
class PlainUserServiceProxy extends PlainUserService {
@Override
public void findUser() {
System.out.println("记录日志");
super.findUser();
}
}因此可以这样记:
| 代理方式 | 代理依据 | 能否代理没有接口的普通类 |
|---|---|---|
| JDK 动态代理 | 生成接口的实现类 | 不能直接代理 |
| CGLIB、Byte Buddy | 生成目标类的子类 | 通常可以代理 |
这里的 ## 只是 Markdown 的标题符号,不是 Java 代码;本篇的下一节标题“## 三、理论:它是怎么工作的”表示新的章节开始。
三、⚙️ 理论:它是怎么工作的
3.1 创建代理对象的三个参数
JDK 动态代理最常用的入口是:
在看下面的代码之前,先说明 UserService 是什么:它是本篇为了演示动态代理而自己定义的业务接口,不是 Java 或 JDK 自带的类。它代表“用户服务”,里面声明了调用方可以使用的方法。
// 业务接口:只规定可以做什么,不负责具体实现。
interface UserService {
// 声明一个查询用户的方法。
String findUser(String username);
}后面会再写一个 UserServiceImpl 来实现这个接口,真正完成查询业务;动态代理则会生成另一个实现 UserService 的代理对象。
Proxy.newProxyInstance(classLoader, interfaces, invocationHandler)三个参数分别是:
| 参数 | 含义 |
|---|---|
classLoader | 用来加载接口和生成代理类的类加载器,通常使用接口或目标对象的类加载器 |
interfaces | 代理对象需要实现的接口数组 |
invocationHandler | 代理对象收到方法调用后要执行的处理器 |
代理对象创建后,可以转换成接口类型:
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class<?>[]{UserService.class},
handler
);这里的 handler 是后面定义的调用处理器对象,暂时可以先理解成“代理收到方法调用后要执行的规则”。
3.1.1 类加载器是什么
ClassLoader(类加载器)属于 JVM 基础知识,具体属于“JVM 的类加载机制”。它的作用是把编译后的 .class 文件加载到 JVM 中,让 Java 程序可以使用这个类。
可以把它理解成“把类送进 JVM 的加载员”:
UserService.class
↓
ClassLoader 加载
↓
JVM 可以使用 UserService动态代理同样需要一个代理类。代理类是在运行时生成的,生成之后也要被加载到 JVM 中,所以 Proxy.newProxyInstance(...) 需要一个类加载器。
UserService.class.getClassLoader()这句话的意思是:
取出加载
UserService接口的那个类加载器,用它来加载动态生成的代理类。
因此,创建代理对象的三个参数可以先这样记:
类加载器:把代理类加载进 JVM
接口:规定代理对象能调用什么方法
handler:规定调用方法时要做什么类加载器不是动态代理最核心的难点。初学动态代理时,可以先把下面这句当作固定写法:
UserService.class.getClassLoader()以后学习 JVM 时,再专门理解类加载过程、类加载器类型和双亲委派机制。
3.1.2 new Class<?>[]{UserService.class} 是什么
这段代码看起来复杂,我们按“由简入难”的顺序拆开。数组可以先理解成一排有编号的盒子,编号从 0 开始。
第一步,先看最简单的数组:
// 创建一个能存放 3 个整数的数组。
int[] numbers = new int[3];
// 把数字放进数组的第 0、1、2 个位置。
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;第二步,创建数组时直接放入元素:
// 创建整数数组,并直接放入 90、80、70。
int[] scores = new int[]{90, 80, 70};上面的写法还可以简写成:
int[] ages = {18, 20, 22};第三步,先理解数组的通用写法:
new 类型[]{元素1, 元素2}有时为了讲解,会把它进一步简写成:
new ... {}这里的 ... 只是“待替换内容”,不是可以直接运行的 Java 代码。把它一步步还原,就是:
// 第一步:先写出创建数组的结构,... 仍然只是占位符。
new ... {}
// 第二步:把 ... 替换成具体的数组类型 Class<?>[]。
new Class<?>[]{}
// 第三步:在大括号中放入数组元素 UserService.class。
new Class<?>[]{UserService.class}第四步,理解上面代码中的各个部分:
| 写法 | 大白话 |
|---|---|
Class<?> | 表示一个类或接口的信息 |
[] | 表示这是一个数组 |
UserService.class | 获取 UserService 接口的信息,不是创建 UserService 对象 |
new Class<?>[]{...} | 创建 Class 信息数组,并在创建时直接放入元素 |
这里的 UserService.class 和下面的 new UserServiceImpl() 不是一回事:
UserService.class // 获取接口的描述信息
new UserServiceImpl() // 创建真实业务对象为了看清数组的创建过程,new Class<?>[]{UserService.class} 还可以拆成两步:
// 先创建一个长度为 1 的 Class 信息数组。
Class<?>[] interfaces = new Class<?>[1];
// 再把 UserService 接口的信息放到数组的第 0 个位置。
interfaces[0] = UserService.class;Proxy.newProxyInstance(...) 的第二个参数要求传入“代理对象需要实现哪些接口”。所以:
new Class<?>[]{UserService.class}就是在告诉 Java:
请生成一个实现
UserService接口的代理对象。
虽然当前示例只有一个接口,但这个参数可以放多个接口:
Class<?>[] interfaces = new Class<?>[]{
UserService.class,
OrderService.class
};简单记忆:
int[] :数组中存放整数
Class<?>[] :数组中存放 Class 信息
UserService.class :拿到 UserService 接口信息
new Class<?>[]{...} :创建 Class 数组并放入接口信息
Proxy :根据这些接口生成代理对象3.2 一次方法调用发生了什么
调用:
String result = proxy.findUser("小明");大致会经历以下过程:
1. 调用方调用 proxy.findUser("小明")
2. 代理对象把方法信息封装成 Method
3. 代理对象把 proxy、Method、参数数组传给 handler.invoke(...)
4. handler 执行前置逻辑
5. handler 使用 method.invoke(target, args) 调用真实对象
6. handler 执行后置逻辑并返回结果
7. 结果回到调用方InvocationHandler.invoke 的三个参数如下:
Object invoke(Object proxy, Method method, Object[] args) throws Throwableproxy:当前收到调用的代理对象;method:本次被调用的方法描述对象;args:调用参数数组。无参数方法时可能为null;- 返回值:必须与接口方法声明的返回类型兼容。
Oracle 的 InvocationHandler 文档明确说明:每个代理对象都关联一个调用处理器,代理方法调用会分发到处理器的 invoke 方法。Oracle:InvocationHandler
3.3 代理和反射的关系
动态代理本身依靠反射 API 完成方法分发:
Proxy.newProxyInstance(...)负责创建代理对象;Method描述当前调用的方法;Method.invoke(...)负责在真实对象上执行方法。
因此,动态代理可以看作是“反射加上一层统一的调用拦截”。如果想先理解 Class、Method 和 Method.invoke,可以先阅读 反射。
3.4 为什么要解包 InvocationTargetException
“解包”可以理解成“拆开包装盒”。InvocationTargetException 是反射帮真正异常套上的外层包装,真正的业务异常在它里面。
InvocationTargetException ← 外面的异常包装盒
↓
真正的业务异常 ← 盒子里面的异常当处理器通过反射调用真实方法时:
method.invoke(target, args);如果真实方法内部抛出异常,反射通常会用 InvocationTargetException 包装它。
例如,真实方法内部发生了业务错误:
// 真实业务方法抛出的异常。
public void pay() {
throw new RuntimeException("余额不足");
}代理处理器捕获到的可能不是直接的“余额不足”,而是外层的 InvocationTargetException。因此需要调用 getTargetException() 取出里面真正的异常,再继续抛出:
try {
return method.invoke(target, args);
} catch (InvocationTargetException exception) {
// 取出真实方法抛出的异常,而不是继续抛出外层包装异常。
throw exception.getTargetException();
}如果不解包,调用方看到的只是:
InvocationTargetException解包之后,调用方才能看到更有用的真实错误:
RuntimeException: 余额不足所以,代理处理器一般应该解包异常,让调用方更容易定位真正的业务问题。
四、🚀 实践:从准备到验证
4.1 前置准备
4.1.1 环境
- JDK 8 或更高版本;
- 一个空目录;
- 新建文件
DynamicProxyDemo.java。
本例只使用 JDK 自带的 java.lang.reflect.Proxy、InvocationHandler 和 Method,不需要添加第三方依赖。
4.2 可以拿来干什么
4.2.1 统一记录日志和耗时
前置条件:真实对象实现业务接口,代理处理器持有真实对象。
long start = System.currentTimeMillis();
System.out.println("开始调用:" + method.getName());
Object result = method.invoke(target, args);
long elapsed = System.currentTimeMillis() - start;
System.out.println("调用结束,耗时:" + elapsed + " ms");
return result;输入是接口方法调用和参数,处理器负责记录方法信息,再把调用转发给真实对象,最后返回真实结果。它适合给多个业务方法增加统一日志,而不把日志代码复制到每个业务类中。
4.2.2 调用前做权限校验
前置条件:处理器可以获得当前用户或权限服务,并能根据 method 判断当前方法需要什么权限。
if (method.getName().equals("deleteUser") && !currentUserIsAdmin()) {
throw new SecurityException("没有删除用户的权限");
}
return method.invoke(target, args);输入是方法名、参数和当前登录状态;处理器先判断是否允许调用,只有通过检查后才执行真实方法。更复杂的项目也可以结合 注解,给方法加上 @RequirePermission,再由处理器通过反射读取注解。
4.2.3 统计执行结果
前置条件:处理器需要在调用前后分别记录时间,并在异常分支记录失败原因。
try {
Object result = method.invoke(target, args);
successCounter.increment();
return result;
} catch (InvocationTargetException exception) {
failureCounter.increment();
throw exception.getTargetException();
}这个用途适合统计成功次数、失败次数和耗时,但不应该把业务逻辑本身塞进通用处理器,否则代理会逐渐变成难以维护的“万能类”。
4.3 完整实践:创建一个日志代理
下面的程序模拟一个用户服务:UserServiceImpl 负责真正查询和删除用户;LoggingHandler 负责在调用前打印参数、调用后打印耗时;调用方只依赖 UserService 接口。
4.3.1 完整代码
文件位置:DynamicProxyDemo.java。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
public class DynamicProxyDemo {
/**
* 业务接口:调用方只依赖这个接口,不直接依赖实现类。
*/
interface UserService {
/**
* 根据用户名查询用户。
*/
String findUser(String username);
/**
* 删除指定编号的用户。
*/
void deleteUser(long userId);
}
/**
* 真实业务对象:真正执行查询和删除操作。
*/
static class UserServiceImpl implements UserService {
@Override
public String findUser(String username) {
// 这里用字符串模拟数据库查询结果。
return "用户:" + username;
}
@Override
public void deleteUser(long userId) {
// 这里用打印语句模拟删除数据库记录。
System.out.println("已删除用户,编号:" + userId);
}
}
/**
* 调用处理器:代理对象收到任何接口方法调用后,都会进入 invoke 方法。
*/
static class LoggingHandler implements InvocationHandler {
// handler 必须保存真实对象,后续才能把调用转发给真实对象。
private final Object target;
LoggingHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
long startTime = System.currentTimeMillis();
// args 可能为 null,所以打印时使用空数组避免阅读和处理上的歧义。
Object[] actualArgs = args == null ? new Object[0] : args;
System.out.println("[前置日志] 方法:" + method.getName()
+ ",参数:" + Arrays.toString(actualArgs));
try {
// 使用反射在真实对象上执行同一个方法。
Object result = method.invoke(target, args);
System.out.println("[返回结果] " + result);
return result;
} catch (InvocationTargetException exception) {
// 真实方法抛出的异常会被反射包装,这里还原真实异常再向外抛出。
throw exception.getTargetException();
} finally {
long elapsed = System.currentTimeMillis() - startTime;
System.out.println("[后置日志] 方法:" + method.getName()
+ ",耗时:" + elapsed + " ms");
}
}
}
public static void main(String[] args) {
// 创建真实对象。它负责业务,不负责统一日志。
UserService target = new UserServiceImpl();
// 通过 JDK 动态代理创建代理对象。
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class<?>[]{UserService.class},
new LoggingHandler(target)
);
// 调用方使用接口调用,表面上和调用真实对象没有区别。
System.out.println(proxy.findUser("小明"));
proxy.deleteUser(1001L);
// 验证:JDK 确实为该对象生成了动态代理类。
System.out.println("是否为代理类:"
+ Proxy.isProxyClass(proxy.getClass()));
System.out.println("代理类名:" + proxy.getClass().getName());
}
}代码中的调用链是:
proxy.findUser("小明")
↓
LoggingHandler.invoke(...)
↓
method.invoke(target, args)
↓
UserServiceImpl.findUser("小明")4.3.2 编译和启动
在 DynamicProxyDemo.java 所在目录执行:
javac DynamicProxyDemo.java
java DynamicProxyDemo4.3.3 预期结果
具体耗时和生成的代理类名称可能不同,但输出结构类似:
[前置日志] 方法:findUser,参数:[小明]
[返回结果] 用户:小明
[后置日志] 方法:findUser,耗时:0 ms
用户:小明
[前置日志] 方法:deleteUser,参数:[1001]
已删除用户,编号:1001
[返回结果] null
[后置日志] 方法:deleteUser,耗时:0 ms
是否为代理类:true
代理类名:com.sun.proxy.$Proxy0在较新的 JDK 中,代理类名的具体格式可能不同;只要最后的 是否为代理类:true 出现,就说明代理对象创建成功。
4.3.4 关键代码复盘
| 代码 | 作用 |
|---|---|
new LoggingHandler(target) | 让处理器保存真实对象 |
Proxy.newProxyInstance(...) | 根据接口和处理器创建代理对象 |
UserService proxy | 让调用方通过接口使用代理 |
invoke(...) | 接收代理对象的每一次方法调用 |
method.invoke(target, args) | 把调用转发给真实对象 |
finally | 无论成功或失败都执行耗时统计 |
4.4 常见报错排查
4.4.1 IllegalArgumentException: ... is not an interface
原因:传给 Proxy.newProxyInstance 的类型不是接口,或者把实现类写进了接口数组。
检查:
new Class<?>[]{UserService.class}这里应该放 UserService 接口,而不是 UserServiceImpl.class。
4.4.2 ClassCastException
原因:代理对象没有实现你转换目标的接口,或者转换成了错误的类型。
检查接口数组和强制转换是否一致:
new Class<?>[]{UserService.class}
UserService proxy = (UserService) ...;4.4.3 NullPointerException 或返回类型错误
原因:invoke 返回的对象必须兼容接口方法的返回类型。接口方法返回基本类型时,也不能返回 null。
例如:
int count();对应的 invoke 必须返回 Integer,不能返回 null 或字符串。
4.4.4 业务异常变成 InvocationTargetException
原因:处理器直接把反射异常向外抛出,没有取出真实异常。
处理方式:
catch (InvocationTargetException exception) {
throw exception.getTargetException();
}五、📌 总结
- JDK 动态代理在运行时生成代理对象,不需要手写具体代理类。
- JDK 动态代理主要面向接口,代理对象实现创建时传入的接口。
Proxy负责创建代理对象,InvocationHandler负责接收方法调用。Method描述当前调用的方法,method.invoke(target, args)将调用转发给真实对象。- 日志、权限、事务、耗时统计等横切逻辑适合放进代理处理器。
- 真实方法抛出的异常可能被
InvocationTargetException包装,通常需要解包后再抛出。
记忆句:接口定义调用方式,Proxy 生成代理对象,Handler 拦截调用,Method 反射执行真实方法。