Skip to content

Java 反射全过程原理

反射不是“背几个 API”就能掌握的知识点。真正要理解它,必须知道:类什么时候被 JVM 加载,Class 对象从哪里来,字段、方法、构造方法怎么被查找,Method.invoke 为什么会包装异常,框架为什么能靠注解自动创建对象,以及生产环境为什么不能滥用反射。

一句话先建立直觉:

反射就是把“编译期写死的调用”变成“运行时根据类信息动态决定怎么创建对象、读写字段、调用方法、读取注解”的能力。

学习目标

学完这一页,你应该能说清楚:

  1. 反射解决什么问题,为什么 Spring、MyBatis、Jackson、JUnit、RPC 框架都离不开它。
  2. Class 对象和类加载、方法区、元空间是什么关系。
  3. User.classuser.getClass()Class.forName()ClassLoader.loadClass() 有什么区别。
  4. getFieldgetDeclaredFieldgetMethodgetDeclaredMethod 的查找边界。
  5. setAccessible(true) 到底做了什么,Java 9 之后为什么可能失败。
  6. Method.invoke 的完整调用流程,为什么业务异常会被包成 InvocationTargetException
  7. 注解为什么必须是 RetentionPolicy.RUNTIME 才能被反射读到。
  8. 泛型擦除后,反射还能从哪里读到泛型声明信息。
  9. 商业项目里反射怎么用于 IOC、对象映射、接口分发、注解扫描。
  10. 反射有哪些性能、安全和维护风险,线上出问题怎么排查。

反射解决的根本问题

普通业务代码通常是这样写的:

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

这段代码在编译期已经确定了三件事:

  1. 要创建的是 UserService
  2. 要调用的是 create 方法。
  3. 参数类型是 String

但是框架不可能提前知道你的业务类叫什么。比如 Spring 启动时,它面对的是一堆 class 文件,它要自己判断:

  1. 哪些类加了 @Component,需要创建 Bean。
  2. 哪些字段加了 @Autowired,需要注入依赖。
  3. 哪些方法加了 @RequestMapping,可以处理 HTTP 请求。
  4. 哪些方法加了事务注解,调用时要包一层事务逻辑。

如果没有反射,就只能让你手工写大量配置:

java
container.register("userService", new UserService(new UserRepository()));
container.registerMapping("/users", userController::list);

反射让框架可以做到:

java
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
}

你只写业务类和注解,框架运行时读取类结构并完成对象创建、依赖注入、方法调用。

总体流程图

反射完整链路可以拆成两段:先有类加载和 Class 对象,再基于 Class 对象操作成员。

mermaid
flowchart TD
    A["编写 User.java"] --> B["javac 编译为 User.class"]
    B --> C["ClassLoader 读取字节码"]
    C --> D["JVM 验证、准备、解析、初始化"]
    D --> E["生成 Class 对象"]
    E --> F["查找构造方法 Constructor"]
    E --> G["查找字段 Field"]
    E --> H["查找方法 Method"]
    E --> I["查找注解 Annotation"]
    F --> J["newInstance 创建对象"]
    G --> K["get 或 set 读写字段"]
    H --> L["invoke 调用方法"]
    I --> M["根据注解决定框架行为"]

注意:Class 对象不是业务对象。业务对象是 new User() 得到的对象;Class<User> 是描述 User 这个类结构的元信息对象。

Class 对象到底是什么

每个被 JVM 加载的类,都会在 JVM 内部形成一份类元数据,Java 代码侧可以通过一个 Class 对象访问这些元数据。

它保存的信息包括:

信息例子作用
类名com.example.User框架扫描、日志、路由
父类BaseEntity判断继承关系
接口Serializable判断能力和扩展点
字段idname依赖注入、JSON 映射、ORM 映射
方法getName()RPC 分发、Controller 路由
构造方法User()反射创建对象
注解@Service框架根据注解做自动配置

JDK 7 和 JDK 8 之后有一个常见区别:

版本类元数据主要放在哪里常见问题
JDK 7 及以前 HotSpot永久代 PermGen容易出现 java.lang.OutOfMemoryError: PermGen space
JDK 8 及以后 HotSpot元空间 Metaspace,使用本地内存容易出现 java.lang.OutOfMemoryError: Metaspace

