苍穹外卖-公共字段自动填充详解

对于项目中多个地方都需要给实体类赋值操作,其中create_time,create_user,update_time,update_user这四个属性是需要经常赋值的,所以为了避免这样重复的操作,同时也考虑到了代码的可复用性,需要使用AOP思想将这块的代码给抽离出来,统一进行赋值。下面是具体的分析:

在这里插入图片描述

在这里插入图片描述

1 .自定义注解和常量类的定义

☘AutoFillConstant

package com.sky.constant;

/**
 * 公共字段自动填充相关常量
 */
public class AutoFillConstant {
    /**
     * 实体类中的方法名称
     */
    public static final String SET_CREATE_TIME = "setCreateTime";
    public static final String SET_UPDATE_TIME = "setUpdateTime";
    public static final String SET_CREATE_USER = "setCreateUser";
    public static final String SET_UPDATE_USER = "setUpdateUser";
}

☘@AutoFill

package com.sky.annotation;

import com.sky.enumeration.OperationType;

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 AutoFill {
    //数据库操作类型,UPDATE、INSERT
    OperationType value();
}

2 .自定义切面类

首先在引入代码之前必须要知道的知识点:

  • 切点表达式
  • @Aspect注解
  • Advice(通知)
  • JoinPoint(连接点)
2.1 切点表达式

首先注册切入点:

@Pointcut(....)

切入点可以作用到多个位置:
类、方法、对象、字段、注解等等

☘切点表达式详解:基本格式:

execution([访问修饰符] 返回类型 [包名.类名].方法名(参数列表) [throws 异常类型])

其中方括号[]中的内容是可选的,可以表示任何类型,因此最简洁的表示为:execution( **(…))

各部分详细说明:

2.1.1 访问修饰符
  • public:公共方法
  • private:私有方法
  • protected:受保护的方法
  • *:任何访问修饰符

例如:

javaCopyexecution(public * *(..))  // 匹配所有public方法
execution(* * *(..))       // 匹配所有方法,不限制访问修饰符
2.1.2 返回类型
  • 具体类型:如 String、int、void 等
  • *:任何返回类型

例如:

javaCopyexecution(void *(..))     // 匹配返回类型为void的方法
execution(String *(..))   // 匹配返回类型为String的方法
execution(* *(..))       // 匹配任何返回类型的方法
2.1.3 包名和类名
  • 具体包名:com.example.service
  • …:表示包及其子包
  • *:表示任何包或类

例如:

// 匹配com.example.service包中的所有类的所有方法
execution(* com.example.service.*.*(..))

// 匹配com.example.service包及其子包中的所有类的所有方法
execution(* com.example.service..*.*(..))
2.1.4 方法名
  • 具体方法名:save、update等
  • *:任何方法名
  • set*:以set开头的方法
  • *Service:以Service结尾的方法

例如:

javaCopyexecution(* *.save(..))        // 匹配任何类中名为save的方法
execution(* *.set*(..))        // 匹配所有setter方法
2.1.5 参数列表
  • ():空参数
  • (…):任意参数
  • (String):一个String参数
  • (String,…):第一个参数为String,后面可以有任意参数
  • (String,Integer):两个参数,第一个是String,第二个是Integer

例如:

javaCopyexecution(* *())              // 匹配无参方法
execution(* *(..))            // 匹配任意参数方法
execution(* *(String))        // 匹配只有一个String参数的方法
execution(* *(String,..))     // 匹配第一个参数为String的方法
2.1.6 常用的切点表达式

📕基于注解的切点

// 匹配带有@AutoFill注解的方法
@Pointcut("@annotation(com.sky.annotation.AutoFill)")

// 匹配带有@Service注解的类的所有方法
@Pointcut("within(@org.springframework.stereotype.Service *)")

📕基于包名的切点

// 匹配指定包下的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")

// 匹配指定包及其子包下的所有方法
@Pointcut("execution(* com.example..*.*(..))")

📕组合使用

// 匹配service包下的所有public方法
@Pointcut("execution(public * com.example.service.*.*(..))")

// 匹配dao包下的所有方法,但不包括私有方法
@Pointcut("execution(* com.example.dao.*.*(..)) && !execution(private * *.*(..))")
2.2 @Aspect注解

@Aspect 注解用来标识一个类为切面类。被该注解标注的类,会作为一个切面,用来实现切入点(JoinPoint)的增强操作。

一般来说同时也会加上@Component注解标注为 Spring Bean,这样 Spring 容器才能识别它。

☘@Component和@Bean之间的区别

  1. @Component
  • 定义@Component 是一个类级别的注解,用于标记一个类作为 Spring 管理的 Bean。
  • 用法:通常用于标记普通的 Java 类,Spring 会自动扫描并将其注册为 Spring 容器中的 Bean,前提是该类位于 Spring 容器的扫描路径下。
  • 适用场景:适合用于创建组件类,通常不需要特别的配置。

