Skip to content

Java 高级技术详解:单元测试、反射、注解与动态代理(从零基础到精通)

Java 高级技术是构建灵活、可扩展系统的核心,单元测试保障代码质量,反射与注解支撑框架底层,动态代理实现面向切面编程(AOP)。本文从零基础角度出发,通过实例代码+详细注释,带你逐步掌握这些技术的核心原理与实战用法。

一、单元测试:保障代码质量的第一道防线

单元测试(Unit Testing)是对软件中最小可测试单元(如方法、类)的验证,目的是尽早发现代码缺陷,提高重构安全性。Java 中最常用的单元测试框架是 JUnit 5

1. JUnit 5 基础入门

(1)环境准备

在 Maven 项目的 pom.xml 中添加依赖:

xml
<dependencies>
    <!-- JUnit 5 核心依赖 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.9.2</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.9.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

(2)核心注解

JUnit 5 通过注解标识测试方法和生命周期,常用注解:

注解作用
@Test标识测试方法(无需 public void
@BeforeEach每个测试方法执行前运行(初始化资源)
@AfterEach每个测试方法执行后运行(释放资源)
@BeforeAll所有测试方法执行前运行(静态方法)
@AfterAll所有测试方法执行后运行(静态方法)
@Disabled禁用测试方法/类

(3)第一个单元测试

假设我们有一个简单的计算器类,需要测试其加法和减法功能:

java
// 业务类:src/main/java/com/example/Calculator.java
package com.example;

public class Calculator {
    // 加法
    public int add(int a, int b) {
        return a + b;
    }

    // 减法
    public int subtract(int a, int b) {
        return a - b;
    }
}

测试类需放在 src/test/java 目录,包结构与业务类一致:

java
// 测试类:src/test/java/com/example/CalculatorTest.java
package com.example;

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*; // 断言工具类

class CalculatorTest {
    private Calculator calculator;

    // 所有测试前初始化(静态方法)
    @BeforeAll
    static void beforeAll() {
        System.out.println("=== 所有测试开始 ===");
    }

    // 每个测试前初始化
    @BeforeEach
    void setUp() {
        calculator = new Calculator(); // 初始化计算器实例
        System.out.println("--- 开始单个测试 ---");
    }

    // 测试加法
    @Test
    void testAdd() {
        // 断言:预期结果与实际结果是否一致
        assertEquals(5, calculator.add(2, 3)); // 2+3=5
        assertEquals(-1, calculator.add(-2, 1)); // 边界值测试
    }

    // 测试减法
    @Test
    void testSubtract() {
        assertEquals(1, calculator.subtract(3, 2));
        assertNotEquals(0, calculator.subtract(5, 5)); // 断言不等
    }

    // 禁用该测试
    @Test
    @Disabled("暂未实现乘法功能")
    void testMultiply() {
        fail("未实现的测试"); // 强制失败
    }

    // 每个测试后清理
    @AfterEach
    void tearDown() {
        System.out.println("--- 单个测试结束 ---");
    }

    // 所有测试后清理
    @AfterAll
    static void afterAll() {
        System.out.println("=== 所有测试结束 ===");
    }
}

运行测试:在 IDE 中右键测试类选择「Run Tests」,或通过 Maven 命令 mvn test 执行。

2. 高级特性:参数化测试

参数化测试允许用多组数据执行同一个测试方法,避免重复代码。需添加 @ParameterizedTest 注解并指定数据源。

java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.params.provider.CsvSource;

class ParameterizedTestDemo {
    // 单参数测试(字符串数组)
    @ParameterizedTest
    @ValueSource(strings = {"apple", "banana", "cherry"})
    void testStringLength(String fruit) {
        assertTrue(fruit.length() > 2); // 断言字符串长度>2
    }

    // 多参数测试(CSV格式:a, b, 预期结果)
    @ParameterizedTest
    @CsvSource({
        "2, 3, 5",   // 2+3=5
        "-1, 1, 0",  // -1+1=0
        "0, 0, 0"    // 0+0=0
    })
    void testAddWithParams(int a, int b, int expected) {
        Calculator calculator = new Calculator();
        assertEquals(expected, calculator.add(a, b));
    }
}

3. Mock 测试:隔离外部依赖

当代码依赖数据库、网络等外部资源时,可通过 Mockito 模拟这些依赖,专注测试目标逻辑。

(1)添加 Mockito 依赖

xml
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>4.8.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>4.8.1</version>
    <scope>test</scope>
</dependency>

(2)Mock 测试示例

假设我们有一个依赖用户服务的订单服务,需要测试订单创建逻辑,而不实际调用用户服务:

java
// 依赖接口
interface UserService {
    boolean exists(Long userId); // 检查用户是否存在
}

// 待测试类
class OrderService {
    private final UserService userService;

    OrderService(UserService userService) {
        this.userService = userService; // 构造器注入依赖
    }

    // 创建订单(需先检查用户是否存在)
    public String createOrder(Long userId) {
        if (userService.exists(userId)) {
            return "ORDER_" + System.currentTimeMillis();
        }
        return "ERROR: 用户不存在";
    }
}

// Mock 测试
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class) // 启用Mockito
class OrderServiceTest {
    @Mock // 模拟依赖对象
    private UserService userService;

