Skip to content

反射

为什么需要反射?

反射是 Java 在运行时动态获取类的信息、操作类的属性和方法的能力。正常写代码是「类 -> 对象 -> 调用方法」,反射是反过来:「全限定类名 -> 拿到 Class -> 拿到方法/字段 -> 调用」。

框架和中间件大量依赖反射,因为框架不知道你会写什么类,只能通过配置(全限定类名)在运行时动态加载:

  • Spring@Autowired 注入依赖、@Transactional AOP 代理
  • MyBatis — Mapper 接口动态代理
  • JDK 动态代理Proxy.newProxyInstance() 底层就是反射
  • JUnit — 扫描 @Test 方法并调用
  • 序列化 — Jackson、Fastjson 通过反射读写字段

反射的核心:Class 对象

什么是 Class 对象?

JVM 每加载一个类,就会在方法区(元空间)生成一个对应的 java.lang.Class 对象。这个 Class 对象包含了该类的完整元信息:构造器、字段、方法、注解、父类、接口等。反射的所有操作都是通过这个 Class 对象完成的。

获取 Class 对象的三种方式

java
// 方式1:Class.forName() —— 最常用,适合配置文件中读取类名
Class<?> clazz1 = Class.forName("java.lang.String");

// 方式2:类名.class —— 编译期已知类名时使用
Class<String> clazz2 = String.class;

// 方式3:对象.getClass() —— 已有对象实例时使用
String str = "hello";
Class<? extends String> clazz3 = str.getClass();

// 基本类型的 Class
Class<Integer> intClass = int.class;       // int
Class<Integer> wrapClass = Integer.TYPE;   // int(等价于 int.class)
Class<Integer> wrapperClass = Integer.class; // java.lang.Integer

注意:int.classInteger.class 是两个不同的 Class 对象,前者代表基本类型,后者代表包装类。


核心 API

获取构造方法并创建实例

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

// 获取所有 public 构造器
Constructor<?>[] constructors = clazz.getConstructors();

// 获取指定参数的 public 构造器
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);

// 获取所有构造器(包括 private)
Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();

// 创建实例
User user = (User) constructor.newInstance("张三", 25);

// 调用 private 构造器
Constructor<?> privateConstructor = clazz.getDeclaredConstructor(String.class);
privateConstructor.setAccessible(true);
User user2 = (User) privateConstructor.newInstance("李四");

获取字段并读写

java
Class<?> clazz = User.class;
User user = new User("张三", 25);

// 获取 public 字段(包括继承的)
Field[] fields = clazz.getFields();

// 获取指定名称的 public 字段
Field nameField = clazz.getField("name");
String name = (String) nameField.get(user);
nameField.set(user, "李四");

// 获取本类声明的所有字段(包括 private,不包括继承的)
Field[] declaredFields = clazz.getDeclaredFields();

// 获取并操作 private 字段
Field ageField = clazz.getDeclaredField("age");
ageField.setAccessible(true);  // 关键:跳过访问检查
int age = ageField.getInt(user); // 使用类型特化方法,避免装箱
ageField.setInt(user, 30);

// 获取 static 字段(对象参数传 null)
Field staticField = clazz.getDeclaredField("DEFAULT_NAME");
staticField.setAccessible(true);
String defaultValue = (String) staticField.get(null);

获取方法并调用

java
Class<?> clazz = User.class;
User user = new User("张三", 25);

// 获取所有 public 方法(包括继承的,包括 Object 的方法)
Method[] methods = clazz.getMethods();

// 获取指定签名的方法
Method sayHello = clazz.getMethod("sayHello", String.class);
Object result = sayHello.invoke(user, "World");

// 获取本类声明的所有方法(包括 private,不包括继承的)
Method[] declaredMethods = clazz.getDeclaredMethods();

// 调用 private 方法
Method privateMethod = clazz.getDeclaredMethod("secretMethod");
privateMethod.setAccessible(true);
privateMethod.invoke(user);

// 调用 static 方法
Method staticMethod = clazz.getDeclaredMethod("factoryMethod", String.class);
staticMethod.setAccessible(true);
Object obj = staticMethod.invoke(null, "param");

获取注解

java
// 获取类上的注解
if (clazz.isAnnotationPresent(Entity.class)) {
    Entity entity = clazz.getAnnotation(Entity.class);
    String tableName = entity.tableName();
}

// 获取字段上的注解
Field field = clazz.getDeclaredField("name");
Column column = field.getAnnotation(Column.class);
String columnName = column.name();

// 获取方法上的注解
Method method = clazz.getDeclaredMethod("save");
Annotation[] annotations = method.getAnnotations();

// 获取方法参数上的注解
Annotation[][] paramAnnotations = method.getParameterAnnotations();

获取其他元信息

java
// 获取类名
String className = clazz.getName();          // com.example.User
String simpleName = clazz.getSimpleName();   // User

// 获取父类和接口
Class<?> superClass = clazz.getSuperclass();
Class<?>[] interfaces = clazz.getInterfaces();

// 获取修饰符
int modifiers = clazz.getModifiers();
boolean isPublic = Modifier.isPublic(modifiers);
boolean isAbstract = Modifier.isAbstract(modifiers);

// 获取包名
Package pkg = clazz.getPackage();

setAccessible(true) 做了什么?

setAccessible(true) 告诉 JVM 跳过访问权限检查。它不修改字段的 private 修饰符,只是关闭 AccessibleObject 的访问控制。

java
Field nameField = User.class.getDeclaredField("name");
// nameField.setAccessible(true);  // 注释掉这一行
nameField.set(user, "hack");      // 抛 IllegalAccessException

安全影响

