Skip to content

Java 反射

反射是 Java 里非常重要的基础能力。Spring、MyBatis、JUnit、Jackson、Lombok、动态代理、注解扫描、对象映射、RPC 框架都离不开它。

如果要完整理解 Class 对象、类加载关系、Class.forNameClassLoader.loadClass、字段/构造/方法调用全过程、注解读取、泛型擦除、Method.invoke、Java 9+ 模块限制、性能优化和生产排查,建议继续看:反射全过程原理

零基础先记住一句话:

反射就是程序在运行时观察和操作类的信息,比如知道一个类有哪些字段、方法、构造方法、注解,并且可以动态创建对象、调用方法、读取或修改字段。

为什么需要反射

正常写代码时,我们通常在编译期就知道要创建哪个类、调用哪个方法:

java
UserService userService = new UserService();
userService.createUser("Tom");

但框架经常不知道你的业务类叫什么。比如 Spring 启动时,它要扫描很多类,然后判断:

  1. 哪些类加了 @Component
  2. 哪些字段需要 @Autowired 注入。
  3. 哪些方法加了 @PostConstruct
  4. Controller 里哪些方法能处理 HTTP 请求。

这些信息都不是 Spring 写死的,而是运行时通过反射读取出来的。

如果没有反射,框架就很难做到“你写一个类,加几个注解,框架自动识别并处理”。

普通调用和反射调用区别

mermaid
flowchart TD
    A["普通调用"] --> B["编译期知道类和方法"]
    B --> C["直接 new 对象"]
    C --> D["直接调用方法"]
    E["反射调用"] --> F["运行时拿到 Class 对象"]
    F --> G["查找构造方法、字段、方法"]
    G --> H["动态创建对象或调用方法"]

普通调用更直接、更快、更安全;反射更灵活,但更复杂,也有性能和封装性风险。

Class 对象是什么

每个被 JVM 加载的类,都会有一个对应的 Class 对象。它保存这个类的元信息,例如类名、包名、父类、接口、字段、方法、构造方法、注解。

mermaid
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 对象不是业务对象本身,而是描述“这个类长什么样”的对象。

比如:

java
User user = new User();
Class<?> clazz = user.getClass();

这里 user 是业务对象,clazz 是描述 User 类结构的元信息对象。

获取 Class 对象的三种方式

java
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

示例类

后面示例都基于这个类:

java
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;
    }
}

获取字段

java
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 映射。

读取和修改字段

java
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 的意义是封装,反射突破封装主要给框架、工具、测试和底层设施使用。

获取构造方法并创建对象

无参构造:

java
Constructor<User> constructor = User.class.getDeclaredConstructor();
User user = constructor.newInstance();

有参构造:

java
Constructor<User> constructor = User.class.getDeclaredConstructor(Long.class, String.class);
User user = constructor.newInstance(1L, "Tom");

常用方法:

方法说明
getConstructor获取 public 构造方法
getDeclaredConstructor获取当前类声明的构造方法,包含 private
newInstance调用构造方法创建对象

为什么很多框架要求类有无参构造:因为框架反射创建对象时,最容易调用无参构造,再通过字段或 setter 注入属性。

获取和调用方法

调用 public 方法:

java
User user = new User(1L, "Tom");

Method method = User.class.getDeclaredMethod("sayHello");
method.invoke(user);

调用 private 方法:

java
User user = new User(1L, "Tom");

Method method = User.class.getDeclaredMethod("secret");
method.setAccessible(true);
Object result = method.invoke(user);

System.out.println(result);

带参数方法示例:

java
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}
java
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() 里。

java
try {
    method.invoke(target);
} catch (InvocationTargetException ex) {
    Throwable realException = ex.getCause();
    throw new RuntimeException(realException);
}

反射和注解

反射经常和注解一起使用。注解负责“标记”,反射负责“读取标记并执行逻辑”。

自定义注解:

java
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();
}

使用注解:

java
public class OrderService {

    @AuditLog("创建订单")
    public void createOrder() {
        System.out.println("create order");
    }
}

读取注解:

java
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 模拟一个非常简化的依赖注入:扫描字段上的注解,然后给字段赋值。

定义注解:

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Inject {
}

业务类:

java
public class UserController {
    @Inject
    private UserService userService;

    public void hello() {
        userService.sayHello();
    }
}

public class UserService {
    public void sayHello() {
        System.out.println("hello reflection");
    }
}

注入逻辑:

java
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;
    }
}

测试:

java
UserController controller = SimpleContainer.createBean(UserController.class);
controller.hello();

