Java 反射

Java 反射

Java 反射(Reflection)是 Java 语言的一个强大特性,它允许在运行时动态地获取类的信息并操作对象,即使在编译时并不知道这些类的具体结构。


✅ 一句话理解反射:

"程序可以在运行时查看、调用自身或外部类的属性、方法、构造器等,而无需在编译时硬编码。"


一、反射能做什么?

通过反射,你可以:

  1. 获取类的 Class 对象
  2. 查看类的字段(Field)、方法(Method)、构造器(Constructor)
  3. 创建对象实例(即使构造器是 private)
  4. 调用任意方法(包括 private 方法)
  5. 读写任意字段(包括 private 字段)

⚠️ 注意:反射会绕过访问控制检查 (如 private),但需先调用 setAccessible(true)


二、核心 API(位于 java.lang.reflect 包)

作用
Class<?> 代表一个类的元数据(类型信息)
Field 代表类的成员变量
Method 代表类的方法
Constructor 代表类的构造器

三、基本使用示例

1. 获取 Class 对象(三种方式)

java 复制代码
// 方式1:通过类名.class
Class<Person> clazz1 = Person.class;

// 方式2:通过对象.getClass()
Person p = new Person();
Class<? extends Person> clazz2 = p.getClass();

// 方式3:通过全限定类名(常用于配置文件)
Class<?> clazz3 = Class.forName("com.example.Person");

2. 创建对象(即使构造器私有)

java 复制代码
// 调用无参构造器
Person p1 = (Person) clazz.getDeclaredConstructor().newInstance();

// 调用带参构造器
Constructor<Person> ctor = clazz.getConstructor(String.class, int.class);
Person p2 = ctor.newInstance("张三", 25);

3. 访问私有字段

java 复制代码
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true); // 突破 private 限制
nameField.set(p, "李四");      // 设置值
String name = (String) nameField.get(p); // 获取值

4. 调用私有方法

java 复制代码
Method sayHello = clazz.getDeclaredMethod("sayHello");
sayHello.setAccessible(true);
sayHello.invoke(p); // 调用 p.sayHello()

四、完整示例

java 复制代码
import java.lang.reflect.*;

class Person {
    private String name;
    private int age;

    private Person() {}
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private void sayHello() {
        System.out.println("Hello, I'm " + name);
    }
}

public class ReflectionDemo {
    public static void main(String[] args) throws Exception {
        Class<Person> clazz = Person.class;

        // 1. 创建对象(调用私有构造器)
        Constructor<Person> privateCtor = clazz.getDeclaredConstructor();
        privateCtor.setAccessible(true);
        Person p = privateCtor.newInstance();

        // 2. 设置私有字段
        Field nameField = clazz.getDeclaredField("name");
        nameField.setAccessible(true);
        nameField.set(p, "王五");

        // 3. 调用私有方法
        Method method = clazz.getDeclaredMethod("sayHello");
        method.setAccessible(true);
        method.invoke(p); // 输出: Hello, I'm 王五
    }
}

五、反射的典型应用场景

场景 说明
框架开发 Spring(依赖注入)、Hibernate(ORM)、JUnit(测试方法发现)等大量使用反射
通用工具类 如 JSON 序列化(Gson、Jackson)通过反射读取对象字段
动态代理 java.lang.reflect.Proxy 基于反射实现
插件系统 运行时加载未知类并调用其方法

六、反射的缺点(慎用!)

缺点 说明
性能开销大 反射操作比直接调用慢很多(JVM 无法优化)
破坏封装性 可以访问 private 成员,违背面向对象设计原则
安全性问题 可能被用于绕过安全限制(需 SecurityManager 控制)
代码可读性差 难以静态分析,IDE 无法有效提示
编译期不检查 错误只能在运行时暴露(如拼错方法名)

建议 :除非必要(如写框架),否则优先使用常规方式。若必须用反射,尽量缓存 Method/Field 对象以提升性能。