从 Java 9+ 开始,模块系统引入了更严格的访问控制。如果目标类在另一个模块中且该模块没有 opens 其包,反射会失败并抛出 InaccessibleObjectException。JVM 添加 --add-opens 参数可以绕过:

--add-opens java.base/java.lang=ALL-UNNAMED

修改 final 字段

普通 final 字段可以通过反射修改,但 static final 的基本类型和字符串(编译期常量)不行,因为会被内联优化:

java
// 修改普通 final 字段
Field finalField = clazz.getDeclaredField("name");
finalField.setAccessible(true);

// 需要先移除 final 修饰符
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(finalField, finalField.getModifiers() & ~Modifier.FINAL);

finalField.set(obj, "new value");

反射的性能

反射比直接调用慢,原因有三:

  1. 方法查找getDeclaredMethod() 要遍历方法表,时间复杂度 O(n)
  2. 参数装箱invoke() 的参数是 Object[],基本类型要装箱拆箱
  3. 安全检查 — 每次调用都要检查访问权限

性能对比

调用方式相对耗时说明
直接调用1x基线
缓存 Method + setAccessible~2-3x实际业务中常用
不缓存 Method~10x+每次 getMethod
不 setAccessible~20x+每次检查权限

优化手段

java
// 1. 缓存 Method 对象,避免重复查找
private static final Map<String, Method> METHOD_CACHE = new ConcurrentHashMap<>();

// 2. 务必 setAccessible(true)
method.setAccessible(true);

// 3. JDK 7+ 用 MethodHandle(介于反射和直接调用之间)
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle handle = lookup.findVirtual(User.class, "sayHello",
        MethodType.methodType(void.class, String.class));
handle.invoke(user, "World");

// 4. JDK 8+ 用 LambdaMetafactory(接近直接调用速度)
// 适用于需要高频率调用的场景

反射的实际应用场景

1. Spring 依赖注入

java
// 简化版 @Autowired 实现原理
public class SimpleContainer {
    public static void autowire(Object bean) throws Exception {
        for (Field field : bean.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(Autowired.class)) {
                field.setAccessible(true);
                Object dependency = getBean(field.getType()); // 从容器获取
                field.set(bean, dependency);
            }
        }
    }
}

2. ORM 结果映射

java
// 简化版 MyBatis 结果集映射
public static <T> List<T> mapResultSet(ResultSet rs, Class<T> clazz) throws Exception {
    List<T> result = new ArrayList<>();
    while (rs.next()) {
        T instance = clazz.getDeclaredConstructor().newInstance();
        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            String columnName = field.getName(); // 简化:字段名=列名
            Object value = rs.getObject(columnName);
            field.set(instance, value);
        }
        result.add(instance);
    }
    return result;
}

3. JUnit 测试框架

java
// 简化版 JUnit Runner
public static void runTests(Class<?> testClass) throws Exception {
    Object testInstance = testClass.getDeclaredConstructor().newInstance();
    for (Method method : testClass.getDeclaredMethods()) {
        if (method.isAnnotationPresent(Test.class)) {
            try {
                method.invoke(testInstance);
                System.out.println(method.getName() + " PASSED");
            } catch (InvocationTargetException e) {
                System.out.println(method.getName() + " FAILED: " + e.getCause());
            }
        }
    }
}

反射与动态代理

JDK 动态代理的 InvocationHandler.invoke() 签名:

java
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable

Method 就是反射的核心类,method.invoke(target, args) 就是反射调用。所以:

动态代理 = 反射 + 动态字节码生成

代理类 $Proxy0 是动态生成的字节码,但真正执行目标方法时,靠的是 Method.invoke() 反射机制。


常见面试题

Q1: Class.forName()ClassLoader.loadClass() 的区别?

Class.forName() 会执行类的静态初始化static 块),ClassLoader.loadClass() 不会。JDBC 驱动加载就是靠 Class.forName() 触发静态代码块注册驱动。

java
// 会触发 static 块
Class.forName("com.mysql.cj.jdbc.Driver");

// 不会触发 static 块
ClassLoader.getSystemClassLoader().loadClass("com.mysql.cj.jdbc.Driver");

Q2: getDeclaredMethods()getMethods() 的区别?

  • getDeclaredMethods():返回本类声明的所有方法(包括 private),不包括继承的方法
  • getMethods():返回所有 public 方法(包括继承的,包括 Object 类的方法)

Q3: getDeclaredField()getField() 的区别?

同理,getDeclaredField() 获取本类任意访问权限的字段,getField() 只能获取 public 字段(包括继承的)。

Q4: 反射能修改 final 字段吗?

普通 final 字段可以,static final 的基本类型和字符串(编译期常量)不行,因为会被内联优化。需要同时移除 final 修饰符。

Q5: 反射有什么缺点?

  1. 性能开销大,比直接调用慢数倍到数十倍
  2. 破坏封装性,绕过编译期类型检查
  3. 代码可读性差,维护困难
  4. Java 9+ 模块系统进一步限制反射访问

Q6: int.classInteger.class 是同一个对象吗?

不是。int.class 代表基本类型 intInteger.class 代表包装类 Integerint.class == Integer.TYPEtrue,但 int.class == Integer.classfalse

Q7: 反射调用方法时,invoke 的第一个参数为 null 表示什么?

表示调用的是 static 方法。静态方法不依赖实例,所以第一个参数(目标对象)可以为 null

Q8: 如何通过反射实例化一个内部类?

非静态内部类需要外部类实例作为第一个构造参数:

java
Class<?> innerClass = Class.forName("com.example.Outer$Inner");
Constructor<?> ctor = innerClass.getDeclaredConstructor(Outer.class);
Object inner = ctor.newInstance(new Outer());