Skip to content

第30课:反射机制

🎯 学习目标

  • 理解反射的本质(运行时类型自省与操作)与 Class 对象
  • 掌握反射 API:获取 Class、创建实例、调用方法、访问字段
  • 理解动态代理的原理
  • 知道反射的应用场景(框架、注解、序列化)
  • 识别反射的代价与陷阱(性能、破坏封装、安全)

📖 一、概念讲解:什么是反射

1. 反射是什么

反射:程序在运行时动态获取类信息、创建对象、调用方法、读写字段的能力。

普通方式:编译时就知道类型,new Person() 直接创建、p.setName() 直接调用。 反射方式:运行时才知道类名/方法名,动态操作。

java
// 编译时已知类型
Person p = new Person();
p.setName("Alice");

// 反射:运行时动态
Class<?> clazz = Class.forName("com.example.Person");
Object obj = clazz.getDeclaredConstructor().newInstance();
Method m = clazz.getMethod("setName", String.class);
m.invoke(obj, "Alice");

2. 为什么需要反射

  • 框架:Spring 不知道用户的类,靠反射读配置创建 Bean。
  • 注解处理:运行时扫描注解执行逻辑(@Autowired、@Test)。
  • 序列化:把对象字段转 JSON/还原(Jackson 用反射读字段)。
  • ORM:数据库行映射到对象字段。
  • 动态代理:AOP、RPC 框架基于反射生成代理类。

编译时类型不确定的场景,反射是基础。

3. Class 对象

每个加载的类在 JVM 中对应一个 Class 对象(方法区/元空间里)。反射的入口就是拿到 Class 对象。


📖 二、获取 Class 对象(三种方式)

java
// 1. 类名.class(编译时已知类型)
Class<String> c1 = String.class;

// 2. 实例.getClass()
String s = "hello";
Class<?> c2 = s.getClass();

// 3. Class.forName(全限定名)(运行时按字符串加载,最灵活)
Class<?> c3 = Class.forName("java.lang.String");

System.out.println(c1 == c2 && c2 == c3);   // true:同一类的 Class 全局唯一

关键认知:同一类的 Class 对象全局唯一(类加载时创建),所以三种方式拿到的是同一对象,== 成立。


📖 三、反射操作类成员

1. 创建实例

java
Class<?> clazz = Person.class;
// JDK 9+ 推荐
Object obj = clazz.getDeclaredConstructor().newInstance();
// 旧(已废弃):clazz.newInstance()

2. 操作字段

java
Field f = clazz.getDeclaredField("name");   // 含私有字段
f.setAccessible(true);   // 突破 private 访问
f.set(obj, "张三");
Object val = f.get(obj);

3. 调用方法

java
Method m = clazz.getDeclaredMethod("setName", String.class);
m.setAccessible(true);
m.invoke(obj, "李四");

Method getter = clazz.getMethod("getName");   // getMethod 只取 public
Object name = getter.invoke(obj);

4. getMethod vs getDeclaredMethod

  • getMethod:取 public 方法(含继承)。
  • getDeclaredMethod:取本类声明的所有方法(含 private,不含继承)。
  • 字段/构造器同理:getField/getDeclaredField。

📖 四、动态代理

反射能运行时生成代理类,实现 AOP、RPC:

java
interface Service { void doWork(); }

Service target = () -> System.out.println("真实逻辑");

// InvocationHandler:代理方法调用时触发
Service proxy = (Service) Proxy.newProxyInstance(
    Service.class.getClassLoader(),
    new Class[]{Service.class},
    (p, method, args) -> {
        System.out.println("前置:记录日志");
        Object result = method.invoke(target, args);   // 反射调真实对象
        System.out.println("后置:记录日志");
        return result;
    }
);
proxy.doWork();   // 走代理,前后加逻辑

原理:Proxy 运行时生成实现了指定接口的代理类(字节码),方法调用转发到 InvocationHandler。这是 Spring AOP(JDK 动态代理)、MyBatis Mapper 接口的底层。

局限:JDK 动态代理只能代理接口。代理类用 CGLIB(生成子类)可代理普通类。


⚠️ 五、常见陷阱与代价

陷阱1:性能开销

反射比直接调用慢(需要方法查找、参数装箱、安全检查),约几十倍差距。频繁调用注意缓存 Method 对象、避免热路径用反射。

陷阱2:破坏封装

setAccessible(true) 能访问 private,破坏封装,绕过访问控制。框架内部用,业务慎用。

陷阱3:安全限制

有 SecurityManager 或 JDK 17 模块系统时,setAccessible 对非开放模块的类会抛 InaccessibleObjectException。

陷阱4:编译期检查丢失

