目录
🍃作者介绍:双非本科大三网络工程专业在读,阿里云专家博主,专注于Java领域学习,擅长web应用开发、数据结构和算法,初步涉猎Python人工智能开发和前端开发。
🦅主页:@逐梦苍穹📕您的一键三连,是我创作的最大动力🌹
1、AOP 相关概念
本文利用AOP实现数据库表单公共字段填充
Spring 的 AOP 实现底层就是对上面的动态代理的代码进行了封装,封装后我们只需要对需要关注的部分进行代码编写,并通过配置的方式完成指定目标的方法增强。
在正式讲解 AOP 的操作之前,我们必须理解 AOP 的相关术语,常用的术语如下:
- Target(目标对象):代理的目标对象
- Proxy (代理):一个类被 AOP 织入增强后,就产生一个结果代理类
- Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点
- Pointcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义
- Advice(通知/ 增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知
- Aspect(切面):是切入点和通知(引介)的结合
- Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入
2、公共字段填充
2.1、为什么需要
在一个项目中,会涉及到多个表单,而这些表单当中,可能会存在着一些通用的字段。
(比如表单中某条数据的创建时间/修改时间、数据的创建者/修改者等)
在这个例子中,用到的是黑马苍穹外卖项目中的员工表单进行举例说明。
在这个表单中,存在着create_time、update_time、create_user、update_user这四个公共字段。
这类公共字段的数据填充,往往是用着相同的代码,这部分代码没必要重复写,如:
所以这里采用Spring结合AOP动态完成这四个字段的填充
2.2、自定义注解
2.2.1、元注解
在编写自定义注解代码之前,需要先复习一下元注解的内容:
元注解:注解注解的注解。
元注解有两个:
① @Target: 约束自定义注解只能在哪些地方使用
② @Retention:申明注解的生命周期
@Target中可使用的值定义在ElementType枚举类中,常用值如下:
TYPE,类,接口FIELD, 成员变量
METHOD, 成员方法
PARAMETER, 方法参数
CONSTRUCTOR, 构造器
LOCAL_VARIABLE, 局部变量
@Retention中可使用的值定义在RetentionPolicy枚举类中,常用值如:
SOURCE: 注解只作用在源码阶段,生成的字节码文件中不存在
CLASS: 注解作用在源码阶段,字节码文件阶段,运行阶段不存在,默认值.
RUNTIME:注解作用在源码阶段,字节码文件阶段,运行阶段(开发常用)
2.2.2、AutoFill.java
这里需要定义一个自动填充的注解AutoFill.java(接口类型):
这个注解的作用是声明在对应的mapper上,表示这个mapper调用的时候需要自动填充对应的字段。
自动填充的注解的代码AutoFill.java如下:
操作类型OperationType.java如下:
解释:
这段代码定义了一个自定义注解 AutoFill,用于标记在方法上。让我们逐一解释注解的各个部分:
- @Target(ElementType.METHOD): 这是一个元注解,它用于标注自定义注解可以使用的地方。在这里,AutoFill 注解只能用于方法上。也就是说,你只能在方法上使用 @AutoFill 注解。
- @Retention(RetentionPolicy.RUNTIME): 这是另一个元注解,它用于指定自定义注解的保留策略。RetentionPolicy.RUNTIME 表示注解会在运行时保留,因此可以通过反射在运行时获取到注解的信息。
- public @interface AutoFill: 这定义了一个注解类型。关键字 interface 表示定义一个注解。注解名为 AutoFill。
- OperationType value(): 这是注解的成员,被称为注解属性。在这里,AutoFill 注解有一个名为 value 的属性,其类型是 OperationType。注解属性的类型可以是任何基本数据类型、字符串、枚举、注解或上述类型的数组。
总体而言,AutoFill 注解用于标记方法,并且具有一个属性 value,该属性用于指定数据库操作类型。在使用 @AutoFill 注解时,你需要为 value 属性提供一个 OperationType 的值。
2.3、Aspect切面
有了自定义注解来标记哪个方法被调用时需要自动填充之后,就需要来实现自动填充功能了。
这里需要用到AOP思想中的切面,把切入点和通知结合起来。
通俗来说:
切入点:需要拦截的方法;
通知:拦截后需要干的事情
下面开始编写切面类AutoFillAspect.java:
①创建类,加上@Component(IOC控制反转)、@Aspect(定义切面)和@Slf4j(日志)注解
②抽取切点表达式
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
解释说明:
这个切点表达式由两部分组成,使用了逻辑运算符 && 连接起来,表示同时满足两个条件:
- execution(* com.sky.mapper.*.*(..)): 这部分表示切点的方法执行匹配规则。具体来说,它匹配了 com.sky.mapper 包下的任意类的任意方法(*.*(..)表示任意方法,而 * 表示任意类名)。
- @annotation(com.sky.annotation.AutoFill): 这部分表示切点的注解匹配规则。它要求被切点选中的方法上必须有 com.sky.annotation.AutoFill 注解。
综合起来,autoFillPointCut 切点选择了 com.sky.mapper 包下的所有带有 @AutoFill 注解的方法。这样定义的切点通常用于在带有特定注解的方法上执行额外的逻辑,例如在方法执行前后进行日志记录、权限校验等操作。
③定义前置通知
这里的前置通知引用了表达式方法,等价于:
@Before("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
④完善前置通知
这个部分总共分为以下几步:
- 获取方法对象签名
- 获得方法上的注解对象
- 获得数据库操作类型
- 获取到当前被拦截的方法的参数
- 通过反射为对象属性赋值
2.4、完整代码
package com.sky.aspect;
import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.constant.MessageConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import com.sky.exception.AutoFillDatabasePublicFieldException;
import lombok.extern.slf4j.Slf4j;
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.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
/**
* 自定义切面类,实现公共字段自动填充逻辑
*
* @author 逐梦苍穹
* @date 2023/10/8 22:06
*/
@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 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){
throw new AutoFillDatabasePublicFieldException(MessageConstant.AUTO_FILL_ARGS_EXCEPTION);
}
/*
* 约定参数第一个为实体对象
*/
Object entity = args[0];
//准备好赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//根据当前拦截到注解配置的不同的操作类型,为对应的属性通过反射机制来完成赋值
if (operationType == OperationType.INSERT){
/*
* 为四个公共字段赋值
*/
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 (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}
}
if (operationType == OperationType.UPDATE){
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 (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
3、JoinPoint
JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象。
常用api:
方法名 | 功能 |
Signature getSignature(); | 获取封装了署名信息的对象, 在该对象中可以获取到目标方法名,所属类的Class等信息 |
Object[] getArgs(); | 获取传入目标方法的参数对象 |
Object getTarget(); | 获取被代理的对象 |
Object getThis(); | 获取代理对象 |