【每日一技·Java】——上帝视角看Java:反射是如何扒开类的底裤的?
一、从一个让人抓狂的场景说起
先想象这样一个需求:
你正在开发一个支付系统,支持微信支付和支付宝支付。老板说:“把支付方式写在配置文件里,我要能做到不重新编译代码就能切换支付渠道。”
你写了两个类:WechatPay 和 AlipayPay,它们都有一个 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 核心操作速查表
|
操作目标 |
正常写法(编译期) |
反射写法(运行时) |
|
创建对象 |
|
|
|
调用公有方法 |
|
|
|
调用私有方法 |
编译报错 ❌ |
|
|
读取私有字段 |
编译报错 ❌ |
|
|
修改私有字段 |
编译报错 ❌ |
|
|
获取泛型参数 |
无法获取(类型擦除) |
|
五、反射的 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 代码可读性差
抛出大量受检异常(ClassNotFoundException、NoSuchMethodException、IllegalAccessException、InvocationTargetException),代码臃肿笨拙。
七、反射 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 给开发者画好的"格子",那么反射就是给你一把"改锥",允许你在运行时拆掉格子重新拼装。但记住:能力越大,责任越大——能不用反射的地方就不用,框架作者用它是因为它别无选择,而你写业务代码时,多想想有没有更优雅的设计模式可以替代。
更多推荐
所有评论(0)