Java · 反射与动态代理

框架的魔法引擎 — Class 对象 → Method → invoke, JDK 代理只认接口, CGLIB 生成子类, Spring 靠这套在运行时织入 @Transactional

反射调用链路 (贵在哪) Class<User> — 方法区元数据 类加载一次全局唯一: 字段表/方法表/注解 getDeclaredMethod("pay", long.class) 返回 Method 元数据句柄 · 名字拼错 = NoSuchMethodException setAccessible(true) 突破 private 跳过访问检查; JDK 9+ 模块封装下需 --add-opens method.invoke(target, args) 对比直接调用多付: ① 访问检查 ② 参数装 Object[]+装箱 ③ 方法体无法内联 — 三税叠加, 但可被缓存缓解 单次调用开销量级 (JMH 示意) 直接调用 ~1 ns 缓存 Method ~5 ns 裸反射查找 ~30 ns+ 更现代的选择: MethodHandle / VarHandle 更接近静态调用且可被 JIT 内联 代价: Lookup 私有访问权限敏感, 写法更繁琐 反射绕过泛型检查 往 List<String> 塞 Integer: 编译期无感 运行时才在读取处炸 — 锅在远处 动态代理双通道 目标: 给方法织入限流/日志/事务 JDK 动态代理 — 只能代理接口 Proxy.newProxyInstance(loader, 接口[], handler) ↓ 运行时生成 $Proxy0 implements UserApi $Proxy0.pay(100) 字节码里直接转发给 handler ↓ InvocationHandler.invoke(proxy, m, args) 你的拦截逻辑 (限流/日志/事务) 在这里 ↓ m.invoke(target, args) 反射转发真对象 限制: 目标类没实现接口 → 直接失败 CGLIB / ByteBuddy — 生成子类 Enhancer + MethodInterceptor (ByteBuddy 同理) ↓ 运行时生成 SubClass extends UserApi SubClass.pay(100) { intercept(...) } super.pay(100) 调用真实现 ↓ 重写非 final 方法 → 拦截器接管 无接口的类也能织入 (直接 new 出来的业务类) 限制: final 类不可继承 · final 方法静默不拦截 Spring 选型: 有接口默认 JDK / 无接口 CGLIB Boot 2+ proxyTargetClass=true 一律 CGLIB 两者都是"运行时生成字节码", 与 python 装饰器殊途同归 框架落地: @Transactional 代理对象 Proxy#invoke (洋葱最外层) 事务切面 getConnection + setAutoCommit(false) 业务方法 orderService.create(cmd) SQL 执行 (Mapper) INSERT t_order ... UPDATE t_stock ... 异常向上冒泡时 每一层都能拦截 但只有切面层 决定 commit / rollback 正常返回 → commit 异常抛出 → rollback + 异常继续外抛 容器里注入的是代理, 不是裸对象 经典失效: this 自调用 create() 里 this.validate() 直连内部方法 不走代理 → @Transactional/@Cacheable 全失效 正解: 拆 Bean / 自注入代理 / AopContext.currentProxy() 代理织入的是"别人调你", 不是"你调你" Legend 类元数据/代理外壳 JDK 代理通道 CGLIB 通道/业务 SQL/访问检查税 限制/失效区

反射 = 运行时查元数据

  • • Class 对象是方法区元数据的入口
  • • getDeclared 看本类, get 系只看 public 含继承
  • • 慢在访问检查/参数封装/无法内联, 可缓存缓解

代理 = 运行时生成字节码

  • • JDK 代理: $Proxy0 实现接口, 只认接口
  • • CGLIB/ByteBuddy: 生成子类, final 是盲区
  • • Spring 容器里注入的默认就是代理对象

注解是代理的路标

  • • @Retention(RUNTIME) 才能运行时读到
  • • 启动期扫描缓存, 运行期零查找
  • • this 自调用不走代理 — 织入失效第一名

💡 一句话理解

反射是把"类的结构当数据读": 拿到 Class<User> 就能在运行时列出字段/方法/注解, 甚至调用私有方法 — 代价是每次 invoke 都比直接调用多付访问检查、参数封装和无法内联的"三税"。动态代理则更进一步: 运行时直接生成一个新类($Proxy0 或子类), 在每个方法调用前后插入你的逻辑 — Spring 的 @Transactional、MyBatis 的 Mapper、RPC 客户端的桩, 全是这套机制的变体。理解了"容器里拿到的其实是代理", AOP 失效的玄学案例(自调用/静态方法/final 方法)就都有了机械解释。

