Java基础加强:反射、注解、动态代理与单元测试实战解析
发布时间:2026/9/17 5:10:11来源:尧图网络
如果你也和我一样学 Java 学到集合、IO、多线程之后突然卡在“基础加强”这四个字上那这章内容就是专门给你准备的。Chapter11 的学习记录整章其实就围绕四件事展开单元测试、反射、注解、代理。这四样东西单独拎出来哪一个面试都会被问到哪一个在实际项目里都躲不掉。尤其当你开始用 Spring、MyBatis 这类框架时会发现它们背后全是这几样基础功在撑着。这章的内容量比前面任何一章都大但它解决的核心问题其实很集中怎么让代码质量有保障怎么在运行时动态操作类怎么给代码打标记怎么在不改源码的情况下增强功能。“从入门到入土”虽然是句玩笑话但这里确实是很多初学者从“会写代码”到“理解框架”的分水岭。我尽量按自己学习时的真实顺序和踩坑过程来写代码能跑通的尽量放完整版方便你对着敲。1. 先搞清楚这章四个主角是干嘛的1.1 它们各自解决了什么问题单元测试解决的是“我怎么知道这段代码是对的”。以前写完一个方法要么写个 main 方法手动调用要么丢给测试同学。但项目一大手动验证根本不现实。单元测试就是把“验证代码是否正确”这件事自动化让机器批量跑跑了之后告诉你哪些方法挂了哪些是安全的。JUnit 是 Java 世界里最主流的单元测试框架这章学的就是它。反射解决的是“程序运行过程中我能不能拿到类的方法、字段、构造器并且调用它们”。这个需求在写业务代码时不太明显但写框架、写工具类时是核心能力。比如 Spring 容器要创建对象它不可能在代码里写死 new UserService()它是通过配置文件或者注解拿到类名再用反射去实例化。你写代码的时候类是确定好的框架写代码的时候类还是字符串这就是区别。注解解决的是“我能不能给代码加一些额外的标记让框架或者工具去读”。注解本身不干活它更像贴在代码上的便利贴写着“这个方法是覆盖父类的”“这个字段将来要映射到数据库列”“这个方法执行前需要做权限校验”。真正干活的是读注解的那些代码而读注解最常用的手段恰恰就是反射。代理解决的是“我能不能在不改动原有类代码的情况下给它的方法增加额外逻辑”。比如一个方法原来只是保存数据现在想加日志、加耗时统计、加事务控制又不想把原代码改得一团糟那就搞一个代理对象让调用方跟代理对象打交道代理对象再把活转交给真实对象并且在转交前后塞点自己的逻辑。1.2 这四个东西是怎么串成一条线的表面上它们是四个独立知识点实际上它们是一条线。单元测试代码里你会在测试方法上写 Test 注解JUnit 框架就是通过反射拿到这些注解然后执行对应方法。反射注解这是框架最常用的组合拳。而动态代理底层也离不开反射JDK 动态代理就是在运行时通过反射生成代理类并调用方法。你再往后学的 Spring AOP就是把代理应用到了极致配合注解做事务管理、日志记录。所以建议你按“单元测试 → 反射 → 注解 → 代理”这个顺序学不要跳。前面没打好的基础到后面反射和代理部分会反复卡壳。2. 单元测试用 JUnit 5 把代码可靠性管起来2.1 快速上手 JUnit 与第一个测试用例先建一个普通的 Maven 项目在 pom.xml 里引入 JUnit 5 的依赖dependencies dependency groupIdorg.junit.jupiter/groupId artifactIdjunit-jupiter/artifactId version5.10.2/version scopetest/scope /dependency /dependenciesJUnit 5 是当前的主流版本它由三个子项目组成JUnit Platform、JUnit Jupiter、JUnit Vintage。Jupiter 就是我们要用的编程模型和扩展模型scope 设置为 test 意味着这个依赖只在测试代码里生效不会打进生产包。然后我们拿一个非常简单的计算器类来练手public class Calculator { public int add(int a, int b) { return a b; } public int divide(int a, int b) { return a / b; } }在 src/test/java 目录下创建测试类import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class CalculatorTest { Test public void testAdd() { Calculator calculator new Calculator(); int result calculator.add(2, 3); Assertions.assertEquals(5, result); } Test public void testDivide() { Calculator calculator new Calculator(); Assertions.assertThrows(ArithmeticException.class, () - calculator.divide(1, 0)); } }写完直接鼠标右键选择 RunIdea 会调用 JUnit 的引擎跑这两个方法。一个绿色对勾代表通过一个红色代表失败。testAdd 里验证 23 是不是等于 5testDivide 里故意传一个 0然后用 assertThrows 断言它必须抛出 ArithmeticException如果没抛异常测试就是失败的。2.2 生命周期注解和断言的正确打开方式JUnit 5 里有一组非常重要的生命周期注解BeforeEach每个测试方法执行前都会执行一次适合初始化测试对象。AfterEach每个测试方法执行后执行一次适合释放资源。BeforeAll所有测试方法执行前执行一次必须是静态方法。AfterAll所有测试方法执行后执行一次必须是静态方法。比如我们测试一个文件操作的工具类每次执行前都要新建临时文件执行后要删除临时文件这些逻辑写进 BeforeEach 和 AfterEach 里比每个测试方法里重复写要干净得多import org.junit.jupiter.api.*; import java.io.File; public class FileUtilTest { private File tempFile; BeforeEach public void setUp() throws Exception { tempFile new File(test_temp.txt); tempFile.createNewFile(); System.out.println(准备临时文件); } AfterEach public void tearDown() { if (tempFile.exists()) { tempFile.delete(); } System.out.println(清理临时文件); } Test public void testFileExists() { Assertions.assertTrue(tempFile.exists()); } }JUnit 5 的断言也很丰富assertEquals 判断相等assertNotEquals 判断不相等assertTrue 判断条件为真assertFalse 判断条件为假assertNull 判断是否为空assertNotNull 判断是否非空assertArrayEquals 比较数组内容。还有 assertTimeout 用来断言方法执行时间这个对性能测试非常有用Test public void testTimeout() { Assertions.assertTimeout(Duration.ofSeconds(1), () - { Thread.sleep(500); }); }想测一个方法有没有按预期抛出异常用 assertEquals 是测不了的必须用 assertThrowsTest public void testNumberFormatException() { Assertions.assertThrows(NumberFormatException.class, () - Integer.parseInt(abc)); }这个断言方式很多人一开始不习惯总觉得“我都知道它会抛异常了还测它干嘛”。实际上在业务代码里有些异常是必须主动触发的比如参数校验、格式解析、非法状态等。写清楚“此处必须抛异常”是防止别人以后改坏逻辑。2.3 踩坑导入的 Test 包必须是 JUnit 5 的JUnit 5 的 Test 注解是 org.junit.jupiter.api.TestJUnit 4 的 Test 注解是 org.junit.Test。如果你项目里同时存在两个版本的依赖Idea 自动导入时很容易导成 JUnit 4 的那个。这两个注解看起来一模一样但底层引擎完全不同。JUnit 5 的测试方法可以不写 publicJUnit 4 必须写 public。一旦导错了包轻则测试方法没有执行重则报各种奇怪的初始化错误。我的习惯是写完测试类后第一件事就是按住 Ctrl 点击 Test 跳转确认包路径这种低级错误在面试时写代码最容易犯养成检查习惯可以省很多时间。3. 反射运行时操作类的底层钥匙3.1 Class 对象的三种获取方式反射的起点是 Class 对象。每个类在 JVM 加载时都会生成一个 Class 对象它就是这个类的“类说明书”。获取它的方式有三种// 方式一通过类名.class ClassUser clazz1 User.class; // 方式二通过对象的 getClass() 方法 User user new User(); Class? extends User clazz2 user.getClass(); // 方式三通过 Class.forName参数是类的全限定名 Class? clazz3 Class.forName(com.example.entity.User);三种方式里最常用的是第三种因为类名是字符串可以写到配置文件里运行时动态加载。Spring 的 Bean 工厂就是这么干的。前两种在编译器就能确定类型适合已知类的情况。注意第三种的写法这个字符串是包名.类名必须全限定名不能写类名简写。如果类路径写错了会抛出 ClassNotFoundException。3.2 通过反射创建对象、调用方法、操作字段拿到 Class 对象之后最基础的操作是创建实例Class? clazz Class.forName(com.example.entity.User); Object object clazz.getDeclaredConstructor().newInstance();这里用 getDeclaredConstructor().newInstance() 而不是直接调用 newInstance()是因为后者在 JDK 9 以后被标记为废弃而且如果你要调用私有构造器或者带参构造器就必须通过 getDeclaredConstructor(参数类型.class) 来拿具体的构造器Class? clazz User.class; Constructor? constructor clazz.getDeclaredConstructor(String.class, int.class); User user (User) constructor.newInstance(张三, 25);调用方法时可以用 getDeclaredMethod第一个参数是方法名后面的参数是方法形参类型的 Class 对象Method method clazz.getDeclaredMethod(setName, String.class); method.invoke(user, 李四);操作字段用 getDeclaredFieldField field clazz.getDeclaredField(name); field.setAccessible(true); field.set(user, 王五);这里重点提醒如果字段是私有的直接用 get 或 set 会抛出 IllegalAccessException。解决办法就是调用 setAccessible(true)这是访问权限的“暴力解锁”。带来的隐患是破坏了封装性实际项目中尽量只在框架代码里用业务代码少干这种事。3.3 反射的性能问题和常见使用场景反射的性能确实比直接调用慢原因在于它涉及类型检查、方法查找、访问权限检查等一系列额外开销。但绝大多数业务系统里这点开销根本感知不到现代 JVM 也做了很多优化。真正需要注意的是不要在循环里反复调用 method.invoke把 Method 对象缓存起来会好很多。private Method cachedMethod; public void doSomething() throws Exception { if (cachedMethod null) { Class? clazz User.class; cachedMethod clazz.getDeclaredMethod(getName); cachedMethod.setAccessible(true); } Object result cachedMethod.invoke(user); }反射的经典使用场景包括IDE 的代码提示、Spring 的 IOC 容器、MyBatis 的 Mapper 映射、Jackson/Gson 等 JSON 库的序列化与反序列化。写框架、写通用代码时反射是躲不开的。但如果你只是在写业务接口基本用不上这也是很多人学了反射不知道在哪用的原因——不是它没用是你的业务层次还没到。3.4 小练习手写一个获取对象的工具方法学反射最忌讳只背概念。我学习时写了一个小工具方法讲“根据字符串类名 属性名 属性值找到匹配的对象”练完对反射的掌控感就完全不一样了public static Object findObjectByField(String className, String fieldName, Object fieldValue) throws Exception { Class? clazz Class.forName(className); Object target clazz.getDeclaredConstructor().newInstance(); Field field clazz.getDeclaredField(fieldName); field.setAccessible(true); field.set(target, fieldValue); return target; }当然这只是示意真实的查询逻辑肯定不是 set 字段而是用来拼 SQL、做映射。但练这个可以让你理解原来一个字符串“com.example.entity.User”真的可以变成一个能用的对象这个认知对后面理解框架原理特别重要。4. 注解代码之外的元信息4.1 JDK 内置注解到底在告诉你什么注解本质上是一种元数据它是 Java 5 引入的用来给代码添加额外信息。我们先看 JDK 自带的那几个Override告诉编译器这个方法是重写父类的如果父类没有对应方法编译直接报错。Deprecated标记某个方法或类已过时调用处会产生警告。SuppressWarnings告诉编译器忽略指定的警告比如 unchecked、rawtypes。这几个注解里 Override 是最容易理解的它不只是装饰品而是能让编译器帮你做校验。如果你的参数列表跟父类方法不一致又写了 Override编译阶段就会报错比运行时才发现问题舒服多了。4.2 自定义注解的五步写法自定义注解的格式和接口很像区别在于关键字是 interface。下面写一个专门标记“这个方法需要花费较长时间执行”的注解import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface TimeLog { String value() default ; }这里有两个元注解必须理解清楚Target 决定这个注解能写在哪些位置。它有很多枚举值TYPE 表示类、接口上FIELD 表示字段上METHOD 表示方法上PARAMETER 表示参数上CONSTRUCTOR 表示构造器上ANNOTATION_TYPE 表示注解上PACKAGE 表示包上。如果你写多个目标位置可以写成Target({ElementType.METHOD, ElementType.TYPE})Retention 决定注解的生命周期枚举值有三个SOURCE只在源代码中存在编译后丢弃比如 Override。CLASS编译时写入 class 文件但运行时读不到这是默认值。RUNTIME保留到运行时JVM 加载类时还能读到这是写框架最常用的。如果你想让反射在运行时能拿到这个注解就必须设置为 RUNTIME。我之前犯过一个错没写 Retention结果运行时代码怎么都读不到注解查了半天才发现默认的 CLASS 级别根本不会保留到运行时。除了元注解还可以给自定义注解加成员变量类型可以是基本类型、String、Class、枚举类型、注解类型以及以这些为基础的数组。4.3 用反射读取注解实现一个迷你 JUnit注解写出来没人读就是一堆装饰品。读取注解最常用的方式是配合反射去拿方法上的注解然后做相应处理。用一个小案例来演示模拟 JUnit 的核心原理写一个 MyTest 注解然后通过反射执行带有这个注解的方法。先定义注解import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface MyTest { }写一个测试类故意让其中一个方法抛异常public class MyDemoTest { MyTest public void testSuccess() { System.out.println(执行成功的方法); } MyTest public void testFailure() { throw new RuntimeException(模拟测试失败); } public void noTestMethod() { System.out.println(这个方法没有注解不会被执行); } }再写一个简易的执行器import java.lang.reflect.Method; public class MyTestRunner { public static void main(String[] args) throws Exception { Class? clazz MyDemoTest.class; Object obj clazz.getDeclaredConstructor().newInstance(); int successCount 0; int failureCount 0; for (Method method : clazz.getDeclaredMethods()) { if (method.isAnnotationPresent(MyTest.class)) { try { method.invoke(obj); successCount; } catch (Exception e) { failureCount; System.out.println(方法 method.getName() 执行失败: e.getCause().getMessage()); } } } System.out.println(成功: successCount , 失败: failureCount); } }看到没这就是 JUnit 的简化版。JUnit 本身做的事情就是扫描类里的方法找到带 Test 注解的方法用反射去执行捕获异常并统计成败。很多初学者用了一整年 Test 都以为自己只是在“点运行按钮”搞懂这个原理之后框架的滤镜就算摘掉一层了。4.4 注解处理器APT和框架里的注解除了运行时通过反射读注解Java 还提供了编译期的注解处理器Annotation Processor Tool也就是 APT。它在编译器解析源码时介入读取注解并生成额外的源文件或代码。典型代表是 Lombok你在类上写一个 DataLombok 就在编译期帮你自动生成 getter/setter/toString 等方法的字节码你源码里看不到编译后的 class 文件里就有。对初学者来说APT 不用深入去写但至少要明白一件事业务代码里到处是 Component、Service、Autowired、Transactional这些注解不是写给人看的是给 Spring 容器在运行时通过反射代理去处理的。你理解了这个逻辑以后看 Spring 的报错就不会一头雾水了。5. 代理不动源码也能增强方法5.1 静态代理先理解代理的原始形态代理模式的核心是让代理对象持有真实对象的引用并且对外暴露同样的接口。调用方只跟代理对象打交道代理对象在调用真实方法之前或之后加入额外逻辑。先用一个最原始的静态代理来演示。假设有一个业务接口public interface UserService { void addUser(String username); }真实实现类public class UserServiceImpl implements UserService { Override public void addUser(String username) { System.out.println(添加用户: username); } }代理类public class UserServiceProxy implements UserService { private final UserService target; public UserServiceProxy(UserService target) { this.target target; } Override public void addUser(String username) { System.out.println([日志] 调用 addUser 方法参数: username); long start System.currentTimeMillis(); target.addUser(username); long end System.currentTimeMillis(); System.out.println([日志] 调用结束耗时: (end - start) ms); } }调用处UserService target new UserServiceImpl(); UserService proxy new UserServiceProxy(target); proxy.addUser(张三);运行之后你会发现UserServiceImpl 的代码一行没改但是多出了日志和耗时统计。这就是代理模式最朴素的价值。静态代理的问题也很明显如果接口有 100 个方法代理类里就得把 100 个方法全部实现一遍每个方法里重复写日志逻辑代码爆炸。5.2 JDK 动态代理反射代理的结合体静态代理的痛点在于“一个代理类只能代理一种接口”而且必须手写。JDK 动态代理解决了这个问题它允许在运行时动态生成代理类。它依赖两个核心东西java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler。InvocationHandler 是一个接口里面只有一个 invoke 方法所有代理对象的方法调用都会进到这里。我们可以把公共逻辑统一写进 invoke 里import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class LogInvocationHandler implements InvocationHandler { private final Object target; public LogInvocationHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println([动态代理] 方法 method.getName() 被执行); long start System.currentTimeMillis(); Object result method.invoke(target, args); long end System.currentTimeMillis(); System.out.println([动态代理] 方法执行耗时: (end - start) ms); return result; } }然后通过 Proxy.newProxyInstance 创建代理对象import java.lang.reflect.Proxy; public class ProxyDemo { public static void main(String[] args) { UserService target new UserServiceImpl(); ClassLoader loader target.getClass().getClassLoader(); Class?[] interfaces target.getClass().getInterfaces(); UserService proxy (UserService) Proxy.newProxyInstance(loader, interfaces, new LogInvocationHandler(target)); proxy.addUser(张三); } }运行效果[动态代理] 方法 addUser 被执行 添加用户: 张三 [动态代理] 方法执行耗时: 1 ms关键点在于 Proxy.newProxyInstance 的三个参数类加载器、目标类实现的所有接口、InvocationHandler 实例。JDK 会在运行时根据这些信息生成一个实现了指定接口的代理类所有接口方法调用都会派发到我们的 invoke 方法里。这里有个天然的限制JDK 动态代理只能代理接口。如果你的目标类没有实现任何接口这个方法就失效了。原因在于生成的代理类本身会继承 Proxy 类而 Java 是单继承的所以它只能通过实现接口来扩展能力。5.3 CGLIB 动态代理不依赖接口也能代理CGLIBCode Generation Library采用了另一种思路它生成的代理类是目标类的子类通过继承来达到代理效果。因为继承了目标类所以目标类不能被 final 修饰被代理的方法也不能是 final 的。现在主流用 Spring 的 Spring-core 里集成的 CGLIB。我们可以直接在 pom.xml 里引入dependency groupIdcglib/groupId artifactIdcglib/artifactId version3.3.0/version /dependency用 CGLIB 写动态代理核心是 MethodInterceptor 接口它有点类似 InvocationHandlerimport net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class CglibProxyDemo { public static class UserDao { public void save(String name) { System.out.println(保存用户: name); } } public static void main(String[] args) { UserDao target new UserDao(); UserDao proxy (UserDao) Enhancer.create( target.getClass(), new MethodInterceptor() { Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println([CGLIB] 准备保存); Object result proxy.invokeSuper(obj, args); System.out.println([CGLIB] 保存完成); return result; } }); proxy.save(张三); } }CGLIB 的 Enhancer.create 接收两个参数目标类、MethodInterceptor。拦截器里调用 proxy.invokeSuper 去执行真实方法注意不要调 method.invoke否则可能出现无限递归问题。JDK 动态代理和 CGLIB 的对比对比项JDK 动态代理CGLIB 动态代理代理机制基于接口基于继承是否要求接口必须实现接口不要求普通类即可是否要求类可继承无要求类不能是 final是否要求方法可覆盖无要求方法不能是 final性能创建快调用性能一般创建较慢调用性能高典型应用MyBatis Mapper、Spring 默认选择之一Spring、Hibernate 等框架Spring 的 AOP 中如果你的 Bean 实现了接口默认用 JDK 动态代理如果没有实现接口就用 CGLIB。你可以通过配置强制开启 CGLIB这也是现在 Spring Boot 2.x 之后的默认行为调整方向。5.4 写一个带注解的代理实战把四个知识点打通这是这章里我最满意的一个练习因为它同时用上了单元测试、反射、注解、代理。需求非常简单定义一个 Log 注解被这个注解标记的方法在代理执行时自动打印方法名、参数、返回值和耗时。然后写一个单元测试验证效果。第一步定义注解import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface Log { }第二步定义一个业务接口和实现类public interface OrderService { Log String createOrder(String productId, int count); String queryOrder(String orderId); } public class OrderServiceImpl implements OrderService { Override public String createOrder(String productId, int count) { System.out.println(创建订单: productId , 数量: count); return ORDER_20241101; } Override public String queryOrder(String orderId) { System.out.println(查询订单: orderId); return 订单详情; } }注意 createOrder 方法上加了 LogqueryOrder 没有加。第三步写动态代理逻辑。在 InvocationHandler 的 invoke 方法里通过 method.isAnnotationPresent(Log.class) 判断当前调用方法有没有被 Log 标记。import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class AnnotationLogProxy { public static Object createProxy(Object target) { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() { Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.isAnnotationPresent(Log.class)) { System.out.println( Log 日志开始 ); System.out.println(方法名: method.getName()); if (args ! null) { for (int i 0; i args.length; i) { System.out.println(参数 (i 1) : args[i]); } } long start System.currentTimeMillis(); Object result method.invoke(target, args); long end System.currentTimeMillis(); System.out.println(返回值: result); System.out.println(耗时: (end - start) ms); System.out.println( Log 日志结束 ); return result; } // 没有 Log 注解的方法直接执行不打印日志 return method.invoke(target, args); } }); } }第四步用 JUnit 写单元测试验证import org.junit.jupiter.api.Test; public class AnnotationLogProxyTest { Test public void testCreateOrderWithLog() { OrderService target new OrderServiceImpl(); OrderService proxy (OrderService) AnnotationLogProxy.createProxy(target); String orderId proxy.createOrder(P1001, 2); System.out.println(测试通过订单号: orderId); } Test public void testQueryOrderWithoutLog() { OrderService target new OrderServiceImpl(); OrderService proxy (OrderService) AnnotationLogProxy.createProxy(target); String detail proxy.queryOrder(O888); System.out.println(查询结果: detail); } }运行 testCreateOrderWithLog你会看到 Log 日志开始 方法名: createOrder 参数 1: P1001 参数 2: 2 创建订单: P1001, 数量: 2 返回值: ORDER_20241101 耗时: 1 ms Log 日志结束 测试通过订单号: ORDER_20241101运行 testQueryOrderWithoutLog你会看到没有日志输出直接执行原方法。这四个知识点在这个 70 行左右的小项目里全部用上了Test 注解让 JUnit 跑测试Log 注解是我们自己定义的反射负责读取方法上的注解和调用方法动态代理负责拦截方法调用并且只对带 Log 的方法增强。这就是 Spring AOP 的迷你原型Spring 官方搞得神乎其神的“面向切面”剥离到底层就是这么点东西。6. 常见报错与排查思路6.1 JUnit 相关报错Test 方法不执行检查导入的是不是 org.junit.jupiter.api.Test。另外 JUnit 5 的测试方法不能用 private 修饰虽然不用强制 public但 JUnit 4 必须 public。ReportedClass 异常通常是 JUnit 版本冲突pom 里同时引入了 JUnit 4 和 JUnit 5。解决方式是统一版本最好只保留一个。测试方法报空指针但业务代码明明不是空检查是不是忘了在 BeforeEach 里初始化对象。JUnit 会为每个测试方法新建一个测试类实例你在一个测试方法里赋值的对象不会保留到另一个测试方法这个特性很多人一开始会懵。6.2 反射相关报错ClassNotFoundExceptionClass.forName 的字符串写错或者依赖没引进来。NoSuchMethodException方法名写错或者参数类型不匹配。getDeclaredMethod(setName, String.class) 里必须精确匹配参数类型如果参数是 int实际写成了 Integer.class就会报这个错。IllegalAccessException访问私有方法或字段没有调用 setAccessible(true)。如果 JDK 模块系统受限即使调用了它也可能报 InaccessibleObjectException因为模块化后某些包不允许反射访问这时需要在启动参数里加 --add-opens。NullPointerException on method.invokeinvoke 返回 null 不奇怪你需要先确认目标方法是不是 void。如果反射调用了一个 void 方法返回结果就是 null不是出 bug。6.3 代理相关报错ClassCastExceptionJDK 动态代理生成的代理对象只能转成接口类型不能直接转成实现类类型。如果你把 UserServiceImpl 写成目标类而其中有一个独有的方法 stuSpecificMethod()用接口引用调用它编译都过不去运行时强转也失败。这种设计是刻意的它要求调用方面向接口编程。InvocationTargetExceptionmethod.invoke 包装了目标方法抛出的异常。如果你在 invoke 调用处 catch Exception打印出来的不会是业务异常本身而是 InvocationTargetException。你需要调用 e.getCause() 拿到真实原因。这个坑特别容易让新手误判。无限递归在 MethodInterceptor 的 intercept 方法里过度使用 method.invoke(obj, args) 而不用 proxy.invokeSuper可能循环调用。CGLIB 推荐用 MethodProxy 的 invokeSuper。7. 学习顺序的建议和面试准备7.1 这四个知识点怎么学效率最高如果你是跟着教程学的建议的顺序是先看单元测试每题写一个测试用例然后学反射写一个通过字符串类名创建对象的工具类再学注解自定义一个注解并用反射去读最后学代理用静态代理练手再换成 JDK 动态代理最后用 CGLIB 对比一次。当你能独立写出第 5.4 节那个综合案例时这章就算真正过关了。7.2 面试重点怎么准备现在 Java 岗位面试几乎必问这些方向反射是什么常见使用场景性能和安全性有什么问题注解的作用Retention 的三个值分别代表什么为什么框架要用 RUNTIMEJDK 动态代理和 CGLIB 有什么区别Spring AOP 如何选择单元测试常用注解有哪些JUnit 5 和 JUnit 4 的区别准备面试时不要只背概念。面试官一问“你项目里哪里用到了反射”你就答不上来这是很吃亏的。我建议可以把第 5.4 节的综合项目包装成一个自己的小工具写进简历的项目描述里例如“自定义注解 动态代理实现方法级日志打印使用 JUnit 完成自动化测试”这个项目小而精既能展示基础功底又不容易被喷成包装过度。7.3 我个人踩坑后的建议学这一章最忌讳只看不动手。反射和代理的代码运行结果和想象中经常不一样。我自己花了一个下午调一个动态代理找不到接口的报错最后发现是目标类没有实现接口JDK 动态代理根本没法代理它。这种经验不是看视频能得来的必须亲手踩。另外一点学完这章以后一定要回去看一遍 Spring。你会发现 Component、Autowired、Transactional 这些注解突然就不神秘了。它们做的事情就是扫描类、拿到注解、反射创建对象、通过代理包装对象、再把代理对象放进容器。第四章那点代码再放大一百倍就是你每天用的 Spring。最后分享一个小习惯我在学完每个知识点后会强迫自己写一段十行以内的小代码把这个知识点的核心能力浓缩进去。比如反射就是“一个字符串 new 出一个对象”代理就是“加日志不修改源码”。当你能用一句话向别人讲清楚它是干什么的说明你是真的理解了。
网站建设高端定制企业官网