所以反射和类加载不是孤立知识点。大量动态生成类、热部署、插件化、代理类不断生成,都可能导致元空间压力。

Class 对象和业务对象的关系

java
public class User {
    private String name;
}
java
User u1 = new User();
User u2 = new User();

Class<?> c1 = u1.getClass();
Class<?> c2 = u2.getClass();
Class<?> c3 = User.class;

System.out.println(c1 == c2); // true
System.out.println(c1 == c3); // true

同一个类在同一个类加载器下通常只有一个 Class 对象。u1u2 是两个业务对象,但它们的“类说明书”是同一份。

需要特别注意“同一个类名,不同类加载器”:

text
com.example.User + ClassLoaderA -> Class 对象 A
com.example.User + ClassLoaderB -> Class 对象 B

在插件系统、Tomcat 多 Web 应用、热部署、一些中间件隔离场景里,类名相同但类加载器不同,JVM 会认为它们不是同一个类型。这也是有些项目明明类名一样,却出现 ClassCastException 的原因。

获取 Class 的方式

常见写法有四类:

java
Class<User> c1 = User.class;

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

Class<?> c3 = Class.forName("com.example.User");

ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class<?> c4 = loader.loadClass("com.example.User");

对比如下:

方式是否需要对象是否需要字符串类名是否通常触发初始化常见场景
User.class不需要不需要不一定编译期已知类型
user.getClass()需要不需要对象已存在,类肯定已加载根据对象反查类型
Class.forName(name)不需要需要默认会触发初始化JDBC、配置化加载
classLoader.loadClass(name)不需要需要通常只加载,不主动初始化框架扫描、延迟初始化

Class.forNameloadClass 的区别

这是面试高频点,也是理解框架扫描的关键。

java
public class DemoClass {
    static {
        System.out.println("DemoClass init");
    }
}
java
Class.forName("com.example.DemoClass");

默认情况下,Class.forName 会触发类初始化,所以静态代码块会执行。

java
ClassLoader loader = Thread.currentThread().getContextClassLoader();
loader.loadClass("com.example.DemoClass");

loadClass 一般只把类加载进 JVM,不主动触发初始化,静态代码块通常不会立刻执行。

如果想精确控制 Class.forName 是否初始化,可以使用三参数版本:

java
Class<?> clazz = Class.forName(
        "com.example.DemoClass",
        false,
        Thread.currentThread().getContextClassLoader()
);

其中第二个参数 false 表示加载类但不初始化。

为什么框架扫描喜欢避免初始化?因为扫描阶段只是想知道“这个类有没有注解”,如果扫描一个类就执行它的静态代码块,可能提前连接数据库、启动线程、加载大文件,导致启动过程不可控。

示例类

后面的代码都基于下面几个类:

java
package com.example.reflect;

public class BaseUser {
    public String tenantId;

    public void baseMethod() {
        System.out.println("base");
    }
}
java
package com.example.reflect;

public class User extends BaseUser {
    private Long id;
    private String name;

    public User() {
    }

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public String hello(String prefix) {
        return prefix + "," + name;
    }

    private String secret() {
        return "secret:" + id;
    }
}

字段查找规则

字段 API 最容易混淆的是 getFieldsgetDeclaredFields

API查找范围是否包含 private是否包含父类
getFields()public 字段不包含 private包含父类 public
getDeclaredFields()当前类声明字段包含 private不包含父类
getField(name)指定 public 字段不包含 private包含父类 public
getDeclaredField(name)当前类指定字段包含 private不包含父类

示例:

java
Class<User> clazz = User.class;

for (Field field : clazz.getFields()) {
    System.out.println("public field: " + field.getName());
}

for (Field field : clazz.getDeclaredFields()) {
    System.out.println("declared field: " + field.getName());
}

输出会体现差异:

text
public field: tenantId
declared field: id
declared field: name

框架为什么经常用 getDeclaredFields()?因为业务字段通常是 private,例如:

java
private String userName;

Spring 注入字段、Jackson 绑定 JSON、MyBatis 映射结果时,都需要读取非 public 字段。

如果既想拿当前类字段,又想拿父类字段,需要自己沿着继承链遍历:

