深入浅出:Spring AOP 中的 Java 反射

发布于:2025-08-14 ⋅ 阅读:(19) ⋅ 点赞:(0)

🚀 深入浅出:Spring AOP 中的 Java 反射 —— 以公共字段自动填充为例

在现代Java应用开发中,Spring AOP(面向切面编程)和Java反射是两大强大的基石。它们协同工作,能够帮助我们编写出更加灵活、可维护和可扩展的代码。今天,我们就通过一个实际的Spring AOP公共字段自动填充的例子,来深入剖析Java反射在其中扮演的角色。✨

🎯 场景回顾:公共字段自动填充

在许多业务系统中,我们经常会遇到这样的需求:当创建或更新数据库记录时,需要自动填充一些公共字段,比如 createTime(创建时间)、createUser(创建人ID)、updateTime(更新时间)、updateUser(更新人ID)。

如果为每个实体类的增删改操作都手动编写填充逻辑,那将是重复且繁琐的工作。而Spring AOP结合Java反射,则能优雅地解决这个问题。

让我们先看看这段核心代码:

package com.sky.aspect;

import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method; // 👈 核心类
import java.time.LocalDateTime;

/**
 * 自定义切面,实现公共字段自动填充处理逻辑
 */
@Aspect
@Component
@Slf4j
public class AutoFillAspect {

