Appearance
第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)繁荣的根基,代价是性能与安全。
💡 七、最佳实践
- 能用普通方式就别用反射——反射是框架/工具的最后手段。
- 缓存反射对象:Method/Field 重复用,别每次重新查找。
- setAccessible 慎用:破坏封装且有安全/模块限制。
- 动态代理用接口:JDK 代理限接口;代理类用 CGLIB。
- 注意 JDK 9+ 模块:反射访问非开放模块需 --add-opens。
📝 练习预告
完成 练习/Ex30_Reflection.java 中的 6 道题:
- 获取 Class 对象(三种方式)
- 反射创建实例 + 调用方法
- 反射读写私有字段
- 遍历类的所有方法/字段
- 动态代理
- 综合:简易 JSON 序列化(反射读字段)
完成后对比 答案/Sol30.java,查看逐行讲解与多解法。
🧭 八、反射在框架中的真实用途
反射不是为了日常业务代码炫技,它主要服务于框架和通用工具。
典型场景:
text
Spring 扫描类、创建 Bean、注入字段和构造器。
Jackson 根据字段和 getter/setter 完成 JSON 序列化。
MyBatis 根据接口和注解生成代理对象。
JUnit 扫描 @Test 方法并调用。
ORM 框架根据 Entity 字段映射数据库列。
RPC 框架根据接口方法描述执行远程调用。业务代码中如果大量出现反射,通常说明抽象边界有问题。优先考虑接口、多态、策略模式或明确的配置映射。
📖 九、Class 对象与类型信息
每个被 JVM 加载的类都有一个对应的 Class<?> 对象。
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<String>。
框架如果要读取泛型信息,通常依赖字段、方法返回值或父类签名上的 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课:注解 — 内置注解、自定义注解、元注解、注解处理器