# 第04天:Java反射基础 ## 📚 今日目标 1. 理解Java反射的概念 2. 学会通过反射获取类、方法、字段信息 3. 学会通过反射调用方法和获取字段值 4. 实现PropertyUtil工具类 --- ## 🎯 知识点1:什么是反射? ### 生活中的例子 **照镜子**: - 看到自己的样子(类的信息) - 看到自己穿什么衣服(字段) - 看到自己能做什么动作(方法) **Java反射**: - 在运行时获取类的信息 - 获取类的字段、方法 - 动态调用方法和访问字段 ### 为什么需要反射? **场景**:我们需要从方法参数中获取路由键的值 ```java @DBRouter(key = "userId") public void queryUser(User user) { // 我们需要获取 user.getUserId() 的值 // 但不知道参数是什么类型,也不知道有哪些字段 // 怎么办?用反射! } ``` --- ## 🎯 知识点2:反射的核心类 ### Class类:类的元信息 ```java // 获取Class对象的三种方式 Class clazz1 = User.class; // 方式1:类字面量 Class clazz2 = user.getClass(); // 方式2:对象.getClass() Class clazz3 = Class.forName("com.example.User"); // 方式3:类名 // 获取类名 String className = clazz.getName(); // 完整类名:com.example.User String simpleName = clazz.getSimpleName(); // 简单类名:User ``` ### Method类:方法信息 ```java // 获取所有公共方法 Method[] methods = clazz.getMethods(); // 获取指定方法(包括私有方法) Method method = clazz.getDeclaredMethod("getUserId"); // 获取方法名 String methodName = method.getName(); // 调用方法 Object result = method.invoke(user); // 相当于 user.getUserId() ``` ### Field类:字段信息 ```java // 获取所有字段(包括私有) Field[] fields = clazz.getDeclaredFields(); // 获取指定字段 Field field = clazz.getDeclaredField("userId"); // 设置可访问(访问私有字段需要) field.setAccessible(true); // 获取字段值 Object value = field.get(user); // 相当于 user.userId // 设置字段值 field.set(user, 12345L); // 相当于 user.userId = 12345L ``` --- ## 🛠️ 实践任务1:通过反射获取属性值 ### 步骤1:创建PropertyUtil工具类 在 `src/main/java/cn/bugstack/middleware/db/router/util/` 目录下创建 `PropertyUtil.java`: ```java package cn.bugstack.middleware.db.router.util; import org.apache.commons.beanutils.PropertyUtils; import org.springframework.core.env.Environment; /** * 属性工具类 * 用于通过反射获取对象属性值 * * @author 小傅哥 */ public class PropertyUtil { private static int springBootVersion = 1; static { try { // 检测Spring Boot版本 Class.forName("org.springframework.boot.bind.RelaxedPropertyResolver"); } catch (ClassNotFoundException e) { springBootVersion = 2; } } /** * 处理Spring环境属性 * * @param environment Spring环境 * @param prefix 属性前缀 * @param targetClass 目标类型 * @param 泛型 * @return 属性对象 */ public static T handle(Environment environment, String prefix, Class targetClass) { try { if (springBootVersion == 1) { return (T) v1(environment, prefix); } else { return (T) v2(environment, prefix, targetClass); } } catch (Exception e) { throw new RuntimeException("获取属性失败: " + prefix, e); } } /** * Spring Boot 1.x 版本 */ private static Object v1(Environment environment, String prefix) { // Spring Boot 1.x 使用 RelaxedPropertyResolver // 这里简化处理,实际项目中可能需要兼容 throw new UnsupportedOperationException("Spring Boot 1.x 暂不支持"); } /** * Spring Boot 2.x 版本 */ private static Object v2(Environment environment, String prefix, Class targetClass) { // Spring Boot 2.x 使用 Binder try { return org.springframework.boot.context.properties.bind.Binder .get(environment) .bind(prefix, targetClass) .orElse(null); } catch (Exception e) { throw new RuntimeException("绑定属性失败: " + prefix, e); } } /** * 通过反射获取对象属性值 * * @param obj 对象 * @param propertyName 属性名 * @return 属性值 */ public static Object getProperty(Object obj, String propertyName) { try { return PropertyUtils.getProperty(obj, propertyName); } catch (Exception e) { throw new RuntimeException("获取属性值失败: " + propertyName, e); } } /** * 通过反射设置对象属性值 * * @param obj 对象 * @param propertyName 属性名 * @param value 属性值 */ public static void setProperty(Object obj, String propertyName, Object value) { try { PropertyUtils.setProperty(obj, propertyName, value); } catch (Exception e) { throw new RuntimeException("设置属性值失败: " + propertyName, e); } } } ``` ### 代码解释 1. **PropertyUtils.getProperty()**:使用Apache Commons BeanUtils获取属性值 - 支持嵌套属性:`user.address.city` - 支持索引属性:`list[0]` - 自动处理getter方法 2. **Spring Boot版本检测**:不同版本的API不同,需要兼容处理 3. **异常处理**:统一抛出RuntimeException,便于上层处理 --- ## 🛠️ 实践任务2:手动实现属性获取(理解原理) ### 步骤1:创建简化版PropertyUtil 为了理解原理,我们手动实现一个简化版: ```java package cn.bugstack.middleware.db.router.util; import java.lang.reflect.Field; import java.lang.reflect.Method; /** * 属性工具类(手动实现版,用于理解原理) */ public class PropertyUtilManual { /** * 手动获取属性值(理解原理用) * * @param obj 对象 * @param propertyName 属性名 * @return 属性值 */ public static Object getPropertyManual(Object obj, String propertyName) { if (obj == null || propertyName == null) { return null; } Class clazz = obj.getClass(); // 方式1:通过getter方法获取 try { String getterName = "get" + capitalize(propertyName); Method getter = clazz.getMethod(getterName); return getter.invoke(obj); } catch (Exception e) { // getter方法不存在,尝试直接访问字段 } // 方式2:直接访问字段 try { Field field = clazz.getDeclaredField(propertyName); field.setAccessible(true); // 允许访问私有字段 return field.get(obj); } catch (Exception e) { throw new RuntimeException("无法获取属性: " + propertyName, e); } } /** * 首字母大写 */ private static String capitalize(String str) { if (str == null || str.length() == 0) { return str; } return str.substring(0, 1).toUpperCase() + str.substring(1); } } ``` ### 使用示例 ```java public class User { private Long userId; private String name; public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } } // 使用 User user = new User(); user.setUserId(12345L); // 方式1:使用BeanUtils(推荐) Object value1 = PropertyUtil.getProperty(user, "userId"); // 12345L // 方式2:手动实现(理解原理) Object value2 = PropertyUtilManual.getPropertyManual(user, "userId"); // 12345L ``` --- ## 🎓 知识点拓展 ### 拓展1:反射的性能问题 **问题**:反射比直接调用慢 ```java // 直接调用(快) user.getUserId(); // 反射调用(慢,约慢10-100倍) Method method = User.class.getMethod("getUserId"); method.invoke(user); ``` **优化方案**: 1. **缓存Method对象**:不要每次都获取 2. **使用MethodHandle**:Java 7+,性能更好 3. **代码生成**:编译时生成代码 **例子**: ```java // 缓存Method对象 private static final Method GET_USER_ID_METHOD; static { try { GET_USER_ID_METHOD = User.class.getMethod("getUserId"); } catch (Exception e) { throw new RuntimeException(e); } } // 使用时直接调用缓存的Method Object value = GET_USER_ID_METHOD.invoke(user); ``` ### 拓展2:BeanUtils vs 手动反射 **Apache Commons BeanUtils**: - ✅ 功能强大(支持嵌套属性、索引属性) - ✅ 代码简洁 - ❌ 性能稍慢 - ❌ 依赖外部库 **手动反射**: - ✅ 性能稍好 - ✅ 无外部依赖 - ❌ 功能简单 - ❌ 代码复杂 **选择建议**: - 性能要求高:手动反射 + 缓存 - 功能要求高:BeanUtils - 我们项目:使用BeanUtils(功能优先) ### 拓展3:获取方法参数名 **问题**:如何获取方法参数的真实名称? ```java public void queryUser(Long userId, String name) { // 如何知道第一个参数叫userId? } ``` **Java 8之前**: - 参数名会丢失(编译后变成arg0, arg1) - 需要编译时加 `-parameters` 参数 **Java 8+**: ```java // 编译时加参数:javac -parameters Method method = UserService.class.getMethod("queryUser", Long.class, String.class); Parameter[] parameters = method.getParameters(); String paramName = parameters[0].getName(); // userId ``` **我们的项目**: - 不依赖参数名 - 通过遍历参数对象,查找包含指定属性的对象 - 更灵活,不依赖编译参数 --- ## ✅ 今日检查清单 - [ ] 理解了反射的概念和作用 - [ ] 学会了通过反射获取类、方法、字段信息 - [ ] 学会了通过反射调用方法和获取字段值 - [ ] 实现了PropertyUtil工具类 - [ ] 理解了BeanUtils的工作原理 - [ ] 完成了拓展阅读 --- ## 🎯 明日预告 明天我们将学习: - Spring AOP基础 - 切面、切点、通知的概念 - 如何创建AOP切面 --- ## 💡 思考题 1. 为什么反射比直接调用慢? 2. 如何优化反射的性能? 3. 什么时候必须用反射,什么时候可以用其他方式? --- ## 📚 参考资源 - [Java反射官方文档](https://docs.oracle.com/javase/tutorial/reflect/) - [反射性能优化](https://www.baeldung.com/java-reflection-performance) - [Apache Commons BeanUtils](https://commons.apache.org/proper/commons-beanutils/)