Java 反射
Java 反射(Reflection)是 Java 语言的一个强大特性,它允许在运行时动态地获取类的信息并操作对象,即使在编译时并不知道这些类的具体结构。
✅ 一句话理解反射:
"程序可以在运行时查看、调用自身或外部类的属性、方法、构造器等,而无需在编译时硬编码。"
一、反射能做什么?
通过反射,你可以:
- 获取类的 Class 对象
- 查看类的字段(Field)、方法(Method)、构造器(Constructor)
- 创建对象实例(即使构造器是 private)
- 调用任意方法(包括 private 方法)
- 读写任意字段(包括 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对象以提升性能。
七、最佳实践
-
优先使用
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. 通用工具类开发
-
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);
⚠️ 三、使用反射前的自问清单
在写反射代码前,请先问自己:
-
是否在开发框架或通用工具?
→ 如果是业务代码,大概率不需要。
-
是否有更简单的替代方案? (如接口、策略模式、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切换库的功能
第一步:定义自定义注解
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() |
反射调用目标方法 |
🚀 进阶建议(实际项目中)
-
结合 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(); } } -
支持数据源注册表
将
"mysql"映射到真实的DataSourceBean。 -
默认值处理
注解可设默认值:
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 框架的第一步!