13.2 反射:查找、访问与调用
完成注解专题,并熟悉访问控制、异常和泛型擦除后,再用约 50 分钟跑完本篇。目标是能准确查找、访问和调用成员,也能分清反射失败发生在查找层、访问层还是目标方法内部。
1. 读取声明上的运行时元数据
Java 的反射 API 可以在运行时查看类、字段、构造器、方法和注解,也可以在通过访问与参数检查后调用成员。
反射没有让程序真正产生“自我意识”。它只是把编译期写死的类型和成员操作,改成运行时的数据查找与间接调用:
直接调用:new InspectableItem("Hammer").describe(2)
反射调用:Class -> Constructor -> Object -> Method -> invoke如果类和方法在编译时已经确定,直接调用更清楚。反射适合框架、插件、序列化、测试工具和配置驱动的少数边界。
2. Class<?> 是类型入口
2.1 三种常见获取方式
String text = "hello";
Class<?> fromObject = text.getClass();
Class<String> fromLiteral = String.class;
Class<?> fromName = Class.forName("java.lang.String");
System.out.println(fromObject == fromLiteral); // true
System.out.println(fromLiteral == fromName); // trueobj.getClass()从已有对象取得实际运行时类;String.class在源码中明确类型,能保留Class<String>泛型关系;Class.forName(...)接收二进制类名,适合经过验证的配置或插件注册表。
一参数 Class.forName 默认初始化类,静态字段初始化和静态代码块可能执行。只想加载而不初始化:
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class<?> plugin = Class.forName("com.example.Plugin", false, loader);2.2 “每个类只有一个 Class”少了半句
准确说法是:同一个定义类加载器加载的同一个二进制类,只有一个对应 Class 对象。同名 class 由两个类加载器分别定义时,得到两个不同的类型身份。
插件、应用服务器、热部署和测试隔离会遇到这个边界。缓存反射元数据时,不能只用类名作 key;至少要用 Class<?> 或包含类加载器身份的键。
2.3 基本类型、数组和其他类型也有 Class
System.out.println(int.class.getName()); // int
System.out.println(String[].class.isArray()); // true
System.out.println(Runnable.class.isInterface()); // true
System.out.println(Thread.State.class.isEnum()); // trueJava 16+ 还可以用 isRecord() 与 getRecordComponents() 检查 record。Class<?> 描述运行时类,不等于“只描述普通 class 声明”。
3. 查看结构:先选 public 还是 declared
3.1 API 对照
| 目标 | public 视图 | 本类 declared 视图 |
|---|---|---|
| 方法 | getMethods() | getDeclaredMethods() |
| 字段 | getFields() | getDeclaredFields() |
| 构造器 | getConstructors() | getDeclaredConstructors() |
| 单个方法 | getMethod(name, types...) | getDeclaredMethod(name, types...) |
| 单个字段 | getField(name) | getDeclaredField(name) |
getMethods 与 getFields 会包含继承的 public 成员;构造器从不继承,getConstructors 只返回当前类的 public 构造器。declared 视图包含本类声明的所有访问级别,但不包含继承成员。“能发现”与“能访问”是两步,拿到 private Method 后仍可能调用失败。
3.2 一个可重复的检查程序
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
final class Person {
private String name;
public int age;
public String getName() {
return name;
}
private void secret() {}
}
public class InspectDemo {
public static void main(String[] args) {
Class<?> type = Person.class;
String[] fields = Arrays.stream(type.getDeclaredFields())
.map(Field::getName)
.sorted()
.toArray(String[]::new);
String[] methods = Arrays.stream(type.getDeclaredMethods())
.map(Method::getName)
.sorted()
.toArray(String[]::new);
System.out.println(Arrays.toString(fields));
System.out.println(Arrays.toString(methods));
}
}[age, name]
[getName, secret]不要把 getMethods() 的完整输出写成固定列表。Object 和 JDK 版本可能带来额外 public 方法,排序后的 declared 结果更适合教学断言。
3.3 方法签名必须精确
假设 Tool 声明了 public void resize(int size),下面的查找在可抛出 NoSuchMethodException 的方法中执行:
Method resize = Tool.class.getMethod("resize", int.class);
Method wrong = Tool.class.getMethod("resize", Integer.class); // 查找失败核心反射查找不会执行 Java 编译器的重载决议。需要特别留意:
int.class与Integer.class不同;- varargs 参数在签名中是数组,如
String[].class; getMethod只找 public 方法,可能从父类取得;getDeclaredMethod只看当前类;- 泛型擦除后,方法参数可能是上界或 Object;
- 编译器可能生成 bridge/synthetic 方法。
扫描框架通常会过滤 method.isBridge() 与 method.isSynthetic(),再按自己的规则处理重载。
4. 构造对象与调用方法
4.1 不再使用 Class.newInstance()
Class<?> type = Class.forName("java.util.ArrayList");
Object list = type.getDeclaredConstructor().newInstance();
Method add = type.getMethod("add", Object.class);
add.invoke(list, "第一条");
add.invoke(list, "第二条");
Method size = type.getMethod("size");
System.out.println(size.invoke(list)); // 2Class.newInstance() 已弃用:它可能直接传播构造器抛出的受检异常,绕过编译期异常检查。Constructor.newInstance() 会用 InvocationTargetException 包装目标构造器异常,也能明确选择参数签名。
直接版本是:
ArrayList<Object> list = new ArrayList<>();
list.add("第一条");
list.add("第二条");
System.out.println(list.size());把两段并排看,反射只是把每个编译期动作拆成运行时描述对象。
4.2 Method.invoke 的四个要点
Object result = method.invoke(receiver, firstArg, secondArg);- 实例方法需要兼容的 receiver;静态方法忽略 receiver,通常传 null;
- 参数个数必须正确;调用允许拆箱和基本类型拓宽,不允许缩窄,也不会重新选择重载;
- 返回类型在源码中是 Object,void 方法返回 null;
- 目标方法抛出的异常会包装为
InvocationTargetException。
例如 int 形参能接收装箱后的 Byte,不能接收 Long。varargs 方法的形参仍是数组;要把一个 String[] 作为唯一实参传入,需要写 invoke(receiver, (Object) values),防止 invoke 自己的 Object... 将它展开。完整实验会验证这个数组边界。
目标异常处理:
try {
method.invoke(receiver, args);
} catch (java.lang.reflect.InvocationTargetException wrapped) {
Throwable cause = wrapped.getCause();
System.err.println(cause.getClass().getSimpleName() + ": " + cause.getMessage());
}包装说明目标方法已经执行并抛出了异常。反射层直接抛出的 IllegalAccessException 或 IllegalArgumentException 表示调用还没进入目标方法;目标方法自己抛的 IllegalArgumentException 则在包装异常的 cause 中。
5. 完整实验:直接调用、反射调用和目标异常
chapter13_reflection_root=$(mktemp -d)
mkdir -p "$chapter13_reflection_root/out"将下面的代码保存为 $chapter13_reflection_root/ReflectionDemo.java:
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
final class InspectableItem {
private final String name;
InspectableItem(String name) {
this.name = java.util.Objects.requireNonNull(name, "name");
}
public String describe(int count) {
if (count <= 0) {
throw new IllegalArgumentException("数量必须大于 0");
}
return name + " x" + count;
}
public String join(String... parts) {
return String.join("/", parts);
}
private String internalCode() {
return "TOOL-13";
}
}
public class ReflectionDemo {
public static void main(String[] args) throws ReflectiveOperationException {
Class<?> type = InspectableItem.class;
Field[] fields = type.getDeclaredFields();
String[] fieldNames = Arrays.stream(fields).map(Field::getName).sorted().toArray(String[]::new);
Constructor<?> constructor = type.getDeclaredConstructor(String.class);
Object reflectedTool = constructor.newInstance("Hammer");
InspectableItem directTool = new InspectableItem("Hammer");
Method describe = type.getMethod("describe", int.class);
try {
type.getMethod("describe", Integer.class);
throw new AssertionError("参数类型必须精确匹配");
} catch (NoSuchMethodException expected) {
// int.class 与 Integer.class 是不同的反射签名。
}
System.out.println("类:" + type.getSimpleName());
System.out.println("字段:" + Arrays.toString(fieldNames));
System.out.println("直接调用:" + directTool.describe(2));
System.out.println("反射调用:" + describe.invoke(reflectedTool, 3));
try {
describe.invoke(reflectedTool, 0);
throw new AssertionError("目标异常没有传播");
} catch (InvocationTargetException wrapped) {
System.out.println("目标异常:" + wrapped.getCause().getMessage());
}
Method join = type.getMethod("join", String[].class);
Object joined = join.invoke(reflectedTool, (Object) new String[]{"one", "two"});
if (!"one/two".equals(joined)) {
throw new AssertionError("Varargs array was not passed as one argument");
}
Method internalCode = type.getDeclaredMethod("internalCode");
if (!internalCode.trySetAccessible()) {
throw new AssertionError("当前未命名模块应允许访问示例私有方法");
}
if (!"TOOL-13".equals(internalCode.invoke(reflectedTool))) {
throw new AssertionError("私有方法返回值错误");
}
System.out.println("反射检查通过");
}
}javac --release 17 -Xlint:all -Werror \
-d "$chapter13_reflection_root/out" \
"$chapter13_reflection_root/ReflectionDemo.java"
java -ea -cp "$chapter13_reflection_root/out" ReflectionDemo类:InspectableItem
字段:[name]
直接调用:Hammer x2
反射调用:Hammer x3
目标异常:数量必须大于 0
反射检查通过rm -rf -- "$chapter13_reflection_root"这段示例使用未命名模块,因此 trySetAccessible() 可以打开本地 private 方法。如果调用方位于另一个命名模块,而目标模块没有向它开放 package,同样的私有访问请求可能失败。
6. private 访问与模块边界
6.1 先观察失败
import java.lang.reflect.Method;
final class PrivateVault {
private String reveal() {
return "secret";
}
}
public class PrivateAccessFailure {
public static void main(String[] args) throws ReflectiveOperationException {
PrivateVault vault = new PrivateVault();
Method method = PrivateVault.class.getDeclaredMethod("reveal");
method.invoke(vault);
}
}运行会以 IllegalAccessException 失败。getDeclaredMethod 允许发现 private 方法,没有自动授予调用权限。
6.2 trySetAccessible() 比盲目设置更容易处理
if (!method.trySetAccessible()) {
throw new IllegalStateException("模块没有向当前调用方开放目标 package");
}
Object value = method.invoke(vault);模块限制阻止访问时,setAccessible(true) 抛出 InaccessibleObjectException,trySetAccessible() 返回 false。两者都受模块规则约束;在仍启用 SecurityManager 的 Java 17 环境中,权限检查还可能使两者抛出 SecurityException。通常应使用 public API、调整模块的 opens,或由目标模块提供适配器。
不要把宽泛 --add-opens 当作产品默认参数。它把封装问题从编译期推迟到了部署配置。
7. 注解加反射:显式分派器的边界
上一专题定义了 @Loggable。反射可以发现标记并读取配置:
for (Method method : service.getClass().getDeclaredMethods()) {
Loggable log = method.getAnnotation(Loggable.class);
if (log == null) {
continue;
}
System.out.println("[" + log.level() + "] " + method.getName());
}如果处理器随后主动 invoke,它完成了元数据驱动的显式分派。直接调用 service 方法不会自动经过处理器,所以这还不是透明 AOP。
Spring AOP 用代理拦截调用;直接调用目标对象或在对象内部调用自身方法,不会因此自动经过代理。Spring MVC 的映射注解 用来关联请求与控制器方法。JUnit Jupiter 则用测试和生命周期注解组织执行。注解、发现机制和调用机制各有自己的工作。
8. 泛型反射:读取声明,不是猜对象内容
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
final class Shelf {
List<String> titles;
}
Field field = Shelf.class.getDeclaredField("titles");
System.out.println(field.getType()); // interface java.util.List
Type generic = field.getGenericType();
if (generic instanceof ParameterizedType parameterized) {
System.out.println(parameterized.getActualTypeArguments()[0]); // class java.lang.String
}getGenericType() 读取字段声明中的 Signature 元数据。它不能证明某个运行时 List 实例里的元素都为 String。复杂声明还会出现:
ParameterizedType:List<String>;TypeVariable:类或方法的 T;WildcardType:? extends Number;GenericArrayType:T[]。
框架解析这些类型时要处理递归结构和未解析变量。普通业务代码更适合显式传 Class<T>、转换函数或 schema。
9. 性能和缓存
反射查找与间接调用增加了元数据查找、访问检查、参数数组和装箱等工作。具体差距取决于 JDK、调用点和负载,不能用固定倍数概括。
实践顺序:
- 直接调用和接口优先;
- 框架启动阶段扫描一次;
- 缓存经过签名与权限验证的成员;
- 热路径用 JMH 测量;
- 确有瓶颈时再比较
MethodHandle、生成代码或重新设计接口。
缓存不要只按方法名。使用 Method 本身作键,或包含声明 Class<?>、方法名、参数类型和返回类型;泛型桥接方法可能与原方法名称、参数相同而返回类型不同。Class<?> 已包含类加载器身份。插件卸载时还要避免静态强引用阻止类加载器回收。
10. 安全边界
以下值都属于外部输入:配置文件里的类名、插件包、网络请求里的方法名、序列化类型标识。直接交给 Class.forName、getDeclaredMethod 和 invoke 会扩大攻击面。
安全做法:
- 外部操作名映射到固定 allowlist;
- 插件必须实现受控接口,并来自可信目录或签名制品;
- 限制构造器与方法签名,不扫描任意 private 成员;
- 校验参数数量、类型和大小;
- 对执行设置超时、授权和审计;
- 返回受控错误,不向客户端泄露堆栈和 classpath。
超时往往只让调用方停止等待,不能保证目标代码已经停止。不可信插件需要进程隔离和资源限制;签名也只有在签署者可信时才提供来源依据。
11. 常见故障
| 异常或现象 | 含义 | 下一步 |
|---|---|---|
ClassNotFoundException | 类名或加载路径错误 | 检查二进制名、classpath/module path、类加载器 |
NoSuchMethodException | 签名不匹配 | 比对名称、public/declared、primitive、数组参数 |
IllegalAccessException | Java 访问检查失败 | 使用 public API,或明确处理 access opt-in |
InaccessibleObjectException | 模块 package 未开放 | 调整模块契约,不要静默忽略 |
IllegalArgumentException | 接收对象或参数不兼容 | 在调用前验证完整签名 |
InvocationTargetException | 目标方法抛异常 | 检查并映射 getCause() |
| 扫描到两个同名方法 | 重载、bridge 或 synthetic | 用完整签名并检查成员标志 |
| 插件类型“看起来相同”却无法转换 | 类加载器身份不同 | 统一 API 类加载器边界 |
12. 从查找到调用,逐层验证
先写 describeClass(Class<?>),把 declared 字段、方法和构造器排序后输出。再写一个只接受 public 无参构造器的工厂,明确拒绝接口、抽象类和不满足构造契约的类型。这样“发现结构”和“允许实例化”不会混成一步。
给 ReflectionDemo 增加 double scale(double),分别用 double.class 与 Double.class 查找,保留一次成功和一次 NoSuchMethodException。随后让目标方法抛出领域异常,确认外层得到 InvocationTargetException,真正的业务失败仍在 cause 中。
最后解析一个 Map<String, List<Integer>> 字段的嵌套 ParameterizedType,再设计只接受固定 Plugin 接口和 allowlist 的注册表。不要把网络传来的类名或方法名直接送进 Class.forName 和 invoke;动态调用仍要有静态定义的权限边界。
查阅 Java 17 的正式契约
Class:加载、初始化控制、成员发现与类型身份。java.lang.reflect:Method、Field、Constructor与泛型Type。Method.invoke与InvocationTargetException:间接调用和目标异常包装。AccessibleObject.trySetAccessible:Java 语言访问控制与模块开放边界。
继续学习:并发执行
反射已经能读取注解并按元数据定位成员。应用同时处理请求时,下一类故障来自执行顺序:一百个读者同时查询,一个线程修改数据时另一个线程可能读到旧值。