Java反射

反射(Reflection)是 Java 中最强大也最容易被误用的特性之一。它打破了 Java 的封装性,让程序在运行时能够 "看透" 类的内部结构(属性、方法、构造器),并动态操作这些组件。小到框架中的配置解析,大到 Spring、MyBatis 等核心框架的底层实现,反射都是不可或缺的核心技术。本文将从原理、核心 API、实战场景到使用注意事项,帮你彻底掌握反射。

一、反射的核心本质:运行时 "解剖" 类

1. 什么是反射?

Java 程序在编译期会将.java文件编译为.class字节码文件,运行时 JVM 会加载这些字节码并创建对应的Class对象(每个类只有一个Class实例)。

反射的本质 :通过Class对象,在运行时获取类的所有信息(属性、方法、构造器、父类、注解等),并动态调用类的方法、修改属性值,无需在编译期确定具体的类和方法。

简单来说:正常编程是 "写代码时确定调用谁",反射是 "运行时才确定调用谁"------ 这也是动态代理、框架灵活配置的核心底层逻辑。

2. 反射的核心价值

  • 打破封装:可访问类的私有属性 / 方法(需关闭安全检查);
  • 动态性:运行时动态加载、使用未知的类(比如读取配置文件中的类名,动态创建实例);
  • 解耦:框架无需硬编码依赖具体类,通过反射适配任意符合规则的类(如 Spring 的 IOC 容器)。

二、反射的核心 API:从获取 Class 对象到动态操作

反射的所有操作都围绕java.lang.reflect包和Class类展开,核心步骤分为 "获取 Class 对象" 和 "操作类组件" 两步。

1. 第一步:获取 Class 对象(反射的入口)

Class对象是反射的基础,获取方式有 3 种:

复制代码
// 方式1:通过类名.class(编译期确定,最安全)
Class<User> clazz1 = User.class;

// 方式2:通过对象.getClass()(适用于已有实例的场景)
User user = new User();
Class<? extends User> clazz2 = user.getClass();

// 方式3:通过Class.forName("全类名")(运行时动态加载,最灵活,需处理异常)
Class<?> clazz3 = Class.forName("com.example.reflect.User");

注意:Class.forName()需传入类的全限定名(包名 + 类名),且会触发类的静态代码块执行(比如加载数据库驱动时Class.forName("com.mysql.cj.jdbc.Driver"))。

2. 第二步:操作类的核心组件

以一个简单的User类为例,演示反射的核心操作:

复制代码
// 目标类
public class User {
    private Long id;
    public String name;

    public User() {} // 无参构造
    public User(Long id, String name) { // 有参构造
        this.id = id;
        this.name = name;
    }

    private String getUserName() { // 私有方法
        return "用户名:" + name;
    }

    public void setId(Long id) { // 公有方法
        this.id = id;
    }
}
(1)操作构造器:动态创建实例
复制代码
Class<?> userClass = Class.forName("com.example.reflect.User");

// 1. 获取无参构造器,创建实例
Constructor<?> constructor1 = userClass.getConstructor();
User user1 = (User) constructor1.newInstance();

// 2. 获取有参构造器,创建实例(参数需匹配构造器的参数类型)
Constructor<?> constructor2 = userClass.getConstructor(Long.class, String.class);
User user2 = (User) constructor2.newInstance(1L, "张三");

// 3. 获取私有构造器(需使用getDeclaredConstructor,且关闭访问检查)
Constructor<?> privateConstructor = userClass.getDeclaredConstructor(Integer.class);
privateConstructor.setAccessible(true); // 关闭访问检查,否则无法访问私有构造
User user3 = (User) privateConstructor.newInstance(100);
(2)操作属性:读取 / 修改属性值
复制代码
User user = new User(1L, "张三");
Class<? extends User> clazz = user.getClass();

// 1. 获取公有属性并修改
Field nameField = clazz.getField("name"); // getField只能获取公有属性
nameField.set(user, "李四");
System.out.println(nameField.get(user)); // 输出:李四

// 2. 获取私有属性并修改
Field idField = clazz.getDeclaredField("id"); // getDeclaredField可获取所有属性(包括私有)
idField.setAccessible(true); // 关闭访问检查
idField.set(user, 2L);
System.out.println(idField.get(user)); // 输出:2
(3)操作方法:动态调用方法
复制代码
User user = new User(1L, "张三");
Class<? extends User> clazz = user.getClass();