java
public static List<Field> getAllFields(Class<?> type) {
    List<Field> result = new ArrayList<>();
    Class<?> current = type;
    while (current != null && current != Object.class) {
        result.addAll(Arrays.asList(current.getDeclaredFields()));
        current = current.getSuperclass();
    }
    return result;
}

读写字段全过程

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

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

Object oldValue = field.get(user);
field.set(user, "Jerry");

System.out.println(oldValue);
System.out.println(user.hello("hi"));

执行过程:

mermaid
flowchart TD
    A["拿到 User.class"] --> B["按字段名查找 name"]
    B --> C["得到 Field 对象"]
    C --> D["setAccessible(true) 关闭语言访问检查"]
    D --> E["field.get(user) 读取字段值"]
    E --> F["field.set(user, 新值) 修改字段值"]

这里要注意两个点:

  1. Field 是字段元信息,不是字段值。
  2. field.get(user) 才是从某个具体对象上读取这个字段的值。

如果字段是 static,它属于类,不属于某个对象:

java
Object value = staticField.get(null);
staticField.set(null, "newValue");

setAccessible(true) 到底做什么

privateprotected、包访问权限是 Java 语言层面的访问控制。反射默认也会检查这些权限。

java
Field field = User.class.getDeclaredField("name");
field.get(user); // 没有 setAccessible 时可能抛 IllegalAccessException

调用:

java
field.setAccessible(true);

表示告诉反射 API:后续访问时不要再按 Java 语言访问权限做检查。

但这不等于“无条件能访问一切”。Java 9 引入模块系统后,强封装更严格。如果目标模块没有开放包,反射访问 JDK 内部类或其他模块内部成员时,可能出现:

text
java.lang.reflect.InaccessibleObjectException

常见解决思路:

  1. 优先不要反射访问 JDK 内部私有实现。
  2. 使用公开 API。
  3. 启动参数临时开放模块,例如 --add-opens
  4. 框架升级到兼容当前 JDK 的版本。

所以生产排查时要区分:

异常常见含义
IllegalAccessException普通访问权限不够,可能没调用 setAccessible(true)
InaccessibleObjectException模块强封装限制,Java 9+ 常见

构造方法查找和创建对象

java
Constructor<User> c1 = User.class.getDeclaredConstructor();
User user1 = c1.newInstance();

Constructor<User> c2 = User.class.getDeclaredConstructor(Long.class, String.class);
User user2 = c2.newInstance(1L, "Tom");

构造方法 API:

API说明
getConstructor(...)获取 public 构造方法,包含父类不适用,因为构造方法不能继承
getDeclaredConstructor(...)获取当前类声明的构造方法,包含 private
newInstance(...)调用构造方法创建对象

为什么很多框架要求无参构造?

因为框架最通用的创建流程是:

mermaid
flowchart TD
    A["读取 Class 对象"] --> B["查找无参构造"]
    B --> C["反射创建空对象"]
    C --> D["读取配置、JSON 或数据库结果"]
    D --> E["逐个字段或 setter 赋值"]

如果没有无参构造,框架就必须知道每个构造参数应该从哪里来、参数名是什么、类型怎么转换、缺失值怎么办。这个难度明显更高。

但 Spring 不只支持无参构造。现代 Spring 更推荐构造器注入,它会根据 Bean 类型解析构造参数并注入依赖。

方法查找规则

方法 API 和字段类似:

API查找范围是否包含 private是否包含父类
getMethods()public 方法不包含 private包含父类 public,包括 Object 方法
getDeclaredMethods()当前类声明方法包含 private不包含父类
getMethod(name, types...)指定 public 方法不包含 private包含父类 public
getDeclaredMethod(name, types...)当前类指定方法包含 private不包含父类

查找带参数方法时,参数类型必须匹配:

java
Method method = User.class.getDeclaredMethod("hello", String.class);

如果方法参数是基本类型:

java
public int add(int a, int b) {
    return a + b;
}

查找时要写 int.class,不是 Integer.class

java
Method add = Calculator.class.getDeclaredMethod("add", int.class, int.class);

这是 NoSuchMethodException 的高频原因。

Method.invoke 完整流程

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

Method method = User.class.getDeclaredMethod("hello", String.class);
Object result = method.invoke(user, "hi");

System.out.println(result);

执行过程可以拆成:

