Java 反射
反射是 Java 里非常重要的基础能力。Spring、MyBatis、JUnit、Jackson、Lombok、动态代理、注解扫描、对象映射、RPC 框架都离不开它。
如果要完整理解 Class 对象、类加载关系、Class.forName 与 ClassLoader.loadClass、字段/构造/方法调用全过程、注解读取、泛型擦除、Method.invoke、Java 9+ 模块限制、性能优化和生产排查,建议继续看:反射全过程原理。
零基础先记住一句话:
反射就是程序在运行时观察和操作类的信息,比如知道一个类有哪些字段、方法、构造方法、注解,并且可以动态创建对象、调用方法、读取或修改字段。
为什么需要反射
正常写代码时,我们通常在编译期就知道要创建哪个类、调用哪个方法:
UserService userService = new UserService();
userService.createUser("Tom");但框架经常不知道你的业务类叫什么。比如 Spring 启动时,它要扫描很多类,然后判断:
- 哪些类加了
@Component。 - 哪些字段需要
@Autowired注入。 - 哪些方法加了
@PostConstruct。 - Controller 里哪些方法能处理 HTTP 请求。
这些信息都不是 Spring 写死的,而是运行时通过反射读取出来的。
如果没有反射,框架就很难做到“你写一个类,加几个注解,框架自动识别并处理”。
普通调用和反射调用区别
flowchart TD
A["普通调用"] --> B["编译期知道类和方法"]
B --> C["直接 new 对象"]
C --> D["直接调用方法"]
E["反射调用"] --> F["运行时拿到 Class 对象"]
F --> G["查找构造方法、字段、方法"]
G --> H["动态创建对象或调用方法"]普通调用更直接、更快、更安全;反射更灵活,但更复杂,也有性能和封装性风险。
Class 对象是什么
每个被 JVM 加载的类,都会有一个对应的 Class 对象。它保存这个类的元信息,例如类名、包名、父类、接口、字段、方法、构造方法、注解。
flowchart TD
A["User.java 源码"] --> B["javac 编译"]
B --> C["User.class 字节码"]
C --> D["JVM 类加载"]
D --> E["Class<User> 对象"]
E --> F["字段信息"]
E --> G["方法信息"]
E --> H["构造方法信息"]
E --> I["注解信息"]Class 对象不是业务对象本身,而是描述“这个类长什么样”的对象。
比如:
User user = new User();
Class<?> clazz = user.getClass();这里 user 是业务对象,clazz 是描述 User 类结构的元信息对象。
获取 Class 对象的三种方式
Class<User> c1 = User.class;
User user = new User();
Class<?> c2 = user.getClass();
Class<?> c3 = Class.forName("com.example.User");对比:
| 方式 | 说明 | 常见场景 |
|---|---|---|
User.class | 编译期知道类 | 普通开发、框架配置 |
user.getClass() | 已经有对象 | 根据对象反查类型 |
Class.forName() | 只有类全限定名字符串 | JDBC 驱动加载、配置化创建对象 |
Class.forName 会触发类加载和初始化,使用时要注意类名写错会抛 ClassNotFoundException。
示例类
后面示例都基于这个类:
public class User {
private Long id;
private String name;
public User() {
}
public User(Long id, String name) {
this.id = id;
this.name = name;
}
public void sayHello() {
System.out.println("hello " + name);
}
private String secret() {
return "secret:" + id;
}
}获取字段
Class<User> clazz = User.class;
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName() + " -> " + field.getType().getName());
}常用方法:
| 方法 | 说明 |
|---|---|
getFields() | 获取 public 字段,包含父类 public 字段 |
getDeclaredFields() | 获取当前类声明的所有字段,包含 private |
getField(name) | 获取指定 public 字段 |
getDeclaredField(name) | 获取当前类指定字段,包含 private |
为什么经常用 getDeclaredField:框架通常要读取 private 字段,例如 Spring 做依赖注入、Jackson 做 JSON 映射。
读取和修改字段
User user = new User(1L, "Tom");
Field nameField = User.class.getDeclaredField("name");
nameField.setAccessible(true);
Object oldValue = nameField.get(user);
System.out.println(oldValue);
nameField.set(user, "Jerry");
System.out.println(nameField.get(user));setAccessible(true) 表示关闭 Java 语言层面的访问检查,让 private 字段也能被反射访问。
注意:这不是推荐业务代码随便访问 private 字段。private 的意义是封装,反射突破封装主要给框架、工具、测试和底层设施使用。
获取构造方法并创建对象
无参构造:
Constructor<User> constructor = User.class.getDeclaredConstructor();
User user = constructor.newInstance();有参构造:
Constructor<User> constructor = User.class.getDeclaredConstructor(Long.class, String.class);
User user = constructor.newInstance(1L, "Tom");常用方法:
| 方法 | 说明 |
|---|---|
getConstructor | 获取 public 构造方法 |
getDeclaredConstructor | 获取当前类声明的构造方法,包含 private |
newInstance | 调用构造方法创建对象 |
为什么很多框架要求类有无参构造:因为框架反射创建对象时,最容易调用无参构造,再通过字段或 setter 注入属性。
获取和调用方法
调用 public 方法:
User user = new User(1L, "Tom");
Method method = User.class.getDeclaredMethod("sayHello");
method.invoke(user);调用 private 方法:
User user = new User(1L, "Tom");
Method method = User.class.getDeclaredMethod("secret");
method.setAccessible(true);
Object result = method.invoke(user);
System.out.println(result);带参数方法示例:
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}Calculator calculator = new Calculator();
Method add = Calculator.class.getDeclaredMethod("add", int.class, int.class);
Object result = add.invoke(calculator, 1, 2);
System.out.println(result);invoke 返回的是 Object,如果原方法返回基本类型,会自动装箱。
反射异常为什么多
反射发生在运行时,所以很多问题不能在编译期提前发现。
| 异常 | 常见原因 |
|---|---|
ClassNotFoundException | 类名写错,或 classpath 里没有这个类 |
NoSuchFieldException | 字段名写错 |
NoSuchMethodException | 方法名或参数类型不匹配 |
IllegalAccessException | 没有访问权限,可能需要 setAccessible(true) |
InvocationTargetException | 被调用的方法内部抛异常 |
InstantiationException | 抽象类、接口等不能直接实例化 |
InvocationTargetException 要特别注意:它只是反射包装异常,真正业务异常在 getCause() 里。
try {
method.invoke(target);
} catch (InvocationTargetException ex) {
Throwable realException = ex.getCause();
throw new RuntimeException(realException);
}反射和注解
反射经常和注解一起使用。注解负责“标记”,反射负责“读取标记并执行逻辑”。
自定义注解:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AuditLog {
String value();
}使用注解:
public class OrderService {
@AuditLog("创建订单")
public void createOrder() {
System.out.println("create order");
}
}读取注解:
Method method = OrderService.class.getDeclaredMethod("createOrder");
AuditLog auditLog = method.getAnnotation(AuditLog.class);
if (auditLog != null) {
System.out.println("audit: " + auditLog.value());
}为什么 @Retention(RetentionPolicy.RUNTIME) 很重要:只有运行时还保留的注解,才能被反射读取。Spring 的很多注解都需要在运行时被扫描。
反射模拟简单 Bean 注入
下面 Demo 模拟一个非常简化的依赖注入:扫描字段上的注解,然后给字段赋值。
定义注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Inject {
}业务类:
public class UserController {
@Inject
private UserService userService;
public void hello() {
userService.sayHello();
}
}
public class UserService {
public void sayHello() {
System.out.println("hello reflection");
}
}注入逻辑:
public class SimpleContainer {
public static <T> T createBean(Class<T> clazz) throws Exception {
T bean = clazz.getDeclaredConstructor().newInstance();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Inject.class)) {
Object dependency = field.getType().getDeclaredConstructor().newInstance();
field.setAccessible(true);
field.set(bean, dependency);
}
}
return bean;
}
}测试:
UserController controller = SimpleContainer.createBean(UserController.class);
controller.hello();这就是 Spring 依赖注入的极简影子:真实 Spring 更复杂,会处理 Bean 生命周期、单例池、循环依赖、代理对象、作用域、条件装配等。
反射和动态代理
反射还能配合动态代理实现“运行时生成代理对象”。JDK 动态代理要求代理接口。
接口:
public interface UserApi {
void createUser(String name);
}实现类:
public class UserApiImpl implements UserApi {
@Override
public void createUser(String name) {
System.out.println("create user " + name);
}
}代理:
UserApi target = new UserApiImpl();
UserApi proxy = (UserApi) Proxy.newProxyInstance(
UserApi.class.getClassLoader(),
new Class[]{UserApi.class},
(Object proxyObject, Method method, Object[] args) -> {
System.out.println("before method: " + method.getName());
Object result = method.invoke(target, args);
System.out.println("after method: " + method.getName());
return result;
}
);
proxy.createUser("Tom");这就是 AOP 的基础思想:不改原业务类,在方法调用前后织入日志、事务、权限、监控等逻辑。
反射在框架里的典型用途
| 框架或场景 | 反射用途 |
|---|---|
| Spring IOC | 扫描类、读取注解、创建对象、注入字段 |
| Spring MVC | 找到 Controller 方法,绑定请求参数并调用 |
| MyBatis | 把查询结果映射到对象字段 |
| Jackson | JSON 和 Java 对象互相转换 |
| JUnit | 找到 @Test 方法并执行 |
| AOP | 动态代理里通过 Method 调用目标方法 |
| RPC | 根据接口名、方法名、参数类型找到并调用服务方法 |
理解反射后,很多框架就不再神秘。框架做的事情本质上是:读取元信息,按规则创建对象、赋值、调用方法。
反射的性能问题
反射比普通方法调用慢,原因包括:
- 运行时查找字段和方法。
- 访问检查。
- 参数装箱、拆箱和适配。
- 无法像普通调用那样充分被编译器优化。
但是业务系统里不要一听“反射慢”就害怕。多数框架会缓存反射结果,例如缓存 Class、Field、Method,避免每次都查找。
错误写法:
for (User user : users) {
Field nameField = User.class.getDeclaredField("name");
nameField.setAccessible(true);
Object value = nameField.get(user);
}更好的写法:
Field nameField = User.class.getDeclaredField("name");
nameField.setAccessible(true);
for (User user : users) {
Object value = nameField.get(user);
}反射的风险
| 风险 | 后果 | 建议 |
|---|---|---|
| 破坏封装 | private 字段被随意修改 | 业务代码不要滥用 setAccessible(true) |
| 编译期不检查 | 字段名、方法名写错运行时才报错 | 封装工具方法,启动时提前校验 |
| 性能开销 | 高频循环里反复查找反射对象变慢 | 缓存 Field、Method |
| 安全风险 | 可访问敏感字段或方法 | 控制反射范围,不暴露任意类名执行 |
| Java 模块限制 | Java 9 之后强封装可能限制深反射 | 了解模块开放和 JVM 参数 |
尤其不要把外部用户传入的类名、方法名直接用于反射调用,否则可能造成严重安全问题。
反射和泛型擦除
Java 泛型在运行时大多会被擦除。
List<String> names = new ArrayList<>();
Class<?> clazz = names.getClass();
System.out.println(clazz);运行时看到的是 ArrayList,不是完整的 ArrayList<String>。
但字段、方法签名上的泛型信息可以通过反射读取到一部分:
public class UserHolder {
private List<String> names;
}Field field = UserHolder.class.getDeclaredField("names");
Type type = field.getGenericType();
System.out.println(type);这也是一些 JSON 框架、ORM 框架需要处理 Type、ParameterizedType 的原因。
学习路线
flowchart TD
A["先理解 Class 对象"] --> B["获取字段、方法、构造方法"]
B --> C["动态创建对象和调用方法"]
C --> D["读取注解"]
D --> E["理解依赖注入"]
E --> F["理解动态代理"]
F --> G["回头看 Spring、MyBatis、AOP"]常见面试题
反射是什么
反射是 Java 在运行时获取类结构信息并操作对象的能力。它可以获取类名、字段、方法、构造方法、注解,也可以动态创建对象、调用方法、修改字段。
反射有什么缺点
主要缺点是性能比直接调用差、破坏封装、运行时才发现错误、可能带来安全风险。
getFields 和 getDeclaredFields 区别
getFields 只返回 public 字段,包括父类 public 字段。getDeclaredFields 返回当前类声明的所有字段,包括 private,但不包含父类字段。
为什么 Spring 能根据注解创建对象
因为注解设置了运行时保留,Spring 扫描类时可以通过反射读取注解,然后根据规则创建对象、放入容器、注入依赖。
JDK 动态代理为什么要求接口
JDK 动态代理生成的是实现接口的代理类,所以目标类必须有接口。如果没有接口,常见做法是使用 CGLIB 这类基于子类的代理方式。
本章小结
反射让 Java 程序可以在运行时理解和操作类结构。它是框架自动化能力的基础:Spring 用它创建 Bean 和注入依赖,MyBatis 用它做结果映射,JUnit 用它执行测试方法,AOP 用它在方法调用前后织入增强。
学习反射不要只背 API,要理解它解决的问题:框架在编译期不知道你的业务类,但运行时可以通过 Class、Field、Method、Constructor 和注解把这些类组织起来。
