一、引言
我们在业务处理时常用拦截器(Interceptor)拦截请求,执行一些额外的逻辑(如JWT令牌生成校验),但拦截器是一种轻量级的拦截机制,不适合对一些复杂的业务逻辑进行操作。由是,我们引入更适合处理方法调用、异常抛出等业务逻辑的SpringAOP。
二、Spring AOP简介
1.简介
Spring AOP(Aspect-Oriented Programming,面向切面编程) 是 Spring 框架中的一个重要功能,用于将横切关注点(如日志记录、事务管理、安全性等)与业务逻辑分离,从而提高代码的可维护性和可重用性。
这么说可能让第一次接触的朋友摸不着头脑,不过没关系,我们这部分主要领大家简单熟悉一下,可以直接跳至第三大部分,遇到不懂的概念再随时回头看。
2.核心概念
-
切面(Aspect):
-
一个模块化的横切关注点。例如,日志记录或事务管理可以作为一个切面。
-
在 Spring 中,切面通常是一个带有
@Aspect
注解的类。
-
-
连接点(Join Point):
-
程序执行过程中的一个点,例如方法调用或异常抛出。
-
在 Spring AOP 中,连接点通常是方法调用。
-
-
通知(Advice):
-
在连接点上执行的动作。通知可以是前置通知、后置通知、环绕通知等。
-
在 Spring 中,通知通常是一个带有特定注解的方法。
-
-
切入点(Pointcut):
-
定义通知应该在哪些连接点上执行的规则。
-
在 Spring 中,切入点通常是一个带有
@Pointcut
注解的方法。
-
-
目标对象(Target Object):
-
被通知的对象,即业务逻辑类。
-
-
代理对象(Proxy):
-
由 Spring AOP 创建的对象,用于在目标对象上调用通知。
-
三、入门案例
经过上面的简介相信大家对SpringAOP已经有了一定的见解,接下来我们结合案例深入理解下这些概念。
我们以性能监控为案例,假设我们需要临时对一些已经写好的方法进行执行时间的监控,初学者能想到的办法大概是再方法开始和结束时记录时间并运算,这样的思路是对的,可是诸多方法一一修改不单开发费力,代码也不优雅,维护亦不方便。
那有没有什么优雅的方法,有的兄弟有的,就是大名鼎鼎的SpringAOP,接下来就不卖关子直接展示展示了。
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.切面类定义
// 引入必要的注解和类
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;
// 定义一个切面类,使用 @Aspect 注解标记
@Aspect
// 使用 @Component 注解将该切面类注册为 Spring 容器中的一个 Bean
@Component
public class PerformanceAspect {
// 定义一个环绕通知,使用 @Around 注解指定切入点表达式
// 切入点表达式 "execution(* com.example.service.*.*(..))" 表示匹配 com.example.service 包下所有类的所有方法
@Around("execution(* com.example.service.*.*(..))")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
// 记录方法开始执行的时间
long startTime = System.currentTimeMillis();
// 调用目标方法(即被拦截的方法),通过 ProceedingJoinPoint 的 proceed() 方法
// 这里是环绕通知的核心,它允许我们控制目标方法的执行,并在执行前后添加自定义逻辑
Object result = joinPoint.proceed();
// 记录方法执行结束的时间
long endTime = System.currentTimeMillis();
// 计算方法执行所花费的时间
long duration = endTime - startTime;
// 获取被拦截方法的名称
String methodName = joinPoint.getSignature().getName();
// 打印方法执行时间的日志信息
System.out.println("Method " + methodName + " 用时" + duration + "ms");
// 返回目标方法的执行结果
return result;
}
}
很简单明了的一个类,大家可以自行在合适的包下写方法用此类测试,在这个由@Aspect注解的切面类中我们的额外逻辑(此处即为用时监控)被集中管理在一起。
- @Around即环绕通知注解,他允许我们在目标方法执行前后添加自定义逻辑,并且可以控制目标方法是否执行,相信大家也能想到@Before和@After等通知注解的意义了吧,详细内容可自行查阅;
- 切入点(Pointcut)即用于匹配连接点的条件,这里@Around中定义的切入点表达式"execution(*com.example.service.*.*(..))"即一种定义方式,另外切入点表达式在比较复杂时可以通过@Pointcut注解声明在通知注解外部,后文还会介绍这种方式和另一种注解定义的方式;
- joinPoint即连接点 ,它表示程序执行过程中的一个连接点,通常是方法的调用,在 AOP 中,
JoinPoint
提供了关于当前被拦截方法的详细信息(此处我们用的proceed方法就是执行目标方法的代理,关于ProceedingJoinPoint的诸多属性基本都是见文知意,这里就不展开介绍了); - 切面即描述切入点与通知的对应关系,此处PerformanceAspect即可被称为切面。
3.业务中的应用实例
可能有朋友好奇,我们上面做的拦截器(Interceptor)也可以做呀?确实也可以,但正如我前文所说,拦截器是一种轻量级的拦截机制,只适合简单场景,上面的入门案例虽然十分简单,但Interceptor却需实现 MethodInterceptor
或 MethodInvocation
来拦截方法并且要自己创建代理对象,更不必说面对复杂的业务逻辑了。
下面我们介绍一种场景,在实际开发中我们对数据库进行CRUD时常需要对操作时间等字段进行相同操作,为了减少重复代码,提高开发效率,我们将使用AOP来进行字段自动填充。
import com.cds.annotation.AutoFill;
import com.cds.constant.AutoFillConstant;
import com.cds.context.BaseContext;
import com.cds.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 // 将该切面类注册为 Spring 容器中的一个 Bean
@Slf4j // 使用 Lombok 提供的日志注解
public class AutoFillAspect {
/**
* 定义切入点,指定哪些方法会被拦截
*/
@Pointcut("execution(* com.cds.mapper.*.*(..)) && @annotation(com.cds.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(); // 当前用户 ID
// 根据不同操作类型,为对应的属性通过反射赋值
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 (Exception e) {
throw new RuntimeException(e); // 如果发生异常,抛出运行时异常
}
} else 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 (Exception e) {
throw new RuntimeException(e); // 如果发生异常,抛出运行时异常
}
}
}
}
通过这个切面类我们可以实现对新增和修改两种操作的字段自动补全 ,那他是如何识别两种不同操作的呢,这里就用到了我们所说的另一种切入点注解方式@annotation。
首先我们定义了一个枚举类用于在代码中明确区分不同的操作(如插入和更新)。
package com.cds.enumeration;
/**
* 数据库操作类型
*/
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 插入操作
*/
INSERT
}
其次我们通过自定义注解@AutoFill标记需要切入的方法,并通过value属性指定方法对应的操作类型(如 INSERT
或 UPDATE
)。
package com.cds.annotation;
import com.cds.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();//定义了一个属性 value,其类型为 OperationType 枚举。这个属性用于指定方法对应的操作类型(如 INSERT 或 UPDATE)
}
最后在切面类中根据不同操作类型,为对应的属性通过反射赋值。
好了本次分享到此结束,感谢大家阅读。