    @InjectMocks // 将mock对象注入到待测试类
    private OrderService orderService;

    @Test
    void testCreateOrder_UserExists() {
        // 1. 定义mock行为:当调用exists(1L)时返回true
        when(userService.exists(1L)).thenReturn(true);

        // 2. 执行测试
        String result = orderService.createOrder(1L);

        // 3. 验证结果
        assertTrue(result.startsWith("ORDER_"));
        // 验证userService的exists方法是否被调用过一次
        verify(userService, times(1)).exists(1L);
    }

    @Test
    void testCreateOrder_UserNotExists() {
        when(userService.exists(999L)).thenReturn(false);

        String result = orderService.createOrder(999L);

        assertEquals("ERROR: 用户不存在", result);
    }
}

二、反射:突破编译期限制的"黑科技"

反射(Reflection)允许程序在运行时获取类的信息(属性、方法、构造器等)并操作其私有成员,是框架(如 Spring、MyBatis)的核心底层技术。

1. 反射的核心:Class 类

Java 中每个类被加载后,JVM 会为其创建一个 java.lang.Class 对象,包含该类的所有信息。获取 Class 对象的 3 种方式:

java
package com.example.reflection;

public class ReflectionDemo {
    public static void main(String[] args) throws ClassNotFoundException {
        // 方式1:通过对象.getClass()
        User user = new User();
        Class<? extends User> clazz1 = user.getClass();

        // 方式2:通过类名.class(编译期已知类型)
        Class<User> clazz2 = User.class;

        // 方式3:通过Class.forName("全类名")(动态加载,最常用)
        Class<?> clazz3 = Class.forName("com.example.reflection.User");

        // 验证:同一个类的Class对象唯一
        System.out.println(clazz1 == clazz2); // true
        System.out.println(clazz1 == clazz3); // true
    }
}

// 测试类
class User {
    private Long id;
    private String name;

    public User() {}

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    private String getInfo() {
        return id + ":" + name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

2. 反射操作类成员

(1)获取并调用构造器

java
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class ReflectionConstructor {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Class<?> clazz = Class.forName("com.example.reflection.User");

        // 1. 获取无参构造器并创建对象(等价于 new User())
        Constructor<?> constructor1 = clazz.getConstructor();
        User user1 = (User) constructor1.newInstance(); // 无参构造
        System.out.println(user1); // User@xxx

        // 2. 获取有参构造器(Long.class, String.class 是参数类型)
        Constructor<?> constructor2 = clazz.getConstructor(Long.class, String.class);
        User user2 = (User) constructor2.newInstance(1L, "张三"); // 有参构造
        System.out.println(user2);
    }
}

(2)获取并操作属性(包括私有属性)

java
import java.lang.reflect.Field;

public class ReflectionField {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("com.example.reflection.User");
        User user = (User) clazz.getConstructor().newInstance();

