反射
反射是 Java 在运行期检查和操作类、字段、方法、构造器、注解等结构的能力。它让程序可以在“不提前写死具体类”的情况下,根据运行时信息创建对象、调用方法、读取注解。
框架大量依赖反射:
- Spring 创建 Bean、注入依赖、读取注解。
- MyBatis 把查询结果映射成对象。
- Jackson 把 JSON 转成 Java 对象。
- JUnit 扫描并执行测试方法。
- 动态代理根据接口生成代理对象。
反射是理解 Java 框架的入口,但也要注意性能、封装破坏和模块访问限制。
# 1. Class 对象
每个被 JVM 加载的类,在运行时都有一个 Class 对象表示它的类型信息。
User.class 文件
│ 类加载
▼
JVM 中的 Class<User> 对象
│
├─ 类名
├─ 字段
├─ 方法
├─ 构造器
├─ 注解
└─ 父类 / 接口
获取 Class 的常见方式:
Class<String> c1 = String.class;
String text = "hello";
Class<?> c2 = text.getClass();
Class<?> c3 = Class.forName("java.lang.String");
对比:
| 方式 | 说明 |
|---|---|
String.class | 编译期已知类型 |
obj.getClass() | 通过对象获取运行时类型 |
Class.forName | 通过类全限定名加载 |
Class.forName 可能触发类加载和初始化,需要处理 ClassNotFoundException。
# 2. 获取类信息
Class<?> clazz = User.class;
System.out.println(clazz.getName());
System.out.println(clazz.getSimpleName());
System.out.println(clazz.getPackageName());
System.out.println(clazz.getSuperclass());
System.out.println(Arrays.toString(clazz.getInterfaces()));
常用 API:
| API | 作用 |
|---|---|
getName() | 全限定类名 |
getSimpleName() | 简单类名 |
getPackageName() | 包名 |
getSuperclass() | 父类 |
getInterfaces() | 实现的接口 |
isAnnotationPresent() | 是否存在某注解 |
# 3. 构造对象
推荐通过构造器对象创建实例:
Constructor<User> constructor = User.class.getConstructor(String.class);
User user = constructor.newInstance("Tom");
如果是无参构造器:
User user = User.class.getDeclaredConstructor().newInstance();
旧写法 clazz.newInstance() 已不推荐,因为它只能调用 public 无参构造器,异常表达也不够清晰。
创建流程:
Class<User>
│ getDeclaredConstructor
▼
Constructor<User>
│ newInstance
▼
User 对象
如果构造器是私有的:
Constructor<User> constructor = User.class.getDeclaredConstructor();
constructor.setAccessible(true);
User user = constructor.newInstance();
setAccessible(true) 会绕过 Java 语言访问检查,但在模块化环境或安全限制下可能失败。
# 4. 操作字段
示例类:
public class User {
private Long id;
private String name;
}
获取字段:
Field field = User.class.getDeclaredField("name");
field.setAccessible(true);
读取字段:
String name = (String) field.get(user);
设置字段:
field.set(user, "Tom");
字段 API:
| API | 作用 |
|---|---|
getFields() | 获取 public 字段,包含父类 |
getDeclaredFields() | 获取当前类声明字段,包含 private |
getField(name) | 获取指定 public 字段 |
getDeclaredField(name) | 获取当前类指定字段 |
反射设置字段会绕过 setter,因此可能破坏对象校验逻辑。框架可以这样做,业务代码要谨慎。
# 5. 调用方法
Method method = User.class.getDeclaredMethod("setName", String.class);
method.setAccessible(true);
method.invoke(user, "Alice");
调用有返回值方法:
Method getter = User.class.getDeclaredMethod("getName");
String name = (String) getter.invoke(user);
方法 API:
| API | 作用 |
|---|---|
getMethods() | 获取 public 方法,包含父类 |
getDeclaredMethods() | 获取当前类声明方法,包含 private |
getMethod(name, parameterTypes) | 获取指定 public 方法 |
getDeclaredMethod(name, parameterTypes) | 获取当前类指定方法 |
invoke 抛出的底层异常会被包装在 InvocationTargetException 中:
try {
method.invoke(user);
} catch (InvocationTargetException e) {
Throwable target = e.getTargetException();
throw target;
}
排查反射调用异常时,要看 getTargetException()。
# 6. 读取注解
定义注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AuditLog {
String value();
}
使用:
public class OrderService {
@AuditLog("创建订单")
public void createOrder() {
}
}
读取:
Method method = OrderService.class.getDeclaredMethod("createOrder");
AuditLog auditLog = method.getAnnotation(AuditLog.class);
if (auditLog != null) {
System.out.println(auditLog.value());
}
流程:
扫描 Class
│
▼
获取 Method
│
▼
读取 Runtime 注解
│
▼
根据注解值执行框架逻辑
这就是很多 AOP、权限、日志、事务框架的基础套路。
# 7. 反射与泛型
泛型运行期会擦除,但字段、父类、方法签名中声明的泛型信息可以通过 Type 读取。
public class UserRepository extends BaseRepository<User> {
}
读取父类泛型:
Type type = UserRepository.class.getGenericSuperclass();
ParameterizedType parameterizedType = (ParameterizedType) type;
Type actualType = parameterizedType.getActualTypeArguments()[0];
常见 Type 子类型:
| 类型 | 说明 |
|---|---|
Class | 普通类 |
ParameterizedType | 参数化类型,如 List<String> |
TypeVariable | 类型变量,如 T |
WildcardType | 通配符,如 ? extends Number |
GenericArrayType | 泛型数组 |
框架使用这些信息处理泛型 Repository、HTTP 返回类型、JSON 反序列化等。
# 8. 反射与访问控制
setAccessible(true) 可以关闭 Java 语言访问检查。
Field field = User.class.getDeclaredField("name");
field.setAccessible(true);
但这并不意味着任何环境都能随意访问:
- 安全管理器或运行策略可能限制。
- Java 模块系统可能禁止深反射访问未开放包。
- 框架可能需要添加
--add-opens参数。
开发中不要把反射当成访问 private 成员的常规方式。private 表达了封装边界,绕过它会增加维护风险。
# 9. 反射性能
反射比直接调用慢,原因包括:
- 需要查找元数据。
- 需要访问检查。
- 参数和返回值可能需要装箱、拆箱。
- 方法调用路径更复杂。
优化方式:
| 方法 | 说明 |
|---|---|
缓存 Class、Method、Field | 避免重复查找 |
| 启动期扫描 | 将成本放到启动阶段 |
| 使用 MethodHandle | 某些场景性能更好 |
| 生成字节码 | 高性能框架常用 |
| 避免业务热路径频繁反射 | 热点代码直接调用 |
缓存示例:
private static final Map<String, Method> METHOD_CACHE = new ConcurrentHashMap<>();
Spring、Jackson 等框架都会大量缓存反射元数据。
# 10. 反射常见异常
| 异常 | 常见原因 |
|---|---|
ClassNotFoundException | 类名错误或 classpath 中不存在 |
NoSuchMethodException | 方法名或参数类型不匹配 |
NoSuchFieldException | 字段不存在 |
IllegalAccessException | 没有访问权限 |
InstantiationException | 抽象类、接口等不能实例化 |
InvocationTargetException | 被调用方法内部抛出异常 |
反射问题排查顺序:
类是否加载到 classpath
│
▼
类名、方法名、字段名是否正确
│
▼
参数类型是否完全匹配
│
▼
访问权限是否允许
│
▼
底层方法是否自己抛了异常
# 11. 反射在框架中的典型应用
# 11.1 Spring Bean 创建
扫描类
│
▼
发现 @Component
│
▼
选择构造器
│
▼
反射创建对象
│
▼
注入字段或方法
│
▼
放入容器
# 11.2 JSON 反序列化
JSON 字段 name
│
▼
匹配 Java 字段 name 或 setter
│
▼
类型转换
│
▼
反射设置对象属性
# 11.3 单元测试
扫描测试类
│
▼
查找 @Test 方法
│
▼
反射调用测试方法
│
▼
收集执行结果
# 12. 开发建议
- 业务代码优先使用普通方法调用,不要过度反射。
- 框架、工具、通用组件可以使用反射提升灵活性。
- 反射元数据要缓存。
- 反射异常要保留根因。
- 不要用反射随意破坏封装。
- 对模块化项目,要关注包是否开放给反射访问。
# Tips 快问快答
Q:反射是什么? A:运行期检查和操作类、方法、字段、构造器、注解等结构的能力。
Q:Class 对象是什么?
A:JVM 加载类后,用 Class 对象表示该类的运行时类型信息。
Q:获取 Class 有哪些方式?
A:类名.class、对象.getClass()、Class.forName("全限定类名")。
Q:反射能访问 private 字段吗?
A:可以尝试通过 setAccessible(true) 绕过访问检查,但可能受到模块或安全限制。
Q:为什么 method.invoke 抛的是 InvocationTargetException?
A:被调用方法内部抛出的异常会被包装起来,根因在 getTargetException()。
Q:反射一定很慢吗? A:比直接调用慢,但框架通过缓存、方法句柄、字节码生成等方式降低开销。
Q:业务代码应该大量使用反射吗? A:不建议。反射适合框架和通用工具,普通业务逻辑应优先显式调用。
Q:反射能拿到泛型真实类型吗? A:局部变量通常不能,但字段、方法签名、父类声明中的部分泛型信息可以读取。
Q:Spring 为什么需要反射? A:它需要在运行期扫描类、读取注解、创建对象、注入依赖、调用生命周期方法。
Q:模块化后反射有什么变化? A:未开放的模块包可能拒绝深反射访问,需要通过模块声明或 JVM 参数开放。