这就是 Spring 依赖注入的极简影子:真实 Spring 更复杂,会处理 Bean 生命周期、单例池、循环依赖、代理对象、作用域、条件装配等。

反射和动态代理

反射还能配合动态代理实现“运行时生成代理对象”。JDK 动态代理要求代理接口。

接口:

java
public interface UserApi {
    void createUser(String name);
}

实现类:

java
public class UserApiImpl implements UserApi {
    @Override
    public void createUser(String name) {
        System.out.println("create user " + name);
    }
}

代理:

java
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把查询结果映射到对象字段
JacksonJSON 和 Java 对象互相转换
JUnit找到 @Test 方法并执行
AOP动态代理里通过 Method 调用目标方法
RPC根据接口名、方法名、参数类型找到并调用服务方法

理解反射后,很多框架就不再神秘。框架做的事情本质上是:读取元信息,按规则创建对象、赋值、调用方法。

反射的性能问题

反射比普通方法调用慢,原因包括:

  1. 运行时查找字段和方法。
  2. 访问检查。
  3. 参数装箱、拆箱和适配。
  4. 无法像普通调用那样充分被编译器优化。

但是业务系统里不要一听“反射慢”就害怕。多数框架会缓存反射结果,例如缓存 ClassFieldMethod,避免每次都查找。

错误写法:

java
for (User user : users) {
    Field nameField = User.class.getDeclaredField("name");
    nameField.setAccessible(true);
    Object value = nameField.get(user);
}

更好的写法:

java
Field nameField = User.class.getDeclaredField("name");
nameField.setAccessible(true);

for (User user : users) {
    Object value = nameField.get(user);
}

反射的风险

风险后果建议
破坏封装private 字段被随意修改业务代码不要滥用 setAccessible(true)
编译期不检查字段名、方法名写错运行时才报错封装工具方法,启动时提前校验
性能开销高频循环里反复查找反射对象变慢缓存 FieldMethod
安全风险可访问敏感字段或方法控制反射范围,不暴露任意类名执行
Java 模块限制Java 9 之后强封装可能限制深反射了解模块开放和 JVM 参数

尤其不要把外部用户传入的类名、方法名直接用于反射调用,否则可能造成严重安全问题。

反射和泛型擦除

Java 泛型在运行时大多会被擦除。

java
List<String> names = new ArrayList<>();
Class<?> clazz = names.getClass();
System.out.println(clazz);

运行时看到的是 ArrayList,不是完整的 ArrayList<String>

但字段、方法签名上的泛型信息可以通过反射读取到一部分:

java
public class UserHolder {
    private List<String> names;
}
java
Field field = UserHolder.class.getDeclaredField("names");
Type type = field.getGenericType();
System.out.println(type);

这也是一些 JSON 框架、ORM 框架需要处理 TypeParameterizedType 的原因。

学习路线

mermaid
flowchart TD
    A["先理解 Class 对象"] --> B["获取字段、方法、构造方法"]
    B --> C["动态创建对象和调用方法"]
    C --> D["读取注解"]
    D --> E["理解依赖注入"]
    E --> F["理解动态代理"]
    F --> G["回头看 Spring、MyBatis、AOP"]

常见面试题

反射是什么

反射是 Java 在运行时获取类结构信息并操作对象的能力。它可以获取类名、字段、方法、构造方法、注解,也可以动态创建对象、调用方法、修改字段。

反射有什么缺点

主要缺点是性能比直接调用差、破坏封装、运行时才发现错误、可能带来安全风险。

getFieldsgetDeclaredFields 区别

getFields 只返回 public 字段,包括父类 public 字段。getDeclaredFields 返回当前类声明的所有字段,包括 private,但不包含父类字段。

为什么 Spring 能根据注解创建对象

因为注解设置了运行时保留,Spring 扫描类时可以通过反射读取注解,然后根据规则创建对象、放入容器、注入依赖。

JDK 动态代理为什么要求接口

JDK 动态代理生成的是实现接口的代理类,所以目标类必须有接口。如果没有接口,常见做法是使用 CGLIB 这类基于子类的代理方式。

本章小结

反射让 Java 程序可以在运行时理解和操作类结构。它是框架自动化能力的基础:Spring 用它创建 Bean 和注入依赖,MyBatis 用它做结果映射,JUnit 用它执行测试方法,AOP 用它在方法调用前后织入增强。

学习反射不要只背 API,要理解它解决的问题:框架在编译期不知道你的业务类,但运行时可以通过 ClassFieldMethodConstructor 和注解把这些类组织起来。