七、最佳实践

  1. 优先使用 getDeclaredXXX() 而不是 getXXX()

    • getDeclaredMethods():获取本类所有方法(含 private)
    • getMethods():只获取 public 方法(包括继承的)
  2. 记得调用 setAccessible(true)

    否则访问 private 成员会抛 IllegalAccessException

  3. 异常处理要全面

    反射方法可能抛出:

    • ClassNotFoundException
    • NoSuchMethodException
    • IllegalAccessException
    • InvocationTargetException(目标方法内部抛异常)
  4. 考虑使用 MethodHandleVarHandle(Java 7+/9+)

    性能优于传统反射(但更复杂)


✅ 总结

关键点 说明
是什么 运行时动态操作类和对象的能力
核心类 Class, Field, Method, Constructor
三大能力 获取信息、创建对象、调用方法/访问字段
适用场景 框架、工具库、动态加载
慎用原因 性能低、破坏封装、安全性风险

💡 记住 :反射是"双刃剑"------强大,但不要滥用。"能不用就不用,必须用就好好用。"

在 Java 开发中,反射(Reflection)不是日常业务代码的常规工具 ,而是一种"元编程"能力,主要用于框架、工具库或需要高度动态性的场景。普通业务逻辑应尽量避免使用反射。


✅ 一、典型需要使用反射的场景

1. 框架开发(最常见)

框架需要在运行时操作未知类,而这些类在编译时并不存在。

  • Spring 框架

    • 通过 @Autowired 自动注入 Bean → 反射设置 private 字段
    • 扫描 @Component 类并实例化 → Class.forName() + 反射调用构造器
  • Hibernate / MyBatis(ORM 框架)

    • 将数据库查询结果自动映射到 Java 对象 → 反射读写字段(即使 private)
  • JUnit(测试框架)

    • 自动发现并执行 @Test 注解的方法 → 反射获取方法并调用

💡 框架的核心思想:"你提供类和配置,我来帮你创建和调用" ------ 这必须依赖反射。


2. 通用工具类开发

  • JSON 序列化/反序列化(如 Gson、Jackson)

    java 复制代码
    // 把 {"name":"张三"} 转成 User 对象
    User user = gson.fromJson(json, User.class);

    → 内部通过反射遍历 User 的字段,自动赋值。

  • Bean 拷贝工具(如 Apache BeanUtils、Spring BeanUtils)

    自动复制两个对象的同名属性 → 反射读取 getter/setter。

  • 日志打印工具

    动态打印对象所有字段值(用于调试)→ 反射遍历字段。


3. 动态代理(Dynamic Proxy)

Java 的 java.lang.reflect.Proxy 基于反射实现:

java 复制代码
// Spring AOP、RPC 客户端常用
MyService proxy = (MyService) Proxy.newProxyInstance(
    loader,
    new Class[]{MyService.class},
    (proxy, method, args) -> {
        // 在方法调用前后加逻辑(如事务、日志)
        return method.invoke(target, args); // ← 这里用反射调用原方法
    }
);

4. 插件系统 / 热加载

程序在运行时加载外部 JAR 包中的类并执行:

java 复制代码
// 从配置文件读取类名
String className = config.getProperty("plugin.class");
Class<?> pluginClass = Class.forName(className);
Object plugin = pluginClass.getDeclaredConstructor().newInstance();
// 调用插件方法
Method run = pluginClass.getMethod("run");
run.invoke(plugin);

适用于:IDE 插件、游戏模组、可扩展系统。


5. 注解处理(运行时)

虽然很多注解在编译期处理(APT),但有些需在运行时读取:

java 复制代码
// 自定义权限校验
@RequiresRole("admin")
public void deleteUser() { ... }

// 拦截器中
Method method = ...;
if (method.isAnnotationPresent(RequiresRole.class)) {
    String role = method.getAnnotation(RequiresRole.class).value();
    // 用反射检查当前用户角色
}

❌ 二、不应该使用反射的场景

场景 正确做法
调用自己项目中已知类的方法 直接调用,不要反射
访问 public 字段或方法 直接访问
简单的对象创建 new 或工厂模式
性能敏感的循环内部 反射太慢!

🚫 反例(错误用法)

java 复制代码
// 明明可以直接调用,却用反射 ------ 无意义且低效!
Method m = obj.getClass().getMethod("getName");
String name = (String) m.invoke(obj);