🧠 必知必会 必考 & 必会

三种拿 Class
User.class(类字面量, 不触发初始化)、user.getClass()(实例)、Class.forName("com.x.User")(按名加载并初始化, JDBC 老代码常见)。同一个类加载器里 Class 对象全局唯一。
Class<User> a = User.class;                // 类字面量, 不触发 static 初始化
Class<?> b  = new User().getClass();      // 从实例
Class<?> c  = Class.forName("com.x.User"); // 按名加载并初始化
a == b && b == c;                          // → true  同加载器全局唯一
getDeclaredXxx vs getXxx
getDeclaredFields/Methods 只看本类(含 private 不含继承); get 系看 public 含继承。想拿父类 private 字段要沿 getSuperclass() 逐层遍历 — 框架扫描都这么写。
Class<?> c = Child.class;
c.getDeclaredFields().length;   // 只看本类 (含 private, 不含继承)
c.getFields().length;           // 只看 public, 含父类 public
// 父类 private 字段: 沿 c.getSuperclass() 逐层 getDeclaredFields
setAccessible
突破 private 访问检查。JDK 9+ 模块系统封装 JDK 内部 API 后, 对 java.* 内部成员会抛 InaccessibleObjectException, 需要 --add-opens java.base/java.lang=ALL-UNNAMED 这类启动参数。
Field f = Point.class.getDeclaredField("x");
f.setAccessible(true);          // 突破 private
// JDK 9+ 对 java.* 内部成员 → InaccessibleObjectException
// 需 --add-opens java.base/java.lang=ALL-UNNAMED
反射慢在哪
① 方法查找遍历 ② 每次参数装箱进 Object[] ③ 访问检查 ④ 方法体无法内联优化。缓存的 Method + setAccessible 能砍掉大头, 极致性能上 MethodHandle。
m.invoke(target, 100L);       // 多付: 装箱 Object[] + 访问检查 + 无法内联
// 量级: 直接调用 ~1ns / 缓存 Method ~5ns / 裸查找 ~30ns+
缓存 Method
getDeclaredMethod 每次都做查找拷贝; 生产代码在 static 块解析一次存 static final Method。注意找不到时抛 NoSuchMethodException — 静态块里应转成 ExceptionInInitializerError 快速失败。
private static final Method PAY;
static {
    try { PAY = Payment.class.getDeclaredMethod("pay", long.class); }
    catch (NoSuchMethodException e) { throw new ExceptionInInitializerError(e); }
}
JDK 动态代理原理
Proxy.newProxyInstance(loader, interfaces, handler) 运行时合成 $Proxy0 实现 given 接口, 每个方法体只是把 (proxy, Method, args) 转发给 InvocationHandler.invoke — 只能代理接口是字节码层面的硬限制。
UserApi proxy = (UserApi) Proxy.newProxyInstance(
    loader, new Class<?>[]{UserApi.class},
    (p, m, args) -> m.invoke(target, args));  // $Proxy0 只做转发
// 只能代理接口: 生成的是 implements UserApi 的新类
CGLIB / ByteBuddy
生成目标类的子类, 重写非 final 方法转给 MethodInterceptor(intercept 里调 super 或 MethodProxy)。无接口可用是最大优势; ByteBuddy 是现代替代(Spring 6 底层), CGLIB 已停更。
Payment p = new ByteBuddy().subclass(Payment.class)
    .method(named("pay")).intercept(MethodDelegation.to(monitor))
    .make().load(loader).getLoaded().getConstructor().newInstance();
