一、从一个让人抓狂的场景说起

先想象这样一个需求:

你正在开发一个支付系统,支持微信支付和支付宝支付。老板说:“把支付方式写在配置文件里,我要能做到不重新编译代码就能切换支付渠道。”

你写了两个类:WechatPayAlipayPay,它们都有一个 pay(String orderId) 方法。

// 配置文件 config.properties 里写着:
pay.class.name=com.demo.WechatPay

问题来了: 你在写代码的时候,只知道配置文件里存着一个字符串,根本不知道它到底是 WechatPay 还是 AlipayPay,你该怎么创建这个对象并调用它的 pay() 方法?

new 关键字?不行,new 后面必须跟一个确定的类名。

if-else 判断字符串?如果以后加了 100 种支付方式,难道要写 100 个 if

// 这种写法太蠢了,每加一种支付方式就要改代码
if ("WechatPay".equals(className)) {
    new WechatPay().pay();
} else if ("AlipayPay".equals(className)) {
    new AlipayPay().pay();
}

就在你一筹莫展的时候,反射(Reflection) 登场了。

反射是 Java 提供的"自省"能力——允许程序在运行时动态地获取类的信息并操作对象,而不需要在编译期确定具体的类。

也就是说,即使类名只是一个运行时的字符串,反射也能帮你找到这个类、创建它的对象、调用它的方法。

二、什么是反射?用一个比喻彻底搞懂

2.1 官方定义

Java 反射机制是指在运行时,对于任意一个类,都能知道这个类的所有属性和方法;对于任意一个对象,都能调用它的任意方法和属性。这种动态获取信息以及动态调用对象方法的功能称为 Java 的反射机制。

2.2 一个帮助理解的比喻

正常写代码: 你拿着建筑蓝图(源码)去盖房子(对象)。房子盖好后,蓝图就收起来了。你只能按图纸上设计好的方式使用这栋房子——门从哪进,灯开关在哪,一切都是固定的。

用反射: 你拥有了一双"透视眼",即使没有蓝图,你盯着任何一栋已经盖好的房子(Class 对象),就能反推出来:

  • 它的承重墙在哪(成员变量)

  • 它的水电怎么走的(方法)

  • 它的地基有多深(构造器)

  • 更过分的是,你甚至能强行打开锁着的门(调用 private 方法),修改墙壁的颜色(修改 private 属性)。这就是 setAccessible(true) 的威力。

2.3 一句话总结

反射把 Java 从"编译期确定"变成了"运行时动态"——这是它最大的魅力,也是所有框架的基础。

三、获取 Class 对象的三种方式

要想使用反射,第一步永远是获取目标类的 Class 对象。Class 是反射的入口,它包含了这个类的所有元数据。

public class User {
    private String name;
    private int age;
    
    public User() {}
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    private String sayHello() {
        return "Hello, I'm " + name;
    }
}

方式一:通过对象实例获取(用得少)

User user = new User();
Class<?> clazz1 = user.getClass();
// clazz1 就是 User 的 Class 对象

方式二:通过类名直接获取(最常用,编译期确定)

Class<?> clazz2 = User.class;
// 这是最安全、最简洁的方式,但需要在编译期知道类名

方式三:通过全限定名动态获取(这才是核心!

// 类名是字符串,可以来自配置文件、数据库、网络请求……
String className = "com.demo.User";  // 这个字符串可以是动态的
Class<?> clazz3 = Class.forName(className);

方式三就是框架设计的基石。 Spring 读取配置文件或注解中的类名(字符串),然后用 Class.forName() 加载类,动态创建对象——完全不需要你在代码里写 new

四、反射到底能做什么?

4.1 动态创建对象

// 传统方式:编译期确定
User user = new User();

// 反射方式:运行时确定
String className = "com.demo.User";
Class<?> clazz = Class.forName(className);

// 方式A:调用无参构造(JDK 9 后已废弃 clazz.newInstance())
User obj1 = (User) clazz.getDeclaredConstructor().newInstance();

// 方式B:调用有参构造
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);
User obj2 = (User) constructor.newInstance("张三", 18);

4.2 获取和调用方法

Class<?> clazz = Class.forName("com.demo.User");
User user = (User) clazz.getDeclaredConstructor().newInstance();

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

// 获取所有方法(包括 private,但不包括父类)
Method[] declaredMethods = clazz.getDeclaredMethods();