⚠️ 三、使用反射前的自问清单

在写反射代码前,请先问自己:

  1. 是否在开发框架或通用工具?

    → 如果是业务代码,大概率不需要。

  2. 是否有更简单的替代方案? (如接口、策略模式、Lambda)

    → 优先用类型安全的方式。

  3. 是否愿意承担性能损失?

    → 反射比直接调用慢 10~100 倍(虽可通过缓存缓解)。

  4. 是否破坏了封装性?是否有安全风险?

    → 修改 private 字段可能破坏对象状态。


✅ 四、如果必须用反射:最佳实践

  1. 缓存反射对象Method/Field)避免重复查找
  2. 使用 setAccessible(true) 时要小心
  3. 捕获并处理所有反射异常
  4. 考虑使用 MethodHandle(Java 7+)提升性能
  5. 添加详细注释说明为什么必须用反射

📌 总结:什么时候用反射?

场景 是否推荐
写 Spring/Hibernate 这类框架 ✅ 必须用
开发 JSON 解析、Bean 拷贝等通用工具 ✅ 合理使用
实现插件系统、动态加载模块 ✅ 适合
普通业务逻辑(如 Controller、Service) ❌ 不要用
能用接口/多态解决的问题 ❌ 不要用

💡 黄金法则
"如果你不是在写框架或工具库,那你很可能不需要反射。"

反射是 Java 的"超能力",但超能力要用在刀刃上,而不是切菜。

下面是一个使用 Java 反射 + 自定义注解 实现的 数据库切换(多数据源)功能 的完整示例。该功能允许你通过在方法上添加注解(如 @UseDatabase("mysql"))来动态切换数据源。

💡 虽然实际项目中通常用 Spring AOP + 注解实现,但这里我们纯手写反射逻辑来演示核心思想。


✅ 功能目标

  • 定义一个注解 @UseDatabase
  • 在 Service 方法上标注要使用的数据库名
  • 通过一个"代理"或"调度器"调用方法前,读取注解 → 切换数据源 → 执行方法

用反射和注解写一个java切换库的功能

第一步:定义自定义注解

java 复制代码
import java.lang.annotation.*;

// 可用在方法上,运行时保留
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseDatabase {
    String value(); // 数据库标识,如 "mysql", "oracle"
}

第二步:模拟数据源上下文(ThreadLocal)

java 复制代码
public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static void setDataSource(String dbType) {
        contextHolder.set(dbType);
    }

    public static String getDataSource() {
        return contextHolder.get();
    }

    public static void clear() {
        contextHolder.remove();
    }
}

第三步:编写业务 Service(带注解)

java 复制代码
public class UserService {

    @UseDatabase("mysql")
    public void saveUser(String name) {
        String currentDb = DataSourceContextHolder.getDataSource();
        System.out.println("【" + currentDb + "】正在保存用户: " + name);
    }

    @UseDatabase("oracle")
    public void deleteUser(Long id) {
        String currentDb = DataSourceContextHolder.getDataSource();
        System.out.println("【" + currentDb + "】正在删除用户 ID: " + id);
    }

    // 无注解方法,默认数据源
    public void listUsers() {
        String currentDb = DataSourceContextHolder.getDataSource();
        System.out.println("【" + currentDb + "】列出所有用户");
    }
}

第四步:核心调度器(使用反射读取注解并切换数据源)

java 复制代码
import java.lang.reflect.Method;

public class DatabaseSwitcher {

    /**
     * 通过反射调用带 @UseDatabase 注解的方法,并自动切换数据源
     */
    public static void invokeMethodWithDbSwitch(Object target, String methodName, Object... args) 
            throws Exception {
        
        Class<?> clazz = target.getClass();
        Class<?>[] argTypes = new Class[args.length];
        for (int i = 0; i < args.length; i++) {
            argTypes[i] = args[i].getClass();
        }

        Method method = clazz.getMethod(methodName, argTypes);

        // 1. 检查方法是否有 @UseDatabase 注解
        if (method.isAnnotationPresent(UseDatabase.class)) {
            UseDatabase anno = method.getAnnotation(UseDatabase.class);
            String dbType = anno.value();
            // 2. 切换数据源
            DataSourceContextHolder.setDataSource(dbType);
            System.out.println(">>> 切换到数据库: " + dbType);
        } else {
            // 默认数据源(比如 mysql)
            DataSourceContextHolder.setDataSource("default");
            System.out.println(">>> 使用默认数据库");
        }

        try {
            // 3. 调用目标方法
            method.invoke(target, args);
        } finally {
            // 4. 清理 ThreadLocal,避免内存泄漏
            DataSourceContextHolder.clear();
        }
    }
}

