一、为什么需要 Java 反射
Java 反射(Reflection)是运行时探查类结构并动态调用其成员的能力。日常写 CRUD 很少直接碰它,但 Spring 的依赖注入、MyBatis 的结果映射、Jackson 的 JSON 绑定、JUnit 的测试方法调度,几乎都靠反射驱动。理解反射,才算真正读懂框架的”黑盒子”,也才能在排错时知道异常究竟从哪一层抛出。
二、获取 Class 对象的三种方式
反射的起点是 Class 对象。同一个类在 JVM 中只加载一次,因此全进程只有一个 Class 实例,以下三种方式拿到的都是它:
// 1. 类名.class(编译期已知,最安全,无异常)
Class<String> c1 = String.class;
// 2. 实例.getClass()(已有对象时最方便)
String s = "";
Class<? extends String> c2 = s.getClass();
// 3. Class.forName(最常用,类名来自配置/注解/扫描路径)
Class<?> c3 = Class.forName("com.example.service.UserService");
三者各有适用场景,对比如下:
| 方式 | 触发类加载 | 编译期依赖 | 典型用途 |
.class | 是 | 强依赖(需 import) | 已知类型的安全编码 |
getClass() | 否(对象已存在) | 强依赖 | 运行时判断实例类型 |
forName() | 是(懒加载) | 无依赖(字符串) | 框架扫描、SPI、配置驱动 |
三、反射创建对象:不只是 new
最常见的需求是”按类名造对象”。无参构造直接用 getDeclaredConstructor().newInstance();带参构造则需先取对应 Constructor 再传参:
// 无参构造
Object u1 = Class.forName("com.example.User").getDeclaredConstructor().newInstance();
// 带参构造(String, int)
Class<?> userClass = Class.forName("com.example.User");
Object u2 = userClass.getDeclaredConstructor(String.class, int.class)
.newInstance("Alice", 30);
注意 newInstance() 在 JDK 9 后已标记废弃,应改用 Constructor.newInstance(),因为它能正确包装异常、不会把受检异常吞掉。这也是很多老框架升级时踩的坑。
3.1 突破私有构造:单例模式的”后门”
反射可以调用私有构造器,这意味着单纯靠”私有构造 + 静态字段”实现的单例,在反射面前并不安全:
Constructor<?> c = Singleton.class.getDeclaredConstructor();
c.setAccessible(true); // 关闭访问检查
Singleton s1 = (Singleton) c.newInstance();
Singleton s2 = (Singleton) c.newInstance();
System.out.println(s1 == s2); // false:又造出一个实例!
想真正防住反射攻击,需要在构造器里加”第二次调用抛异常”的校验。关于枚举与 DCL 写法的取舍,可参考本站《Java 单例模式实战》一文。
四、读写字段:包括 private 与 final
反射能越过 private 直接读写字段,这也是 ORM 框架填充对象、测试框架注入 Mock 的底层手段:
User user = new User();
Field nameField = User.class.getDeclaredField("name");
nameField.setAccessible(true); // 访问私有字段必须这一步
nameField.set(user, "Bob");
System.out.println(nameField.get(user)); // Bob
修改 final 字段就更”凶险”了:JVM 会对 final 做常量内联,单纯 set 可能不生效,需先去掉 Modifier.FINAL 修饰位再写。这种黑魔法在生产环境应坚决避免,它破坏了 JVM 的优化假设,极易导致难以排查的诡异 Bug。
五、调用方法:私有、变参、静态
方法调用通过 Method.invoke() 完成,第一个参数是目标对象(静态方法传 null):
Method m = User.class.getDeclaredMethod("rename", String.class);
m.setAccessible(true);
m.invoke(user, "Carol"); // 调用私有 rename
// 静态方法:第一个参数传 null
Method staticM = Math.class.getDeclaredMethod("max", int.class, int.class);
int r = (int) staticM.invoke(null, 3, 7); // 7
变参方法在反射里要传一个 Object[] 数组,否则会抛出 IllegalArgumentException。框架在封装通用调用时,通常要先判断 method.isVarArgs() 再做数组包装。
六、反射与泛型:类型擦除下的”残影”
Java 泛型在编译后会被擦除,但方法的返回值、字段的声明类型仍保留在签名里,可通过 getGenericReturnType() 拿到:
Method getter = Box.class.getMethod("getValue");
Type t = getter.getGenericReturnType();
System.out.println(t.getTypeName()); // 可能是 com.example.User 而非 Object
这正是 Jackson、Gson 这类库能在反序列化时还原 List<User> 具体元素类型的原理:它们读取泛型签名信息,再递归构造子类型。关于泛型通配符与类型擦除本身的坑,可延伸阅读《Java 泛型通配符与类型擦除:PECS 避坑实战》。
七、反射在主流框架里的真实位置
反射不是”炫技”,而是框架解耦的基石。几个典型落点:
| 框架 | 用反射做什么 |
| Spring IoC | 扫描 @Component,反射创建 Bean 并注入 @Autowired 字段 |
| MyBatis | 把 ResultSet 列按列名反射 set 进实体字段 |
| Jackson | 读取 getter/setter 与泛型签名完成 JSON 绑定 |
| JUnit 5 | 扫描 @Test 方法并用 Method.invoke 调度执行 |
| 动态代理 | JDK 代理在运行时生成实现类并反射转发调用 |
其中动态代理与反射高度耦合,《Java 动态代理实战:JDK 与 CGLIB 对比》一文详细拆解了代理类如何通过反射转发方法。注解处理器则在编译期走另一条路,可参考《Java 注解处理器实战》。
7.1 手写一个极简 IoC 容器
下面用不到 30 行代码,演示框架最核心的”扫描 + 反射创建 + 注入”逻辑:
public class MiniContainer {
private final Map<Class<?>, Object> beans = new HashMap<>();
public void register(Class<?> clazz) throws Exception {
Object instance = clazz.getDeclaredConstructor().newInstance();
for (Field f : clazz.getDeclaredFields()) {
if (f.isAnnotationPresent(Inject.class)) {
f.setAccessible(true);
// 递归创建依赖(简化版,不处理循环依赖)
Object dep = beans.getOrDefault(f.getType(),
f.getType().getDeclaredConstructor().newInstance());
f.set(instance, dep);
}
}
beans.put(clazz, instance);
}
@SuppressWarnings("unchecked")
public <T> T getBean(Class<T> clazz) {
return (T) beans.get(clazz);
}
}
这段代码逻辑上等价于 Spring 容器的微缩版:用 getDeclaredConstructor().newInstance() 造对象,再用 setAccessible(true) 把依赖塞进私有字段。读懂它,再看 Spring 源码就不再是天书。
八、性能与安全:反射不是免费的
反射要绕过编译期检查、做运行时权限校验,单次调用比直接调用慢几倍到几十倍。但在框架里它通常只发生一次(启动时扫描),结果会被缓存,所以实际影响有限。真正要注意的是三点:
- 缓存 Method/Field/Constructor:别在循环里反复
getMethod(),应取到后缓存到static final或ConcurrentHashMap。 setAccessible(true)的代价:每次调用都会做安全检查,高频路径可用MethodHandle替代,它把访问检查提前到创建期。- JPMS 模块限制:JDK 9+ 后,非开放模块(
opens)的私有成员默认拒绝反射访问,需在module-info.java里声明,否则抛InaccessibleObjectException。
下面是三种调用方式的取舍对照:
| 方式 | 速度 | 灵活性 | 适用 |
| 直接调用 | 最快 | 低(编译期固定) | 业务常规逻辑 |
| Method.invoke | 慢 10~50× | 高(运行时决定) | 框架、通用封装 |
| MethodHandle | 接近直接调用 | 中 | 高频反射路径 |
九、常见异常排障清单
| 异常 | 根因 | 修复 |
ClassNotFoundException | 类名拼错或不在 classpath | 核对全限定名、依赖是否引入 |
NoSuchMethodException | 方法名/参数类型不匹配 | 用 getDeclaredMethods() 打印核对 |
IllegalAccessException | 未 setAccessible(true) | 私有成员调用前先放开 |
IllegalArgumentException | 实参与形参类型/个数不符 | 检查变参是否包成数组 |
InaccessibleObjectException | 模块未 opens 私有成员 | module-info 加 opens 或换公开 API |
十、落地清单与最佳实践
- 优先用
Constructor.newInstance()而非废弃的Class.newInstance()。 - 反射创建的对象与依赖,尽量在启动期完成并缓存,别放进请求热路径。
- 访问私有成员前统一
setAccessible(true),但生产代码要评估安全风险。 - 别用反射去改 final 字段,那是给框架底层和极端调试留的口子。
- JDK 9+ 项目注意
module-info.java的opens声明,避免迁移时被模块系统拦住。 - 高频反射场景评估
MethodHandle或代码生成(如 Byte Buddy)替代。
Java 反射表面是”运行时黑魔法”,本质却是框架解耦与扩展的通用语言。把它和注解、动态代理、泛型结合起来看,你会发现 Spring、MyBatis 这些庞然大物,底层不过是”扫描类 + 反射创建 + 注入依赖”的精巧组合。先把这套机制吃透,再读源码,很多原本模糊的设计瞬间就通了。




