介绍
Spring AOP(面向切面编程)是 Spring 框架提供的一个重要功能,用于实现横切关注点的模块化,AOP 允许你将业务逻辑分离出横切关注点(如日志记录、事务管理等),这些横切关注点可以跨越多个类、对象,形成一个独立的模块。通过 AOP,你可以在不修改原始代码的情况下,将横切关注点应用到程序中。
概念
- 切面(Aspect):
定义:切面是一组横切关注点的模块化单元,它包含了横切关注点以及它们在目标对象中的应用位置。
应用:切面通常以切面类的形式存在,在 Spring AOP 中,切面类使用特定的注解来标识,比如@Aspect注解,切面类中可以包含通知(Advice)和切点(Pointcut)。
- 通知(Advice):
定义:通知是切面的具体行为,定义了在什么时候、在哪里以及如何应用切面逻辑。
应用:通知可以使用不同的注解来标识,具体取决于通知的类型。
类型 | 注解 | 使用场景举例 |
---|---|---|
前置通知 | @Before | 参数校验、权限检查 |
返回通知 | @AfterReturning | 资源释放、日志记录 |
异常通知 | @AfterThrowing | 事务回滚、错误处理 |
后置通知 | @After | 资源清理、日志记录 |
环绕通知 | @Around | 接口防重复提交、性能监控 |
- 切点(Pointcut):
定义:切点是在应用程序中选择连接点的特定点集合,连接点可以是方法调用、方法执行、异常处理等。
应用:切点通过表达式语言定义,以指定要在哪些连接点应用通知,在 Spring AOP 中,切点可以作为切面类中的一个方法,并使用@Pointcut注解标识。
- 连接点(Join point):
定义:连接点是在应用程序执行过程中可能与通知相关的点,在 Spring AOP 中,连接点通常是方法的执行点。
应用:连接点是AOP的基本概念,但在具体编程中不直接指定连接点,连接点是由切点和被代理的类共同确定的。
- 织入(Weaving):
定义:织入是将切面应用到目标对象并创建新的代理对象的过程。
应用:在 Spring AOP 中,织入可以在编译时、类加载时或运行时进行,Spring AOP 通常采用运行时织入的方式,它通过动态代理实现切面逻辑的织入。在 Spring 中,织入通常通过配置来完成,无需显式编写代码。
简单案例
案例结构
其中 MethodRuntimeLogger 为自定义注解类,MethodRuntimeAspect 为自定义切面类,这个案例是获取方法或者请求的运行时长。
具体代码
记得引入 Spring AOP 的 maven 依赖哦~
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
自定义注解类
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 用于标记方法,以便在运行时记录其执行时长
* 该注解应用于方法上,以便在方法执行前后记录其运行时长
*
* @author b16mt
*/
@Target(ElementType.METHOD) // 用于指定该注解只能应用于方法上
@Retention(RetentionPolicy.RUNTIME) // 用于指定该注解在运行时保留
public @interface MethodRuntimeLogger {
}
除了 METHOD 外,@Target 注解还可以使用其他元素类型,如:
- ElementType.TYPE:可以应用于类、接口、枚举等
- ElementType.FIELD:可以应用于字段、属性
- ElementType.PARAMETER:可以应用于方法的参数
- ElementType.CONSTRUCTOR:可以应用于构造函数
- ElementType.LOCAL_VARIABLE:可以应用于局部变量
@Retention 注解则可以接受以下三个枚举值:
- RetentionPolicy.SOURCE:表示注解仅保留在源代码中,在编译时会被丢弃,意味着在编译后的 .class 文件中不会存在该注解
- RetentionPolicy.CLASS:表示注解会被保留到编译后的 .class 文件中,但在运行时会被丢弃,意味着在运行时无法通过反射获取到该注解信息
- RetentionPolicy.RUNTIME:表示注解会被保留到运行时,可以通过反射来获取该注解信息,因此在运行时可以处理该注解
自定义切面类
package com.xjl.sys.method.aspect;
import com.xjl.sys.method.annotation.MethodRuntimeLogger;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* 切面类,用于记录方法的运行时长
*
* @author b16mt
*/
@Slf4j
@Aspect // 标识为切面类,用于定义横切关注点的模块化单元
@Component // 标识为 Spring 组件,以便在 Spring 上下文中自动扫描并加载
public class MethodRuntimeAspect {
/**
* 环绕通知,捕获被 MethodRuntimeLogger 注解的方法,并记录其运行时长
*
* @param joinPoint 切入点,用于获取目标方法的相关信息
* @param methodRuntimeLogger MethodRuntimeLogger 注解,标记了需要记录时长的方法
* @return 目标方法的执行结果
* @throws Throwable 如果目标方法执行过程中发生异常,则抛出异常
*/
@Around("@annotation(methodRuntimeLogger)") // 这是一个环绕通知,切点为methodRuntimeLogger,代表拦截包含MethodRuntimeLogger的方法
public Object logMethodRuntime(ProceedingJoinPoint joinPoint, MethodRuntimeLogger methodRuntimeLogger) throws Throwable {
// 记录方法执行开始时间
long startTime = System.currentTimeMillis();
// 执行目标方法
Object result = joinPoint.proceed();
// 记录方法执行结束时间
long endTime = System.currentTimeMillis();
// 计算方法执行时长
long executionTime = endTime - startTime;
// 打印方法签名和执行时长
log.info(joinPoint.getSignature() + " 执行时长:" + executionTime + "ms");
// 返回目标方法的执行结果
return result;
}
}
上面 @Around 是环绕通知,可以理解为环绕通知是对目标方法的一种包裹,它完全控制了目标方法的执行过程,包括了执行前后的所有逻辑(就是我可以在执行目标方法前或后去执行我想执行的其他方法,比如上面代码的记录方法执行开始时间、记录方法执行结束时间)。
同理,@Before 前置通知可以在在执行目标方法前去执行我想执行的其他方法,就比如:
上面请求接收了前端发送过来的分页查询参数,我希望对它的参数合法性进行校验,比如页码需要大于0,下面这段代码就可以使用前置通知来实现了:
// 参数校验
if (invoiceFormPageVo.getPage() == null || invoiceFormPageVo.getPage() < 1) {
// 如果页码为null或小于1,将页码设为默认值1
invoiceFormPageVo.setPage(1);
}
if (invoiceFormPageVo.getPageSize() == null || invoiceFormPageVo.getPageSize() < 1) {
// 如果每页展示的最大条数为null或小于1,将其设为默认值10
invoiceFormPageVo.setPageSize(10);
}
总结
使用 Spring AOP 去增强方法时必须要同时创建自定义的注解和切面类,自定义注解上常用的注解为:@Target() 、@Retention() ,自定义切面类上常用的注解为:@Aspect、@Component、@Around()。