反射(Reflection)是 Java 中最强大也最容易被误用的特性之一。它打破了 Java 的封装性,让程序在运行时能够 "看透" 类的内部结构(属性、方法、构造器),并动态操作这些组件。小到框架中的配置解析,大到 Spring、MyBatis 等核心框架的底层实现,反射都是不可或缺的核心技术。本文将从原理、核心 API、实战场景到使用注意事项,帮你彻底掌握反射。
一、反射的核心本质:运行时 "解剖" 类
1. 什么是反射?
Java 程序在编译期会将.java文件编译为.class字节码文件,运行时 JVM 会加载这些字节码并创建对应的Class对象(每个类只有一个Class实例)。
反射的本质 :通过Class对象,在运行时获取类的所有信息(属性、方法、构造器、父类、注解等),并动态调用类的方法、修改属性值,无需在编译期确定具体的类和方法。
简单来说:正常编程是 "写代码时确定调用谁",反射是 "运行时才确定调用谁"------ 这也是动态代理、框架灵活配置的核心底层逻辑。
2. 反射的核心价值
- 打破封装:可访问类的私有属性 / 方法(需关闭安全检查);
- 动态性:运行时动态加载、使用未知的类(比如读取配置文件中的类名,动态创建实例);
- 解耦:框架无需硬编码依赖具体类,通过反射适配任意符合规则的类(如 Spring 的 IOC 容器)。
二、反射的核心 API:从获取 Class 对象到动态操作
反射的所有操作都围绕java.lang.reflect包和Class类展开,核心步骤分为 "获取 Class 对象" 和 "操作类组件" 两步。
1. 第一步:获取 Class 对象(反射的入口)
Class对象是反射的基础,获取方式有 3 种:
// 方式1:通过类名.class(编译期确定,最安全)
Class<User> clazz1 = User.class;
// 方式2:通过对象.getClass()(适用于已有实例的场景)
User user = new User();
Class<? extends User> clazz2 = user.getClass();
// 方式3:通过Class.forName("全类名")(运行时动态加载,最灵活,需处理异常)
Class<?> clazz3 = Class.forName("com.example.reflect.User");
注意:
Class.forName()需传入类的全限定名(包名 + 类名),且会触发类的静态代码块执行(比如加载数据库驱动时Class.forName("com.mysql.cj.jdbc.Driver"))。
2. 第二步:操作类的核心组件
以一个简单的User类为例,演示反射的核心操作:
// 目标类
public class User {
private Long id;
public String name;
public User() {} // 无参构造
public User(Long id, String name) { // 有参构造
this.id = id;
this.name = name;
}
private String getUserName() { // 私有方法
return "用户名:" + name;
}
public void setId(Long id) { // 公有方法
this.id = id;
}
}
(1)操作构造器:动态创建实例
Class<?> userClass = Class.forName("com.example.reflect.User");
// 1. 获取无参构造器,创建实例
Constructor<?> constructor1 = userClass.getConstructor();
User user1 = (User) constructor1.newInstance();
// 2. 获取有参构造器,创建实例(参数需匹配构造器的参数类型)
Constructor<?> constructor2 = userClass.getConstructor(Long.class, String.class);
User user2 = (User) constructor2.newInstance(1L, "张三");
// 3. 获取私有构造器(需使用getDeclaredConstructor,且关闭访问检查)
Constructor<?> privateConstructor = userClass.getDeclaredConstructor(Integer.class);
privateConstructor.setAccessible(true); // 关闭访问检查,否则无法访问私有构造
User user3 = (User) privateConstructor.newInstance(100);
(2)操作属性:读取 / 修改属性值
User user = new User(1L, "张三");
Class<? extends User> clazz = user.getClass();
// 1. 获取公有属性并修改
Field nameField = clazz.getField("name"); // getField只能获取公有属性
nameField.set(user, "李四");
System.out.println(nameField.get(user)); // 输出:李四
// 2. 获取私有属性并修改
Field idField = clazz.getDeclaredField("id"); // getDeclaredField可获取所有属性(包括私有)
idField.setAccessible(true); // 关闭访问检查
idField.set(user, 2L);
System.out.println(idField.get(user)); // 输出:2
(3)操作方法:动态调用方法
User user = new User(1L, "张三");
Class<? extends User> clazz = user.getClass();
// 1. 调用公有方法
Method setIdMethod = clazz.getMethod("setId", Long.class); // 参数:方法名+参数类型
setIdMethod.invoke(user, 3L); // invoke:执行方法,参数:目标对象+方法入参
// 2. 调用私有方法
Method getUserNameMethod = clazz.getDeclaredMethod("getUserName");
getUserNameMethod.setAccessible(true);
String result = (String) getUserNameMethod.invoke(user);
System.out.println(result); // 输出:用户名:张三
三、反射的典型应用场景
反射看似抽象,但在实际开发中无处不在,以下是最核心的 3 个场景:
1. 动态代理(JDK 动态代理的核心)
JDK 动态代理的InvocationHandler.invoke()方法中,正是通过Method.invoke()(反射)调用目标对象的方法:
// 核心逻辑片段
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置增强逻辑
System.out.println("执行前置校验");
// 反射调用目标对象的方法
Object result = method.invoke(targetObject, args);
// 后置增强逻辑
System.out.println("执行后置日志");
return result;
}
没有反射,就无法在运行时动态调用任意目标对象的任意方法,动态代理也就无从谈起。
2. 框架的灵活配置(Spring/MyBatis 核心)
- Spring IOC :读取 XML / 注解配置中的类名,通过
Class.forName()动态加载类,再通过反射创建实例、注入属性; - MyBatis:通过反射将数据库查询结果集映射为 Java 实体类对象(自动给实体类的属性赋值);
- 配置文件解析 :比如读取
properties文件中的 "类名 = com.example.User",动态创建 User 实例,无需硬编码new User()。
3. 通用工具类开发
比如封装一个 "对象属性拷贝工具类",通过反射遍历源对象的所有属性,自动赋值给目标对象的对应属性(类似 Spring 的BeanUtils.copyProperties):
public static void copyProperties(Object source, Object target) throws Exception {
Class<?> sourceClass = source.getClass();
Class<?> targetClass = target.getClass();
// 获取源对象的所有属性
Field[] fields = sourceClass.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
// 获取目标对象的同名属性
Field targetField = targetClass.getDeclaredField(field.getName());
targetField.setAccessible(true);
// 反射赋值
targetField.set(target, field.get(source));
}
}
四、反射的注意事项:优势与风险并存
1. 反射的缺点
- 性能损耗 :反射需在运行时解析类结构,比直接调用方法 / 属性慢 10-100 倍(可通过
MethodHandle优化); - 打破封装:可访问私有成员,违背面向对象的封装原则,增加代码安全风险;
- 可读性差:反射代码比普通代码更抽象,调试难度高;
- 编译期无法校验 :比如调用一个不存在的方法,编译期不会报错,运行时才抛出
NoSuchMethodException。
2. 合理使用反射的建议
- 仅在需要 "动态性" 的场景使用(如框架开发),业务代码尽量避免;
- 对频繁调用的反射操作,缓存
Class、Method、Field对象(避免重复解析); - 必要时才关闭访问检查(
setAccessible(true)),用完后恢复; - 结合异常处理,捕获反射相关的受检异常(如
NoSuchFieldException、IllegalAccessException)。