JavaWeb笔记整理14——公共字段自动填充技术实现

目录


为什么需要公共字段自动填充?

步骤1 自定义注解AutoFill

步骤2 自定义切面AutoFillAspect

步骤3 在Mapper接口的方法上加入AutoFill注解

@Before("autoFillPointCut()")

JoinPoint

你能通过 JoinPoint 获取哪些信息?

例子中的 JoinPoint

获取方法签名和注解

获取被拦截方法的参数

反射

什么是反射

获取 Class 对象

获取 Method 对象

动态调用方法——invoke()


为什么需要公共字段自动填充?

  1. 避免手动重复操作:每次插入、更新数据库时,都要手动设置 createTimeupdateTimecreateUserupdateUser 等字段,容易出现遗漏或不一致的问题。自动填充可以保证这些公共字段在插入或更新时自动赋值,无需手动干预。

  2. 提高代码的可维护性:如果在每个数据插入和更新的地方都写上手动的字段赋值代码,当需求变化时,需要逐一修改所有相关的代码。这不仅增加了维护成本,还增加了出错的几率。通过自动填充,这些字段可以集中管理,方便维护。

  3. 保证数据一致性:自动填充可以保证所有数据记录的时间戳和用户信息是一致的,并且可以通过统一的逻辑进行约束,减少人为错误带来的数据不一致问题。

步骤1 自定义注解AutoFill

/**
 * 数据库操作类型
 */
public enum OperationType {

    /**
     * 更新操作
     */
    UPDATE,

    /**
     * 插入操作
     */
    INSERT

}
/**
 * 自定义注解 用于表示某个方法需要进行功能字段自动填充处理
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {

    //数据库操作类型 update insert
    OperationType value();
    
}

步骤2 自定义切面AutoFillAspect

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

    /**
     * 切入点
     */

    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    /**
     * 前置通知 在通知中进行公共字段的赋值
     */

    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint){
        log.info("开始进行公共字段的填充...");

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

        //获取当前被拦截到的方法参数——实体对象
        Object[] args = joinPoint.getArgs();
        if(args == null || args.length == 0){
            return;
        }

        Object entity = args[0];

        //准备赋值数据
        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();

        //根据当前不同的操作类型 为属性赋值 通过反射
        if(operationType == OperationType.INSERT){
            //为四个公共字段赋值
            try {
                Method setCreatTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method setCreatUser = 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);

                //通过反射为对象属性赋值
                setCreatTime.invoke(entity,now);
                setCreatUser.invoke(entity,currentId);
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);

            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }else if(operationType == OperationType.UPDATE){
            //为2个公共字段赋值
            try {
                Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);

                //通过反射为对象属性赋值
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);

            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

步骤3 在Mapper接口的方法上加入AutoFill注解

 /**
     * 根据主键来动态修改属性
     * @param employee
     */
    @AutoFill(value = OperationType.UPDATE)
    void update(Employee employee);

/**
     * 插入员工数据
     * @param employee
     */
    @Insert("insert into employee (name, username, password, phone, sex, id_number, status, create_time, update_time, create_user, update_user) " +
            "values (" +
            "#{name}, #{username}, #{password}, #{phone}, #{sex}, #{idNumber}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})"
    )
    @AutoFill(value = OperationType.INSERT)
    void insert(Employee employee);

/**
     * 根据id修改分类
     * @param category
     */
    @AutoFill(value = OperationType.UPDATE)
    void update(Category category);

 /**
     * 插入数据
     * @param category
     */
    @AutoFill(value = OperationType.INSERT)
    @Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
            " VALUES" +
            " (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
    void insert(Category category);

@Before("autoFillPointCut()")

@Before("autoFillPointCut()") 中的参数 "autoFillPointCut()" 用来指定前置通知的切入点。它告诉 AOP 框架,这个前置通知(autoFill() 方法)应该在什么地方执行。 

"autoFillPointCut()" 是一个切入点表达式,引用了之前用 @Pointcut 注解定义的切入点方法 autoFillPointCut()

@Pointcut 定义的 autoFillPointCut() 方法:

@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}

 这个方法并没有实际的代码执行,它仅仅是一个标识符,用来描述一个切入点,指定了哪些方法应该被拦截。它定义了拦截 com.sky.mapper 包下所有带有 @AutoFill 注解的方法。

然后,@Before("autoFillPointCut()") 表示:

在所有符合 autoFillPointCut() 所定义的切入点表达式的目标方法之前,执行 autoFill() 方法。

JoinPoint

JoinPoint 是 AOP(面向切面编程)中的一个核心概念,它代表了在程序执行过程中的某个连接点。在 Spring AOP 中,JoinPoint 通常指的是拦截的方法调用,你可以通过 JoinPoint 获取很多与当前执行方法有关的信息,比如方法名、参数、目标对象等。

在 Spring AOP 中,JoinPoint 表示一个拦截点,即某个被拦截的方法执行时的上下文信息。你可以把 JoinPoint 看作一个对象,里面存储了和当前拦截方法相关的各种数据。通过 JoinPoint,我们可以访问到很多和当前方法执行有关的详细信息。

你能通过 JoinPoint 获取哪些信息?

  • 目标方法的签名(方法名称、返回类型、参数类型等)。
  • 目标方法的参数
  • 目标对象(即该方法所属的对象)。
  • 方法执行的位置(前置通知、后置通知等)。

