JDK JDK 学习手册8 → 27 · JDK 专用学习站
QUICKREF · REFLECTION · METHOD HANDLES

反射、注解与模块

JDK 21 · 速查主题 10 / 12 · 预计 28 分钟
进阶28 分钟JEP 451
一句话反射让程序在运行时检查与调用类型,是框架的基石;代价是编译期安全、性能与模块封装。JDK 21 的反射要同时理解三件事:Class/Method API、MethodHandles 的低层访问、以及模块强封装对 setAccessible 的限制。

学完你要能自己回答

  1. getDeclaredMethod 和 getMethod 有什么区别?反射调用私有方法会遇到什么限制?
  2. MethodHandle 和传统反射相比优势在哪?invokeExact 为什么特殊?
  3. JDK 16/17 的强封装对反射有什么影响?什么时候需要 --add-opens?

Class 与成员访问01

反射的入口是 Class<?>。getMethod 只找 public(含继承),getDeclaredMethod 找本类声明的所有访问级别(不含继承)。参数类型必须精确匹配,int.class 与 Integer.class 不同。

class Secret {
    private String value() { return "42"; }
    public static int add(int a, int b) { return a + b; }
}

Class<?> clazz = Secret.class;

Method priv = clazz.getDeclaredMethod("value");
priv.setAccessible(true);
Object result = priv.invoke(new Secret());
System.out.println(result); // 42

Method add = clazz.getMethod("add", int.class, int.class);
Object sum = add.invoke(null, 1, 2); // 静态方法 target 传 null
System.out.println(sum); // 3
  • invoke 抛出的受检异常会被包进 InvocationTargetException,取真实异常用 getCause()。
  • setAccessible(true) 对 JDK 内部模块的私有成员可能抛 InaccessibleObjectException。
  • 反射调用有访问检查与装箱开销;高频路径应缓存 Method 或改用 MethodHandle。
  • 数组类型用 String[].class 或 Array.newInstance;Class.isArray() 判断。
try {
    Method m = clazz.getDeclaredMethod("value");
    m.setAccessible(true);
    m.invoke(new Secret());
} catch (InvocationTargetException e) {
    Throwable real = e.getCause(); // 被调方法内部抛出的异常
    real.printStackTrace();
}

JDK 16 起,反射访问 record 组件推荐用 getRecordComponents(),而不是猜字段名。它是 record 的官方反射模型。

record Point(int x, int y) {}

RecordComponent[] comps = Point.class.getRecordComponents();
for (RecordComponent c : comps) {
    System.out.println(c.getName() + " -> " + c.getAccessor());
}
// x -> public int Point.x()
// y -> public int Point.y()

System.out.println(Point.class.isRecord()); // true

sealed 与类型 introspection02

反射可以查询 sealed 层级与允许的子类型,这让框架能在运行期验证类型封闭性,或为模式匹配生成适配代码。

sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double side) implements Shape {}

Class<?> shape = Shape.class;
System.out.println(shape.isSealed()); // true
for (Class<?> sub : shape.getPermittedSubclasses()) {
    System.out.println(sub.getSimpleName());
}
// Circle
// Square
getPermittedSubclasses() 对非 sealed 类型返回 null,不是空数组。判空后再遍历。

MethodHandle 与 VarHandle03

MethodHandle 是对方法、构造器、字段的可执行引用,由 JVM 直接链接,JIT 可以内联。它比传统反射更快,也更适合实现动态语言与框架。

MethodHandles.Lookup lookup = MethodHandles.lookup();

MethodHandle upper = lookup.findVirtual(
        String.class, "toUpperCase",
        MethodType.methodType(String.class));

String s = (String) upper.invokeExact("jdk");
System.out.println(s); // JDK

MethodHandle concat = lookup.findStatic(
        String.class, "join",
        MethodType.methodType(String.class, CharSequence.class, CharSequence[].class));
String joined = (String) concat.invoke(",", new String[]{"a", "b"});
System.out.println(joined); // a,b

invokeExact 要求调用点的静态类型与方法类型完全一致,否则抛 WrongMethodTypeException;invoke 会做自适应转换但更慢。invokeExact 的签名是 signature polymorphic,编译器会根据实参静态类型生成调用点。

class Counter { private int value; }

MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(
        Counter.class, MethodHandles.lookup());
VarHandle vh = lookup.findVarHandle(Counter.class, "value", int.class);

Counter c = new Counter();
vh.set(c, 1);
System.out.println(vh.getAndAdd(c, 2)); // 1
System.out.println(vh.get(c));          // 3
  • VarHandle 支持普通、volatile、acquire/release、opaque 等访问模式,适合并发数据结构。
  • MethodHandles 也可以绑定字段、构造器、数组元素。
  • 私有成员用 privateLookupIn;否则 Lookup 只能访问当前类可见的成员。
  • MethodHandle 不是 Serializable,不能跨进程传输。

模块强封装与 --add-opens04

JDK 9 引入模块,JDK 16 默认强封装(JEP 396),JDK 17 进一步收紧(JEP 403)。深度反射访问 JDK 内部包(如 java.lang、sun.misc)默认被拒绝,必须显式 --add-opens。

Module base = String.class.getModule();
System.out.println(base.getName());              // java.base
System.out.println(base.isExported("java.lang")); // true
System.out.println(base.isOpen("java.lang"));     // false(默认不开放深度反射)