// 生成子类: 无接口也能织入; final 类/方法是盲区
Spring 怎么选
目标有接口 → 默认 JDK 代理; 无接口 → CGLIB; Boot 2+ 默认 proxyTargetClass=true 全走 CGLIB, 避免"注入类型必须是接口"的心智负担。同类内部调用两种代理都不拦截。
// B implements A → 默认 JDK 代理: 注入必须按接口类型
A bean = context.getBean(B.class);      // 对: 按接口注入
// B b2 = context.getBean(B.class);  ✗ JDK 代理不是 B
// Boot 2+ proxyTargetClass=true 一律 CGLIB, 按类也行
@Retention 三档
SOURCE(编译后丢弃, 如 @Override)、CLASS(进 class 文件但运行时不可见)、RUNTIME(反射可读)。框架注解(@Transactional/@Column)必须 RUNTIME, 写成 CLASS 运行时静默读不到。
@Retention(RetentionPolicy.RUNTIME)     // 框架注解必须 RUNTIME
@interface Table { String value(); }
@Table("t_order") class Order {}
Order.class.getAnnotation(Table.class).value();  // → "t_order"
@Inherited 误区
类上的注解默认子类读不到; 标了 @Inherited 才沿继承链向上找。且它只对类生效 — 接口上的注解对实现类永远不可见, 这是"注解找得到吗"排障的第一怀疑点。
@Inherited @interface Vip {}
@Vip class Base {}
class Sub extends Base {}
Sub.class.isAnnotationPresent(Vip.class);   // → true (仅类级+@Inherited)
// 接口上的注解对实现类永远不可见, @Inherited 也救不了
InvocationTargetException
invoke 抛出的目标方法异常会被包一层; 想拿真实异常必须 e.getCause()。catch (IOException e) 直接套在 invoke 外面是编译不过的经典错误。
try { m.invoke(target, args); }
catch (InvocationTargetException e) {
    Throwable real = e.getCause();       // 关键: 真实异常在 cause 里
}
// catch (IOException e) 套在 invoke 外 → 编译不过
MethodHandle/VarHandle
jsr 292 产物: 类型精确的间接调用, JIT 可内联, 性能接近静态调用; 但 MethodHandles.Lookup() 只信任创建它的上下文, 私有访问要用 privateLookupIn。
MethodHandle mh = MethodHandles.lookup()
    .findVirtual(String.class, "length", MethodType.methodType(int.class));
int n = (int) mh.invoke("abc");      // → 3, JIT 可内联
反射 vs 泛型
泛型是编译期检查, 擦除后 List 里只有 Object; 反射 set 能绕过检查往 List<String> 塞 Integer — 编译无感, 读取处 ClassCastException 才炸。安全边界在编译器, 不在 JVM。
List<String> list = new ArrayList<>();
Method add = List.class.getMethod("add", Object.class);
add.invoke(list, 42);              // 反射绕过泛型检查, 编译无感
String s = list.get(0);            // → ClassCastException 炸在读取处

🏭 生产实战 real world

场景 1 · 自研轻量 ORM: 注解扫表 + 反射读写字段

报表服务不想为 200 张表引入 MyBatis 全家桶; 注解声明映射, 启动时扫一次缓存, 热路径零查找:

@Table("t_order")
public class Order {
    @Column("id")      private Long id;
    @Column("sku_id") private String skuId;      // 驼峰 ↔ 下划线由注解说了算
    @Transient        private BigDecimal feeCached; // 不落库的字段跳过
}
// 启动期解析一次: Field → 列名 缓存住, 运行期不再 getDeclaredFields
static Map<Field, String> columns(Class<?> clazz) {
    return Arrays.stream(clazz.getDeclaredFields())
        .filter(f -> f.isAnnotationPresent(Column.class))
        .peek(f -> f.setAccessible(true))               // 免掉每次 invoke 的访问检查
        .collect(toMap(f -> f, f -> f.getAnnotation(Column.class).value()));
}

读写各一行: f.set(o, rs.getObject(col, f.getType())) 与 f.get(o) 拼 SQL, 200 张表零手写映射。

场景 2 · 通用 CSV 导出器: 注解即标题, 字段序即列序

每张导出表手写表头/取值/空值处理, 改一个字段要动三处; 反射 + 注解一次收口:

static String toCsv(List<?> rows, Class<?> type) throws IllegalAccessException {
    List<Field> cols = new ArrayList<>();
    StringBuilder head = new StringBuilder();
    for (Field f : type.getDeclaredFields()) {
        Csv h = f.getAnnotation(Csv.class);
        if (h == null) continue;               // 没标注解 = 不导出, 新增字段默认安全
        f.setAccessible(true);
        cols.add(f); head.append(h.value()).append(',');
    }
    StringBuilder body = new StringBuilder(head).append('\n');
    for (Object row : rows) {
        for (Field f : cols)
            body.append(escape(f.get(row))).append(','); // 转义防 CSV 注入(= + - @ 开头)
        body.append('\n');
    }
    return body.toString();   // 大数据量换 Stream 逐行写出, 别攒内存
}