// 1. 调用公有方法
Method setIdMethod = clazz.getMethod("setId", Long.class); // 参数:方法名+参数类型
setIdMethod.invoke(user, 3L); // invoke:执行方法,参数:目标对象+方法入参

// 2. 调用私有方法
Method getUserNameMethod = clazz.getDeclaredMethod("getUserName");
getUserNameMethod.setAccessible(true);
String result = (String) getUserNameMethod.invoke(user);
System.out.println(result); // 输出:用户名:张三

三、反射的典型应用场景

反射看似抽象,但在实际开发中无处不在,以下是最核心的 3 个场景:

1. 动态代理(JDK 动态代理的核心)

JDK 动态代理的InvocationHandler.invoke()方法中,正是通过Method.invoke()(反射)调用目标对象的方法:

复制代码
// 核心逻辑片段
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 前置增强逻辑
    System.out.println("执行前置校验");
    // 反射调用目标对象的方法
    Object result = method.invoke(targetObject, args);
    // 后置增强逻辑
    System.out.println("执行后置日志");
    return result;
}

没有反射,就无法在运行时动态调用任意目标对象的任意方法,动态代理也就无从谈起。

2. 框架的灵活配置(Spring/MyBatis 核心)

  • Spring IOC :读取 XML / 注解配置中的类名,通过Class.forName()动态加载类,再通过反射创建实例、注入属性;
  • MyBatis:通过反射将数据库查询结果集映射为 Java 实体类对象(自动给实体类的属性赋值);
  • 配置文件解析 :比如读取properties文件中的 "类名 = com.example.User",动态创建 User 实例,无需硬编码new User()

3. 通用工具类开发

比如封装一个 "对象属性拷贝工具类",通过反射遍历源对象的所有属性,自动赋值给目标对象的对应属性(类似 Spring 的BeanUtils.copyProperties):

复制代码
public static void copyProperties(Object source, Object target) throws Exception {
    Class<?> sourceClass = source.getClass();
    Class<?> targetClass = target.getClass();
    
    // 获取源对象的所有属性
    Field[] fields = sourceClass.getDeclaredFields();
    for (Field field : fields) {
        field.setAccessible(true);
        // 获取目标对象的同名属性
        Field targetField = targetClass.getDeclaredField(field.getName());
        targetField.setAccessible(true);
        // 反射赋值
        targetField.set(target, field.get(source));
    }
}

四、反射的注意事项:优势与风险并存

1. 反射的缺点

  • 性能损耗 :反射需在运行时解析类结构,比直接调用方法 / 属性慢 10-100 倍(可通过MethodHandle优化);
  • 打破封装:可访问私有成员,违背面向对象的封装原则,增加代码安全风险;
  • 可读性差:反射代码比普通代码更抽象,调试难度高;
  • 编译期无法校验 :比如调用一个不存在的方法,编译期不会报错,运行时才抛出NoSuchMethodException

2. 合理使用反射的建议

  • 仅在需要 "动态性" 的场景使用(如框架开发),业务代码尽量避免;
  • 对频繁调用的反射操作,缓存ClassMethodField对象(避免重复解析);
  • 必要时才关闭访问检查(setAccessible(true)),用完后恢复;
  • 结合异常处理,捕获反射相关的受检异常(如NoSuchFieldExceptionIllegalAccessException)。
相关推荐
2301_8135995514 小时前
Go语言怎么做秒杀系统_Go语言秒杀系统实战教程【实用】
jvm·数据库·python
NCIN EXPE19 小时前
redis 使用
数据库·redis·缓存
MongoDB 数据平台19 小时前
为编码代理引入 MongoDB 代理技能和插件
数据库·mongodb
极客on之路19 小时前
mysql explain type 各个字段解释
数据库·mysql
代码雕刻家19 小时前
MySQL与SQL Server的基本指令
数据库·mysql·sqlserver
lThE ANDE19 小时前
开启mysql的binlog日志
数据库·mysql
yejqvow1219 小时前
CSS如何控制placeholder文字的颜色_使用--placeholder伪元素
jvm·数据库·python
oLLI PILO19 小时前
nacos2.3.0 接入pgsql或其他数据库
数据库
m0_7436239219 小时前
HTML怎么创建多语言切换器_HTML语言选择下拉结构【指南】
jvm·数据库·python