// 调用 public 方法
Method publicMethod = clazz.getMethod("setName", String.class);
publicMethod.invoke(user, "李四");

// 调用 private 方法(重点!)
Method privateMethod = clazz.getDeclaredMethod("sayHello");
privateMethod.setAccessible(true);  // 暴力破解:打破封装
String result = (String) privateMethod.invoke(user);
System.out.println(result);  // 输出:Hello, I'm 李四

4.3 获取和修改字段

Class<?> clazz = Class.forName("com.demo.User");
User user = (User) clazz.getDeclaredConstructor().newInstance();

// 获取 private 字段
Field field = clazz.getDeclaredField("name");
field.setAccessible(true);  // 暴力破解

// 读取值
String oldName = (String) field.get(user);
System.out.println("修改前:" + oldName);  // null

// 修改值
field.set(user, "王五");
System.out.println("修改后:" + user.getName());  // 王五

4.4 操作数组(特殊场景)

// 用反射创建数组
int[] array = (int[]) Array.newInstance(int.class, 5);
Array.set(array, 0, 100);
Array.set(array, 1, 200);
System.out.println(Array.get(array, 0));  // 100

4.5 操作泛型(获取运行时泛型类型)

// 由于 Java 的类型擦除,运行时泛型信息会被擦除
// 但通过反射可以获取到方法/字段上声明的泛型类型
public class GenericDemo {
    private List<String> nameList;
    
    public void test() throws NoSuchFieldException {
        Field field = GenericDemo.class.getDeclaredField("nameList");
        Type genericType = field.getGenericType();
        if (genericType instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) genericType;
            Type[] actualTypes = pt.getActualTypeArguments();
            System.out.println(actualTypes[0]);  // class java.lang.String
        }
    }
}

4.6 核心操作速查表

操作目标

正常写法(编译期)

反射写法(运行时)

创建对象

new User()

clazz.getDeclaredConstructor().newInstance()

调用公有方法

user.setName("张三")

method.invoke(user, "张三")

调用私有方法

编译报错 ❌

method.setAccessible(true) + invoke()

读取私有字段

编译报错 ❌

field.setAccessible(true) + get(obj)

修改私有字段

编译报错 ❌

field.setAccessible(true) + set(obj, value)

获取泛型参数

无法获取(类型擦除)

getGenericType() 可获取声明时的类型

五、反射的 4 大核心应用场景

场景一:JDBC 加载驱动(最经典的反射应用)


// 你肯定写过这行代码:
Class.forName("com.mysql.cj.jdbc.Driver");

为什么不用 new Driver()?因为 Driver 类在 static 静态代码块中向 DriverManager 注册了自己:

// com.mysql.cj.jdbc.Driver 源码中
static {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}

Class.forName() 会触发类的静态初始化,驱动就自动注册了。而类名写在配置文件中,换数据库只需改配置文件,不用重新编译代码。

场景二:Spring IoC 容器(依赖注入)

Spring 启动时做了这样几件事(极度简化版):

// 1. 扫描所有带 @Component 注解的类
Set<Class<?>> classes = scanPackages("com.demo");

// 2. 遍历这些类,通过反射创建对象
for (Class<?> clazz : classes) {
    Object instance = clazz.getDeclaredConstructor().newInstance();
    container.put(clazz.getName(), instance);  // 存入容器
}

// 3. 扫描所有带 @Autowired 的字段,通过反射注入
for (Object bean : container.values()) {
    Field[] fields = bean.getClass().getDeclaredFields();
    for (Field field : fields) {
        if (field.isAnnotationPresent(Autowired.class)) {
            field.setAccessible(true);
            Object dependency = container.get(field.getType().getName());
            field.set(bean, dependency);  // 把依赖注入进去
        }
    }
}

一句话:没有反射,Spring 只能写死 new XXX(),根本做不到"解耦"。

场景三:Spring AOP(动态代理)

JDK 动态代理必须在运行时生成一个代理类,拦截所有方法调用:

public class MyInvocationHandler implements InvocationHandler {
    private Object target;
    
    public MyInvocationHandler(Object target) {
        this.target = target;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("前置增强:方法 " + method.getName() + " 即将执行");
        Object result = method.invoke(target, args);  // 通过反射调用目标方法
        System.out.println("后置增强:方法 " + method.getName() + " 执行完毕");
        return result;
    }
}