场景 3 · JDK 动态代理: 接口级限流 + 访问日志装饰器

给第三方支付接口加限流与审计, 不想改任何实现代码 — 12 行代理搞定:

@SuppressWarnings("unchecked")
PayApi limited = (PayApi) Proxy.newProxyInstance(
    loader, new Class<?>[]{PayApi.class},
    (proxy, method, args) -> {
        Limit lim = method.getAnnotation(Limit.class);      // 方法级 QPS 配置
        if (lim != null && !acquire(lim, method.getName()))
            throw new BizException(TOO_BUSY, Map.of("m", method.getName()));
        long t0 = System.nanoTime();
        try { return method.invoke(target, args); }
        catch (InvocationTargetException e) {
            throw (e.getCause() instanceof RuntimeException r) ? r
                  : new BizException(PAY_FAILED, Map.of(), e.getCause()); // 拆包再抛
        } finally { accessLog(method.getName(), t0); }
    });

场景 4 · ByteBuddy 给无接口的业务类织入监控

老代码 Payment 类没实现任何接口, JDK 代理无从下手; 子类代理 + 委托拦截:

Class<? extends Payment> enhanced = new ByteBuddy()
    .subclass(Payment.class)
    .method(ElementMatchers.isPublic().and(ElementMatchers.not(isSynthetic())))
    .intercept(MethodDelegation.to(new TimedInterceptor()))
    .make().load(loader).getLoaded();
Payment p = enhanced.getDeclaredConstructor().newInstance();

static class TimedInterceptor {
    @RuntimeType public Object intercept(@SuperCall Callable<?> call) throws Exception {
        long t0 = System.nanoTime();
        try { return call.call(); }
        finally { metrics.observe(System.nanoTime() - t0); } // P99 直方图埋点
    }
}
// 注意: 生成的是子类实例, 别再 new 原类 — 拦截就白做了

场景 5 · 运行时注解做路由注册 (mini MVC)

Spring MVC 扫 @RequestMapping 的本质就是这段; 启动期建索引, 运行期 O(1) 派发:

static Map<String, Handler> scan(Class<?>... controllers) {
    Map<String, Handler> routes = new HashMap<>();
    for (Class<?> c : controllers)
        for (Method m : c.getDeclaredMethods()) {
            Get g = m.getAnnotation(Get.class);          // @Retention 必须 RUNTIME
            if (g != null) {
                m.setAccessible(true);
                routes.put(g.value(), new Handler(c, m));  // "GET /orders/:id" → 方法
            }
        }
    return routes;   // 启动时校验路由冲突, 快速失败优于 404 玄学
}
// 派发: routes.get(path).invoke(controllerInstance, boundArgs)

场景 6 · Method 缓存 + setAccessible 的性能优化 (基准对比)

规则引擎每请求反射调用 500 次, profiler 显示 40% CPU 在 getDeclaredMethod; 缓存改造:

// 差: 每次调用都查找 (遍历方法表 + 权限检查 + 参数匹配)
Method m = target.getClass().getDeclaredMethod("pay", long.class);
m.invoke(target, 100L);

// 好: static final 缓存 + 一次性 setAccessible
private static final Method PAY;
static {
    try { PAY = Payment.class.getDeclaredMethod("pay", long.class);
           PAY.setAccessible(true); }
    catch (NoSuchMethodException e) { throw new ExceptionInInitializerError(e); }
}
// JMH 量级: 裸查找 ~30ns → 缓存+免检查 ~6ns → MethodHandle ~3ns → 直接调用 ~1ns
PAY.invoke(target, 100L);   // 压测: 热路径 CPU 从 40% 降到 8%

场景 7 · 反射对象 diff: 审计日志的变更留痕

工单审核要展示"改了哪些字段, 从什么改成什么"; 不想为每个 DTO 手写 equals/diff:

static Map<String, Object[]> diff(Object before, Object after) throws IllegalAccessException {
    Map<String, Object[]> changes = new LinkedHashMap<>();
    for (Field f : before.getClass().getDeclaredFields()) {
        if (Modifier.isStatic(f.getModifiers()) || f.isAnnotationPresent(Transient.class))
            continue;                                  // 静态/瞬态不参与 diff
        f.setAccessible(true);
        Object a = f.get(before), b = f.get(after);
        if (!Objects.equals(a, b)) changes.put(f.getName(), new Object[]{a, b});
    }
    return changes;   // 审计日志: {amount=[100, 80], status=[NEW, PAID]}
}