示例

@Component
public class MyService {
    public void doSomething() {
        System.out.println("Doing something");
    }
}

在 Spring Boot 中,通常通过 @SpringBootApplication 注解自动开启组件扫描。

  1. @Bean
  • 定义@Bean 是一个方法级别的注解,用于将一个方法的返回值注册为 Spring 容器中的 Bean。
  • 用法:它通常在配置类(即用 @Configuration 注解的类)中使用,表示通过该方法创建的对象应当作为 Spring Bean 注入到容器中。
  • 适用场景:适合需要创建一些复杂的、第三方类的 Bean,或需要更细粒度控制 Bean 初始化的场景。

示例

@Configuration
public class AppConfig {
    
    @Bean
    public MyService myService() {
        return new MyService();
    }
}
2.3 Advice通知

在 Spring AOP 中,通知(Advice)是指在目标方法执行前、执行后或者出现异常时所执行的增强操作。通知定义了增强逻辑,并被绑定到切入点上。Spring 提供了几种不同类型的通知,每种通知都在不同的时机触发。

具体有一下通知类型:

@Before:前置通知,在方法执行前执行。

@After:后置通知,在方法执行后执行(无论方法是正常执行还是抛出异常)。

@AfterReturning:返回通知,仅在目标方法正常返回时执行。

@AfterThrowing:异常通知,仅在目标方法抛出异常时执行。

@Around:环绕通知,可以在目标方法执行前后做一些操作,允许控制目标方法是否执行。

下面一一详细介绍一下各个通知:

  1. @Before(前置通知)
  • 定义:在目标方法执行之前执行的通知。
  • 触发时机:方法执行前,@Before 注解标记的方法会先执行。
  • 用法:适用于在方法执行之前需要做一些处理的场景,如日志记录、权限检查、输入验证等。
  • 限制:无法修改目标方法的返回值。

示例

@Before("execution(* com.example.service.UserService.*(..))")
public void beforeMethod(JoinPoint joinPoint) {
    System.out.println("Before method: " + joinPoint.getSignature().getName());
}
  • JoinPoint:表示连接点(即方法执行),可以用它获取方法的名称、参数等信息。
  1. @After(后置通知)
  • 定义:在目标方法执行之后执行的通知,不论目标方法是否正常执行(包括是否抛出异常)。
  • 触发时机:目标方法执行完成后,@After 注解标记的方法会被触发。
  • 用法:适用于清理工作,或者你需要在方法执行后做一些不依赖于方法执行结果的操作,比如关闭资源、记录日志等。

示例

@After("execution(* com.example.service.UserService.*(..))")
public void afterMethod(JoinPoint joinPoint) {
    System.out.println("After method: " + joinPoint.getSignature().getName());
}
  • 注意@After 通知不区分方法是否抛出异常,它总是会被执行。
  1. @AfterReturning(返回通知)
  • 定义:在目标方法正常返回之后执行的通知。
  • 触发时机:只有目标方法没有抛出异常并且成功返回时,@AfterReturning 注解标记的方法才会被触发。
  • 用法:适用于处理方法正常执行后的逻辑,比如记录日志、返回值处理等。

示例

@AfterReturning(value = "execution(* com.example.service.UserService.*(..))", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
    System.out.println("After returning from method: " + joinPoint.getSignature().getName());
    System.out.println("Returned value: " + result);
}
  • returning:指定方法返回值的变量名,用于获取目标方法的返回值。
  • 应用场景:例如,返回通知可以用来处理缓存,或者修改返回结果。
  1. @AfterThrowing(异常通知)
  • 定义:在目标方法抛出异常时执行的通知。
  • 触发时机:仅当目标方法抛出异常时,@AfterThrowing 注解标记的方法会被触发。
  • 用法:适用于异常处理和日志记录,用于在方法抛出异常时执行一些清理或通知操作。

示例

@AfterThrowing(value = "execution(* com.example.service.UserService.*(..))", throwing = "exception")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable exception) {
    System.out.println("Method " + joinPoint.getSignature().getName() + " threw an exception: " + exception.getMessage());
}
  • throwing:指定抛出异常的变量名,用于捕获目标方法抛出的异常。
  • 应用场景:例如,在发生异常时记录日志、发送警告或执行一些恢复操作。
  1. @Around(环绕通知)
  • 定义:可以在目标方法执行之前和之后做增强的通知。它是最强大的一种通知类型,能够完全控制目标方法的执行。
  • 触发时机@Around 通知方法在目标方法执行前和执行后都会被触发。你可以通过 proceedingJoinPoint.proceed() 来控制目标方法是否执行。
  • 用法:适用于需要完全控制方法执行的场景,例如性能监控、日志记录、事务控制等。

