Java 反射
Java 反射
Java 反射(Reflection)是 Java 语言的一个强大特性,它允许在运行时动态地获取类的信息并操作对象,即使在编译时并不知道这些类的具体结构。
✅ 一句话理解反射:
“程序可以在运行时查看、调用自身或外部类的属性、方法、构造器等,而无需在编译时硬编码。”
一、反射能做什么?
通过反射,你可以:
- 获取类的 Class 对象
- 查看类的字段(Field)、方法(Method)、构造器(Constructor)
- 创建对象实例(即使构造器是 private)
- 调用任意方法(包括 private 方法)
- 读写任意字段(包括 private 字段)
⚠️ 注意:反射会绕过访问控制检查(如 private),但需先调用 setAccessible(true)。二、核心 API(位于 java.lang.reflect 包)
| 类 | 作用 |
|---|---|
Class<?> | 代表一个类的元数据(类型信息) |
Field | 代表类的成员变量 |
Method | 代表类的方法 |
Constructor | 代表类的构造器 |
三、基本使用示例
1. 获取 Class 对象(三种方式)
// 方式1:通过类名.classClass<Person> clazz1 =Person.class;// 方式2:通过对象.getClass()Person p =newPerson();Class<?extendsPerson> clazz2 = p.getClass();// 方式3:通过全限定类名(常用于配置文件)Class<?> clazz3 =Class.forName("com.example.Person");2. 创建对象(即使构造器私有)
// 调用无参构造器Person p1 =(Person) clazz.getDeclaredConstructor().newInstance();// 调用带参构造器Constructor<Person> ctor = clazz.getConstructor(String.class,int.class);Person p2 = ctor.newInstance("张三",25);3. 访问私有字段
Field nameField = clazz.getDeclaredField("name"); nameField.setAccessible(true);// 突破 private 限制 nameField.set(p,"李四");// 设置值String name =(String) nameField.get(p);// 获取值4. 调用私有方法
Method sayHello = clazz.getDeclaredMethod("sayHello"); sayHello.setAccessible(true); sayHello.invoke(p);// 调用 p.sayHello()四、完整示例
importjava.lang.reflect.*;classPerson{privateString name;privateint age;privatePerson(){}publicPerson(String name,int age){this.name = name;this.age = age;}privatevoidsayHello(){System.out.println("Hello, I'm "+ name);}}publicclassReflectionDemo{publicstaticvoidmain(String[] args)throwsException{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对象以提升性能。
七、最佳实践
- 优先使用
getDeclaredXXX()而不是getXXX()getDeclaredMethods():获取本类所有方法(含 private)getMethods():只获取 public 方法(包括继承的)
- 记得调用
setAccessible(true)
否则访问 private 成员会抛IllegalAccessException - 异常处理要全面
反射方法可能抛出:ClassNotFoundExceptionNoSuchMethodExceptionIllegalAccessExceptionInvocationTargetException(目标方法内部抛异常)
- 考虑使用
MethodHandle或VarHandle(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. 通用工具类开发
- Bean 拷贝工具(如 Apache BeanUtils、Spring BeanUtils)
自动复制两个对象的同名属性 → 反射读取 getter/setter。 - 日志打印工具
动态打印对象所有字段值(用于调试)→ 反射遍历字段。
JSON 序列化/反序列化(如 Gson、Jackson)
// 把 {"name":"张三"} 转成 User 对象User user = gson.fromJson(json,User.class);→ 内部通过反射遍历 User 的字段,自动赋值。
3. 动态代理(Dynamic Proxy)
Java 的 java.lang.reflect.Proxy 基于反射实现:
// Spring AOP、RPC 客户端常用MyService proxy =(MyService)Proxy.newProxyInstance( loader,newClass[]{MyService.class},(proxy, method, args)->{// 在方法调用前后加逻辑(如事务、日志)return method.invoke(target, args);// ← 这里用反射调用原方法});4. 插件系统 / 热加载
程序在运行时加载外部 JAR 包中的类并执行:
// 从配置文件读取类名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),但有些需在运行时读取:
// 自定义权限校验@RequiresRole("admin")publicvoiddeleteUser(){...}// 拦截器中Method method =...;if(method.isAnnotationPresent(RequiresRole.class)){String role = method.getAnnotation(RequiresRole.class).value();// 用反射检查当前用户角色}❌ 二、不应该使用反射的场景
| 场景 | 正确做法 |
|---|---|
| 调用自己项目中已知类的方法 | 直接调用,不要反射 |
| 访问 public 字段或方法 | 直接访问 |
| 简单的对象创建 | 用 new 或工厂模式 |
| 性能敏感的循环内部 | 反射太慢! |
🚫 反例(错误用法):
⚠️ 三、使用反射前的自问清单
在写反射代码前,请先问自己:
- 是否在开发框架或通用工具?
→ 如果是业务代码,大概率不需要。 - 是否有更简单的替代方案?(如接口、策略模式、Lambda)
→ 优先用类型安全的方式。 - 是否愿意承担性能损失?
→ 反射比直接调用慢 10~100 倍(虽可通过缓存缓解)。 - 是否破坏了封装性?是否有安全风险?
→ 修改 private 字段可能破坏对象状态。
✅ 四、如果必须用反射:最佳实践
- 缓存反射对象(
Method/Field)避免重复查找 - 使用
setAccessible(true)时要小心 - 捕获并处理所有反射异常
- 考虑使用
MethodHandle(Java 7+)提升性能 - 添加详细注释说明为什么必须用反射
📌 总结:什么时候用反射?
| 场景 | 是否推荐 |
|---|---|
| 写 Spring/Hibernate 这类框架 | ✅ 必须用 |
| 开发 JSON 解析、Bean 拷贝等通用工具 | ✅ 合理使用 |
| 实现插件系统、动态加载模块 | ✅ 适合 |
| 普通业务逻辑(如 Controller、Service) | ❌ 不要用 |
| 能用接口/多态解决的问题 | ❌ 不要用 |
💡 黄金法则:
“如果你不是在写框架或工具库,那你很可能不需要反射。”
反射是 Java 的“超能力”,但超能力要用在刀刃上,而不是切菜。
下面是一个使用 Java 反射 + 自定义注解 实现的 数据库切换(多数据源)功能 的完整示例。该功能允许你通过在方法上添加注解(如 @UseDatabase("mysql"))来动态切换数据源。
💡 虽然实际项目中通常用 Spring AOP + 注解实现,但这里我们纯手写反射逻辑来演示核心思想。
✅ 功能目标
- 定义一个注解
@UseDatabase - 在 Service 方法上标注要使用的数据库名
- 通过一个“代理”或“调度器”调用方法前,读取注解 → 切换数据源 → 执行方法
用反射和注解写一个java切换库的功能
第一步:定义自定义注解
importjava.lang.annotation.*;// 可用在方法上,运行时保留@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceUseDatabase{Stringvalue();// 数据库标识,如 "mysql", "oracle"}第二步:模拟数据源上下文(ThreadLocal)
publicclassDataSourceContextHolder{privatestaticfinalThreadLocal<String> contextHolder =newThreadLocal<>();publicstaticvoidsetDataSource(String dbType){ contextHolder.set(dbType);}publicstaticStringgetDataSource(){return contextHolder.get();}publicstaticvoidclear(){ contextHolder.remove();}}第三步:编写业务 Service(带注解)
publicclassUserService{@UseDatabase("mysql")publicvoidsaveUser(String name){String currentDb =DataSourceContextHolder.getDataSource();System.out.println("【"+ currentDb +"】正在保存用户: "+ name);}@UseDatabase("oracle")publicvoiddeleteUser(Long id){String currentDb =DataSourceContextHolder.getDataSource();System.out.println("【"+ currentDb +"】正在删除用户 ID: "+ id);}// 无注解方法,默认数据源publicvoidlistUsers(){String currentDb =DataSourceContextHolder.getDataSource();System.out.println("【"+ currentDb +"】列出所有用户");}}第四步:核心调度器(使用反射读取注解并切换数据源)
importjava.lang.reflect.Method;publicclassDatabaseSwitcher{/** * 通过反射调用带 @UseDatabase 注解的方法,并自动切换数据源 */publicstaticvoidinvokeMethodWithDbSwitch(Object target,String methodName,Object... args)throwsException{Class<?> clazz = target.getClass();Class<?>[] argTypes =newClass[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();}}}第五步:测试
publicclassMain{publicstaticvoidmain(String[] args)throwsException{UserService userService =newUserService();// 通过我们的调度器调用,自动切换 DBDatabaseSwitcher.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() | 反射调用目标方法 |
🚀 进阶建议(实际项目中)
- 支持数据源注册表
将"mysql"映射到真实的DataSourceBean。 - 默认值处理
注解可设默认值:String value() default "master";
结合 Spring AOP
用 @Around 切面代替手动 invokeMethodWithDbSwitch,更透明:
@Around("@annotation(useDb)")publicObjectswitchDataSource(ProceedingJoinPoint joinPoint,UseDatabase useDb){DataSourceContextHolder.setDataSource(useDb.value());try{return joinPoint.proceed();}finally{DataSourceContextHolder.clear();}}✅ 总结
通过 注解 + 反射,我们实现了:
- 声明式数据源切换
- 运行时动态解析配置
- 低侵入性(业务代码只需加注解)
💡 这正是 Spring、MyBatis 等框架的核心思想:“约定优于配置,注解驱动行为”。
这个例子虽小,但体现了 Java 元编程的强大能力!
简单实现mybatis @select 的功能
下面是一个简化版 MyBatis @Select 功能的实现,使用 Java 注解 + 反射 + 动态代理 来模拟 MyBatis 的核心思想:
✅ 目标:
通过在接口方法上写@Select("SELECT name FROM user WHERE id = #{id}"),
调用该方法时自动执行 SQL 并返回结果(模拟数据库查询)。
第一步:定义 @Select 注解
importjava.lang.annotation.*;@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceSelect{Stringvalue();// SQL 语句,如 "SELECT name FROM user WHERE id = #{id}"}第二步:模拟数据库查询(简化版)
importjava.util.HashMap;importjava.util.Map;// 模拟一个内存数据库publicclassMockDatabase{privatestaticfinalMap<Integer,String> USER_DB =newHashMap<>();static{ USER_DB.put(1,"张三"); USER_DB.put(2,"李四"); USER_DB.put(3,"王五");}// 模拟执行 SQL(只支持简单占位符 #{id})publicstaticStringquery(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=")+1).trim();Integer id =Integer.parseInt(idStr);return USER_DB.get(id);}returnnull;}}第三步:创建 Mapper 接口(用户编写)
publicinterfaceUserMapper{@Select("SELECT name FROM user WHERE id = #{id}")StringfindNameById(Integer id);}第四步:核心 —— 动态代理 + 反射解析注解
importjava.lang.reflect.*;importjava.util.Arrays;publicclassMapperProxyimplementsInvocationHandler{privatefinalClass<?> mapperInterface;publicMapperProxy(Class<?> mapperInterface){this.mapperInterface = mapperInterface;}@OverridepublicObjectinvoke(Object proxy,Method method,Object[] args)throwsThrowable{// 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;}// 如果没有注解,抛异常或走其他逻辑thrownewUnsupportedOperationException("Only @Select methods are supported");}// 工厂方法:创建代理 Mapper@SuppressWarnings("unchecked")publicstatic<T>TcreateMapper(Class<T> mapperInterface){return(T)Proxy.newProxyInstance( mapperInterface.getClassLoader(),newClass[]{mapperInterface},newMapperProxy(mapperInterface));}}第五步:测试
publicclassMain{publicstaticvoidmain(String[] args){// 创建代理对象(类似 MyBatis 的 sqlSession.getMapper(UserMapper.class))UserMapper userMapper =MapperProxy.createMapper(UserMapper.class);// 调用方法,自动执行 SQLString 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 框架的第一步!