java中的反射详解

反射

什么是反射

反射是Java的核心特性之一,它允许程序在运行状态中,动态的获取任何一个类的所有属性和方法,并能调用这些属性和方法。这种动态性使得反射成为框架开发(如 Spring、MyBatis)、注解处理、动态代理等场景的基石

通常情况下,我们是"通过类创建对象",而反射是"通过对象反向获取类的信息"。

反射的使用场景

以下是反射在实际开发和主流框架中的五大核心使用场景:

  1. 深度解耦:Spring 框架的 IOC 与 DI

这是反射最经典的应用。在 Spring 中,你不需要手动 new 对象,只需要加一个 @Component 或在 XML 中配置。

  • 场景: Spring 容器启动时,会读取配置文件或扫描包路径,拿到一串类名字符串(如 "com.xxx.UserServiceImpl")。

  • 反射操作: Spring 利用 Class.forName() 加载该类,通过 Constructor.newInstance() 创建实例,再通过反射扫描字段上的 @Autowired,将依赖的对象注入进去。

  • 价值: 实现了对象创建与使用的完全解耦,你可以通过修改配置直接更换实现类,而无需改动源代码。


  1. 切面编程:动态代理 (AOP)

AOP(如日志记录、权限校验、事务管理)的底层全是反射。

  • 场景: 当你需要给上百个方法都加上"开启事务"和"提交事务"的逻辑时。

  • 反射操作: JDK 动态代理利用反射在内存中动态生成一个代理类。当你调用目标方法时,实际上是触发了反射的 Method.invoke()

  • 价值: 可以在不修改原有业务代码的情况下,动态地横向切入新功能。


  1. 注解驱动开发 (Annotation Processing)

注解本身只是一个"标记",它之所以能跑起来,全靠反射去"读"它。

  • 场景: 比如你在类上写了 @TableName("users"),MyBatis 怎么知道这个类对应哪张表?

  • 反射操作: 框架在运行时通过 clazz.getAnnotation(TableName.class) 获取注解对象,进而读取到 "users" 这个字符串。

  • 价值: 极大地简化了配置,让代码更加简洁(即所谓的"约定优于配置")。


  1. 通用工具类与 JSON 序列化

像 Jackson、Fastjson 或 Gson 这种库,能够把任何一个 Java 对象转成 JSON 字符串。

  • 场景: 工具类编写者并不知道你会定义什么样的类(User、Order、Book...)。

  • 反射操作: 序列化工具通过反射获取你传入的对象的所有 Field,遍历它们并获取值,最后拼接成 JSON 格式。

  • 价值: 编写一套代码,就能处理成千上万种不同的类。


  1. 数据库驱动加载 (JDBC)

这是每个 Java 程序员初学时都会写的一行代码:Class.forName("com.mysql.cj.jdbc.Driver")

  • 场景: 程序需要在不重新编译的情况下支持不同的数据库(MySQL、Oracle、PostgreSQL)。

  • 反射操作: 这一行代码利用反射加载驱动类,触发该类内部的 static 静态代码块,从而完成驱动注册。

  • 价值: 实现了程序与具体数据库驱动的解耦。


  1. 单元测试框架 (JUnit)

当你点击 IDE 里的"运行测试"按钮时:

  • 场景: JUnit 需要找出你的类里哪些方法需要被执行。

  • 反射操作: 它通过反射获取类中所有方法,筛选出带有 @Test 注解的方法并依次执行。

反射的基本使用

反射的基本使用可以拆解为:获取 Class 对象构造对象操作属性调用方法

下面通过一个具体的示例来演示这四个核心步骤。

  1. 准备一个简单的类

首先,我们假设有一个 User 类(包含私有属性和私有方法,用来测试反射的"暴力访问"能力):

java 复制代码
public class User {
    private String name;
    public int age;
​
    public User() {} // 无参构造
    private User(String name) { this.name = name; } // 私有构造
​
    public void talk(String content) {
        System.out.println(name + " 说: " + content);
    }
​
    private String getSecret() { return "这是我的私密信息"; }
}
  1. 反射的具体操作步骤

第一步:获取 Class 对象

这是反射的入口。通常有三种方式,最常用的是 Class.forName()

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

第二步:通过反射创建对象

  • 调用公共无参构造: clazz.getDeclaredConstructor().newInstance();

  • 调用私有有参构造: 需要先获取构造器并解除权限检查。

java 复制代码
// 获取私有的 User(String name) 构造器
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
constructor.setAccessible(true); // 暴力反射:忽略私有权限
Object userObj = constructor.newInstance("张三");

第三步:操作属性 (Field)

反射可以突破 private 限制修改属性值。