// 使用:
UserService service = new UserService();
UserService proxy = (UserService) Proxy.newProxyInstance(
    service.getClass().getClassLoader(),
    service.getClass().getInterfaces(),
    new MyInvocationHandler(service)
);
proxy.doSomething();  // 实际执行的是增强后的逻辑

场景四:测试框架(JUnit / TestNG)

// JUnit 怎么找到你写的 @Test 方法?
public class JUnitRunner {
    public void run(Class<?> testClass) throws Exception {
        Object instance = testClass.getDeclaredConstructor().newInstance();
        
        // 遍历所有方法,找出标注了 @Test 的方法
        for (Method method : testClass.getDeclaredMethods()) {
            if (method.isAnnotationPresent(Test.class)) {
                System.out.println("执行测试方法:" + method.getName());
                method.invoke(instance);  // 通过反射调用
            }
        }
    }
}

六、反射的缺点

6.1 性能损耗

反射涉及动态解析,JVM 无法做内联优化(比如方法内联、逃逸分析),比直接调用慢。

// 直接调用:约 1ns
user.sayHello();

// 反射调用:约 50-100ns(现代 JVM 已大幅优化)
method.invoke(user);

注意: 在绝大多数业务场景下,这个差距可以忽略不计。只有在极端高性能场景(比如每秒百万级调用)才需要考虑。Spring 等框架会做缓存,同一个 Method 对象只解析一次。

6.2 破坏封装性

setAccessible(true)private 形同虚设,可能破坏设计模式,导致不可预期的副作用。

// 可以修改 final 字段的值(虽然是 hack 行为)
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
field.set("hello", new char[]{'h', 'a', 'c', 'k'});  
// "hello" 变成了 "hack"

6.3 安全隐患

可以绕过访问控制,修改敏感数据,可能被恶意代码利用。

6.4 代码可读性差

抛出大量受检异常(ClassNotFoundExceptionNoSuchMethodExceptionIllegalAccessExceptionInvocationTargetException),代码臃肿笨拙。

七、反射 vs 其他动态特性

特性

反射 (Reflection)

方法句柄 (MethodHandle)

直接调用

性能

较慢

接近直接调用(需配合 LambdaMetafactory)

最快

使用难度

简单

较复杂

最简单

访问控制

可突破(setAccessible)

遵循 JVM 访问控制

正常访问

典型应用

框架、工具

脚本语言、高性能框架

业务代码

从 Java 7 开始引入了 java.lang.invoke.MethodHandle,性能更好,但 API 更复杂。反射依然是框架开发的首选,因为它的 API 直观、功能强大。

八、避坑指南:这些坑你一定遇到过

坑 1:newInstance() 已废弃

// ❌ JDK 9+ 已标记废弃
clazz.newInstance();

// ✅ 正确写法
clazz.getDeclaredConstructor().newInstance();

坑 2:基本类型和包装类型的 Class 不一样

int.class == Integer.class;         // false
int.class == Integer.TYPE;          // true(TYPE 是 int 的原始类型)

坑 3:数组的 Class 对象有特殊命名

String[].class.getName();    // "[Ljava.lang.String;"
int[].class.getName();       // "[I"

坑 4:反射获取不到泛型真实类型

虽然 getGenericType() 能拿到声明时的泛型类型,但对象实例的泛型已被擦除:

List<String> list = new ArrayList<>();
// 无法通过反射获取 list 是 List<String>,运行时只知道它是 List

坑 5:Module 系统限制(Java 9+)

如果目标类所在的模块没有对当前模块 opens,反射会失败,需要添加 JVM 参数:

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

九、总结

一句话总结

反射是 Java 动态性的基石,它让程序从"硬编码"走向"配置化"。几乎所有主流框架(Spring、MyBatis、Netty)都建立在反射之上。

什么时候用反射?

场景

是否使用反射

理由

编写框架(Spring、JUnit)

✅ 必须用

框架必须在运行时动态处理未知类

编写工具类(通用 JSON 序列化)

✅ 可以用

需要遍历对象的字段

编写业务代码(Service、Controller)

❌ 不建议

直接调用更清晰、更安全、更快

使用配置文件驱动行为

✅ 可以用

反射是配置化的天然实现方式

结尾金句

如果说 new 是 Java 给开发者画好的"格子",那么反射就是给你一把"改锥",允许你在运行时拆掉格子重新拼装。

但记住:能力越大,责任越大——能不用反射的地方就不用,框架作者用它是因为它别无选择,而你写业务代码时,多想想有没有更优雅的设计模式可以替代。

更多推荐