对于项目中多个地方都需要给实体类赋值操作,其中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之间的区别
- @Component
- 定义:
@Component
是一个类级别的注解,用于标记一个类作为 Spring 管理的 Bean。 - 用法:通常用于标记普通的 Java 类,Spring 会自动扫描并将其注册为 Spring 容器中的 Bean,前提是该类位于 Spring 容器的扫描路径下。
- 适用场景:适合用于创建组件类,通常不需要特别的配置。
示例:
@Component
public class MyService {
public void doSomething() {
System.out.println("Doing something");
}
}
在 Spring Boot 中,通常通过 @SpringBootApplication
注解自动开启组件扫描。
- @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
:环绕通知,可以在目标方法执行前后做一些操作,允许控制目标方法是否执行。
下面一一详细介绍一下各个通知:
- @Before(前置通知)
- 定义:在目标方法执行之前执行的通知。
- 触发时机:方法执行前,
@Before
注解标记的方法会先执行。 - 用法:适用于在方法执行之前需要做一些处理的场景,如日志记录、权限检查、输入验证等。
- 限制:无法修改目标方法的返回值。
示例:
@Before("execution(* com.example.service.UserService.*(..))")
public void beforeMethod(JoinPoint joinPoint) {
System.out.println("Before method: " + joinPoint.getSignature().getName());
}
- JoinPoint:表示连接点(即方法执行),可以用它获取方法的名称、参数等信息。
- @After(后置通知)
- 定义:在目标方法执行之后执行的通知,不论目标方法是否正常执行(包括是否抛出异常)。
- 触发时机:目标方法执行完成后,
@After
注解标记的方法会被触发。 - 用法:适用于清理工作,或者你需要在方法执行后做一些不依赖于方法执行结果的操作,比如关闭资源、记录日志等。
示例:
@After("execution(* com.example.service.UserService.*(..))")
public void afterMethod(JoinPoint joinPoint) {
System.out.println("After method: " + joinPoint.getSignature().getName());
}
- 注意:
@After
通知不区分方法是否抛出异常,它总是会被执行。
- @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
:指定方法返回值的变量名,用于获取目标方法的返回值。- 应用场景:例如,返回通知可以用来处理缓存,或者修改返回结果。
- @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
:指定抛出异常的变量名,用于捕获目标方法抛出的异常。- 应用场景:例如,在发生异常时记录日志、发送警告或执行一些恢复操作。
- @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)对目标方法进行增强。
- 连接点的定义
连接点(JoinPoint)表示程序中的某个“执行点”,通常是一个方法的调用或执行。AOP 的核心目标之一是通过通知在这些连接点上添加增强逻辑。
- 在 Spring AOP 中,连接点是方法的执行点。
- 通过切入点表达式,Spring 会识别哪些连接点需要应用通知。
- 连接点的特性
- 方法执行:连接点通常与目标对象的方法执行相关,代表的是方法调用的时机。
- 方法签名:连接点包含了方法签名(包括方法名、参数类型等)以及方法执行时的上下文。
- 连接点信息:在通知方法中,Spring 提供了
JoinPoint
或ProceedingJoinPoint
参数,可以让你获取关于目标方法的详细信息,如方法名称、参数、目标对象等。
- 连接点的类型
在 Spring AOP 中,连接点主要与以下两个方面有关:
- 方法执行:这是最常见的连接点类型,指的是方法的调用或执行。
- 构造器执行(仅在 AspectJ 中支持):指的是构造函数的执行。
- JoinPoint 接口
JoinPoint
是一个接口,Spring AOP 会在通知方法中注入一个 JoinPoint
对象,允许开发者获取与当前方法执行相关的信息。常用的 JoinPoint
方法如下:
- getSignature():获取当前连接点的方法签名,返回一个
Signature
对象。Signature
包含了方法的名称、返回类型、参数类型等信息。 - getArgs():返回方法的参数列表,类型为
Object[]
,包含了目标方法执行时传入的参数。 - getTarget():返回当前连接点对应的目标对象(即目标类的实例)。
- getThis():返回当前连接点的代理对象,若是基于 JDK 动态代理,则是接口代理对象;若是基于 CGLIB 代理,则是目标类的代理对象。
- ProceedingJoinPoint 接口
ProceedingJoinPoint
是 JoinPoint
的一个子接口,继承了 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);