java 复制代码
// 获取私有属性 name
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true); // 解除私有访问检查
​
// 修改属性值
nameField.set(userObj, "李四");
​
// 获取属性值
String value = (String) nameField.get(userObj);
System.out.println("修改后的名字: " + value);

第四步:调用方法 (Method)

使用 invoke 方法动态执行。

java 复制代码
// 调用公共带参方法 talk(String content)
Method talkMethod = clazz.getMethod("talk", String.class);
talkMethod.invoke(userObj, "你好,反射!");
​
// 调用私有无参方法 getSecret()
Method secretMethod = clazz.getDeclaredMethod("getSecret");
secretMethod.setAccessible(true);
String secret = (String) secretMethod.invoke(userObj);
System.out.println("获取到的私密内容: " + secret);

获取类信息的三种方式

获取类信息(即获取 Class 对象)主要有 3 种 常用方式。这三种方式分别对应了不同的使用场景:代码编写阶段已有对象阶段 、以及动态加载阶段

  1. 类名.class(最安全的方式)

如果你在编写代码时已经确定了要操作哪个类,这种方式是首选。

  • 语法: Class clazz = User.class;

  • 特点:

    • 性能最高:在编译期间就确定了,不需要运行时的额外开销。

    • 最安全:编译器会检查类名是否正确,不会出现拼写错误导致的异常。

    • 非侵入性:不需要创建类的实例。

  • 适用场景: 方法参数传递、明确知道类名的静态操作。


  1. 对象.getClass()(运行期获取)

