主题
界面
主题
反射(Reflection)让你在运行时检查类的结构、创建对象、读写字段、调用方法——这些操作平时都由编译器帮你完成。
它是"框架为什么能自动干活"的答案:Spring 靠它注入依赖,MyBatis 靠它把查询结果映射成对象,Jackson 靠它序列化。理解反射,是从"会用框架"走向"看懂框架"的门槛。
完成本节后,你应该能够:
Class 对象,并读取类名、父类、字段、方法;setAccessible(true) 在做什么,以及它为什么有风险;ClassNotFoundException 与 NoClassDefFoundError 的区别。先看一个矛盾:框架要在不知道你的类长什么样的情况下操作它。
// 你自己的代码:编译期就知道类型
User user = new User("张三");
user.getName();
// 框架的代码:编译时它根本不知道有 User 这个类
// 它只有配置文件里的一行:com.example.User
// 它必须"在运行时"加载这个类 → 创建实例 → 赋值 → 调用方法没有反射,框架就只能靠代码生成或手工配置。有了反射,Spring 才能只凭一个类名完成依赖注入。
另一个视角:反射让"编译期检查"失效了——method.invoke(obj) 拼错方法名不会编译报错,只在运行时抛异常。所以它是一把双刃剑:框架用它换灵活性,业务代码用它通常是给自己挖坑。
每个被 JVM 加载的类,都有一个对应的 Class 对象(在内存里只有一份)。三种获取方式:
Class<Product> byLiteral = Product.class; // 1. 类字面量,编译期已知
Class<?> byInstance = new Product().getClass(); // 2. 从实例获取
Class<?> byName = Class.forName("cn.kb.learn.common.Product"); // 3. 按全限定名加载(框架用这个)| 方式 | 时机 | 典型场景 |
|---|---|---|
X.class | 编译期 | 已知类型,最安全 |
obj.getClass() | 运行时 | 拿到对象后想知道它到底是什么类 |
Class.forName("全限定名") | 运行时 | 配置文件驱动(框架的用法) |
三种方式得到的是同一个对象:byLiteral == byInstance 为 true。因为类在 JVM 里只有一个 Class 实例。
clazz.getName(); // 全限定名 cn.kb.learn.common.Product
clazz.getSimpleName(); // 简单名 Product
clazz.getSuperclass(); // 父类
clazz.getInterfaces(); // 实现的接口
clazz.getModifiers(); // 修饰符(配合 Modifier.toString)
clazz.getDeclaredFields(); // 本类声明的字段(含私有)
clazz.getDeclaredMethods(); // 本类声明的方法(含私有)
clazz.getDeclaredConstructors(); // 构造方法getDeclaredXxx 与 getXxx 的区别(高频考点):
| 方法 | 范围 | 访问权限 |
|---|---|---|
getDeclaredFields() | 本类声明的 | 包含 private |
getFields() | 本类 + 父类的 | 只有 public |
// 方式一:无参构造(已过时,异常处理粗糙)
Product instance = Product.class.newInstance();
// 方式二:显式取构造方法(推荐)
Constructor<Product> constructor = Product.class.getConstructor(String.class, long.class);
Product created = constructor.newInstance("键盘", 19_900L);方式二更好:能指定参数类型、异常信息更明确,也能通过 getDeclaredConstructor() 访问非公开构造方法。
Field nameField = Product.class.getDeclaredField("name");
nameField.setAccessible(true); // 关闭访问检查
nameField.set(instance, "机械键盘"); // 写
Object value = nameField.get(instance); // 读
Method describe = Product.class.getMethod("describe");
Object result = describe.invoke(instance); // 调用
Method secret = Product.class.getDeclaredMethod("secret");
secret.setAccessible(true);
secret.invoke(instance); // 调用私有方法setAccessible(true) 在做什么:跳过 Java 的访问权限检查。它是打破封装的操作——所以:
trySetAccessible():返回 boolean 而不是抛异常,更适合"不确定能否访问"的场景。| 代价 | 说明 |
|---|---|
| 性能 | 方法调用比直接调用慢(实测约 5 倍,见第 7 节),因为要做访问检查、参数装箱、方法解析 |
| 失去编译期检查 | 方法名、参数类型写错,编译不报错,运行时才抛异常 |
| 代码可读性 | invoke 调用看不出调了什么,IDE 无法跳转 |
| 破坏封装 | setAccessible(true) 可以读写任何字段,包括 final 的 |
| 安全性 | 在受限环境(安全管理器)下会被拒绝 |
判断标准:需要"在不知道具体类型的情况下操作对象"时才用反射。如果你的代码里能写出 obj.getName(),就不要用 invoke。
| 场景 | 说明 |
|---|---|
| 框架/容器 | Spring 的依赖注入、MyBatis 的结果映射、Jackson 的序列化 |
| 通用工具 | Bean 拷贝、对象比较、ORM 的实体映射 |
| 注解处理 | 读取注解并据此执行逻辑(《注解基础》) |
| 插件系统 | 按配置加载实现类(Class.forName) |
| 单元测试 | 测试私有方法(通常有更好的选择) |
业务代码里用反射的典型信号:你在写框架层或通用工具层。普通的 CRUD 业务里出现反射,通常意味着设计出了问题。
Class<?> clazz = Product.class;
System.out.println(clazz.getName()); // cn.kb.learn...Product
System.out.println(clazz.getSimpleName()); // Product
System.out.println(clazz.getSuperclass().getSimpleName()); // Object// 配置文件里写:handler=cn.kb.learn.handler.OrderHandler
String className = "cn.kb.learn.common.ReflectionLesson$Product";
Class<?> clazz = Class.forName(className);
Object instance = clazz.getDeclaredConstructor().newInstance();这就是"插件化"的最小形态:换配置就能换实现,不用改代码、不用重新编译。
// 反例一:明明编译期就知道类型,却用反射
User user = (User) Class.forName("com.example.User").getDeclaredConstructor().newInstance();
// 直接 new User() 更安全、更快、IDE 能检查
// 反例二:用反射访问私有字段(破坏封装)
Field field = User.class.getDeclaredField("password");
field.setAccessible(true);
// 现在你可以读写任何字段,包括本该被保护的
// 反例三:用反射调私有方法做单元测试
// 测试应该测"公开行为",而不是依赖内部实现细节文件:labs/common-classes/src/main/java/cn/kb/learn/common/ReflectionLesson.java(节选核心部分,完整代码见仓库)
package cn.kb.learn.common;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
public class ReflectionLesson {
public static class Product {
public static final String CATEGORY = "默认分类";
private String name;
private long priceInCents;
public Product() {
this.name = "未命名";
}
public Product(String name, long priceInCents) {
this.name = name;
this.priceInCents = priceInCents;
}
public String describe() {
return name + " / " + priceInCents + " 分";
}
/** 仅通过反射调用(getDeclaredMethod + setAccessible),编译期看不到引用 */
@SuppressWarnings("unused")
private String secret() {
return "私有方法被反射调用";
}
@Override
public String toString() {
return "Product{name='" + name + "', priceInCents=" + priceInCents + "}";
}
}
public static void main(String[] args) throws Exception {
Class<Product> byLiteral = Product.class;
Class<?> byInstance = new Product().getClass();
Class<?> byName = Class.forName("cn.kb.learn.common.ReflectionLesson$Product");
System.out.println("三种方式得到同一个 Class: "
+ (byLiteral == byInstance && byInstance == byName));
System.out.println("全限定名: " + byLiteral.getName());
System.out.println("简单名: " + byLiteral.getSimpleName());
System.out.println("父类: " + byLiteral.getSuperclass().getSimpleName());
System.out.println("修饰符: " + Modifier.toString(byLiteral.getModifiers()));
System.out.println("声明的字段: " + Arrays.toString(
Arrays.stream(byLiteral.getDeclaredFields()).map(Field::getName).sorted().toArray()));
System.out.println("声明的方法: " + Arrays.toString(
Arrays.stream(byLiteral.getDeclaredMethods()).map(Method::getName).sorted().toArray()));
Constructor<Product> constructor = byLiteral.getConstructor(String.class, long.class);
Product created = constructor.newInstance("键盘", 19_900L);
System.out.println("反射创建实例: " + created);
Field nameField = byLiteral.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(created, "机械键盘");
System.out.println("反射修改私有字段后: " + created);
Method describe = byLiteral.getMethod("describe");
System.out.println("调用公开方法: " + describe.invoke(created));
Method secret = byLiteral.getDeclaredMethod("secret");
secret.setAccessible(true);
System.out.println("调用私有方法: " + secret.invoke(created));
Field category = byLiteral.getDeclaredField("CATEGORY");
System.out.println("静态字段: " + category.get(null));
int times = 200_000;
long start = System.nanoTime();
for (int i = 0; i < times; i++) {
describe.invoke(created);
}
long reflectCost = (System.nanoTime() - start) / 1_000_000;
start = System.nanoTime();
for (int i = 0; i < times; i++) {
created.describe();
}
long directCost = (System.nanoTime() - start) / 1_000_000;
System.out.println("调用 " + times + " 次 —— 反射: " + reflectCost + " ms,直接调用: " + directCost + " ms");
}
}运行命令:
cd labs
./mvnw -pl common-classes compile
java -cp common-classes/target/classes cn.kb.learn.common.ReflectionLesson预期输出:
三种方式得到同一个 Class: true
全限定名: cn.kb.learn.common.ReflectionLesson$Product
简单名: Product
父类: Object
修饰符: public static
声明的字段: [CATEGORY, name, priceInCents]
声明的方法: [describe, secret, toString]
反射创建实例: Product{name='键盘', priceInCents=19900}
反射修改私有字段后: Product{name='机械键盘', priceInCents=19900}
调用公开方法: 机械键盘 / 19900 分
调用私有方法: 私有方法被反射调用
静态字段: 默认分类
调用 200000 次 —— 反射: 63 ms,直接调用: 11 ms四处值得注意:
| 输出 | 含义 |
|---|---|
三种方式得到同一个 Class: true | 类在 JVM 里只有一份 Class 对象 |
声明的字段: [CATEGORY, name, priceInCents] | 包含私有字段与静态常量 |
反射修改私有字段后: ...'机械键盘' | 私有字段被成功修改——封装被打破 |
最后一行:63 ms vs 11 ms | 反射比直接调用慢约 5 倍(数值依机器而变,关注数量级) |
byLiteral == byInstance 处断点,观察两个变量的 id 相同——同一个 Class 对象;nameField.set(created, "机械键盘") 处按 F7 进入,观察它最终走到 Unsafe 或字段偏移量的写入——这就是"绕过访问控制"的实现层面;describe.invoke(created) 处按 F7 进入,观察 Method.invoke 内部的参数校验与装箱过程,理解性能开销来自哪里;getDeclaredField("name") 改成 getField("name"),观察抛出的 NoSuchFieldException(因为 getField 只找 public 字段)——一次实验记住两者的区别。ClassNotFoundException 与 NoClassDefFoundError 分不清 ClassNotFoundException ← 你主动调用 Class.forName(字符串写错了)
NoClassDefFoundError ← JVM 要用这个类,但 classpath 里没有两者的区别在 《Java 语言与运行机制》 讲过:前者是"主动找没找到",后者是"被动用发现缺失"。
setAccessible(true) 在模块系统下失败 InaccessibleObjectException: Unable to make field private final ... accessible原因:Java 9+ 的模块系统限制了对 JDK 内部类的深度反射。修复:不要反射 JDK 内部类;确有必要时用 --add-opens 启动参数(并明白这会带来兼容性风险)。
Product.class.getMethod("getname"); // NoSuchMethodException:大小写错了修复:反射代码必须有测试覆盖,因为编译器帮不了你。
见 6.3 节。修复:能用接口 + 多态解决的问题,不要用反射。反射是框架的工具,不是业务代码的工具。
目标:用反射写一个极简的"对象转字符串"工具。
步骤:
String describe(Object target):用反射输出对象的类名与所有字段名值对(如 Product{name=机械键盘, priceInCents=19900});private 字段(用 setAccessible(true)),并说明这在真实项目里是否合适;Object create(String className):按类名创建实例(无参构造),类不存在时抛出带清晰消息的异常;Product、String(无无参构造,应失败)各一次,观察异常差异。验收方式:能解释第 3 步"为什么反射能读私有字段",以及第 5 步 String 为什么会失败。
扩展任务:给第 2 步的 describe 加上"跳过静态字段"与"跳过 null 字段"的逻辑(提示:Modifier.isStatic(field.getModifiers()))。
基础题
getFields() 能获取到父类的私有字段。(对 / 错)Class 对象的三种方式是 、、______。代码阅读题
class Item {
private String name = "默认";
private String name() {
return name;
}
}
Object item = Item.class.getDeclaredConstructor().newInstance();
Method method = Item.class.getDeclaredMethod("name");
System.out.println(method.invoke(item));编程题
Map<String, Object> toMap(Object target): Map<字段名, 字段值>;IllegalAccessException(转换成 IllegalStateException,保留原因)。综合任务
@Inject(可用于字段);Object createAndInject(Class<?> type): @Inject 的,就创建该字段类型的实例并注入;OrderService(依赖 OrderRepository),并打印注入结果;提示 1(方向):第 4 题的两个方法都是 private,getDeclaredMethod 能找到,但调用前需要打开访问权限。
提示 2(关键点):第 5 题的字段遍历用 getDeclaredFields(),跳过 Modifier.isStatic(...) 的字段。
提示 3(接近答案):第 6 题的注入流程是"创建实例 → 遍历字段 → 有 @Inject 就 new 一个字段类型 → setAccessible(true) → set 进去",这正是 Spring 早期版本的核心逻辑。
getFields() 只返回 public 字段(包括继承来的);要拿本类的所有字段(含 private)用 getDeclaredFields()。父类的私有字段任何方法都拿不到(getSuperclass().getDeclaredFields() 可以拿父类自己声明的)。X.class、obj.getClass()、Class.forName("全限定名")。不能运行,会抛 IllegalAccessException。
getDeclaredMethod("name") 能找到私有方法,但 invoke 前必须打开访问权限:
Method method = Item.class.getDeclaredMethod("name");
method.setAccessible(true); // 关键的一行
System.out.println(method.invoke(item)); // 默认顺带一个注意点:Item 是包级私有类(没有 public),如果测试代码在别的包,连 getDeclaredConstructor() 都需要 setAccessible(true)。
package cn.kb.learn.common;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.LinkedHashMap;
import java.util.Map;
public class ObjectMapper {
static Map<String, Object> toMap(Object target) {
if (target == null) {
throw new IllegalArgumentException("目标对象不能为 null");
}
Map<String, Object> result = new LinkedHashMap<>();
for (Field field : target.getClass().getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
field.setAccessible(true);
try {
Object value = field.get(target);
if (value != null) {
result.put(field.getName(), value);
}
} catch (IllegalAccessException exception) {
throw new IllegalStateException("无法读取字段: " + field.getName(), exception);
}
}
return result;
}
record User(String name, Integer age) {
}
public static void main(String[] args) {
System.out.println(toMap(new User("张三", 30)));
System.out.println(toMap(new User("李四", null)));
}
}预期输出(record 的字段是 private final,反射可读):
{name=张三, age=30}
{name=李四}package cn.kb.learn.common;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
public class MiniContainer {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Inject {
}
static class OrderRepository {
String describe() {
return "订单仓储已就绪";
}
}
static class OrderService {
@Inject
private OrderRepository repository;
String handle() {
return "服务调用: " + repository.describe();
}
}
static Object createAndInject(Class<?> type) throws Exception {
Object instance = type.getDeclaredConstructor().newInstance();
for (Field field : type.getDeclaredFields()) {
if (field.isAnnotationPresent(Inject.class)) {
Object dependency = field.getType().getDeclaredConstructor().newInstance();
field.setAccessible(true);
field.set(instance, dependency);
}
}
return instance;
}
public static void main(String[] args) throws Exception {
OrderService service = (OrderService) createAndInject(OrderService.class);
System.out.println(service.handle());
}
}Spring 的依赖注入本质上做了什么:
Field.set + setAccessible)或构造参数;为什么能"自动"装配:因为注解告诉它"这里需要一个依赖",而反射让它能在运行时不写死类型地创建与注入。这也解释了为什么"改一个注解就能换实现"——所有信息都在运行时由框架读取。
预期输出:
服务调用: 订单仓储已就绪Class 对象并读取类信息;setAccessible(true) 在做什么,以及它的风险;getDeclaredXxx 与 getXxx 的区别;《注解基础》:注解负责"声明意图",反射负责"执行意图"——两者合起来才是 Spring 的完整面貌。