        // 1. 获取公有属性(实际User中没有公有属性,此处仅为示例)
        // Field publicField = clazz.getField("publicField");

        // 2. 获取私有属性(必须用 getDeclaredField)
        Field nameField = clazz.getDeclaredField("name");
        // 暴力破解私有属性访问权限(关键)
        nameField.setAccessible(true);

        // 设置属性值
        nameField.set(user, "李四"); // 等价于 user.name = "李四"
        // 获取属性值
        String name = (String) nameField.get(user); // 等价于 user.name
        System.out.println("修改后name:" + name); // 李四
    }
}

(3)获取并调用方法(包括私有方法)

java
import java.lang.reflect.Method;

public class ReflectionMethod {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("com.example.reflection.User");
        User user = (User) clazz.getConstructor(Long.class, String.class).newInstance(2L, "王五");

        // 1. 调用公有方法 setName
        Method setNameMethod = clazz.getMethod("setName", String.class);
        setNameMethod.invoke(user, "赵六"); // 等价于 user.setName("赵六")

        // 2. 调用私有方法 getInfo
        Method getInfoMethod = clazz.getDeclaredMethod("getInfo");
        getInfoMethod.setAccessible(true); // 允许访问私有方法
        String info = (String) getInfoMethod.invoke(user); // 等价于 user.getInfo()
        System.out.println("私有方法返回:" + info); // 2:赵六
    }
}

3. 反射的应用场景与局限

(1)典型应用

  • 框架底层:Spring 的依赖注入(DI)通过反射创建对象并注入属性;
  • 序列化/反序列化:JSON 工具(如 Jackson)通过反射将 JSON 转换为对象;
  • 单元测试:Mockito 模拟对象时使用反射修改私有成员。

(2)局限

  • 性能损耗:反射操作绕过编译期检查,性能比直接调用低约 10-100 倍;
  • 破坏封装:直接访问私有成员可能导致代码逻辑混乱;
  • 可读性差:反射代码较繁琐,不如直接调用直观。

三、注解:给代码添加"元数据"

注解(Annotation)是 Java 5 引入的特殊标记,用于为代码添加元数据(描述数据的数据),可被编译器或运行时解析。

1. 注解的分类

类型说明示例
内置注解JDK 自带的注解@Override@Deprecated
元注解修饰其他注解的注解@Target@Retention
自定义注解开发者根据需求定义的注解@Log@Validate

2. 元注解:定义注解的"注解"

元注解用于约束自定义注解的行为,常用元注解:

元注解作用可选值
@Target限制注解可修饰的元素TYPE(类)、METHOD(方法)等
@Retention定义注解的保留策略SOURCE(源码)、CLASS(字节码)、RUNTIME(运行时)
@Documented注解是否包含在 Javadoc 中-
@Inherited注解是否可被继承-

3. 自定义注解与解析

(1)定义自定义注解

java
import java.lang.annotation.*;

// 元注解:该注解可修饰方法和类
@Target({ElementType.METHOD, ElementType.TYPE})
// 元注解:保留到运行时(可通过反射解析)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    // 注解属性(默认值为"操作日志")
    String value() default "操作日志";
    // 可选属性:是否记录参数
    boolean recordParams() default true;
}

(2)使用自定义注解

java
// 注解修饰类
@Log("用户服务")
public class UserService {

    // 注解修饰方法(覆盖类上的注解值)
    @Log(value = "查询用户", recordParams = true)
    public String getUser(Long id) {
        return "用户" + id;
    }

    @Log(value = "删除用户", recordParams = false)
    public void deleteUser(Long id) {
        System.out.println("删除用户" + id);
    }
}

(3)通过反射解析注解

java
import java.lang.reflect.Method;

