跳到内容

第 1 卷 / 第 13 章 / 第 2 课

13.2 反射:查找、访问与调用 ​

完成注解专题,并熟悉访问控制、异常和泛型擦除后,再用约 50 分钟跑完本篇。目标是能准确查找、访问和调用成员,也能分清反射失败发生在查找层、访问层还是目标方法内部。

1. 读取声明上的运行时元数据 ​

Java 的反射 API 可以在运行时查看类、字段、构造器、方法和注解,也可以在通过访问与参数检查后调用成员。

反射没有让程序真正产生“自我意识”。它只是把编译期写死的类型和成员操作,改成运行时的数据查找与间接调用:

text
直接调用:new InspectableItem("Hammer").describe(2)
反射调用:Class -> Constructor -> Object -> Method -> invoke

如果类和方法在编译时已经确定,直接调用更清楚。反射适合框架、插件、序列化、测试工具和配置驱动的少数边界。

2. Class<?> 是类型入口 ​

2.1 三种常见获取方式 ​

java
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);   // true
  • obj.getClass() 从已有对象取得实际运行时类;
  • String.class 在源码中明确类型,能保留 Class<String> 泛型关系;
  • Class.forName(...) 接收二进制类名,适合经过验证的配置或插件注册表。

一参数 Class.forName 默认初始化类,静态字段初始化和静态代码块可能执行。只想加载而不初始化:

java
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class<?> plugin = Class.forName("com.example.Plugin", false, loader);

2.2 “每个类只有一个 Class”少了半句 ​

准确说法是:同一个定义类加载器加载的同一个二进制类,只有一个对应 Class 对象。同名 class 由两个类加载器分别定义时,得到两个不同的类型身份。

插件、应用服务器、热部署和测试隔离会遇到这个边界。缓存反射元数据时,不能只用类名作 key;至少要用 Class<?> 或包含类加载器身份的键。

2.3 基本类型、数组和其他类型也有 Class ​

java
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());  // true

Java 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 一个可重复的检查程序 ​

java
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));
    }
}
text
[age, name]
[getName, secret]

不要把 getMethods() 的完整输出写成固定列表。Object 和 JDK 版本可能带来额外 public 方法,排序后的 declared 结果更适合教学断言。

3.3 方法签名必须精确 ​

假设 Tool 声明了 public void resize(int size),下面的查找在可抛出 NoSuchMethodException 的方法中执行:

java
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() ​

java
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)); // 2

Class.newInstance() 已弃用:它可能直接传播构造器抛出的受检异常,绕过编译期异常检查。Constructor.newInstance() 会用 InvocationTargetException 包装目标构造器异常,也能明确选择参数签名。

直接版本是:

java
ArrayList<Object> list = new ArrayList<>();
list.add("第一条");
list.add("第二条");
System.out.println(list.size());

把两段并排看,反射只是把每个编译期动作拆成运行时描述对象。

4.2 Method.invoke 的四个要点 ​

java
Object result = method.invoke(receiver, firstArg, secondArg);
  1. 实例方法需要兼容的 receiver;静态方法忽略 receiver,通常传 null;
  2. 参数个数必须正确;调用允许拆箱和基本类型拓宽,不允许缩窄,也不会重新选择重载;
  3. 返回类型在源码中是 Object,void 方法返回 null;
  4. 目标方法抛出的异常会包装为 InvocationTargetException。

例如 int 形参能接收装箱后的 Byte,不能接收 Long。varargs 方法的形参仍是数组;要把一个 String[] 作为唯一实参传入,需要写 invoke(receiver, (Object) values),防止 invoke 自己的 Object... 将它展开。完整实验会验证这个数组边界。

目标异常处理:

java
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. 完整实验:直接调用、反射调用和目标异常 ​

bash
chapter13_reflection_root=$(mktemp -d)
mkdir -p "$chapter13_reflection_root/out"

将下面的代码保存为 $chapter13_reflection_root/ReflectionDemo.java:

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("反射检查通过");
    }
}
bash
javac --release 17 -Xlint:all -Werror \
  -d "$chapter13_reflection_root/out" \
  "$chapter13_reflection_root/ReflectionDemo.java"
java -ea -cp "$chapter13_reflection_root/out" ReflectionDemo
text
类:InspectableItem
字段:[name]
直接调用:Hammer x2
反射调用:Hammer x3
目标异常:数量必须大于 0
反射检查通过
bash
rm -rf -- "$chapter13_reflection_root"

这段示例使用未命名模块,因此 trySetAccessible() 可以打开本地 private 方法。如果调用方位于另一个命名模块,而目标模块没有向它开放 package,同样的私有访问请求可能失败。

6. private 访问与模块边界 ​

6.1 先观察失败 ​

java
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() 比盲目设置更容易处理 ​

java
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。反射可以发现标记并读取配置:

java
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. 泛型反射:读取声明,不是猜对象内容 ​

java
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、调用点和负载,不能用固定倍数概括。

实践顺序:

  1. 直接调用和接口优先;
  2. 框架启动阶段扫描一次;
  3. 缓存经过签名与权限验证的成员;
  4. 热路径用 JMH 测量;
  5. 确有瓶颈时再比较 MethodHandle、生成代码或重新设计接口。

缓存不要只按方法名。使用 Method 本身作键,或包含声明 Class<?>、方法名、参数类型和返回类型;泛型桥接方法可能与原方法名称、参数相同而返回类型不同。Class<?> 已包含类加载器身份。插件卸载时还要避免静态强引用阻止类加载器回收。

10. 安全边界 ​

以下值都属于外部输入:配置文件里的类名、插件包、网络请求里的方法名、序列化类型标识。直接交给 Class.forName、getDeclaredMethod 和 invoke 会扩大攻击面。

安全做法:

  • 外部操作名映射到固定 allowlist;
  • 插件必须实现受控接口,并来自可信目录或签名制品;
  • 限制构造器与方法签名,不扫描任意 private 成员;
  • 校验参数数量、类型和大小;
  • 对执行设置超时、授权和审计;
  • 返回受控错误,不向客户端泄露堆栈和 classpath。

超时往往只让调用方停止等待,不能保证目标代码已经停止。不可信插件需要进程隔离和资源限制;签名也只有在签署者可信时才提供来源依据。

11. 常见故障 ​

异常或现象含义下一步
ClassNotFoundException类名或加载路径错误检查二进制名、classpath/module path、类加载器
NoSuchMethodException签名不匹配比对名称、public/declared、primitive、数组参数
IllegalAccessExceptionJava 访问检查失败使用 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 的正式契约 ​

继续学习:并发执行 ​

反射已经能读取注解并按元数据定位成员。应用同时处理请求时,下一类故障来自执行顺序:一百个读者同时查询,一个线程修改数据时另一个线程可能读到旧值。

前往第 14 章:并发编程基础

Built with VitePress | Software Systems Atlas