mermaid
flowchart TD
    A["Method 保存方法名、参数类型、返回类型等元信息"] --> B["invoke 接收目标对象和实参数组"]
    B --> C["检查目标对象是否属于声明类或其子类"]
    C --> D["检查访问权限"]
    D --> E["检查参数个数和类型"]
    E --> F["执行真实业务方法"]
    F --> G["返回结果,基本类型会装箱为 Object"]

几个细节很重要:

  1. 实例方法必须传目标对象。
  2. 静态方法可以传 null
  3. 返回基本类型时会被装箱。
  4. 业务方法抛出的异常会被包装成 InvocationTargetException

示例:

java
public class OrderService {
    public void pay() {
        throw new IllegalStateException("balance not enough");
    }
}
java
try {
    Method pay = OrderService.class.getDeclaredMethod("pay");
    pay.invoke(new OrderService());
} catch (InvocationTargetException e) {
    Throwable realException = e.getTargetException();
    System.out.println(realException.getClass().getName());
    System.out.println(realException.getMessage());
}

为什么要包装?因为 invoke 自己也可能抛反射层面的异常,例如访问权限不够、参数不匹配。业务方法内部抛的异常需要和反射 API 自己的异常区分开,所以用 InvocationTargetException 包一层。

注解读取全过程

先定义注解:

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.TYPE)
public @interface TableName {
    String value();
}

使用注解:

java
@TableName("sys_user")
public class UserEntity {
    private Long id;
}

读取注解:

java
TableName tableName = UserEntity.class.getAnnotation(TableName.class);
System.out.println(tableName.value());

关键是:

java
@Retention(RetentionPolicy.RUNTIME)

注解保留策略决定注解能活到什么时候:

策略含义能否运行时反射读取
SOURCE只存在源码中,编译后没有不能
CLASS进入 class 文件,但 JVM 运行时不一定保留给反射通常不能
RUNTIME运行时可见可以

Spring、MyBatis、JUnit 这类运行时框架依赖注解驱动,所以它们的核心注解一般都是 RUNTIME

注解目标也很重要:

java
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})

它决定注解能标在哪里。比如 @Autowired 可以标字段、构造方法、方法;@Controller 标在类上。

泛型擦除和反射

Java 泛型主要是编译期类型检查。运行时很多泛型信息会被擦除:

java
List<String> names = new ArrayList<>();
List<Integer> ages = new ArrayList<>();

System.out.println(names.getClass() == ages.getClass()); // true

运行时都是 ArrayList,不是两个不同类型的类。

但是,反射仍然可以读取“声明位置”上的泛型信息。例如字段声明:

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

if (type instanceof ParameterizedType parameterizedType) {
    Type[] arguments = parameterizedType.getActualTypeArguments();
    System.out.println(arguments[0].getTypeName());
}

这就是很多 JSON 框架、ORM 框架、RPC 框架需要 TypeReference 的原因。因为运行时对象本身的泛型类型容易丢失,只能从字段、方法返回值、方法参数、父类泛型签名等声明位置找回来。

JDK 7/8 写法不能使用模式匹配,需要写成:

java
if (type instanceof ParameterizedType) {
    ParameterizedType parameterizedType = (ParameterizedType) type;
    Type[] arguments = parameterizedType.getActualTypeArguments();
    System.out.println(arguments[0].getTypeName());
}

Demo 1:简化版 IOC 注入

这个 Demo 模拟 Spring:扫描字段上的注解,然后把容器里的对象注入进去。

java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

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

class UserRepository {
    public String findNameById(Long id) {
        return "Tom-" + id;
    }
}

class UserService {
    @Inject
    private UserRepository userRepository;

    public void printUser(Long id) {
        System.out.println(userRepository.findNameById(id));
    }
}

public class MiniIocDemo {
    public static void main(String[] args) throws Exception {
        Map<Class<?>, Object> container = new HashMap<>();
        container.put(UserRepository.class, new UserRepository());

        UserService userService = new UserService();
        inject(userService, container);

        userService.printUser(1L);
    }

    static void inject(Object bean, Map<Class<?>, Object> container) throws Exception {
        Class<?> clazz = bean.getClass();
        for (Field field : clazz.getDeclaredFields()) {
            if (!field.isAnnotationPresent(Inject.class)) {
                continue;
            }
            Object dependency = container.get(field.getType());
            if (dependency == null) {
                throw new IllegalStateException("missing dependency: " + field.getType().getName());
            }
            field.setAccessible(true);
            field.set(bean, dependency);
        }
    }
}