第五步:测试

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        UserService userService = new UserService();

        // 通过我们的调度器调用,自动切换 DB
        DatabaseSwitcher.invokeMethodWithDbSwitch(userService, "saveUser", "张三");
        System.out.println();

        DatabaseSwitcher.invokeMethodWithDbSwitch(userService, "deleteUser", 1001L);
        System.out.println();

        DatabaseSwitcher.invokeMethodWithDbSwitch(userService, "listUsers");
    }
}

🖨️ 输出结果

复制代码
>>> 切换到数据库: mysql
【mysql】正在保存用户: 张三

>>> 切换到数据库: oracle
【oracle】正在删除用户 ID: 1001

>>> 使用默认数据库
【default】列出所有用户

🔍 关键点解析

技术 作用
@UseDatabase 注解 声明方法所需的数据源
method.isAnnotationPresent() 反射检查注解是否存在
method.getAnnotation() 反射获取注解值
ThreadLocal 保证每个线程的数据源隔离(避免并发问题)
method.invoke() 反射调用目标方法

🚀 进阶建议(实际项目中)

  1. 结合 Spring AOP

    @Around 切面代替手动 invokeMethodWithDbSwitch,更透明:

    java 复制代码
    @Around("@annotation(useDb)")
    public Object switchDataSource(ProceedingJoinPoint joinPoint, UseDatabase useDb) {
        DataSourceContextHolder.setDataSource(useDb.value());
        try {
            return joinPoint.proceed();
        } finally {
            DataSourceContextHolder.clear();
        }
    }
  2. 支持数据源注册表

    "mysql" 映射到真实的 DataSource Bean。

  3. 默认值处理

    注解可设默认值:String value() default "master";


✅ 总结

通过 注解 + 反射,我们实现了:

  • 声明式数据源切换
  • 运行时动态解析配置
  • 低侵入性(业务代码只需加注解)

💡 这正是 Spring、MyBatis 等框架的核心思想:"约定优于配置,注解驱动行为"

这个例子虽小,但体现了 Java 元编程的强大能力!

简单实现mybatis @select 的功能

下面是一个简化版 MyBatis @Select 功能的实现 ,使用 Java 注解 + 反射 + 动态代理 来模拟 MyBatis 的核心思想:

✅ 目标:

通过在接口方法上写 @Select("SELECT name FROM user WHERE id = #{id}")

调用该方法时自动执行 SQL 并返回结果(模拟数据库查询)。


第一步:定义 @Select 注解

java 复制代码
import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Select {
    String value(); // SQL 语句,如 "SELECT name FROM user WHERE id = #{id}"
}

第二步:模拟数据库查询(简化版)

java 复制代码
import java.util.HashMap;
import java.util.Map;

// 模拟一个内存数据库
public class MockDatabase {
    private static final Map<Integer, String> USER_DB = new HashMap<>();
    
    static {
        USER_DB.put(1, "张三");
        USER_DB.put(2, "李四");
        USER_DB.put(3, "王五");
    }

    // 模拟执行 SQL(只支持简单占位符 #{id})
    public static String query(String sql, Object... args) {
        // 简单替换 #{id} -> 实际值(仅用于演示!真实 MyBatis 会解析参数名)
        String finalSql = sql;
        for (Object arg : args) {
            finalSql = finalSql.replaceFirst("#\\{\\w+\\}", arg.toString());
        }
        System.out.println("执行 SQL: " + finalSql);

        // 模拟从 DB 查数据(这里只处理 id 查询)
        if (finalSql.contains("WHERE id = ")) {
            String idStr = finalSql.substring(finalSql.lastIndexOf("=") + 1).trim();
            Integer id = Integer.parseInt(idStr);
            return USER_DB.get(id);
        }
        return null;
    }
}

