2.8 Java帝国之动态代理
沿接口取得、代理创建、调用拦截、目标委托和返回抛错追踪动态代理生命周期,用递归陷阱和异常边界实验解释横切行为。
学习目标
- 能沿取得接口、创建代理、拦截调用、委托目标和返回或抛错追踪一次动态代理调用
- 能用
InvocationHandler的输入、目标调用、返回值和异常边界解释横切行为如何插入 - 能在正常、边界和故障场景中定位代理自调用、接口缺失或异常吞没的首个偏离,并从干净状态重放
为什么需要这一机制
“动态代理”解决的不是把业务对象变得神秘,而是把日志、事务、鉴权等横切行为放到统一的调用入口。手写在每个业务类里的重复代码,容易漏改、顺序不一,也很难证明每次调用都经过同一条策略。JDK 动态代理为接口创建代理实例,InvocationHandler 在转发真实方法前后插入行为,并必须正确处理返回值与异常。
核心合同
↡本页把动态代理重构为:代理接收接口方法调用,处理器插入横切行为,随后把调用委托给独立目标并保留返回或异常语义。最小合同写成:
这里的 proxy 是调用入口,method 是接口方法,args 是本次参数;处理器还必须绑定一个独立 target。最终结果不是“处理器执行过”这么简单,而是前置行为、目标调用、后置行为和异常传播共同构成的轨迹。
四个官方概念到机制证据
2.8 Java帝国之动态代理
↡由接口、代理实例、InvocationHandler、独立目标和返回或异常路径组成的 JDK 动态代理运行模型。它描述的是一条可观察的调用链,不是“自动加功能”的口号。每次运行都要保存代理身份、目标身份、方法签名、参数、前后置事件和最终判定。
深夜奏对
↡把动态代理看成多个角色协作的调用对话:调用者进入代理,处理器决定横切行为,目标对象完成真实业务。这个概念用于检查角色边界:调用者不应直接知道处理器细节,处理器不应把代理入口误当成目标,目标也不应丢失原始异常。
明察暗访
↡通过代理入口观察调用前后的证据,把不可见的日志、事务或鉴权行为变成可核对的事件序列。观察不能只看最终结果。要比较 before → target → after 与 before → target throws → after/propagate 两条路径,才能知道横切行为是否覆盖了失败场景。
Java 动态代理
↡JDK 基于接口创建代理实例的机制,InvocationHandler 接收方法和参数并负责委托、返回值处理与异常传播。它受接口约束:接口集合、方法签名和返回类型必须满足代理合同。没有接口或目标绑定错误时,正确行为是记录拒绝原因,而不是伪造成功。
五个节点到机制证据
取得接口
↡读取目标对象实现的接口集合与方法签名,为 JDK 动态代理确定可以暴露的调用合同。保存接口名称、方法签名和目标身份;缺少接口或签名不兼容时,在代理创建前拒绝。
创建代理
↡使用接口集合、类加载器和 InvocationHandler 创建代理实例,形成新的调用入口。记录类加载器、接口顺序、代理身份和处理器绑定。代理不是目标的复制品,它只负责承接符合接口的调用。
拦截调用
↡代理把接口方法、参数和自身引用交给 InvocationHandler,处理器在目标执行前后插入横切行为。为每次调用生成一个事件 ID,保存前置行为、方法和参数摘要。处理器再次调用代理自身是需要立即拒绝的故障信号。
委托目标
↡处理器使用独立目标对象执行真实方法,并将调用结果或异常返回到代理边界。记录目标身份、方法调用次数和目标产生的结果或异常。不能用 proxy 代替 target,否则会把一次调用重新送回拦截器。
返回或抛错
↡处理器结束前置与后置行为后,按接口合同返回兼容值,或保留目标异常的类型和传播语义。分别验证正常返回、目标抛错和处理器自身抛错。后置清理是否执行、异常是否被包装或吞掉,都属于可测试合同。
最小可重放实现
interfaces = target.interfaces()
proxy = createProxy(loader, interfaces, handler(target))
result = proxy.call(args)
assertTrace(before, target, after, resultOrThrown)
assertTrue(resetAndRun() == baselineTrace)这段草图只表达接口代理合同,不复制书中叙事或代码。实际复核应保存接口列表、加载器、代理与目标身份、事件顺序、返回值、异常和复位结果。
五步复核一条代理调用
1. 固定接口和目标
记录接口方法、目标对象、返回类型和预期异常。先预测基线的 before → target → after → return 轨迹。
Lab
代理入口与目标委托实验
一次只改变目标绑定或异常路径,观察事件顺序、调用深度和最终传播。
处理器前置后调用独立目标,随后返回结果
proxy=P1 → before → target=T1.call → after → return 200
判定
通过:目标调用一次,事件顺序与返回值一致
当前场景:基线委托;记录代理/目标身份、方法、前后置事件、调用深度、异常链和复位。
正常、边界与故障证据
| 场景 | 只改变的变量 | 预期判定 | 必存证据 |
|---|---|---|---|
| 正常 | 接口、目标和处理器均兼容 | 前置、目标、后置顺序稳定,返回值一致 | 身份、方法、事件、返回值 |
| 边界 | 无接口、返回类型边界或目标抛错 | 在代理创建或异常路径明确拒绝/传播 | 接口集、类型、异常、清理事件 |
| 故障 | 处理器自调用代理或吞掉异常 | 首个递归/传播偏离可定位,修复后重放 | 调用深度、目标次数、异常链、复位 |
专属因果实验
先运行基线,预测代理调用的五个节点和事件顺序;再一次只切换目标绑定、接口集合或异常路径。实验状态要同时显示代理/目标身份、调用深度、目标调用次数和结果判定,避免把“有日志”误当成“委托正确”。
Lab
代理入口与目标委托实验
一次只改变目标绑定或异常路径,观察事件顺序、调用深度和最终传播。
处理器前置后调用独立目标,随后返回结果
proxy=P1 → before → target=T1.call → after → return 200
判定
通过:目标调用一次,事件顺序与返回值一致
当前场景:基线委托;记录代理/目标身份、方法、前后置事件、调用深度、异常链和复位。
故障诊断:从代理入口向目标边界找首错
- 核对接口集合:确认目标实现的接口、方法签名和返回类型满足 JDK 代理合同。
- 核对身份绑定:比较代理、处理器和目标对象的身份,排除处理器保存了代理而非目标。
- 核对事件顺序:检查前置、目标、后置事件,定位递归增长、目标零调用或后置缺失。
- 核对异常传播:区分目标异常、处理器异常和包装异常,从清空代理与缓存的基线重放。
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 2.8 Java帝国之动态代理
由接口、代理实例、处理器、目标对象和返回/异常路径组成的调用模型。
- 深夜奏对
调用者、处理器和目标对象之间的角色分工与调用对话。
- 明察暗访
用前后置事件观察代理横切行为的证据方法。
- Java 动态代理
基于接口创建代理实例并由 InvocationHandler 负责转发的 JDK 机制。
- 取得接口
读取接口集合与方法签名的代理准备阶段。
- 创建代理
用接口、加载器和处理器形成代理调用入口的阶段。
- 拦截调用
处理器接收代理方法和参数并插入横切行为的阶段。
- 委托目标
处理器调用独立目标对象执行真实方法的阶段。
- 返回或抛错
按合同返回兼容值或保留异常传播语义的终点。
练习
练习
问题 1(2.8 Java帝国之动态代理、深夜奏对): 为什么处理器中调用 proxy.method() 可能造成无限递归?
问题 2(明察暗访): 动态代理为什么必须先检查接口集合和方法签名?
问题 3(Java 动态代理): 目标方法抛错时,如何证明代理没有吞掉异常?
本页小结
2.8 Java帝国之动态代理的关键不是给业务对象贴上“自动增强”的标签,而是能沿取得接口、创建代理、拦截调用、委托目标和返回或抛错保存证据。完成标准是识别代理自调用、接口约束和异常传播的首个偏离,并用清空状态后的重放证明横切行为可复现。