这个 Demo 背后的真实框架原理:

mermaid
flowchart TD
    A["扫描 class 文件"] --> B["找到 Bean 类"]
    B --> C["创建 Bean 对象"]
    C --> D["遍历字段和方法"]
    D --> E["识别注入注解"]
    E --> F["从容器查找依赖"]
    F --> G["反射写入字段或调用 setter"]
    G --> H["Bean 可以被业务使用"]

真实 Spring 比这个复杂得多,还包括 BeanDefinition、生命周期、循环依赖、代理对象、条件装配等,但“读取注解 + 反射注入”是理解入口。

Demo 2:简化版对象映射

商业系统经常把数据库结果、JSON、Map 映射成 Java 对象。下面模拟一个最小对象映射器:

java
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

class Order {
    private Long id;
    private String orderNo;
    private Integer status;

    public String toString() {
        return "Order{id=" + id + ", orderNo='" + orderNo + "', status=" + status + "}";
    }
}

public class SimpleMapperDemo {
    public static void main(String[] args) throws Exception {
        Map<String, Object> row = new HashMap<>();
        row.put("id", 1001L);
        row.put("orderNo", "PO202607060001");
        row.put("status", 1);

        Order order = mapToBean(row, Order.class);
        System.out.println(order);
    }

    static <T> T mapToBean(Map<String, Object> row, Class<T> type) throws Exception {
        T bean = type.getDeclaredConstructor().newInstance();

        for (Field field : type.getDeclaredFields()) {
            if (!row.containsKey(field.getName())) {
                continue;
            }
            field.setAccessible(true);
            field.set(bean, row.get(field.getName()));
        }

        return bean;
    }
}

真实 MyBatis、Jackson 会额外处理:

  1. 下划线转驼峰,例如 order_no -> orderNo
  2. 类型转换,例如字符串转时间、数字转枚举。
  3. 嵌套对象和集合。
  4. 构造器、setter、字段赋值多种策略。
  5. 缓存反射元数据,避免每次都查字段。

如果不理解反射,就很难理解为什么字段名不一致会映射失败,为什么没有无参构造会报错,为什么 JSON 反序列化有安全风险。

Demo 3:简化版 RPC 方法分发

RPC、Controller 路由、命令处理器,本质上都有一个共同问题:收到一个字符串形式的请求后,如何找到并调用对应方法。

java
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

class PaymentService {
    public String pay(String orderNo) {
        return "paid:" + orderNo;
    }
}

public class RpcDispatchDemo {
    private static final Map<String, Object> services = new HashMap<>();

    public static void main(String[] args) throws Exception {
        services.put("paymentService", new PaymentService());

        Object result = invoke("paymentService", "pay", new Class<?>[]{String.class}, new Object[]{"PO1001"});
        System.out.println(result);
    }

    static Object invoke(String serviceName, String methodName, Class<?>[] parameterTypes, Object[] args) throws Exception {
        Object service = services.get(serviceName);
        if (service == null) {
            throw new IllegalArgumentException("service not found: " + serviceName);
        }

        Method method = service.getClass().getMethod(methodName, parameterTypes);
        return method.invoke(service, args);
    }
}

生产环境不能直接让外部用户任意传类名、方法名然后反射调用。正确做法是:

  1. 只允许调用白名单服务。
  2. 方法必须通过注解或接口注册。
  3. 参数要校验和反序列化限制。
  4. 权限、审计、限流必须在调用前完成。
  5. 异常不能把内部类名、路径、SQL 等敏感信息直接返回给前端。

反射为什么慢

反射慢不是一句“因为动态”就结束,要拆开看:

  1. 普通调用在编译期和 JIT 优化阶段更容易内联、去虚拟化、做逃逸分析。
  2. 反射调用需要经过 MethodField 这类元信息对象,多了一层间接调用。
  3. Method.invoke 需要做访问权限、参数个数、参数类型、装箱拆箱等检查。
  4. 反射通常破坏静态类型信息,编译器更难提前发现错误。

但也不要过度恐惧反射。框架通常会缓存反射元数据:

java
private static final Map<Class<?>, Field[]> FIELD_CACHE = new ConcurrentHashMap<>();