示例

@Around("execution(* com.example.service.UserService.*(..))")
public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    System.out.println("Before method: " + proceedingJoinPoint.getSignature().getName());
    
    // 执行目标方法
    Object result = proceedingJoinPoint.proceed();
    
    System.out.println("After method: " + proceedingJoinPoint.getSignature().getName());
    return result;
}
  • ProceedingJoinPoint:继承自 JoinPoint,它提供了 proceed() 方法来继续目标方法的执行。
  • proceed() 方法:如果在环绕通知中不调用 proceed(),目标方法将不会执行。因此,你可以根据逻辑决定是否执行目标方法。
2.4 JoinPoint 连接点

在 Spring AOP 中,连接点(JoinPoint) 是指程序执行中的一个特定点,它是能够应用通知(Advice)的地方。连接点表示的是一个方法的执行或一个方法调用,在 AOP 中,我们通过定义切入点(Pointcut)来指定在哪些连接点上应用通知。

连接点是 AOP 的一个核心概念,理解它有助于更好地掌握 AOP 的工作原理及如何通过切面(Aspect)对目标方法进行增强。

  1. 连接点的定义

连接点(JoinPoint)表示程序中的某个“执行点”,通常是一个方法的调用或执行。AOP 的核心目标之一是通过通知在这些连接点上添加增强逻辑。

  • 在 Spring AOP 中,连接点是方法的执行点
  • 通过切入点表达式,Spring 会识别哪些连接点需要应用通知。
  1. 连接点的特性
  • 方法执行:连接点通常与目标对象的方法执行相关,代表的是方法调用的时机。
  • 方法签名:连接点包含了方法签名(包括方法名、参数类型等)以及方法执行时的上下文。
  • 连接点信息:在通知方法中,Spring 提供了 JoinPointProceedingJoinPoint 参数,可以让你获取关于目标方法的详细信息,如方法名称、参数、目标对象等。
  1. 连接点的类型

在 Spring AOP 中,连接点主要与以下两个方面有关:

  • 方法执行:这是最常见的连接点类型,指的是方法的调用或执行。
  • 构造器执行(仅在 AspectJ 中支持):指的是构造函数的执行。
  1. JoinPoint 接口

JoinPoint 是一个接口,Spring AOP 会在通知方法中注入一个 JoinPoint 对象,允许开发者获取与当前方法执行相关的信息。常用的 JoinPoint 方法如下:

  • getSignature():获取当前连接点的方法签名,返回一个 Signature 对象。Signature 包含了方法的名称、返回类型、参数类型等信息。
  • getArgs():返回方法的参数列表,类型为 Object[],包含了目标方法执行时传入的参数。
  • getTarget():返回当前连接点对应的目标对象(即目标类的实例)。
  • getThis():返回当前连接点的代理对象,若是基于 JDK 动态代理,则是接口代理对象;若是基于 CGLIB 代理,则是目标类的代理对象。
  1. ProceedingJoinPoint 接口

ProceedingJoinPointJoinPoint 的一个子接口,继承了 JoinPoint,并增加了 proceed() 方法。proceed() 方法用于控制目标方法的执行,可以在 @Around 通知中使用它来执行目标方法,并且可以在执行目标方法前后做一些处理。

  • proceed():继续执行目标方法。它的返回值是目标方法的返回值,可以在环绕通知中通过它来决定是否执行目标方法。如果不调用 proceed(),目标方法将不会执行。

示例

@Around("execution(* com.example.service.UserService.*(..))")
public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    System.out.println("Before method: " + proceedingJoinPoint.getSignature().getName());
    
    // 执行目标方法
    Object result = proceedingJoinPoint.proceed();
    
    System.out.println("After method: " + proceedingJoinPoint.getSignature().getName());
    return result;
}

☘最后回到当前项目中,看如何通过使用上面的知识点来实现公共字段填充:

项目切面类代码如下:

package com.sky.aspect;

import com.fasterxml.jackson.databind.ser.Serializers;
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.apache.poi.hssf.record.DVALRecord;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
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 {
    /**
     * 切入点
     * 只需要对update和insert操作进行拦截
     */
    @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 operation = 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(operation == 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(operation == 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();
            }

        }
    }
}

🐟注:这里面结合了反射的知识完成了对四个常用属性的赋值。

3 . 应用自定义注解

因为当时是定义了注解只能作用到方法上,因此在代码中,例如新增员工操作Mapper层代码中使用到了:

    /**
     * 新增员工
     * @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);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学习两年半的Javaer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值