public class AnnotationParser {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> clazz = Class.forName("com.example.annotation.UserService");

        // 1. 解析类上的注解
        if (clazz.isAnnotationPresent(Log.class)) {
            Log classLog = clazz.getAnnotation(Log.class);
            System.out.println("类注解值:" + classLog.value()); // 用户服务
        }

        // 2. 解析方法上的注解
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            if (method.isAnnotationPresent(Log.class)) {
                Log methodLog = method.getAnnotation(Log.class);
                System.out.println(
                    "方法名:" + method.getName() + 
                    ",注解值:" + methodLog.value() + 
                    ",是否记录参数:" + methodLog.recordParams()
                );
            }
        }
    }
}
// 输出:
// 类注解值:用户服务
// 方法名:getUser,注解值:查询用户,是否记录参数:true
// 方法名:deleteUser,注解值:删除用户,是否记录参数:false

4. 注解的典型应用

  • 框架配置:Spring 的 @Controller@Service 标识组件,通过注解扫描实现自动装配;
  • 参数验证:JSR-303 规范的 @NotNull@Size 用于验证请求参数;
  • AOP 切点:Spring 的 @Transactional 标识事务方法,通过 AOP 自动开启/提交事务。

四、动态代理:无侵入式增强方法功能

动态代理(Dynamic Proxy)允许在不修改原始类代码的情况下,为方法添加额外功能(如日志、事务、权限校验),是 AOP(面向切面编程)的核心实现。

1. 代理模式基础

代理模式包含三个角色:

  • 目标对象(Target):原始业务对象(如 UserService);
  • 代理对象(Proxy):增强目标对象功能的对象;
  • 抽象接口(Interface):目标对象和代理对象共同实现的接口。

代理模式结构

2. JDK 动态代理(基于接口)

JDK 动态代理是 JDK 自带的代理实现,要求目标对象必须实现接口。核心是 InvocationHandler 接口和 Proxy 类。

(1)定义业务接口和实现类

java
// 业务接口
public interface OrderService {
    void createOrder(Long userId);
    void cancelOrder(Long orderId);
}

// 目标对象(实现接口)
public class OrderServiceImpl implements OrderService {
    @Override
    public void createOrder(Long userId) {
        System.out.println("创建用户" + userId + "的订单");
    }

    @Override
    public void cancelOrder(Long orderId) {
        System.out.println("取消订单" + orderId);
    }
}

(2)实现 InvocationHandler 接口(增强逻辑)

java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Arrays;

// 代理处理器:实现增强逻辑
public class LogInvocationHandler implements InvocationHandler {
    private final Object target; // 目标对象(被代理的对象)

    public LogInvocationHandler(Object target) {
        this.target = target;
    }