第三步:创建 Mapper 接口(用户编写)

java 复制代码
public interface UserMapper {
    @Select("SELECT name FROM user WHERE id = #{id}")
    String findNameById(Integer id);
}

第四步:核心 ------ 动态代理 + 反射解析注解

java 复制代码
import java.lang.reflect.*;
import java.util.Arrays;

public class MapperProxy implements InvocationHandler {
    private final Class<?> mapperInterface;

    public MapperProxy(Class<?> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 1. 检查方法是否有 @Select 注解
        if (method.isAnnotationPresent(Select.class)) {
            Select select = method.getAnnotation(Select.class);
            String sql = select.value();

            // 2. 执行 SQL(这里简化:直接传 args)
            Object result = MockDatabase.query(sql, args);

            // 3. 返回结果(实际 MyBatis 会做类型转换、映射等)
            return result;
        }

        // 如果没有注解,抛异常或走其他逻辑
        throw new UnsupportedOperationException("Only @Select methods are supported");
    }

    // 工厂方法:创建代理 Mapper
    @SuppressWarnings("unchecked")
    public static <T> T createMapper(Class<T> mapperInterface) {
        return (T) Proxy.newProxyInstance(
            mapperInterface.getClassLoader(),
            new Class[]{mapperInterface},
            new MapperProxy(mapperInterface)
        );
    }
}

第五步:测试

java 复制代码
public class Main {
    public static void main(String[] args) {
        // 创建代理对象(类似 MyBatis 的 sqlSession.getMapper(UserMapper.class))
        UserMapper userMapper = MapperProxy.createMapper(UserMapper.class);

        // 调用方法,自动执行 SQL
        String name1 = userMapper.findNameById(1);
        System.out.println("ID=1 的用户: " + name1); // 输出: 张三

        String name2 = userMapper.findNameById(2);
        System.out.println("ID=2 的用户: " + name2); // 输出: 李四
    }
}

🖨️ 输出结果

复制代码
执行 SQL: SELECT name FROM user WHERE id = 1
ID=1 的用户: 张三
执行 SQL: SELECT name FROM user WHERE id = 2
ID=2 的用户: 李四

🔍 核心原理总结

技术 作用
@Select 注解 声明 SQL 语句
JDK 动态代理 (Proxy) 拦截接口方法调用
反射 (Method.getAnnotation) 读取方法上的 SQL
参数绑定(简化版) 将方法参数填入 #{} 占位符
模拟 DB 查询 返回假数据(真实 MyBatis 会连 JDBC)

⚠️ 注意(与真实 MyBatis 的差距)

  • ❌ 不支持复杂参数(如 User user 对象)
  • ❌ 不支持 #{user.id} 这种嵌套属性
  • ❌ 没有结果集映射(ResultSet → Object
  • ❌ 没有连接池、事务、缓存等

但这个例子完整展示了 MyBatis 的核心思想

"接口 + 注解 = 自动执行 SQL"


✅ 总结

你刚刚用不到 100 行代码,实现了 MyBatis 最迷人的特性之一!

这就是 注解 + 反射 + 动态代理 的威力 ------ 让框架为你干活,你只需专注业务逻辑。

💡 真实 MyBatis 源码更复杂,但骨架与此一致。理解这个简化版,就迈出了读懂 ORM 框架的第一步!

相关推荐
寻寻觅觅☆9 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
l1t10 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
青云计划10 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿10 小时前
Jsoniter(java版本)使用介绍
java·开发语言
ceclar12311 小时前
C++使用format
开发语言·c++·算法
探路者继续奋斗11 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd
码说AI11 小时前
python快速绘制走势图对比曲线
开发语言·python
Gofarlic_OMS11 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
星空下的月光影子11 小时前
易语言开发从入门到精通:补充篇·网络爬虫与自动化采集分析系统深度实战·HTTP/HTTPS请求·HTML/JSON解析·反爬策略·电商价格监控·新闻资讯采集
开发语言
老约家的可汗11 小时前
初识C++
开发语言·c++