static Field[] getCachedFields(Class<?> type) {
    return FIELD_CACHE.computeIfAbsent(type, Class::getDeclaredFields);
}

对于业务系统,真正的性能瓶颈往往是数据库、网络、锁竞争、序列化、大对象分配。反射在框架启动阶段使用很多,运行期通常通过缓存、字节码生成、MethodHandle 等方式降低成本。

MethodHandle 和 VarHandle

JDK 7 引入 MethodHandle,它比传统反射更接近 JVM 方法调用模型,适合动态语言、框架、底层库做高性能动态调用。

JDK 9 引入 VarHandle,可以用来访问变量,并支持 volatile、CAS 等访问模式。它在一些场景下替代了 Unsafe 的部分能力。

对普通业务开发来说:

技术常见使用者适用场景
反射业务框架、测试工具、ORM、JSON通用动态元信息读取和调用
MethodHandle框架、动态语言、底层库高性能动态方法调用
VarHandleJDK、并发库、底层框架高性能变量访问、内存语义控制

面试时不需要把 MethodHandle 源码背下来,但要知道:传统反射灵活但开销更明显,框架为了性能可能会缓存反射结果、生成字节码、使用 MethodHandle。

商业项目怎么用

Spring IOC 和注解扫描

场景:后台管理系统有订单服务、库存服务、支付服务。

框架启动时扫描 classpath:

  1. 发现 @Service 类,注册成 BeanDefinition。
  2. 创建 Bean 对象。
  3. 读取字段、构造方法、setter 上的注入信息。
  4. 从容器找依赖。
  5. 用反射或方法调用完成注入。

如果没有反射,所有依赖都要手写装配,项目一大就很难维护。

MyBatis 结果映射

场景:查询订单表:

sql
select id, order_no, status from t_order where id = ?

MyBatis 拿到结果集后,需要把列值写入 Java 对象:

java
public class Order {
    private Long id;
    private String orderNo;
    private Integer status;
}

它会根据 resultMap、字段名、setter、类型转换规则,把数据库列映射到对象属性。字段名、setter 名、类型不匹配,就会出现映射失败或值为 null。

Jackson JSON 反序列化

场景:前端传入:

json
{"userId": 1, "userName": "Tom"}

后端接收:

java
public class UserCreateRequest {
    private Long userId;
    private String userName;
}

Jackson 需要创建对象并给字段赋值。没有无参构造、字段名不一致、类型转换失败,都会导致反序列化异常。

RPC 和接口聚合

场景:网关收到请求后,根据接口名调用不同服务方法。框架会维护一个方法注册表,把请求名映射到具体 Method 或方法句柄。

这样新增接口时,只要加注解或实现接口,框架就能自动注册。

不这样会怎样

如果完全不用反射,框架能力会明显退化:

不使用反射后果
不能运行时读取注解@Service@RequestMapping@Transactional 这类声明式能力难以实现
不能动态创建对象配置化、插件化、序列化、ORM 都会变得笨重
不能动态读写字段JSON 映射、数据库结果映射需要大量手写代码
不能动态调用方法RPC、Controller 路由、测试框架难以通用化

但如果滥用反射,也会带来问题:

滥用反射后果
业务代码到处反射调用可读性差,编译期无法检查,重构容易漏
反射访问 private 字段破坏封装,类内部约束可能被绕过
用户输入决定类名方法名安全风险,可能调用不该调用的内部能力
高频循环里重复查 Method/Field性能下降
Java 9+ 反射 JDK 内部类模块访问失败,升级 JDK 后容易炸

常见坑

参数类型写包装类导致找不到方法

java
public int add(int a, int b) {
    return a + b;
}

错误:

java
clazz.getDeclaredMethod("add", Integer.class, Integer.class);

正确:

java
clazz.getDeclaredMethod("add", int.class, int.class);

只查当前类,漏掉父类字段

getDeclaredField 不查父类。如果字段在父类,需要沿着 getSuperclass() 遍历。

忘记处理 InvocationTargetException

反射调用业务方法时,真正异常在:

java
e.getTargetException()

日志里只看外层异常,容易误判成反射 API 问题。

在线程热点路径重复查反射元信息

不要在每条数据处理时都:

java
clazz.getDeclaredFields()

