每日一技第五天——上帝视角看Java:反射是如何扒开类的底裤的? 一、从一个让人抓狂的场景说起先想象这样一个需求你正在开发一个支付系统支持微信支付和支付宝支付。老板说“把支付方式写在配置文件里我要能做到不重新编译代码就能切换支付渠道。”你写了两个类WechatPay和AlipayPay它们都有一个pay(String orderId)方法。// 配置文件 config.properties 里写着 pay.class.namecom.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, Im 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, Im 李四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)); // 1004.5 操作泛型获取运行时泛型类型// 由于 Java 的类型擦除运行时泛型信息会被擦除 // 但通过反射可以获取到方法/字段上声明的泛型类型 public class GenericDemo { private ListString 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(Cant register driver!); } }Class.forName()会触发类的静态初始化驱动就自动注册了。而类名写在配置文件中换数据库只需改配置文件不用重新编译代码。场景二Spring IoC 容器依赖注入Spring 启动时做了这样几件事极度简化版// 1. 扫描所有带 Component 注解的类 SetClass? 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 变成了 hack6.3 安全隐患可以绕过访问控制修改敏感数据可能被恶意代码利用。6.4 代码可读性差抛出大量受检异常ClassNotFoundException、NoSuchMethodException、IllegalAccessException、InvocationTargetException代码臃肿笨拙。七、反射 vs 其他动态特性特性反射 (Reflection)方法句柄 (MethodHandle)直接调用性能较慢接近直接调用需配合 LambdaMetafactory最快使用难度简单较复杂最简单访问控制可突破setAccessible遵循 JVM 访问控制正常访问典型应用框架、工具脚本语言、高性能框架业务代码从 Java 7 开始引入了java.lang.invoke.MethodHandle性能更好但 API 更复杂。反射依然是框架开发的首选因为它的 API 直观、功能强大。八、避坑指南这些坑你一定遇到过坑 1newInstance()已废弃// ❌ JDK 9 已标记废弃 clazz.newInstance(); // ✅ 正确写法 clazz.getDeclaredConstructor().newInstance();坑 2基本类型和包装类型的 Class 不一样int.class Integer.class; // false int.class Integer.TYPE; // trueTYPE 是 int 的原始类型坑 3数组的 Class 对象有特殊命名String[].class.getName(); // [Ljava.lang.String; int[].class.getName(); // [I坑 4反射获取不到泛型真实类型虽然getGenericType()能拿到声明时的泛型类型但对象实例的泛型已被擦除ListString list new ArrayList(); // 无法通过反射获取 list 是 ListString运行时只知道它是 List坑 5Module 系统限制Java 9如果目标类所在的模块没有对当前模块opens反射会失败需要添加 JVM 参数--add-opens java.base/java.langALL-UNNAMED九、总结一句话总结反射是 Java 动态性的基石它让程序从硬编码走向配置化。几乎所有主流框架Spring、MyBatis、Netty都建立在反射之上。什么时候用反射场景是否使用反射理由编写框架Spring、JUnit✅ 必须用框架必须在运行时动态处理未知类编写工具类通用 JSON 序列化✅ 可以用需要遍历对象的字段编写业务代码Service、Controller❌ 不建议直接调用更清晰、更安全、更快使用配置文件驱动行为✅ 可以用反射是配置化的天然实现方式结尾金句如果说new是 Java 给开发者画好的格子那么反射就是给你一把改锥允许你在运行时拆掉格子重新拼装。但记住能力越大责任越大——能不用反射的地方就不用框架作者用它是因为它别无选择而你写业务代码时多想想有没有更优雅的设计模式可以替代。