// 反射访问私有字段(JDK 17+ 默认失败)
try {
    Field f = String.class.getDeclaredField("value");
    f.setAccessible(true); // InaccessibleObjectException
} catch (InaccessibleObjectException e) {
    System.out.println("需要 --add-opens java.base/java.lang=ALL-UNNAMED");
}
# 只对指定模块开放深度反射
java --add-opens java.base/java.lang=ALL-UNNAMED -jar app.jar

# 启动时显式声明(推荐,避免运行期动态加载 agent)
java -javaagent:agent.jar -jar app.jar

JDK 21 的 JEP 451 对动态加载 agent 发出警告:运行期通过 Instrumentation 加载 agent 的行为将逐步被禁止,未来需要 -javaagent 在启动时声明。依赖 APM/字节码增强的团队应尽早切换。

  • 优先使用公开 API;强封装不是障碍,而是提醒你不要依赖 JDK 内部实现。
  • --add-opens 是临时兼容手段,不是长期方案。
  • 框架升级时检查是否依赖 sun.misc.Unsafe、java.lang.reflect 内部字段。
  • 模块化应用用 module-info.java 的 opens 精确开放包,而不是全局 --add-opens。

注解:保留策略与读取05

注解能否被反射看到,取决于 @Retention。只有 RUNTIME 才会保留到运行期;CLASS 只存在于 class 文件,SOURCE 编译后丢弃。框架通常依赖 RUNTIME 或注解处理器。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(Tags.class)
@interface Tag { String value(); }

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Tags { Tag[] value(); }

class Service {
    @Tag("read") @Tag("cache")
    public void load() {}
}

Method m = Service.class.getMethod("load");
Tag[] tags = m.getAnnotationsByType(Tag.class);
for (Tag t : tags) System.out.println(t.value());
// read
// cache
Retention保留到典型用途
SOURCE编译期@Override、Lombok、注解处理器
CLASSclass 文件(默认)字节码工具、编译期检查
RUNTIME运行期Spring、JUnit、序列化框架

@Inherited 只对类注解有效,且只影响 getAnnotation 的继承查找;方法注解不会继承。组合注解(meta-annotation)需要框架自己递归解析。

动态代理与反射边界06

JDK 动态代理在运行期生成实现指定接口的类,把调用转发给 InvocationHandler。它是 AOP、RPC、Mock 框架的基础。

interface Greeter { String greet(String name); }

InvocationHandler handler = (proxy, method, args) -> {
    if (method.getName().equals("greet")) {
        return "Hello, " + args[0];
    }
    return null;
};

Greeter greeter = (Greeter) Proxy.newProxyInstance(
        Greeter.class.getClassLoader(),
        new Class<?>[]{Greeter.class},
        handler);

System.out.println(greeter.greet("JDK")); // Hello, JDK
  • JDK 代理只能代理接口;代理类需要类时用 ByteBuddy / CGLIB。
  • 代理调用会经过 InvocationHandler,异常、默认方法、equals/hashCode 都要正确处理。
  • 反射 + 代理会放大性能开销;高频路径考虑缓存 MethodHandle 或生成字节码。
  • 记录调用链时注意 Method.invoke 的异常包装,不要把 InvocationTargetException 直接抛给业务。
反射不是「高级特性」,而是框架作者的工具。业务代码优先用接口、泛型和依赖注入;只有确实需要运行期动态性时才使用反射。

版本边界BOUNDARY

这张表比 API 本身更值得记。写代码时先确认你的运行时版本。

特性 / API状态说明
强封装 JDK 内部16 默认 / 17 加强深度反射需要 --add-opens。
record 反射 API16 正式getRecordComponents / isRecord。
sealed 反射 API17 正式isSealed / getPermittedSubclasses。
动态加载 agent 警告21 警告JEP 451,后续版本将禁止;改用 -javaagent。
MethodHandle / VarHandle7 / 9 正式VarHandle 在 JDK 9 引入。

费曼自检SELF CHECK

先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。

01. getDeclaredMethod 找不到父类的方法,为什么?
getDeclaredMethod 只查找当前类声明的成员,不搜索父类。需要沿继承链逐级查找,或使用 getMethod(只找 public 且包含继承)。
02. MethodHandle.invokeExact 与 invoke 的区别是什么?
invokeExact 要求调用点的静态类型与方法类型完全匹配,否则抛 WrongMethodTypeException;invoke 会做自适应类型转换,更灵活但更慢。性能敏感路径用 invokeExact 并保证类型精确。
03. JDK 17 之后为什么 setAccessible 会失败?
JDK 16/17 默认强封装 JDK 内部模块,深度反射访问未开放(opens)的包会被拒绝,抛 InaccessibleObjectException。需要显式 --add-opens 或 module-info 的 opens 声明。
04. 注解的 RUNTIME 保留策略解决什么问题?
只有 RUNTIME 保留的注解才会进入 class 文件并在运行期通过反射读取。CLASS 只保留在 class 文件中但运行期不可见,SOURCE 在编译后丢弃。框架若要在运行时读取注解,必须使用 RUNTIME。
学习进度 记录保存在本机浏览器 · 不上传

JDK 学习手册 · JDK / Java 专用学习站 · 纯静态 · 双击 index.html 即可使用

本站专注 JDK 版本演进与 Java 教学,非 Oracle / OpenJDK 官方站点。版本数据来自 OpenJDK 官方 Features 表;课程示例已在对应本机 JDK 实测。构建日期 2026-11-12。