news 2026/6/26 1:35:07

每日一技第五天——上帝视角看Java:反射是如何扒开类的底裤的?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
每日一技第五天——上帝视角看Java:反射是如何扒开类的底裤的?

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

先想象这样一个需求:

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

你写了两个类: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 给开发者画好的"格子",那么反射就是给你一把"改锥",允许你在运行时拆掉格子重新拼装。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/26 1:33:05

react源码学习之Scheduler

简介 Scheduler主要用于调度执行Reconciler 组成 api unstable_scheduleCallbackunstable_cancelCallback 调度回调有两种方式 setTimeout通过MessageChannel Task 为任务调度单元 type Task {id: number,callback: Callback | null,priorityLevel: PriorityLevel,st…

作者头像 李华
网站建设 2026/6/26 1:29:36

HDFS javaAPI-windows的IDEA中java文件在linux中的hadoop平台运行

运行前提1、windows能ping通虚拟机IP地址【虚拟机网络改为桥接模式&#xff0c;改为固定IP】2、Linux开放端口&#xff1a;9000,9870,8088【使用windows的 PowerShell&#xff0c;执行&#xff1a;Test-NetConnection IP地址 -Port 9000&#xff0c;需要修改linux中hadoop配置文…

作者头像 李华
网站建设 2026/6/26 1:28:16

X浏览器:免费又极简,建议收藏 !

手机上自带的浏览器要么臃肿要么广告满天飞&#xff0c;用着糟心。之前一直用夸克&#xff0c;后来广告越来越多就换了&#xff0c;折腾了好几个才发先X浏览器&#xff0c;用到现在没再换过。这浏览器安装包才2MB&#xff0c;启动基本是秒开&#xff0c;装完占的存储也小德离谱…

作者头像 李华
网站建设 2026/6/26 1:27:35

Web渗透测试实战入门:从信息收集到漏洞利用的标准化流程

1. 项目概述&#xff1a;从“黑盒”到“白盒”的实战思维刚入行那会儿&#xff0c;看别人挖洞、渗透&#xff0c;总觉得像电影里的黑客&#xff0c;敲几行代码&#xff0c;网站就“沦陷”了&#xff0c;神秘又刺激。后来自己上手&#xff0c;才发现根本不是那么回事。真正的网站…

作者头像 李华
网站建设 2026/6/26 1:26:29

MuleSoft企业级AI编排:LLM服务化治理与生产落地实践

1. 项目概述&#xff1a;当企业级集成平台遇上大语言模型“AI Orchestration in Action: How MuleSoft and LLMs Fuel the Future of Enterprise AI”——这个标题不是一句空泛的行业口号&#xff0c;而是我在过去18个月里亲手落地的三个生产级AI增强型集成项目的统一内核。它讲…

作者头像 李华
网站建设 2026/6/26 1:24:03

TestDisk PhotoRec终极指南:三步快速恢复丢失的分区和文件

TestDisk & PhotoRec终极指南&#xff1a;三步快速恢复丢失的分区和文件 【免费下载链接】testdisk TestDisk & PhotoRec 项目地址: https://gitcode.com/gh_mirrors/te/testdisk 你是否曾因误删分区而惊慌失措&#xff1f;是否因格式化硬盘而追悔莫及&#xff…

作者头像 李华