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)。
相关推荐
huahailing10242 小时前
Spring Boot 异步事务最佳实践:TransactionTemplate 实战指南
数据库·spring boot·后端
Data_Journal2 小时前
如何将网站数据抓取到 Excel:一步步指南
大数据·开发语言·数据库·人工智能·php
米码收割机2 小时前
【AI】OpenClaw问题排查
开发语言·数据库·c++·python
色空大师2 小时前
【mysql建表避坑指南】
数据库·mysql
V1ncent Chen2 小时前
从零学SQL 02 MySQL架构介绍
数据库·sql·mysql·架构·数据分析
大母猴啃编程2 小时前
MySQL内置函数
数据库·sql·mysql·adb
@小匠2 小时前
Spring-Gateway-理论知识总结/常问面试题
数据库·spring·gateway
逍遥德2 小时前
postgresql数据库连接问题
数据库·postgresql
此方ls2 小时前
Redis源码研读八——listpack.c 1080-1528行
c语言·数据库·redis