例子中的 JoinPoint

在代码中,JoinPoint 被传递到了 autoFill() 方法里。这个 JoinPoint 表示当前拦截的数据库操作方法(如插入或更新操作)。我们通过 JoinPoint 来获取方法的签名、注解、以及被调用的方法的参数。

获取方法签名和注解

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

joinPoint.getSignature():获取当前被拦截方法的签名信息。Signature 是方法的签名对象,包含方法名、返回类型、参数类型等信息。

MethodSignature:是 Signature 的子类,提供了更多关于方法的信息。这里通过类型转换,将 Signature 转为 MethodSignature

signature.getMethod().getAnnotation(AutoFill.class):获取方法上的 @AutoFill 注解对象。通过这个注解对象可以获取到注解的元数据,比如 value(数据库操作类型)。

autoFill.value():获取 @AutoFill 注解中的 value 属性,它表示数据库操作的类型(如 INSERT 或 UPDATE)。

获取被拦截方法的参数

Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {
    return;
}
Object entity = args[0];

joinPoint.getArgs():获取当前被拦截方法的参数列表(数组形式)。如果参数为空或参数长度为 0,说明没有实体对象需要填充,直接返回。

Object entity = args[0];:假设被拦截的方法的第一个参数是需要填充的实体对象,将其取出用于后续操作。

反射

什么是反射

反射是 Java 提供的一种功能,允许在运行时动态获取类的信息,并且可以操作这些类的信息,比如获取类的字段、方法、构造函数,甚至调用方法。它的关键在于灵活性,因为我们可以在编译时不知道类的细节,但在运行时操作它们。

通常情况下,我们编写代码时,类、方法、属性等信息都是在编译时就已经确定好的。但是在某些情况下,我们需要编写更加通用的代码,让代码在不知道具体类型的情况下,仍然能够操作这些对象。这种需求就可以通过反射实现。

获取 Class 对象

反射的核心在于获取类的**Class 对象**,通过这个对象可以获取类的各种信息。你有三种常用方式获取 Class 对象:

1.通过类名

Class clazz = Class.forName("com.example.MyClass");

Class.forName() 通过类的全限定名(包名+类名)来获取类的 Class 对象。这种方式适用于你知道类名(可能从配置文件或数据库中读取)的情况。 

2.通过类的实例: 

MyClass obj = new MyClass();
Class clazz = obj.getClass();

通过一个对象实例来获取该对象的 Class 对象。这种方式适用于你已经有该类的实例对象的情况。 

3.通过类的字面量: (本次公共字段技术就是通过类的字面量)

Class clazz = MyClass.class;

直接通过类名加 .class 获取 Class 对象。这种方式适用于在代码中直接指定类的情况。 

获取 Method 对象

一旦你有了 Class 对象,你可以通过**getDeclaredMethod()** 方法来获取类中的某个方法。getDeclaredMethod() 需要两个参数:

        第一个参数:方法名(字符串形式)。

        第二个参数:方法的参数类型(可以是多个,如果方法有多个参数)。

Method method = clazz.getDeclaredMethod("setCreateTime", LocalDateTime.class);

这个代码的作用是通过类的 Class 对象 clazz 获取名为 setCreateTime 的方法,该方法接受一个 LocalDateTime 类型的参数。

方法名和参数类型必须完全匹配,否则会抛出 NoSuchMethodException。所以在使用时,你需要确保方法名称和参数类型一致。

动态调用方法——invoke()

Method 对象不仅仅能用来描述类中的某个方法,它还提供了一个功能强大的方法——invoke(),用来在运行时调用方法。

invoke() 方法需要两个参数:

  1. 调用哪个对象上的方法:也就是你想在哪个对象上执行该方法。
  2. 传递给方法的参数值:传递给方法的实际参数值(如果方法有多个参数,这些参数依次传入)。

假设有如下代码:

public class User {
    private LocalDateTime createTime;
    private Long createUser;

    public void setCreateTime(LocalDateTime createTime) {
        this.createTime = createTime;
    }

    public void setCreateUser(Long createUser) {
        this.createUser = createUser;
    }
}

 我们希望通过反射调用 setCreateTimesetCreateUser 方法:

// 获取 User 类的 Class 对象
Class clazz = user.getClass();

// 获取 setCreateTime 方法对象
Method setCreateTime = clazz.getDeclaredMethod("setCreateTime", LocalDateTime.class);

// 获取 setCreateUser 方法对象
Method setCreateUser = clazz.getDeclaredMethod("setCreateUser", Long.class);

// 创建要传入的方法参数
LocalDateTime now = LocalDateTime.now();
Long currentUserId = 12345L;

// 通过反射调用 setCreateTime 方法,给 createTime 字段赋值
setCreateTime.invoke(user, now);

// 通过反射调用 setCreateUser 方法,给 createUser 字段赋值
setCreateUser.invoke(user, currentUserId);

在这里,我们通过反射动态调用了 user 对象的 setCreateTimesetCreateUser 方法,分别给 createTimecreateUser 字段赋值。

你可能感兴趣的:(笔记,反射,Spring,AOP,公共字段自动填充,JavaWeb,面向切面编程,Aspect)