反射调用方法名/参数类型写错,编译不报错,运行时抛 NoSuchMethodException。丢失静态类型安全。

陷阱5:构造方法访问

newInstance() 调无参构造,若无无参构造抛异常。用 getDeclaredConstructor(参数类型).newInstance(参数)


🆚 六、Java vs C 对比

特性C 语言Java
类型自省无(运行时类型信息丢失)反射(Class/Field/Method)
动态调用函数指针(但需已知签名)Method.invoke(任意方法)
运行时生成代码动态代理/字节码生成
注解处理运行时反射读注解

对 C 程序员:C 编译后类型信息基本丢失,函数指针要签名匹配。Java 反射保留了完整的运行时类型信息,能动态操作——这是 Java 框架(Spring/MyBatis/Jackson)繁荣的根基,代价是性能与安全。


💡 七、最佳实践

  1. 能用普通方式就别用反射——反射是框架/工具的最后手段。
  2. 缓存反射对象:Method/Field 重复用,别每次重新查找。
  3. setAccessible 慎用:破坏封装且有安全/模块限制。
  4. 动态代理用接口:JDK 代理限接口;代理类用 CGLIB。
  5. 注意 JDK 9+ 模块:反射访问非开放模块需 --add-opens。

📝 练习预告

完成 练习/Ex30_Reflection.java 中的 6 道题:

  1. 获取 Class 对象(三种方式)
  2. 反射创建实例 + 调用方法
  3. 反射读写私有字段
  4. 遍历类的所有方法/字段
  5. 动态代理
  6. 综合:简易 JSON 序列化(反射读字段)

完成后对比 答案/Sol30.java,查看逐行讲解与多解法。


🧭 八、反射在框架中的真实用途

反射不是为了日常业务代码炫技,它主要服务于框架和通用工具。

典型场景:

text
Spring 扫描类、创建 Bean、注入字段和构造器。
Jackson 根据字段和 getter/setter 完成 JSON 序列化。
MyBatis 根据接口和注解生成代理对象。
JUnit 扫描 @Test 方法并调用。
ORM 框架根据 Entity 字段映射数据库列。
RPC 框架根据接口方法描述执行远程调用。

业务代码中如果大量出现反射,通常说明抽象边界有问题。优先考虑接口、多态、策略模式或明确的配置映射。


📖 九、Class 对象与类型信息

每个被 JVM 加载的类都有一个对应的 Class&lt;?&gt; 对象。

java
Class<String> c1 = String.class;
Class<?> c2 = Class.forName("java.lang.String");
Class<?> c3 = "abc".getClass();

三种方式的差异:

text
T.class:编译期已知类型,不触发初始化。
Class.forName:按类名加载,默认会触发初始化。
obj.getClass:运行时对象获取实际类型。

反射 API 的入口几乎都是 Class

text
getDeclaredFields
getDeclaredMethods
getDeclaredConstructors
getAnnotations
newInstance / getDeclaredConstructor().newInstance

📖 十、反射与泛型擦除

Java 泛型在运行时大多被擦除:

java
List<String> names = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();

System.out.println(names.getClass() == numbers.getClass()); // true

这意味着反射看到的通常是 List,不是 List&lt;String&gt;

框架如果要读取泛型信息,通常依赖字段、方法返回值或父类签名上的 ParameterizedType

java
Field field = UserGroup.class.getDeclaredField("users");
Type type = field.getGenericType();
if (type instanceof ParameterizedType pt) {
    Type actual = pt.getActualTypeArguments()[0];
    System.out.println(actual);
}

这也是 Jackson、Spring 在处理泛型集合时需要额外类型信息的原因。


🧪 十一、实战案例:简易对象转 Map

java
public static Map<String, Object> toMap(Object target) {
    Map<String, Object> result = new LinkedHashMap<>();
    Class<?> type = target.getClass();

    for (Field field : type.getDeclaredFields()) {
        try {
            field.setAccessible(true);
            result.put(field.getName(), field.get(target));
        } catch (IllegalAccessException e) {
            throw new IllegalStateException("read field failed: " + field.getName(), e);
        }
    }

    return result;
}

这个例子能帮助理解 JSON 序列化的底层思想,但生产环境不要自己重复造 Jackson。


📌 十二、性能与安全建议

反射慢主要慢在:

text
方法查找
访问检查
装箱拆箱
动态分派
JIT 优化受限

优化方式:

text
缓存 Class、Field、Method。
初始化阶段完成扫描,运行时直接使用缓存。
高性能场景考虑 MethodHandle。
避免在高频循环中反复反射查找。

安全方面:

text
不要对不可信类名执行 Class.forName。
不要随意 setAccessible(true) 访问私有字段。
JDK 9+ 模块封装可能阻止深反射。
反射异常要保留上下文,方便排查。

🔍 十三、自测问题

text
Class.forName 和 T.class 有什么区别?
getFields 和 getDeclaredFields 有什么区别?
为什么 setAccessible 有风险?
反射为什么会丢失编译期类型安全?
JDK 动态代理和反射有什么关系?
为什么 JSON 框架需要反射?
泛型擦除对反射有什么影响?
MethodHandle 相比 Method.invoke 有什么优势?

🧭 十四、反射落地清单

在真实项目中使用反射前,建议先完成下面这组判断:

text
这个需求是否必须运行时动态决定类型?
是否可以通过接口、多态、策略模式解决?
反射调用是否处于高频路径?
字段、方法、构造器是否可以在启动阶段缓存?
是否需要访问 private 成员?
是否会运行在 JDK 9+ 模块化环境?
异常信息是否能定位到类名、方法名、参数类型?
是否可能加载用户输入的类名?

推荐使用反射的场景:

text
框架启动期扫描类和注解。
通用序列化、反序列化工具。
ORM 对象映射。
测试框架查找测试方法。
依赖注入容器创建对象。
插件系统按配置加载实现类。

不推荐使用反射的场景:

text
普通业务分支选择。
频繁调用的核心循环。
为了绕过 private 修改对象内部状态。
为了避免写接口而动态调用方法。
直接执行外部传入的类名和方法名。

实践原则很简单:反射适合写框架,不适合让业务逻辑变得不可追踪。


🛠 十五、反射异常排查

反射 API 的异常通常比普通调用更绕,排查时要先识别异常类型。

异常常见原因排查方向
ClassNotFoundException类名错误、classpath 缺 jar打印全限定类名,检查依赖
NoSuchMethodException方法名或参数类型不匹配参数类型必须完全一致
NoSuchFieldException字段不存在或只查了 public 字段区分 getField 与 getDeclaredField
IllegalAccessException访问权限不足是否需要 setAccessible
InvocationTargetException被调用方法内部抛异常看 getCause() 的真实异常
InstantiationException抽象类、接口无法实例化检查目标类型
InaccessibleObjectException模块系统禁止深反射JDK 9+ 检查 --add-opens

处理 InvocationTargetException 时不要只打印外层异常:

java
try {
    method.invoke(target, args);
} catch (InvocationTargetException e) {
    Throwable real = e.getCause();
    throw new IllegalStateException("method failed: " + method.getName(), real);
}

外层异常只表示“反射调用失败”,真正的业务异常藏在 getCause() 里。


🧪 十六、进阶案例:按注解调用方法

下面这个例子模拟一个简化版测试框架:

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyCase {
    String value() default "";
}

public class DemoCases {
    @MyCase("正常登录")
    public void loginSuccess() {
        System.out.println("login ok");
    }

    @MyCase("密码错误")
    private void loginFail() {
        System.out.println("login failed");
    }
}

运行器:

java
public static void runCases(Class<?> type) {
    try {
        Object instance = type.getDeclaredConstructor().newInstance();
        for (Method method : type.getDeclaredMethods()) {
            MyCase annotation = method.getAnnotation(MyCase.class);
            if (annotation == null) {
                continue;
            }
            method.setAccessible(true);
            System.out.println("run case: " + annotation.value());
            method.invoke(instance);
        }
    } catch (ReflectiveOperationException e) {
        throw new IllegalStateException("run cases failed: " + type.getName(), e);
    }
}

这个例子串起了反射的三个关键动作:

text
读取类结构:getDeclaredMethods。
读取运行时注解:getAnnotation。
突破访问限制并调用:setAccessible + invoke。

JUnit、Spring、ORM 框架的复杂度更高,但基础模式就是“扫描元数据 + 反射执行 + 缓存结果”。


✅ 十七、掌握标准

学完本课后,应能做到:

text
能解释 Class 对象为什么是反射入口。
能区分 getMethod 与 getDeclaredMethod。
能用构造器反射创建对象,而不是使用过时的 newInstance。
能读写字段并知道 setAccessible 的风险。
能解释 Method.invoke 的异常包装。
能说明 JDK 动态代理为什么要求接口。
能判断什么时候不应该用反射。
能在 JDK 17 模块化限制下定位反射访问失败。
能把反射对象缓存到启动阶段,避免运行时重复查找。

如果只能写出反射 API,但说不清它带来的性能、安全和可维护性代价,说明还没有真正掌握这一课。


🎓 下一步

  • 第31课:注解 — 内置注解、自定义注解、元注解、注解处理器