读完这篇,你能做什么
能写出基于接口注解的代理示例,并识别运行时增强的调用边界。
学习理由、准备要求与知识点
为什么学它
反射和代理解释框架怎样读取元数据并增强调用,也帮助定位增强未生效。
先读这些内容
知识点清单
- 公开成员反射访问
RetentionPolicy.RUNTIME- Target 使用位置
- 接口动态代理
InvocationHandlerInvocationTargetException解包- 接口注解查找
- 代理调用与内部调用
运行前先确认Java 17;源码在本机 Microsoft OpenJDK 17.0.20.1 src.zip 核验,在线链接为上游 jdk-17.0.20-ga。深入模块可在掌握基础后回读。
01先弄懂它是什么
框架怎样在运行时知道一个类有哪些方法?反射提供了查看类型、构造器、字段和方法信息的入口,也能在允许访问时调用它们。Class 表示类型信息,Method 表示方法信息,拿到它并不等于已经调用。getMethods 主要获取包括继承方法在内的公开方法;getDeclaredMethods 看当前类声明的方法,但不会自动绕过访问限制。
注解是在代码上附加的说明数据,本身不会自动开启事务、注册接口或做校验。编译器、处理器或框架必须读取它,再执行相应规则。Retention 决定它保留到哪个阶段,需要反射读取时应明确使用 RUNTIME;Target 决定可以写在哪里。Inherited 也有自己的适用范围,不表示所有方法或字段注解都会自动继承。
JDK 动态代理会在运行时创建一个实现指定接口的对象。调用者调用这个代理的方法时,调用会交给 InvocationHandler,由它决定转给真实对象、记录日志还是拒绝。它不是把任意普通类自动变成子类代理,而且只有经过代理入口的调用才会触发处理器。
处理器还要按接口要求返回正确类型、处理异常,并考虑 equals、hashCode、toString 等方法,不能统一返回 null。框架 AOP 可能采用其他代理方式,还要单独查看框架规则。
文中这几个词,先认一认
- 运行时元数据
- 程序运行时还能读取的声明信息,例如类名、方法参数和保留下来的注解。
- InvocationHandler
- JDK 动态代理收到方法调用后,把代理、方法和参数交给这个处理器执行相应逻辑。
- 保留策略
- 规定注解是否保留在源码、class 文件或运行阶段,以及何时能被读取。
- 反射访问检查
- 找到字段或方法后,仍要按语言和模块规则判断是否有权访问或调用。
02看看它怎么工作
反射框架通常分为发现、验证、缓存和调用四步。启动时扫描明确范围,检查注解与方法签名,建立可用映射;请求时只通过已验证映射定位方法。若每次请求都重新扫描,成本与错误暴露都更晚;若允许用户任意提交类名和方法名,则把过大的执行能力交到了输入边界。
Method.invoke 可能把目标方法抛出的异常包装在 InvocationTargetException 中。调用层应保留实际 cause 并按接口约定转换,而不是只显示反射包装异常。目标异常、访问失败和参数不匹配是不同问题,错误分类能帮助区分业务失败与注册表配置错误。
代理调用经过生成的接口实现进入处理器,处理器再决定委托、记录或拒绝。一个普通目标对象内部直接调用自身方法,未必再次经过外部代理入口。图中显式标出代理引用和目标对象,帮助解释为什么只在类上写注解,不保证所有内部调用都具有相同拦截效果。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["声明 RUNTIME 注解"] --> B["启动阶段扫描与签名验证"]
B --> C["构建允许的调用映射"]
D["调用者持有接口代理"] --> E["InvocationHandler"]
E --> F["委托目标公开方法"]
C --> F
F --> G["返回结果或保留目标异常原因"]按顺序看,每一步做了什么
- 限定要扫描的类型与成员范围。
- 验证注解保留和目标位置。
- 检查公开签名及重复注册。
- 缓存合法方法映射。
- 调用时保留目标异常原因并遵守模块边界。
03对照实现代码
Microsoft OpenJDK 17.0.20.1;java.base/java/lang/reflect/Proxy.java · java.lang.reflect.Proxy#newProxyInstance(ClassLoader,Class[],InvocationHandler)
以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.UndeclaredThrowableException;
public final class SourceModel {
public interface Service { String call(String input); }
public static Service proxy(InvocationHandler handler) {
return new Service() {
@Override public String call(String input) {
try {
// 手写一个固定接口的代理入口,展示方法与参数转发。
return (String) handler.invoke(this,
Service.class.getMethod("call",String.class), new Object[]{input});
} catch (RuntimeException | Error e) { throw e; }
catch (Throwable checked) { throw new UndeclaredThrowableException(checked); }
}
};
}
}真实 Proxy.newProxyInstance 校验处理器并取得适合接口组合的代理构造器,然后创建实例。代理类的生成与缓存还涉及类加载器、接口可见性和模块规则。模型手写单个固定接口实现来展示转发形状,不生成字节码,也不复制真实缓存、访问检查或全部方法处理。
调用处理器时传入的是代理对象、对应接口 Method 和实参数组。模型中的强转说明返回值必须符合接口返回类型;基本类型返回还涉及装箱拆箱,错误返回可能造成类型或空值异常。接口签名并不会因为经过通用 Object 返回值的处理器就失效。
接口没声明的受检异常,代理不能想抛什么就原样抛给调用者,需要按规则包装。模型保留 RuntimeException 和 Error,并包装其他 Throwable,只展示这个受限接口的简单情况。如果接口声明了受检异常,真实代理还会进一步判断,所以不能把模型的统一包装照搬到所有接口。
Object 方法、默认方法、重复接口方法与类加载器边界都属于更完整代理语义,模型没有涵盖。反射与代理应使用公开支持的入口,遇到模块强封装失败时应检查设计与依赖版本,而不是默认 setAccessible 或添加开放所有包的参数来掩盖边界。
代码先调用谁,再调用谁
newProxyInstance验证处理器与接口组合- 查询或建立代理构造路径
- 创建实现接口的代理对象
- 代理方法将 Method 与实参交给
InvocationHandler - 处理器返回值或异常按接口契约传递
04动手写一个例子
构建只允许公开注解方法的路由注册表
扫描处理器当前类声明的 @Route 方法,只接受 public 非 static 的 String method(String) 签名。路由名唯一;调用时只允许已注册名称,保留目标异常原因,不开放私有访问。
- 输入 1
@Route(up) 对应公开up方法,输入ab- 输出 1
AB
- 输入 2
两个方法同名路由dup- 输出 2
启动注册时拒绝
- 输入 3
未知路由- 输出 3
拒绝调用
先扫描全部声明方法,筛出运行时注解,再验证可见性、静态属性、参数和返回类型。发现注解却签名不符合时立即失败,比静默忽略更容易定位配置错误。只扫描当前声明类是明确范围,不自动继承父类路由;若要支持继承,应另行定义覆盖与冲突规则。
用 Map 保存路由名到 Method,putIfAbsent 检测重复。反射返回方法的枚举次序没有稳定业务保证,因此不能靠“后扫描覆盖先扫描”决定冲突赢家。注册完成后保存不可修改映射,请求阶段只做名称查询与已验证调用,不根据外部输入重新查任意方法。
调用失败时区分未知路由、反射访问问题和目标方法异常。InvocationTargetException 的 cause 才是目标抛出的实际错误,包装时保留它。示例没有 HTTP 服务、鉴权或序列化,只研究本地受限映射机制;成为 Web 路由还需更完整的协议与权限层。
为什么这样写是对的注册表中每个名字唯一,并且只对应已验证公开、非静态且签名准确的方法。
启动阶段每个方法检查一次,调用阶段不重复扫描;名称长度、反射开销和目标业务耗时需按实际情况额外计入。 getDeclaredMethods() 返回长度为 m 的方法数组,因此注册期间不能只计算最终映射的 r 项。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;
public class Main {
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
@interface Route { String value(); }
static final class Registry {
private final Object target;
private final Map<String,Method> routes;
Registry(Object target) {
if (target == null) throw new IllegalArgumentException("target");
this.target = target;
Map<String,Method> found = new HashMap<>();
for (Method method : target.getClass().getDeclaredMethods()) {
Route route = method.getAnnotation(Route.class);
if (route == null) continue;
int flags = method.getModifiers();
// 只注册公开、非静态且精确签名的方法,不开启私有访问。
if (!Modifier.isPublic(flags) || Modifier.isStatic(flags)
|| method.getReturnType() != String.class
|| method.getParameterCount() != 1
|| method.getParameterTypes()[0] != String.class
|| !route.value().matches("[a-z]+"))
throw new IllegalArgumentException("invalid route");
if (found.putIfAbsent(route.value(),method) != null)
throw new IllegalArgumentException("duplicate route");
}
routes = Map.copyOf(found);
}
String call(String name, String input) {
Method method = routes.get(name);
if (method == null) throw new IllegalArgumentException("unknown route");
try { return (String) method.invoke(target,input); }
catch (InvocationTargetException e) {
throw new IllegalStateException("handler failed",e.getCause());
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("reflection configuration",e);
}
}
}
public static final class Handler {
@Route("up") public String up(String s) { return s.toUpperCase(java.util.Locale.ROOT); }
@Route("fail") public String fail(String s) { throw new IllegalArgumentException("business"); }
}
public static final class Duplicate {
@Route("dup") public String a(String s) { return s; }
@Route("dup") public String b(String s) { return s; }
}
public static final class Hidden {
@Route("hidden") private String secret(String s) { return s; }
}
static void reject(Runnable action) {
try { action.run(); } catch (IllegalArgumentException e) { return; }
throw new AssertionError("must reject");
}
public static void main(String[] args) {
Registry registry = new Registry(new Handler());
if (!registry.call("up","ab").equals("AB")) throw new AssertionError();
// 未知、重复和私有路由分别在应有边界失败。
reject(() -> registry.call("missing","x"));
reject(() -> new Registry(new Duplicate()));
reject(() -> new Registry(new Hidden()));
reject(() -> new Registry(new Object()).call("up","x"));
// 目标异常原因必须保留,不能只剩反射包装层。
boolean preserved = false;
try { registry.call("fail","x"); }
catch (IllegalStateException e) { preserved = e.getCause() instanceof IllegalArgumentException; }
if (!preserved) throw new AssertionError("cause");
System.out.println("PASS 6");
}
}这些特殊情况也要试一试
- 没有路由的方法被忽略,带注解但非法的方法拒绝。
- 重复名字不依赖扫描顺序覆盖。
- 私有方法不通过
setAccessible开放。 - 目标异常原因保留为 cause。
进阶选读 · 可选深入:类型级缓存与类加载器生命周期
场景框架重复扫描很多对象,插件还可能卸载或热更新。
做法按真实类型缓存已验证元数据,考虑 ClassValue 或明确生命周期清理;缓存键同时关注类加载器身份,避免全局强引用阻止卸载。
复杂度首次扫描 O(m),复用降低发现成本,但调用与缓存维护仍有成本。
好处与代价缓存提高效率却增加失效和生命周期复杂性,不能按类名字符串把不同加载器的类型混合。
05项目里怎么用
反射框架应尽早验证配置,让重复路由、错误签名和缺失注解在启动阶段暴露。运行阶段只接受受限映射的逻辑名称,不让用户任意指定类和方法。注解是声明意图,不是授权凭证;真正权限控制应在业务和协议边界明确实施。
异常与性能观察要分层。反射调用开销可能在高频微操作中可见,也可能被数据库和网络远远覆盖;先缓存可复用元数据,再测实际路径,不必为了猜测性能提前采用不稳定内部接口。方法句柄等替代方案也有自己的访问和适配成本。
模块、代理方式和框架版本都会影响哪些成员可访问、代理怎样生成。小例子能在未命名模块运行,不代表各种部署都兼容。优先使用公开 API、明确注册方式和版本要求;动态加载和扫描越多,越要说明扫哪些类、缓存何时清理、错误怎样定位。
容易踩的坑
- 没有 RUNTIME 保留却期待反射读到注解。
- 把
getDeclaredMethods理解为自动允许调用私有方法。 - 忽略
InvocationTargetException的实际 cause。 - 认为目标对象内部调用必然再次经过外部代理。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 显式注册接口 | 类型检查与调用路径清楚 | 需要手工配置更多连接 | 稳定核心业务和小型框架 |
| 反射注解发现 | 减少重复注册并可扩展 | 启动验证与访问边界更复杂 | 受控框架基础设施 |
| JDK 接口代理 | 可集中处理接口调用 | 受接口、异常和类加载边界约束 | 装饰、拦截与适配 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01注解为什么不会自动执行功能?
展开参考答案与推导
它只是元数据,需要编译器、处理器或框架读取后实施行为。没有消费者时,写一个注解不会注册路由、开启事务或校验字段,保留策略也决定消费者能否看到它。
02发现私有 Method 是否表示可以 invoke?
展开参考答案与推导
不表示。反射发现与访问权限检查不同,Java17 模块边界还可能增加限制。应使用公开支持的访问路径,不把开放全部封装当作默认解决方式。
03JDK 动态代理为什么通常围绕接口?
展开参考答案与推导
它创建实现指定接口的代理类,并把接口调用送到 InvocationHandler。它不是任意具体类的通用子类生成器;其它框架代理机制可能不同,需要分开说明。
换个情况,还会处理吗
01路由名字重复时有时调用A有时调用B,应该怎么修?
展开参考答案与推导
不要依赖反射扫描顺序覆盖。注册阶段检测重复并明确拒绝或采用显式优先级,只有规则确定后才建立映射;运行时猜测赢家会让配置错误难以复现。
02日志只有 InvocationTargetException,业务原因在哪?
展开参考答案与推导
查看 getCause,它通常保存被调用方法抛出的目标异常。调用边界应保留或转换这个原因,同时区分访问失败和参数错误,避免所有反射失败都显示同一包装名称。
03插件卸载后内存仍保留,反射缓存可能哪里有问题?
展开参考答案与推导
全局强引用缓存可能持有 Class、Method 或处理器对象,从而连带保留类加载器。检查缓存键值生命周期并设计清理或适当类型级缓存,不能仅用类名字符串混同不同加载实例。
写代码试一试
01增加错误返回类型的注册测试
- 输入 / 条件
@Route方法返回int- 输出 / 预期
拒绝注册
提示 签名验证应发生在调用前。
展开参考答案与推导
添加公开 int method(String) 的处理器并断言构造 Registry 失败。若等调用后强转才失败,说明启动验证遗漏了返回类型,错误边界过晚。
02允许继承的公开路由
- 输入 / 条件
父类公开带注解方法- 输出 / 预期
按新覆盖规则注册
提示 先定义同名与重写语义。
展开参考答案与推导
可以从 getMethods 获取公开继承方法,但必须明确重写后注解是否存在、同路由冲突如何处理及桥接方法是否参与。不能只换一个反射 API 就声称继承规则完整。
03验证普通未注解方法不会开放
- 输入 / 条件
公开helper方法无Route- 输出 / 预期
未知路由
提示 白名单来自显式注解。
展开参考答案与推导
向 Handler 增加 public helper(String),调用同名逻辑路由应仍被拒绝。这个测试确认注册范围不是所有公开方法,避免辅助能力意外进入执行入口。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能写出基于接口注解的代理示例,并识别运行时增强的调用边界。
本篇重点加练
重点练代理调用与异常传播,检查基础设施包装之后业务失败能否被准确识别。
InvocationHandlerInvocationTargetException解包
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
设计用于公开计算方法的 @Operation 注解,声明允许标注位置与运行时读取要求;说明普通调用、反射发现和真正执行方法分别需要哪些条件。
- 提交内容
- 注解定义、发现步骤及访问边界说明。
- 怎样算完成
- 声明元数据与执行行为分开
- 访问权限要求明确
练到的知识点公开成员反射访问 · RetentionPolicy.RUNTIME · Target 使用位置
- 作业 02代码推演
接口 Calculator 有 add 和 divide,
InvocationHandler记录方法名后转发。画出代理、处理器、目标对象的关系,预测直接调用目标与经代理调用的事件差异。- 提交内容
- 对象图、调用样例与验证记录。
- 怎样算完成
- 区分接口调用入口与目标对象
- 处理器职责不混入接口定义
练到的知识点接口动态代理 · InvocationHandler
综合应用
- 作业 03排错实验
分别把注解放在接口方法和实现方法上,通过两种 Method 获取方式读取;让目标方法抛出自定义异常,记录反射调用的异常结构。
- 提交内容
- 最小 Java 实验、自写发现/异常测试与实际记录。
- 怎样算完成
- 注解来源可定位
- 保留真正业务失败原因
练到的知识点InvocationTargetException 解包 · 接口注解查找
- 作业 04面试场景
代理计时覆盖 process 方法,但 process 内部调用本对象 save。业务报告要求单独统计 save,设计三种调用路径的观测实验,说明报告应如何避免误计。
- 提交内容
- 调用路径图、日志字段和验收矩阵。
- 怎样算完成
- 内部调用与代理调用有独立证据
- 计时范围与名称一致
练到的知识点代理调用与内部调用
- 作业 05编程实践
实现只登记公开、带 @Operation 且单个
String参数的方法目录,输入操作名重复、返回类型不符合约定和未注解方法时要有明确政策;禁止任意类名输入执行。- 提交内容
- Java 实现、自写注册/调用/边界测试和运行证据。
- 怎样算完成
- 注册合同逐项验证
- 暴露范围可审计
- 异常信息不丢失原因
练到的知识点公开成员反射访问 · RetentionPolicy.RUNTIME · Target 使用位置 · InvocationTargetException 解包 · 接口注解查找
- 作业 06编程实践
为计算接口实现记录调用次数的代理,目标可能返回
null或抛出业务异常;要求审计记录与调用者看到的结果符合预先声明的合同。- 提交内容
- Java 实现、自写成功
/null/异常测试和运行证据。 - 怎样算完成
- 包装前后行为逐类比较
- 记录次数可被独立验证
练到的知识点InvocationHandler · InvocationTargetException 解包
- 作业 07代码推演
代理接收业务方法调用和
toString、equals、hashCode调用。为这些入口制定处理合同,记录 Method 来源与参数,禁止默认所有方法都是业务操作。- 提交内容
- 入口矩阵和最小验证记录。
- 怎样算完成
- 对象基础方法被纳入检查
- 日志本身没有制造无界调用
练到的知识点InvocationHandler
- 作业 08排错实验
让目标方法抛出带 cause 的异常,经直接调用、
Method.invoke和代理调用三条路径观察异常类型与原因链;只使用公开成员。- 提交内容
- Java 实验、自写原因保留检查及路径对照。
- 怎样算完成
- 同一业务故障能跨路径追踪
- 包装结构与业务类别分开
练到的知识点InvocationTargetException 解包
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
完成模块系统和类加载基础后:插件重新加载时代理与 Method 缓存仍存在,设计缓存生命周期与公开接口兼容性验收,说明哪些访问限制不应靠绕过来解决。
- 提交内容
- 生命周期图、释放检查和兼容性矩阵。
- 怎样算完成
- 缓存所有者明确
- 不把反射当作无限权限入口
练到的知识点公开成员反射访问 · 接口动态代理 · InvocationHandler · 代理调用与内部调用
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 条件变化
接口从同步返回值改为返回稍后完成的任务对象。计时和错误统计应以哪一刻为边界?比较“方法调用完成”与“业务任务完成”两种监测需求。
- 提交内容
- 双层时序图和统计合同。
- 怎样算完成
- 失败发生时点可区分
- 结论注明异步能力前置
练到的知识点InvocationHandler · InvocationTargetException 解包
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
Java 17 API:Proxy (新标签页)↗
阅读接口代理、
InvocationHandler与Object方法的调用规则。 - 实践参考文档英文
Java 17 API:Retention (新标签页)↗
确认默认 CLASS 保留策略以及 RUNTIME 对反射读取的作用。
- 深入参考文档英文
Spring Framework:代理机制 (新标签页)↗
阅读 JDK
/CGLIB代理和内部调用限制,把增强是否生效落实到调用路径。 - 深入参考文档英文
OpenJDK 17u java.lang.reflect.Proxy (新标签页)↗
定位
Proxy.newProxyInstance的接口代理构造入口,模型仅手写固定接口的处理器转发形状。 - 深入参考文档英文
Java17 Method API (新标签页)↗
核对 invoke 参数、访问检查和
InvocationTargetException。