    /**
     * 切入点:拦截com.sky.mapper包下所有方法,且方法上带有@AutoFill注解
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    /**
     * 前置通知:在目标方法执行前,进行公共字段的赋值
     */
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint){
        log.info("开始进行公共字段自动填充...");

        // 1. 获取当前被拦截方法上的数据库操作类型
        MethodSignature signature = (MethodSignature) joinPoint.getSignature(); // 方法签名对象
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); // 获得方法上的注解对象
        OperationType operationType = autoFill.value(); // 获得数据库操作类型 (INSERT/UPDATE)

        // 2. 获取被拦截方法的参数——实体对象
        Object[] args = joinPoint.getArgs();
        if(args == null || args.length == 0){
            return;
        }
        Object entity = args[0]; // 假设第一个参数就是需要自动填充的实体对象

        // 3. 准备赋值的数据
        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId(); // 从上下文获取当前用户ID

        // 4. 根据不同的操作类型,通过反射为实体对象的属性赋值
        if(operationType == OperationType.INSERT){
            // 为4个公共字段赋值
            try {
                // 核心反射操作:获取方法对象
                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                // 核心反射操作:动态调用方法
                setCreateTime.invoke(entity, now);
                setCreateUser.invoke(entity, currentId);
                setUpdateTime.invoke(entity, now);
                setUpdateUser.invoke(entity, currentId);
            } catch (Exception e) {
                // 异常处理,例如方法不存在等
                e.printStackTrace();
            }
        }else if(operationType == OperationType.UPDATE){
            // 为2个公共字段赋值
            try {
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                setUpdateTime.invoke(entity, now);
                setUpdateUser.invoke(entity, currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

🔍 什么是Java反射?

Java反射(Reflection)是Java语言的一个强大特性,它允许程序在运行时动态地获取类的信息,并能够动态地操作类的对象、方法和字段。简单来说,反射让Java程序拥有了“自我审查”和“自我修改”的能力。

反射的核心“工具”:

  • Class 类:反射的入口,代表一个类或接口的运行时描述。
  • Method 类:代表一个方法。
  • Field 类:代表一个字段(成员变量)。
  • Constructor 类:代表一个构造器。

通过这些类,你可以在不知道具体类型的情况下,动态地创建对象、调用方法、访问或修改字段。

💡 代码中的反射魔法解析

现在,让我们一步步揭示上述代码中反射的“魔法”所在。

1. 获取被拦截方法的元数据 🕵️‍♀️

MethodSignature signature = (MethodSignature) joinPoint.getSignature();
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
OperationType operationType = autoFill.value();
  • joinPoint.getSignature(): 这是Spring AOP提供的,用于获取当前被拦截方法的“签名”。
  • signature.getMethod(): 第一次反射登场! 💥 它返回一个 java.lang.reflect.Method 对象。这个 Method 对象就代表了当前正在被AOP拦截执行的那个具体方法(例如 com.sky.mapper.EmployeeMapper.insert(Employee employee))。
  • .getAnnotation(AutoFill.class): 第二次反射登场! 💥 通过获取到的 Method 对象,我们可以动态地检查该方法上是否存在 @AutoFill 注解,并获取其实例。这样,程序在运行时就能知道这个方法是进行 INSERT 还是 UPDATE 操作。

反射作用: 在运行时动态地读取方法上的注解信息,从而决定后续的业务逻辑。

2. 获取被拦截方法的参数(实体对象)📦

Object[] args = joinPoint.getArgs();
Object entity = args[0]; // 假设第一个参数是实体对象
  • joinPoint.getArgs(): 获取被拦截方法的参数列表。
  • Object entity = args[0]: 这里将第一个参数视为需要自动填充的实体对象。注意,此时 entity 是一个 Object 类型。这意味着在编译时,我们并不知道它具体是 EmployeeDish 还是其他什么类型。这正是反射大显身手的前提!

3. 核心反射操作:动态调用Setter方法赋值 ✨

这部分是反射最精彩的地方,它根据操作类型动态地为实体对象填充字段。以 INSERT 操作为例:

// ...
// 核心反射操作:获取方法对象
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
// ...

// 核心反射操作:动态调用方法
setCreateTime.invoke(entity, now);
setCreateUser.invoke(entity, currentId);
// ...
  1. entity.getClass(): 第三次反射登场! 💥 获取 entity 这个 Object 在运行时的实际 Class 对象。例如,如果 entity 是一个 Employee 实例,那么 entity.getClass() 将返回 Employee.class 这个 Class 对象。这是进行后续方法查找的基础。

  2. getDeclaredMethod(String name, Class<?>... parameterTypes): 第四次反射登场! 💥

    • 作用: 通过 Class 对象,在运行时查找并获取指定名称和参数类型的方法。
    • AutoFillConstant.SET_CREATE_TIME:这是一个常量字符串,比如 "setCreateTime"getDeclaredMethod 会根据这个字符串名称去查找方法。
    • LocalDateTime.class:这是方法的参数类型。getDeclaredMethod 需要知道参数类型才能准确找到唯一的方法(因为方法可能重载)。
    • 返回值: 如果找到对应的方法,则返回一个 java.lang.reflect.Method 对象。这个 Method 对象就代表了 entity 所属类中的 setCreateTime(LocalDateTime) 方法。
  3. method.invoke(Object obj, Object... args): 第五次反射登场,也是最核心的! 💥

    • 作用: 动态地调用由 Method 对象所代表的方法。
    • entity:这是方法调用的目标对象。invoke 方法会在这个 entity 实例上执行方法。
    • now / currentId:这是传递给方法的实际参数。
    • 效果: 相当于在运行时执行了 entity.setCreateTime(now);entity.setCreateUser(currentId); 等语句。但关键在于,这些调用是在编译时不知道 entity 具体类型的情况下完成的!

反射作用: 在运行时,程序能够根据实体对象的实际类型,动态地找到并调用其对应的setter方法,从而实现公共字段的自动赋值,而无需在编译时硬编码各种实体类型。

🤔 为什么这里要用反射?

你可能会问,为啥要这么“折腾”用反射呢?直接调用不行吗?答案是:通用性和运行时灵活性!

  1. 🚀 通用性与解耦: 这个 AutoFillAspect 切面是通用的。它不关心具体的实体类是 EmployeeDish 还是 Category。只要这些实体类遵循约定(有 setCreateTimesetUpdateTime 等方法),这个切面就能工作。如果没有反射,你可能需要为每种实体类编写不同的自动填充逻辑,或者使用大量的 if-else instanceof 判断,这会使得代码非常臃肿且难以维护。
  2. ⚙️ 运行时决策: Spring AOP的本质就是在运行时动态地增强功能。反射是实现这种运行时动态性的关键工具。它允许程序在运行时根据上下文(被拦截的方法、其注解、参数类型)来决定如何操作对象。
  3. 🏗️ 框架设计: 像Spring这样的框架,大量使用了反射来实现其核心功能,例如依赖注入(DI)、AOP、MVC控制器方法的调用等。反射是构建灵活、可扩展框架的基石。

⚠️ 反射的潜在缺点

当然,反射并非没有缺点,使用时也需要注意:

  1. 📉 性能开销: 相对于直接的方法调用,反射调用的性能开销更大。因为它涉及到JVM在运行时查找类、方法、字段等信息,并进行安全检查。不过,对于大多数业务场景,这种开销通常可以忽略不计。
  2. ❌ 编译时检查缺失: 使用反射时,编译器无法在编译阶段检查方法名、参数类型是否正确。如果方法名写错或参数类型不匹配,只有在运行时才会抛出 NoSuchMethodExceptionIllegalArgumentException 等异常。这增加了调试的难度,因此代码中使用了 try-catch 块来处理这些运行时异常。
  3. 🔒 封装性破坏: 反射可以访问类的私有成员(通过 setAccessible(true)),这在某些情况下可能会破坏类的封装性。不过本例中只调用了公共的setter方法,所以没有这方面的问题。


网站公告

今日签到

点亮在社区的每一天
去签到