应把字段、方法、注解解析结果缓存起来。

让外部请求直接控制反射目标

危险示例:

java
Class<?> clazz = Class.forName(request.getClassName());
Method method = clazz.getDeclaredMethod(request.getMethodName());
method.invoke(clazz.getDeclaredConstructor().newInstance());

这会把系统内部能力暴露给外部输入。生产必须白名单、鉴权、参数校验。

线上排查流程

mermaid
flowchart TD
    A["反射相关异常"] --> B["看异常类型"]
    B --> C["ClassNotFoundException"]
    B --> D["NoSuchMethodException"]
    B --> E["IllegalAccessException"]
    B --> F["InvocationTargetException"]
    B --> G["InaccessibleObjectException"]
    C --> H["检查类名、包名、依赖、classpath、类加载器"]
    D --> I["检查方法名、参数类型、基本类型和包装类型"]
    E --> J["检查访问权限和 setAccessible"]
    F --> K["打印 getTargetException 真实业务异常"]
    G --> L["检查 Java 9+ 模块开放和框架版本"]

排查建议:

  1. 先看完整堆栈,不要只看第一行异常。
  2. ClassNotFoundException 重点查依赖是否打包、类名是否写错、插件类加载器是否隔离。
  3. NoSuchMethodException 重点查参数类型,尤其是 int.classInteger.class
  4. InvocationTargetException 重点看 getTargetException(),真实问题常常是业务代码空指针、SQL 异常、远程调用失败。
  5. JDK 升级后出现反射访问失败,重点查模块系统和框架兼容版本。
  6. 性能问题先采样火焰图或 profiler,不要凭感觉认定是反射慢。

面试标准回答

反射是什么

反射是 Java 在运行时获取类结构并动态操作对象的能力。它可以读取类名、字段、方法、构造方法、注解,也可以动态创建对象、调用方法、读写字段。Spring、MyBatis、Jackson、JUnit、RPC 框架都大量使用反射,因为框架在编译期不知道用户会写哪些类,只能运行时扫描和处理。

Class 对象是什么

Class 对象是 JVM 加载某个类后暴露给 Java 代码的类元信息入口。它不是业务对象,而是描述这个类的结构信息。同一个类在同一个类加载器下通常只有一个 Class 对象;同名类如果由不同类加载器加载,JVM 会认为是不同类型。

Class.forNameloadClass 区别

Class.forName 默认会加载并初始化类,可能触发静态代码块;ClassLoader.loadClass 通常只加载类,不主动初始化。框架扫描阶段通常希望只读取类信息,不希望提前执行静态初始化,所以会更谨慎地控制是否初始化。

getFieldsgetDeclaredFields 区别

getFields 只获取 public 字段,并且包含父类 public 字段;getDeclaredFields 获取当前类声明的所有字段,包括 private,但不包含父类字段。框架要处理 private 字段时常用 getDeclaredFields,如果还要父类字段,需要自己沿继承链遍历。

setAccessible(true) 是什么

它表示关闭 Java 语言层面的访问检查,让反射可以访问 private、protected 等成员。但 Java 9 模块系统之后,强封装会让一些跨模块私有访问失败,可能抛 InaccessibleObjectException,这时要考虑公开 API、升级框架或通过 --add-opens 临时开放模块。

Method.invoke 为什么抛 InvocationTargetException

Method.invoke 自己可能因为访问权限、参数不匹配等反射问题失败;被调用的业务方法内部也可能抛异常。为了区分这两类异常,业务方法抛出的异常会被包在 InvocationTargetException 里,真实异常要通过 getTargetException() 获取。

反射为什么慢,怎么优化

反射调用多了元信息查找、访问检查、参数检查、装箱拆箱和间接调用,JIT 也更难像普通调用那样优化。优化方式包括缓存 ClassFieldMethodConstructor,避免热点循环重复解析;框架还可能使用字节码生成、MethodHandle、代码生成等方式降低运行期开销。

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

Spring 启动时扫描 classpath,读取类上的 @Component@Service 等注解,把符合条件的类注册成 BeanDefinition,然后通过构造方法或工厂方法创建对象,再读取字段、构造器、方法上的注入注解,完成依赖注入。反射提供了读取类结构和注解、创建对象、写字段或调用方法的基础能力。

关联知识点