如果你已经拿到了一个类的实例(对象),想知道这个对象到底属于哪个类。

  • 语法: ```java User user = new User(); Class clazz = user.getClass();

  • 特点:

    • 基于实例:必须先有一个对象才能调用。

    • 准确性 :可以识别出多态情况下的实际类型。如果父类引用指向子类对象,getClass() 返回的是子类的 Class 对象。

  • 适用场景: 在通用方法中处理传入的对象,需要判断其具体类型时。


  1. Class.forName("全类名")(最灵活的方式)

这是反射中最常用、也是框架中使用最频繁的方式。

  • 语法: Class clazz = Class.forName("com.mysql.cj.jdbc.Driver");

  • 特点:

    • 极具灵活性:类名可以是一个字符串。这意味着你可以从配置文件、数据库或网络中读取类名,动态加载。

    • 会触发类初始化 :默认情况下,这种方式会执行类中的 static 静态代码块。

    • 需处理异常 :必须捕获或抛出 ClassNotFoundException

  • 适用场景: JDBC 加载驱动、Spring 配置文件加载 Bean、插件化开发。

获取方式 对应阶段 是否触发静态代码块 检查时机
类名.class 编译期 编译期检查(报错即无法编译)
obj.getClass() 运行期 运行期检查
Class.forName() 运行期 运行期检查(需处理异常)

反射的核心api

Java 反射的核心 API 都在 java.lang.Class 类和 java.lang.reflect 包下。

  1. Class 类:反射的入口

Class 对象是反射的"入场券",它代表了运行在 JVM 中的类或接口。

  • 获取构造器:

    • getConstructors(): 获取所有 public 的构造方法。

    • getDeclaredConstructors(): 获取 所有 构造方法(包括私有)。

  • 获取成员变量:

    • getField(String name): 获取特定的 public 属性。

    • getDeclaredFields(): 获取 所有 声明的属性。

  • 获取方法:

    • getMethod(String name, Class... p): 获取特定的 public 方法。

    • getDeclaredMethods(): 获取类中 所有 的方法。


  1. Constructor 类:创建对象的"模具"

通过 Constructor 对象,你可以动态地创建类的实例。

  • 核心方法:

    • newInstance(Object... initargs): 实际创建对象的方法。

    • setAccessible(true): 如果构造方法是私有的,必须调用此方法才能创建实例。


  1. Field 类:操作属性的"指针"

它代表类的成员变量,可以用来获取或修改对象中某个属性的值。

  • 核心方法:

    • set(Object obj, Object value): 给指定对象的该属性赋值。

    • get(Object obj): 获取指定对象该属性的值。

    • getType(): 获取属性的类型。

    • setAccessible(true): 访问私有属性(暴力反射)的开关。


  1. Method 类:执行方法的"触发器"

这是反射中最常用的 API,用于动态执行对象的方法。

  • 核心方法:

    • invoke(Object obj, Object... args) : 最核心方法 。执行指定对象的该方法,obj 是对象实例,args 是参数。

    • getReturnType(): 获取方法的返回值类型。

    • getParameterTypes(): 获取方法的参数列表类型。

反射 API 代表含义 最核心操作
Class 整个类的结构 getDeclaredMethod(), forName()
Constructor 类的构造函数 newInstance() (创建对象)
Field 类的属性/变量 set() / get() (读写数据)
Method 类的方法 invoke() (执行逻辑)

反射的优缺点

  1. 反射的优点 (Pros)
  • 极大的灵活性与扩展性: 反射允许程序在运行时动态地加载类、创建对象和调用方法,而不需要在编译时将类硬编码。这使得程序可以根据配置文件或用户输入来改变行为。

  • 实现解耦: 通过反射,我们可以通过类名的字符串来实例化对象。这意味着调用者和被调用者之间不需要强依赖。

    典型应用 :Spring 的 IOC(控制反转) 。Spring 读取配置文件或注解,通过反射创建 Bean 并注入依赖,你不需要在代码里写大量的 new

  • 支持通用的库和框架: 反射是 Java 几乎所有重量级框架设计的基石:

    • 序列化/反序列化:如 JSON 转换工具(Jackson, Fastjson)通过反射读取对象的属性名和值。

    • 单元测试 :JUnit 通过反射找到类中带有 @Test 注解的方法并运行。

    • 动态代理:实现 AOP(切面编程),在不修改原代码的情况下增加日志或事务功能。


  1. 反射的缺点 (Cons)
  • 性能损耗: 反射操作比直接的 Java 代码要慢得多。

    • 原因:JVM 无法对反射代码进行优化(如内联优化);反射涉及类型检查、安全检查、以及在常量池中查找类成员的字符串匹配等开销。
  • 破坏封装性(安全性风险) : 反射可以绕过访问修饰符,访问并修改类的 private 私有属性和方法。

    • 后果 :通过 setAccessible(true),你可以修改本不该被外部触碰的内部状态,这可能导致程序逻辑混乱,甚至引发安全漏洞。
  • 代码维护与调试困难

    • 编译期检查失效 :普通代码报错在编译时就能发现,而反射报错(如类名写错、方法不存在)只有在程序运行时 才会抛出异常(如 ClassNotFoundException)。

    • 逻辑不直观:反射代码通常比常规代码更复杂、晦涩,后续维护人员很难一眼看出程序到底调用了哪个方法。

解决反射缺点的优化

针对反射存在的性能损耗安全检查开销 以及代码维护困难等缺点,在实际开发(尤其是高性能框架设计)中,通常采用以下几种优化方案:

  1. 缓存反射元数据(最基础、最有效)

反射最慢的地方在于"查找"过程:每次调用 getMethod()getField(),JVM 都要在类的字节码中进行字符串匹配和权限校验。

  • 优化策略: 将获取到的 MethodFieldConstructor 对象存储在 ConcurrentHashMap 中。

  • 效果: 只有第一次调用时存在查找开销,后续直接从内存获取,性能提升显著。


  1. 关闭访问权限检查 (setAccessible)

默认情况下,反射在每次执行时都会检查是否有权访问目标成员(即使是 public 成员也会进行安全检查)。

  • 优化策略: 明确调用 accessibleObject.setAccessible(true)

  • 效果: 告诉 JVM 跳过安全检查步骤。这不仅能让你访问 private 成员,还能让反射调用的速度提升约 20%~30%


  1. 使用 MethodHandle (JDK 7+)

MethodHandle 是 JDK 7 引入的更加底层、更轻量级的反射机制。它被称为"类型安全的函数指针"。

  • 优化策略: 使用 MethodHandles.Lookup 找到方法句柄并调用。

  • 优点: * 比 Method 更接近底层指令。

    • JVM 可以在运行时对其进行更多的内联优化 (Inlining)

    • 在重复调用的场景下,性能优于传统反射。


  1. 使用字节码增强技术 (ReflectASM / ByteBuddy)

对于追求极致性能的场景(如高性能 JSON 解析器),原生反射往往无法满足要求。

  • 优化策略: * ReflectASM: 通过直接操作字节码,在运行时生成一个辅助类。该辅助类直接调用目标方法,将"反射调用"转变为"普通调用"。

    • ByteBuddy / CGLIB: 动态生成子类或代理类。
  • 效果: 调用速度几乎等同于直接通过 new 创建对象并调用方法。


  1. LambdaMetafactory (JDK 8+)

这是现代高性能框架常用的"黑科技",可以将一个反射方法转化为一个 Lambda 表达式

  • 原理: 利用 LambdaMetafactoryMethod 包装成一个功能接口(如 FunctionBiConsumer)。

  • 效果: 一旦转化完成,该调用的性能与直接编写的 Lambda 表达式一致,JVM 可以对其进行完全的编译优化。


  1. 应对"代码可维护性"缺点的优化
  • 结合注解 (Annotation): 不要盲目使用反射查找。通过注解标记目标,在程序启动时进行一次性扫描并校验,如果反射目标不存在,立即抛出异常停止启动。

  • 封装工具类: 业务代码中严禁直接写 try-catch 包围的反射逻辑,应统一封装在 ReflectionUtils 中。

性能要求 推荐方案 适用场景
中低频率 原生反射 + 缓存 + setAccessible 普通工具类、通用后台逻辑
高频循环 MethodHandle 规则引擎、轻量级中间件
极致性能 ReflectASM / LambdaMetafactory 序列化框架 (Jackson/Fastjson)、ORM 框架

反射在动态代理里面的表现

在动态代理(Dynamic Proxy)中,反射不仅是"辅助工具",更是其赖以生存的基石

动态代理的核心逻辑是:在程序运行时,根据指定的接口动态地在内存中生成一个代理类,并利用反射将方法调用转发到目标对象上。

以下是反射在动态代理中的具体表现:

  1. 核心 API:Proxy.newProxyInstance()

这个方法是 JDK 动态代理的入口,它的三个参数无一不体现了反射的应用:

java 复制代码
public static Object newProxyInstance(ClassLoader loader, 
                                      Class<?>[] interfaces, 
                                      InvocationHandler h)

ClassLoader (类加载器):反射需要类加载器将动态生成的字节码加载进 JVM。

Class<?>[] interfaces :反射读取目标类实现的所有接口信息。代理对象必须知道要"模仿"哪些接口。

InvocationHandler:这是反射执行逻辑的核心跳转台。

2 核心逻辑:Method.invoke() 的转发

动态代理最神奇的地方在于:你调用代理对象的方法,最终都会跑到 InvocationHandlerinvoke() 方法里。

java 复制代码
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 1. 反射的表现:这里的 method 对象就是当前正在被调用的方法元数据
    System.out.println("日志:准备执行 " + method.getName());
​
    // 2. 反射的表现:利用 method.invoke 动态调用目标对象的真实方法
    Object result = method.invoke(target, args); 
​
    System.out.println("日志:方法执行完毕");
    return result;
}

表现形式method 参数本身就是通过反射获取的。

作用 :代理类不需要在代码中硬编码调用 target.save(),而是通过反射通用的 invoke 接口,处理目标类中的任何方法。

  1. 运行时的"偷梁换柱":字节码生成

动态代理不需要你手动写代理类的 .java 文件。

  • 反射表现:JVM 在运行时利用反射机制检视接口结构,直接在内存中生成该接口的实现类字节码。

  • 结果 :生成的代理类会持有一个 InvocationHandler 的引用。当你调用代理类的方法时,它内部的代码其实是:handler.invoke(this, m3, args)(这里的 m3 是该方法对应的反射 Method 对象)。

反射在动态代理中的三个角色

角色 具体操作 目的
探测器 interfaces 数组获取 确定代理类需要实现哪些方法。
搬运工 method.invoke() 将对代理的操作无差别地转发给真实对象。
解耦器 动态生成类 使得一个代理处理器(Handler)可以代理成千上万个不同的接口。

为什么不直接调用,而要用反射?

如果没有反射,你就必须为每一个目标类手动写一个代理类(即静态代理)。 有了反射,你可以写一个通用的日志处理器、事务处理器,它能自动适应任何传入的对象。这就是 Spring AOP 的底层奥秘。

相关推荐
程序猿零零漆2 小时前
Spring之旅 - 记录学习 Spring 框架的过程和经验(五)Spring的后处理器BeanFactoryPostProcessor
java·学习·spring
特立独行的猫a2 小时前
C++23 std::expected 详解:告别传统错误码和异常,构建现代健壮代码
开发语言·c++23·expected·错误码处理
星火飞码iFlyCode2 小时前
iFlyCode实践规范驱动开发(SDD):招考平台报名相片质量抽检功能开发实战
java·前端·python·算法·ai编程·科大讯飞
廋到被风吹走2 小时前
【Spring】HandlerInterceptor解析
java·后端·spring
leaves falling2 小时前
c语言-根据输入的年份和月份,计算并输出该月份的天数
c语言·开发语言·算法
云栖梦泽2 小时前
鸿蒙企业级工程化与终极性能调优实战
开发语言·鸿蒙系统
毛小茛2 小时前
若依框架搭建基础知识
java
Eloudy2 小时前
通过示例看 C++ 函数对象、仿函数、operator( )
开发语言·c++·算法
leaves falling2 小时前
c语言将三个整数数按从大到小输出
c语言·开发语言