从0开始学习Java+AI知识点总结-14.反射、注解、动态代理
反射指程序在运行时,通过加载类的字节码文件,动态获取类的成员变量、构造器、方法等成分,并对其进行操作的能力。它打破了类的封装性,让程序能灵活处理未知类型。
一、单元测试:保障代码正确性的利器
在日常开发中,我们经常需要验证方法的正确性,传统的测试方式存在诸多局限,而单元测试框架能有效解决这些问题。
1. 传统测试方式的痛点
- 耦合性高:通常在main方法中编写测试代码,测试逻辑与业务逻辑混杂。
- 无法自动化:需要手动调用方法,一个方法测试失败可能阻塞后续测试。
- 无明确报告:需手动观察输出结果判断是否成功,效率低下。
// 传统测试示例(存在明显缺陷) public class Test { public static void main(String[] args) { // 测试方法调用混杂在main中 testAdd(); testDelete(); } private static void testAdd() { /* 测试逻辑 */ } private static void testDelete() { /* 测试逻辑 */ } } |
2. JUnit 单元测试框架
JUnit 是 Java 领域最流行的单元测试框架,通过它可以轻松实现高效、自动化的测试。
核心优势
- 灵活测试:可单独测试某个方法,也能一键执行所有测试方法,且测试间相互独立(一个失败不影响其他)。
- 自动报告:运行后通过颜色直观反馈结果(绿色成功,红色失败),无需手动分析。
- 集成便捷:主流 IDE(如 IDEA)已内置,无需手动导入 JAR 包。
使用步骤(以 JUnit 4 和 JUnit 5 为例)
- 定义测试类:通常以Test结尾(如UserServiceTest),与被测试类对应。
- 编写测试方法:
- JUnit 4:方法需为public void,无参数,添加@Test注解(包路径org.junit.Test)。
- JUnit 5:方法可简化为void(无需public),添加@Test注解(包路径org.junit.jupiter.api.Test)。
- 执行测试:右键测试方法 / 类选择 “Run”,查看结果。
// JUnit 4测试示例 import org.junit.Test; public class StringUtilTest { @Test public void testgetMaxIndex() { int result = StringUtil.getMaxIndex("hello"); // 断言或输出验证结果 } } // JUnit 5测试示例 import org.junit.jupiter.api.Test; public class StringUtilTest { @Test void testgetMaxIndex() { // 无需public int result = StringUtil.getMaxIndex("hello"); // 验证逻辑 } } |
注意事项
- 测试方法命名建议遵循test+被测试方法名(如testAddUser),增强可读性。
- 避免在测试方法中使用Scanner等交互逻辑,如需使用可配置 IDE 支持。
二、反射:动态操作类结构的黑科技
反射是 Java 的 “动态” 核心,允许程序在运行时加载、探测和使用未知的类,是框架开发的基石。
1. 反射的定义与核心价值
反射指程序在运行时,通过加载类的字节码文件,动态获取类的成员变量、构造器、方法等成分,并对其进行操作的能力。它打破了类的封装性,让程序能灵活处理未知类型。
2. 反射第一步:获取 Class 对象
Class 对象是反射的入口,代表类的字节码文件,获取方式有三种:
方式 |
语法 |
适用场景 |
类名.class |
Class clazz = User.class; |
已知具体类,编译期确定类型 |
Class.forName() |
Class clazz = Class.forName("com.example.User"); |
已知类的全路径名(字符串形式),动态加载 |
对象.getClass () |
User user = new User(); Class clazz = user.getClass(); |
已有对象实例,需获取其类型信息 |
// 获取Class对象示例 public class ReflectDemo { public static void main(String[] args) throws ClassNotFoundException { // 方式1:类名.class Class<User> clazz1 = User.class;
// 方式2:Class.forName(全类名) Class<?> clazz2 = Class.forName("com.example.User");
// 方式3:对象.getClass() User user = new User(); Class<? extends User> clazz3 = user.getClass(); } } |
3. 反射操作类成分
通过 Class 对象可获取类的构造器、成员变量和方法,并执行相应操作。
(1)操作构造器
通过Constructor对象创建类的实例,支持访问私有构造器(暴力反射)。
Class 方法 |
说明 |
getConstructors() |
获取所有 public 构造器 |
getDeclaredConstructors() |
获取所有构造器(包括私有) |
getConstructor(Class<?>... parameterTypes) |
获取指定参数的 public 构造器 |
getDeclaredConstructor(Class<?>... parameterTypes) |
获取指定参数的任意构造器 |
Constructor 核心方法:
- newInstance(Object... initargs):调用构造器创建对象。
- setAccessible(true):关闭访问检查,允许操作私有构造器。
// 反射操作构造器示例 public class ConstructorDemo { public static void main(String[] args) throws Exception { Class<User> clazz = User.class;
// 获取无参public构造器并创建对象 Constructor<User> constructor1 = clazz.getConstructor(); User user1 = constructor1.newInstance();
// 获取私有构造器(参数为String)并创建对象 Constructor<User> constructor2 = clazz.getDeclaredConstructor(String.class); constructor2.setAccessible(true); // 暴力反射 User user2 = constructor2.newInstance("张三"); } } |
(2)操作成员变量
通过Field对象对成员变量进行赋值和取值,支持私有变量。
Class 方法 |
说明 |
getFields() |
获取所有 public 成员变量 |
getDeclaredFields() |
获取所有成员变量(包括私有) |
getField(String name) |
获取指定名称的 public 成员变量 |
getDeclaredField(String name) |
获取指定名称的任意成员变量 |
Field 核心方法:
- set(Object obj, Object value):为对象的变量赋值。
- get(Object obj):获取对象的变量值。
- setAccessible(true):允许操作私有变量。
// 反射操作成员变量示例 public class FieldDemo { public static void main(String[] args) throws Exception { User user = new User(); Class<User> clazz = User.class;
// 操作public变量 Field nameField = clazz.getField("name"); nameField.set(user, "张三"); // 赋值 System.out.println(nameField.get(user)); // 取值
// 操作私有变量 Field ageField = clazz.getDeclaredField("age"); ageField.setAccessible(true); // 暴力反射 ageField.set(user, 20); System.out.println(ageField.get(user)); } } |
(3)操作成员方法
通过Method对象调用类的方法,支持私有方法。
Class 方法 |
说明 |
getMethods() |
获取所有 public 方法(包括父类) |
getDeclaredMethods() |
获取所有方法(包括私有,仅当前类) |
getMethod(String name, Class<?>... parameterTypes) |
获取指定 public 方法 |
getDeclaredMethod(String name, Class<?>... parameterTypes) |
获取指定任意方法 |
Method 核心方法:
- invoke(Object obj, Object... args):调用对象的方法,返回方法执行结果。
- setAccessible(true):允许调用私有方法。
// 反射操作方法示例 public class MethodDemo { public static void main(String[] args) throws Exception { User user = new User(); Class<User> clazz = User.class;
// 调用public方法 Method sayHelloMethod = clazz.getMethod("sayHello", String.class); String result = (String) sayHelloMethod.invoke(user, "张三"); System.out.println(result);
// 调用私有方法 Method privateMethod = clazz.getDeclaredMethod("privateMethod"); privateMethod.setAccessible(true); // 暴力反射 privateMethod.invoke(user); } } |
4. 反射的应用场景与优势
- 框架开发:Spring、MyBatis 等框架通过反射实现对象创建、属性注入等功能。
- 动态处理未知类:如 JSON 解析工具(Jackson、Gson)通过反射将 JSON 字符串转换为对象。
- 突破封装与泛型约束:可访问私有成员,或向泛型集合中添加任意类型元素(绕过编译期检查)。
实用案例:通用对象数据保存框架
通过反射将任意对象的字段名和值写入文件,无需为每个类单独编写代码。
public class ObjectSaver { public static void save(Object obj) throws Exception { try (PrintWriter out = new PrintWriter("data.txt")) { Class<?> clazz = obj.getClass(); out.println("=====" + clazz.getSimpleName() + "=====");
Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); // 暴力反射 String fieldName = field.getName(); Object value = field.get(obj); out.println(fieldName + "=" + value); } } } } |
三、注解:代码中的 “标签” 与元数据
注解是 Java 代码中的特殊标记,用于为类、方法、变量等添加元数据,让程序根据注解信息动态处理逻辑。
1. 注解的定义与作用
注解本质是一种接口(继承java.lang.annotation.Annotation),通过@注解名的形式标记在代码中,用于:
- 标记特殊代码(如@Override标记重写方法)。
- 传递配置信息(如 Spring 的@Component标记组件)。
- 简化代码(如 Lombok 的@Data自动生成 getter/setter)。
2. 自定义注解
通过@interface关键字定义注解,可包含属性(类似接口方法),属性可指定默认值。
语法格式
public @interface 注解名 { // 属性定义(类型 名称() [default 默认值];) 类型 属性名() default 默认值; } |
特殊规则
- 属性名为value时,使用注解可省略属性名(仅当只有一个属性时)。
- 属性类型只能是基本类型、String、Class、枚举、注解或其数组。
// 自定义注解示例 public @interface MyAnnotation { String name(); // 无默认值,使用时必须赋值 int age() default 18; // 有默认值,可省略 String[] hobbies(); // 数组类型 String value(); // 特殊属性value } // 使用注解 @MyAnnotation(name = "张三", hobbies = {"编程", "运动"}, value = "测试") public class User { /* ... */ } |
3. 元注解:注解的 “注解”
元注解用于约束自定义注解的使用范围和生命周期,常用元注解有:
(1)@Target:指定注解的使用位置
通过ElementType枚举指定,常见取值:
- TYPE:类、接口、枚举。
- METHOD:方法。
- FIELD:成员变量。
- PARAMETER:方法参数。
- CONSTRUCTOR:构造器。
@Target({ElementType.TYPE, ElementType.METHOD}) // 可用于类和方法 public @interface MyAnnotation { /* ... */ } |
(2)@Retention:指定注解的保留周期
通过RetentionPolicy枚举指定,决定注解在哪个阶段有效:
- SOURCE:仅在源码中存在,编译后消失(如@Override)。
- CLASS:保留到字节码文件,运行时消失(默认值)。
- RUNTIME:保留到运行时,可通过反射获取(开发常用)。
@Retention(RetentionPolicy.RUNTIME) // 运行时可反射获取 public @interface MyAnnotation { /* ... */ } |
4. 注解解析
通过反射获取注解信息并处理,核心是AnnotatedElement接口(Class、Method、Field 等实现该接口)。
核心方法
方法 |
说明 |
isAnnotationPresent(Class<? extends Annotation> annotationClass) |
判断是否存在指定注解 |
getDeclaredAnnotation(Class<T> annotationClass) |
获取指定注解对象 |
getDeclaredAnnotations() |
获取所有注解 |
示例:模拟 JUnit 的@Test注解功能
定义@MyTest注解,通过反射执行标记了该注解的方法。
// 定义注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyTest { } // 测试类 public class TestDemo { @MyTest public void test1() { System.out.println("test1执行"); }
public void test2() { System.out.println("test2不执行"); } } // 注解解析与执行 public class TestRunner { public static void main(String[] args) throws Exception { Class<TestDemo> clazz = TestDemo.class; TestDemo obj = clazz.getConstructor().newInstance();
Method[] methods = clazz.getDeclaredMethods(); for (Method method : methods) { if (method.isAnnotationPresent(MyTest.class)) { method.invoke(obj); // 执行标记了@MyTest的方法 } } } } |
四、动态代理:不修改源码增强功能的设计模式
动态代理允许在不修改目标对象代码的前提下,为其方法添加增强逻辑(如日志、权限校验、性能统计),是 AOP(面向切面编程)的核心实现方式。
1. 为什么需要代理?
- 功能增强:在目标方法执行前后添加额外逻辑(如统计耗时、日志记录)。
- 解耦:增强逻辑与业务逻辑分离,避免代码侵入。
- 灵活性:动态生成代理对象,无需手动编写代理类。
2. 动态代理的实现:JDK 动态代理
Java 通过java.lang.reflect.Proxy类和InvocationHandler接口实现动态代理,要求目标对象必须实现接口。
核心 API
- Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h):生成代理对象。
- loader:目标对象的类加载器。
- interfaces:目标对象实现的接口数组(代理对象需实现相同接口)。
- h:增强逻辑处理器,通过invoke方法定义增强行为。
- InvocationHandler.invoke(Object proxy, Method method, Object[] args):代理对象调用方法时触发,定义增强逻辑。
- proxy:代理对象本身。
- method:当前调用的目标方法。
- args:方法参数。
3. 动态代理实战:统计方法执行耗时
为用户服务类的所有方法添加耗时统计功能,不修改原有业务代码。
// 目标接口 public interface UserService { void add(); void delete(); } // 目标实现类 public class UserServiceImpl implements UserService { @Override public void add() { System.out.println("执行添加用户"); try { Thread.sleep(100); } catch (InterruptedException e) { } }
@Override public void delete() { System.out.println("执行删除用户"); try { Thread.sleep(150); } catch (InterruptedException e) { } } } // 动态代理工厂 public class TimeProxyFactory { public static <T> T createProxy(T target) { // 生成代理对象 return (T) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) -> { // 增强逻辑:方法执行前记录开始时间 long start = System.currentTimeMillis(); // 执行目标方法 Object result = method.invoke(target, args); // 增强逻辑:方法执行后计算耗时 long end = System.currentTimeMillis(); System.out.println(method.getName() + "耗时:" + (end - start) + "ms"); return result; } ); } } // 使用代理 public class Main { public static void main(String[] args) { UserService target = new UserServiceImpl(); UserService proxy = TimeProxyFactory.createProxy(target); proxy.add(); // 执行添加用户 + 耗时统计 proxy.delete(); // 执行删除用户 + 耗时统计 } } |
4. 动态代理的优势与应用
- 无侵入增强:不修改目标对象代码,符合开闭原则。
- 通用性:一套增强逻辑可复用在多个目标对象。
- 框架应用:Spring AOP 的核心实现方式,用于事务管理、日志记录等场景。
总结:高级技术在开发中的价值
本文梳理的单元测试、反射、注解和动态代理,是 Java 开发中的 “进阶四件套”:
- 单元测试是代码质量的保障,让你快速定位问题。
- 反射赋予程序动态能力,是框架开发的灵魂。
- 注解简化配置与标记,让代码更简洁高效。
- 动态代理实现无侵入增强,是解耦和扩展的利器。
如果觉得本文对你有帮助,欢迎收藏、点赞,关注后续更多 Java 技术干货分享!
更多推荐
所有评论(0)