场景 8 · 插件化 SPI: 新支付渠道丢 jar 即生效

支付渠道频繁新增, 不想每次改主工程代码发版; ServiceLoader 按声明发现实现:

// 每个渠道 jar 内: META-INF/services/com.x.PaymentChannel 写实现类全限定名
ServiceLoader<PaymentChannel> channels = ServiceLoader.load(PaymentChannel.class);
for (PaymentChannel ch : channels) {           // 懒加载: 迭代到才实例化
    if (ch.supports(payType)) return ch;    // 新渠道 = 提供新 jar, 主工程零改动
}
throw new BizException(CHANNEL_NOT_FOUND, Map.of("type", payType));
// 与类加载页衔接: ServiceLoader 走线程上下文类加载器, 打破双亲委派让"父级"能看到"子级"实现
// 隔离要求高时换自定义 ClassLoader 每渠道一个命名空间, 避免依赖版本互相踩

场景 9 · 字段白名单脱敏: 注解标记敏感字段, 出口统一打码

客服后台日志把手机号/身份证打全了, 合规整改; 反向白名单 — 标了才放行:

static Map<String, Object> safeView(Object o) throws IllegalAccessException {
    Map<String, Object> out = new LinkedHashMap<>();
    for (Field f : o.getClass().getDeclaredFields()) {
        f.setAccessible(true);
        Object v = f.get(o);
        if (f.isAnnotationPresent(Sensitive.class))       // 敏感标记
            v = mask(v, f.getAnnotation(Sensitive.class).policy()); // 手机/证件/卡号各有掩码策略
        out.put(f.getName(), v);
    }
    return out;   // 出口(日志/工单/客服页)统一走 safeView, 新字段默认全显 — 评审卡 @Sensitive
}

场景 10 · 测试里注入私有字段 (边界内允许的反射)

遗留代码把 Clock 写死在字段里, 没有构造器入口; 先用反射救测试, 再推动还债:

OrderService svc = new OrderService(repo);   // 遗留: clock 字段 private final
Field f = OrderService.class.getDeclaredField("clock");
f.setAccessible(true);                       // JDK 16+ 需 --add-opens 测试 JVM 参数
f.set(svc, Clock.fixed(Instant.parse("2026-01-01T00:00:00Z"), ZoneOffset.UTC));
// 断言"过期判断"不再碰运气: 时间被钉死, 测试可重复
assertTrue(svc.isExpired(order));
// 边界: 这招只准在测试用; 生产代码靠构造器注入. 大量这么写 = 可测性欠债, 回头补构造器参数

⚠️ 编码注意与常见坑 pitfalls