    /**
     * 代理方法:每次调用目标方法时都会触发
     * @param proxy  代理对象
     * @param method 目标方法
     * @param args   方法参数
     * @return 方法返回值
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 前置增强:方法执行前添加日志
        System.out.println("===== 日志开始 =====");
        System.out.println("调用方法:" + method.getName());
        System.out.println("参数:" + Arrays.toString(args));

        // 调用目标方法(核心:执行原始业务逻辑)
        Object result = method.invoke(target, args);

        // 后置增强:方法执行后添加日志
        System.out.println("方法执行完成");
        System.out.println("===== 日志结束 =====");
        return result;
    }
}

(3)创建代理对象并使用

java
import java.lang.reflect.Proxy;

public class JdkProxyDemo {
    public static void main(String[] args) {
        // 1. 创建目标对象
        OrderService target = new OrderServiceImpl();

        // 2. 创建代理处理器
        LogInvocationHandler handler = new LogInvocationHandler(target);

        // 3. 生成代理对象(核心方法:Proxy.newProxyInstance)
        OrderService proxy = (OrderService) Proxy.newProxyInstance(
            target.getClass().getClassLoader(), // 类加载器
            target.getClass().getInterfaces(), // 目标对象实现的接口
            handler // 代理处理器
        );

        // 4. 调用代理对象的方法(实际会触发handler的invoke方法)
        proxy.createOrder(100L);
        System.out.println("---");
        proxy.cancelOrder(999L);
    }
}
// 输出:
// ===== 日志开始 =====
// 调用方法:createOrder
// 参数:[100]
// 创建用户100的订单
// 方法执行完成
// ===== 日志结束 =====
// ---
// ===== 日志开始 =====
// 调用方法:cancelOrder
// 参数:[999]
// 取消订单999
// 方法执行完成
// ===== 日志结束 =====

3. CGLIB 动态代理(基于继承)

JDK 动态代理要求目标对象实现接口,而 CGLIB(Code Generation Library) 通过继承目标类生成代理对象,无需接口。

(1)添加 CGLIB 依赖

xml
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

(2)实现 MethodInterceptor 接口(CGLIB 增强逻辑)

java
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
import java.util.Arrays;

// CGLIB 代理拦截器
public class LogMethodInterceptor implements MethodInterceptor {
    private final Object target; // 目标对象

    public LogMethodInterceptor(Object target) {
        this.target = target;
    }

    // 创建代理对象
    public Object createProxy() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass()); // 设置父类(目标类)
        enhancer.setCallback(this); // 设置拦截器
        return enhancer.create(); // 生成代理对象
    }

    // 拦截方法调用(类似JDK的invoke)
    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 前置增强
        System.out.println("===== CGLIB 日志开始 =====");
        System.out.println("调用方法:" + method.getName());

        // 调用目标方法
        Object result = method.invoke(target, args);

        // 后置增强
        System.out.println("===== CGLIB 日志结束 =====");
        return result;
    }
}

(3)使用 CGLIB 代理(目标类无需实现接口)

java
// 目标类(无需实现接口)
public class PayService {
    public void pay(Long orderId) {
        System.out.println("支付订单" + orderId);
    }
}

// 测试 CGLIB 代理
public class CglibProxyDemo {
    public static void main(String[] args) {
        // 1. 目标对象(无需接口)
        PayService target = new PayService();

        // 2. 创建 CGLIB 代理
        LogMethodInterceptor interceptor = new LogMethodInterceptor(target);
        PayService proxy = (PayService) interceptor.createProxy();

        // 3. 调用代理方法
        proxy.pay(12345L);
    }
}
// 输出:
// ===== CGLIB 日志开始 =====
// 调用方法:pay
// 支付订单12345
// ===== CGLIB 日志结束 =====

4. 两种动态代理的对比

特性JDK 动态代理CGLIB 动态代理
依赖JDK 自带(无需额外依赖)需导入 CGLIB 库
原理实现目标接口生成代理类继承目标类生成代理类
目标类要求必须实现接口无要求(不能是 final 类)
性能调用效率高,创建代理慢创建代理快,调用效率略低
典型应用Spring AOP(目标有接口时)Spring AOP(目标无接口时)

总结

本文系统讲解了 Java 高级技术的核心知识点,从单元测试到动态代理,覆盖了框架开发的底层基石:

  • 单元测试:通过 JUnit 5 实现代码自动化验证,Mockito 隔离外部依赖,是持续集成的基础;
  • 反射:突破编译期限制,运行时操作类成员,支撑 Spring 等框架的依赖注入和配置解析;
  • 注解:作为元数据描述代码,结合反射实现灵活的配置与增强(如日志、事务);
  • 动态代理:JDK 基于接口,CGLIB 基于继承,是 AOP 的核心,实现无侵入式功能增强。

这些技术的核心价值在于解耦:单元测试解耦测试与业务,反射与注解解耦框架与业务,动态代理解耦增强逻辑与原始逻辑。掌握它们,不仅能读懂框架源码,更能设计出灵活、可扩展的系统。

建议结合实际场景练习(如实现一个简单的注解式日志框架),逐步加深理解,从"会用"到"精通"。

Released under the MIT License.