反射、注解与模块
学完你要能自己回答
- getDeclaredMethod 和 getMethod 有什么区别?反射调用私有方法会遇到什么限制?
- MethodHandle 和传统反射相比优势在哪?invokeExact 为什么特殊?
- 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、注解处理器 |
CLASS | class 文件(默认) | 字节码工具、编译期检查 |
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 反射 API | 16 正式 | getRecordComponents / isRecord。 |
| sealed 反射 API | 17 正式 | isSealed / getPermittedSubclasses。 |
| 动态加载 agent 警告 | 21 警告 | JEP 451,后续版本将禁止;改用 -javaagent。 |
| MethodHandle / VarHandle | 7 / 9 正式 | VarHandle 在 JDK 9 引入。 |
费曼自检SELF CHECK
先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。