坑 1 · 热路径每次 getDeclaredMethod — 每次调用遍历方法表+做访问检查, 规则引擎 CPU 一半烧在"找方法". 正解: static final Method 启动期解析缓存 + 一次 setAccessible(true), 见场景 6。
// 错: m = t.getClass().getDeclaredMethod("pay", long.class);  // 每次都查找
// 对: private static final Method PAY = ...  启动期解析缓存一次
坑 2 · JDK 9+ 模块封装下 setAccessible 失效 — 对 JDK 内部类反射抛 InaccessibleObjectException: "does not open java.lang to unnamed module". 正解: 升级依赖用公开 API; 确实要捅内部, 启动参数 --add-opens java.base/java.lang=ALL-UNNAMED 并记录还债。
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);      // 错: JDK 9+ → InaccessibleObjectException
// 对: java --add-opens java.base/java.lang=ALL-UNNAMED -jar app.jar
//     更好: 换公开 API, 别捅 JDK 内部
坑 3 · CGLIB 代理 final 方法静默不拦截 — final 方法无法被重写, 生成的子类直接透传父类实现, 切面完全没跑但无任何报错. 正解: 检查目标方法/类是否 final; Kotlin 默认 final — 用 spring 插件 kotlin-maven-allopen 打开。
public final Result pay() { ... }   // 错: 切面静默不拦截, 无报错
// 对: 去掉 final; Kotlin 默认 final → kotlin-maven-allopen 插件打开
坑 4 · JDK 代理遇无接口目标直接失败 — newProxyInstance 要求至少一个接口, 纯类报 IllegalArgumentException. 正解: 没接口换 CGLIB/ByteBuddy; 或先抽接口再代理。
// 错: Proxy.newProxyInstance(ld, new Class[0], h)  → IllegalArgumentException
// 对: new ByteBuddy().subclass(Payment.class)...intercept(...)
//     或先抽 interface PaymentApi 再走 JDK 代理
坑 5 · 代理对象的 instanceof/强转不是目标类 — CGLIB 代理 instanceof 目标类为 true 但 getClass() 是 SubClass$$EnhancerByCGLIB; JDK 代理更是完全另一个族. 正解: 类型判断用接口; 别把代理对象反射当原始对象缓存字段布局。
proxy instanceof UserApi;          // → true (实现了接口)
proxy instanceof UserServiceImpl;   // 错: false, JDK 代理另一族
proxy.getClass().getName();          // → com.x.$Proxy0 不是目标类
坑 6 · 反射改 final 字段"成功"了却读回旧值 — set 可写但 JIT 已按常量内联读取处, 看似改了别处读到的还是老值, 且高版本 JVM 直接拒绝修改 static final. 正解: 别依赖反射改 final; 可配置项用普通字段+volatile。
Field f = Config.class.getDeclaredField("MAX");
f.setAccessible(true);  f.set(null, 200);   // "成功"
readMax();                            // 错: 可能还是旧值, JIT 已常量内联
// 对: 可配置项用普通字段 + volatile, 别反射改 final
坑 7 · @Retention(CLASS) 运行时读不到 — getAnnotation 永远返回 null, "注解明明写了却不生效". 正解: 自定义框架注解必须 @Retention(RetentionPolicy.RUNTIME); 排查时先看这个再查别的。
@Retention(RetentionPolicy.CLASS) @interface Table {}  // 错
Order.class.getAnnotation(Table.class);              // → 永远 null
// 对: @Retention(RetentionPolicy.RUNTIME)
坑 8 · 方法名/参数类型拼错 — getDeclaredMethod("pya") 或参数写 Integer.class(实参是 int) 抛 NoSuchMethodException, 常在低频分支才触发. 正解: 解析放 static 块让启动即失败; 方法引用常量代替魔法字符串。
// 错: getDeclaredMethod("pya", long.class) → NoSuchMethodException
//     参数写 Integer.class 实参是 int → 同样找不到
// 对: 解析放 static 块, 拼错启动即红; 用方法引用常量替代字符串
坑 9 · invoke 的异常被包了一层 — 目标方法抛 IOException, 你 catch 到的是 InvocationTargetException, 直接 e.getMessage() 丢真实信息. 正解: Throwable cause = e.getCause() 后再判断类型/记日志。
try { m.invoke(target); }
catch (InvocationTargetException e) {
    log.error("fail", e.getCause());   // 对: 真实异常在 cause
}
// 错: log.error(e.getMessage())  → 丢真实异常信息
坑 10 · 数组的反射要走 Array 工具 — 数组类没有可列举的字段, obj.getClass().getComponentType() 判类型, Array.newInstance(elemType, len) 建, Array.get/set 访问; 直接 field.set 到数组元素是不存在的 API。
int[] a = {1, 2, 3};
a.getClass().getComponentType();        // → int.class
Object arr = Array.newInstance(int.class, 5);
Array.set(arr, 0, 42);                  // 对: 用 Array 工具读写
坑 11 · 私有内部类反射访问看包名 — 同 JVM 里跨不同包名(或 shaded 重定位后的包)访问私有内部类, setAccessible 可能被拒或 NoSuchField. 正解: 反射工具代码与目标类同包, 或显式处理 IllegalAccess; shade 插件 relocate 时同步改字符串类名。
// 错: 跨包 / shade relocate 后包名变了 → NoSuchField / IllegalAccess
// 对: 反射工具与目标类同包; relocate 时同步改字符串里的类名
坑 12 · 以为子类能读到父类注解 — isAnnotationPresent 不沿继承链(除非 @Inherited 且是类级注解); 接口方法上的注解实现类永远读不到. 正解: 查找逻辑自己沿 getSuperclass/getInterfaces 遍历, Spring 的 AnnotatedElementUtils 就是这么补的。
class Base { @Vip void run() {} }
class Sub extends Base {}
Sub.class.getDeclaredMethod("run").isAnnotationPresent(Vip.class);
// 错: → false  方法注解不沿继承链
// 对: 自己沿 getSuperclass()/getInterfaces() 遍历查找
坑 13 · invoke 可变参数的 Object[] 展开歧义 — method.invoke(t, new Object[]{a, b}) 可能被当成"单参数 Object[]"; null 参数更要 (Object) null 消除歧义, 否则 IllegalArgumentException: argument type mismatch. 正解: 精确按形参个数传, 编译不过就强转。
m.invoke(t, null);             // 错: null 歧义 → IllegalArgumentException
m.invoke(t, (Object) null);     // 对: 消除歧义
m.invoke(t, new Object[]{a, b}); // 对: 按形参个数精确传
坑 14 · 反射绕过泛型塞脏数据 — List<String> 经反射 set 进 Integer, 编译期无感, 遍历时 ClassCastException 炸在"完全不相干"的代码行. 正解: 外部输入进集合前做类型校验; 序列化边界用带类型的容器或 record。
List.class.getMethod("add", Object.class).invoke(list, 42);
String s = list.get(0);         // 错: CCE 炸在"不相干"的行
// 对: 外部输入进集合前类型校验; 边界用带类型容器/record
坑 15 · CGLIB 动态生成类撑爆 Metaspace — 每次都 new Enhancer 生成新类, 类不复用不卸载, Metaspace 涨到 OOM: Metaspace. 正解: 代理类按目标类缓存复用; 用 ByteBuddy 的 type cache; 监控 Metaspace 增长趋势。
// 错: 每次请求 new Enhancer().setSuperclass(...).create()
//     → 类不复用不卸载, Metaspace 涨到 OOM
// 对: 代理类按目标类缓存复用 (ByteBuddy type cache)
坑 16 · this 自调用不走拦截器 — a() 里 this.b(), b() 上的 @Transactional/@Cacheable 全失效: 内部调用是原始对象直连, 不经过代理. 正解: 拆到另一个 Bean / 自注入代理 / AopContext.currentProxy(); 这是 Spring 面试与事故双料第一名。
public void a() { this.b(); }      // 错: 直连原始对象, 切面失效
@Transactional public void b() { ... }
// 对: self.b() 自注入代理 / 拆 Bean / AopContext.currentProxy()
坑 17 · 反射破坏单例 — setAccessible(true) 调私有构造器, 懒加载单例被造出第二实例. 正解: 构造器里加已存在检查抛异常; 或用枚举 — 枚举实例由 JVM 保证, 反射克隆/实例化都被拒绝。
Constructor<Singleton> c = Singleton.class.getDeclaredConstructor();
c.setAccessible(true);
c.newInstance();               // 错: 第二实例被造出来
// 对: 构造器已存在检查抛异常; 或 enum 单例 (JVM 拒绝反射实例化)
坑 18 · MethodHandles 权限敏感 — MethodHandles.lookup() 拿到的 Lookup 只信任创建处的访问上下文, 访问别的类私有成员抛 IllegalAccessException. 正解: 目标类里显式 MethodHandles.privateLookupIn(Target.class, lookup()), 别指望它像 setAccessible 一样万能。
// 错: MethodHandles.lookup() 只信任创建处上下文
//     → 访问 Target 私有成员抛 IllegalAccessException
// 对: MethodHandles.privateLookupIn(Target.class, MethodHandles.lookup())
坑 19 · record 没有 setter 语义 — record 组件是 final 字段, Field.set 抛 IllegalAccessException; "改"只能换实例. 正解: record 用构造器重建(Java 21 有 wither 草案的记录器习惯); 需要反射可写的 DTO 用普通类。
record Order(String id) {}
Field f = Order.class.getDeclaredField("id");
f.setAccessible(true);  f.set(order, "x2");  // 错: final 抛异常
// 对: 换实例 new Order("x2"); 需反射可写就用普通类
坑 20 · 运行期循环里做注解扫描 — 每个请求都 getAnnotations 遍历注解数组(每次返回新拷贝), GC 压力与延迟双高. 正解: 启动期扫描一次建 Map 缓存; 注解读取是"冷路径"设计, 别放进热循环。
// 错: 每请求都 getDeclaredFields() + getAnnotation(...)  新拷贝, GC 双高
// 对: 启动期扫一次存 Map<Method, Config>, 热循环只查缓存