Spring AOP
2998字约10分钟
2024-08-08
在系统开发中,我们可能会碰到一些需求,比如记录操作日志、权限校验等,当然,你可以在每个接口请求的方法中调用公共方法来做这些事情,但是记录日志、权限校验这些和业务是没有耦合的,因此,我们需要将这些操作与业务代码分离开来,我们可以通过 AOP
+ 自定义注解 的方式,灵活的为特定接口进行增强
实战内容请移步 SpringAOP实战
AOP 理解
AOP(Aspect Oriented Programming)
,面向切面思想,是 Spring
的两大核心思想(AOP
-面向切面编程、IOC
-控制反转)之一。需要注意的是,Spring
的 AOP
只能支持到方法级别的切入,即:切入点只能是某个方法
简单的理解,实现 AOP
要做三类事:
切入点:在哪里切入,也就是记录日志、权限校验等非业务操作在哪些业务代码中执行
切入时间:在什么时候切入,是在业务代码执行前还是执行后
切入处理:切入之后做什么事,比如记录日志、权限校验等
AOP
要做的事可以梳理为下图:
部分概念解释:
Pointcut
:切点,记录日志、权限校验等逻辑在何处切入业务代码中(即织入切面)execution
切点方式:用路径表达式指定哪些类织入切面annotation
切点方式:指定被哪些注解修饰的代码织入切面
Advice
:处理处理时机:在什么时机执行处理内容,分为前置处理(业务代码执行前)、后置处理(业务代码执行后)等
处理内容:要做的处理内容,比如记录日志、权限校验
Aspect
:切面,即Pointcut
和Advice
Joint point
:连接点,程序执行的一个点。在Spring AOP
中,一个连接点总是代表一个方法执行Weaving
:织入,就是通过动态代理,在目标对象方法中执行处理内容的过程
AOP 注解
@Aspect
作用于类上,将类标识为一个切面
@Pointcut
@Pointcut
注解指定一个切点,定义需要拦截的内容,有以下两个常用表达式
execution()
表达式,execution(* com.marui.controller..*.*(..)))
表示拦截com.marui.controller
包下所有类中所有方法annotation()
表达式,@annotation(org.springframework.web.bind.annotation.GetMapping)
表示拦截所有被@GetMapping
注解修饰的方法
// 所有返回类型 拦截的包名 所有类 所有方法(所有参数)
@Pointcut("execution(* com.marui.controller..*.*(..)))")
public void allRequestPointCut(){
}
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void getRequestPointCut(){
}
@Around
@Around
注解用于修饰 Around
增强处理
@Around
可以自由选择增强动作与目标方法的执行顺序,这个特性的实现在于,调用ProceedingJoinPoint
参数的proceed()
方法才会执行目标方法@Around
可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值
@Around
功能虽然强大,但通常需要在线程安全的环境下使用。因此,如果使用Before
、AfterReturning
就能解决的问题,就没有必要使用Around
。如果需要目标方法执行之前和之后共享某种状态数据,则应该考虑使用Around
。尤其是需要使用增强处理阻止目标的执行,或需要改变目标方法的返回值时,则只能使用Around
增强处理了
@Around("getRequestPointCut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Get请求拦截,Around 开始增强处理");
Object proceed = joinPoint.proceed();
log.info("Get请求拦截,Around 结束增强处理");
return proceed;
}
@Before
@Before
注解修饰的方法在切面切入目标方法之前执行
@Before("getRequestPointCut()")
public void doBefore(JoinPoint joinPoint) {
log.info("Get请求拦截,方法执行前");
}
@After
@After
注解和 @Before
注解是相对应的,@After
注解修饰的方法在切面切入目标方法之后执行
@After("getRequestPointCut()")
public void doAfter(JoinPoint joinPoint) {
log.info("Get请求拦截,方法执行后");
}
@AfterReturning
@AfterReturning
注解和 @After
有些类似,区别在于 @AfterReturning
注解可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理
@AfterReturning(pointcut = "getRequestPointCut()", returning = "result")
public void doAfterReturning(JoinPoint joinPoint, Object result) {
Signature signature = joinPoint.getSignature();
String classMethod = signature.getName();
log.info("Get请求拦截,方法 {} 执行后,返回值:{}", classMethod, result);
}
需要注意的是,在
@AfterReturning
注解中,属性returning
的值必须要和参数保持一致,否则会检测不到。该方法中的第二个入参就是被切方法的返回值,在doAfterReturning
方法中可以对返回值进行增强,可以根据业务需要做相应的封装
@AfterThrowing
当被切方法执行过程中抛出异常时,会进入 @AfterThrowing
注解的方法中执行,在该方法中可以做一些异常的处理逻辑
@AfterThrowing(pointcut = "getRequestPointCut()", throwing = "exception")
public void doAfterThrowing(JoinPoint joinPoint, Throwable exception) {
Signature signature = joinPoint.getSignature();
String classMethod = signature.getName();
log.info("Get请求拦截,方法 {} 执行出错,异常为:{}", classMethod, exception.getMessage());
}
要注意的是
throwing
属性的值必须要和参数一致,否则会报错。该方法中的第二个入参即为抛出的异常
ProceedingJoinPoint
JoinPoint
对象封装了 Spring Aop
中切面方法的信息,在切面方法中添加 JoinPoint
参数,就可以获取到封装了该方法信息的 JoinPoint
对象
@Before("getRequestPointCut()")
public void doBefore(JoinPoint joinPoint) {
log.info("Get请求拦截,方法执行前");
Object[] args = joinPoint.getArgs();
log.info("====>JoinPoint 获取参数:{}", JSONArray.toJSONString(args));
Object target = joinPoint.getTarget();
log.info("====>JoinPoint 被代理目标对象:{}", target);
Object proxyObject = joinPoint.getThis();
log.info("====>JoinPoint 代理对象:{}", proxyObject);
// 封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
Signature signature = joinPoint.getSignature();
log.info("====>JoinPoint 封装的署名信息的对象:{}", JSONObject.toJSONString(signature));
String name = signature.getName();
log.info("====>JoinPoint signature 目标方法名:{}", name);
String declaringTypeName = signature.getDeclaringTypeName();
log.info("====>JoinPoint signature 目标方法所属类的类名:{}", declaringTypeName);
Class declaringType = signature.getDeclaringType();
log.info("====>JoinPoint signature 目标方法所属类的类型名:{}", declaringType);
String simpleName = declaringType.getSimpleName();
log.info("====>JoinPoint signature 目标方法所属类的简单类名:{}", simpleName);
int modifiers = declaringType.getModifiers();
log.info("====>JoinPoint signature 目标方法声明类型:{},{}", modifiers, Modifier.toString(modifiers));
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String url = request.getRequestURL().toString();
log.info("====>请求地址:{}", url);
String ip = request.getRemoteAddr();
log.info("====>请求ip:{}", ip);
}
切面注解执行顺序
如果一个接口想设置多个切面类进行不同的增强怎么办?这些切面的执行顺序如何管理?
1、可以定义多个自定义注解,分别对应不同的切面类
2、一个自定义的注解可以对应多个切面类
切面类执行顺序由 @Order
注解管理,该注解后的数字越小,所在切面类越先执行
@Aspect
@Component
@Order(0)
public class GetRequestAspect {
}
@Aspect
@Component
@Order(1)
public class AllRequestAspect {
}
Java 注解基础
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogRecord {
}
元注解
元注解是专门用来注解其他注解的注解,实际上就是专门为自定义注解提供的注解。java.lang.annotation
提供了五种元注解
@Documented
:注解是否要包含在JavaDoc
中@Retention
:定义被它注解的注解保留多久,一共有source
、class
、runtime
这三种策略@Target
:描述注解的使用范围(即被描述的注解可以用在什么地方)@Inherited
:是否允许子类继承该注解,被此注解修饰的注解,作用于某个类上,其子类可以继承该注解@Repeatable
:是否可重复注解,jdk1.8
引入
生命周期
通过 @Retention
定义注解的生命周期
@Retention(RetentionPolicy.SOURCE)
RetentionPolicy
的不同策略对应不同的生命周期
RetentionPolicy.CLASS
:默认策略,在class
字节码文件中存在,在类加载时被丢弃,运行时无法获取到RetentionPolicy.SOURCE
:仅存在于源代码中,编译阶段就会被丢弃,不会包含在class
字节码文件中,例如@Override
、@SuppressWarnings
这类注解RetentionPolicy.RUNTIME
:运行期,始终不会丢弃,可以使用反射获得该注解的信息,自定义注解最常用的策略
作用目标
@Target({ElementType.METHOD})
TYPE
:Class, interface (including annotation type), or enum declaration(用于描述类、接口(包括注解类型) 或enum
声明)FIELD
:Field declaration (includes enum constants) (用于描述域)METHOD
:Method declaration(用于描述方法)PARAMETER
:Formal parameter declaration(用于描述参数)CONSTRUCTOR
:Constructor declaration(用于描述构造器)LOCAL_VARIABLE
:Local variable declaration(用于描述局部变量)ANNOTATION_TYPE
:Annotation type declaration(用于描述注解)PACKAGE
:Package declaration(用于描述包)TYPE_PARAMETER
:Type parameter declaration(用来标注类型参数)TYPE_USE
:Use of a type(能标注任何类型名称)
浅浅的试一试
1、依赖引入
在 Spring Boot
项目中,pom.xml
文件只引入 spring-boot-starter-aop
即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.33</version>
</dependency>
2、自定义注解
/**
* @ClassName LogRecord
* @Desciption 日志操作记录注解
* @Author MaRui
* @Date 2023/2/27 16:30
* @Version 1.0
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogRecord {
String content();
}
3、测试接口
/**
* @ClassName OrderController
* @Desciption 订单控制层
* @Author MaRui
* @Date 2023/2/27 16:24
* @Version 1.0
*/
@Slf4j
@RestController
@RequestMapping(value = "/order")
public class OrderController {
@LogRecord(content = "查询订单信息")
@GetMapping("/getOne")
public ResultDTO getOne(@RequestParam String orderNo) {
log.info("/getOne 请求执行");
return ResultDTO.ok(orderNo);
}
}
4、AOP 切面逻辑
定义一个切面类需要如下三步:
1、在类上使用
@Aspect
注解,将标识的类作为一个切面容器读取2、在类上使用
@Component
注解,交由Spring
管理3、在类中定义接受通知的方法,例如
logRecordPointCut()
/**
* @ClassName LogRecordAspect
* @Desciption 日志记录拦截切面
* @Author MaRui
* @Date 2023/2/27 16:45
* @Version 1.0
*/
@Aspect
@Component
public class LogRecordAspect {
private static final Logger log = LoggerFactory.getLogger(LogRecordAspect.class);
@Pointcut("@annotation(com.marui.common.LogRecord)")
public void logRecordPointCut(){
}
@Before("logRecordPointCut()")
public void doBefore(JoinPoint joinPoint) {
log.info("====>日志记录 doBefore");
Object[] args = joinPoint.getArgs();
log.info("====>JoinPoint 获取参数:{}", JSONArray.toJSONString(args));
Object target = joinPoint.getTarget();
log.info("====>JoinPoint 被代理目标对象:{}", target);
Object proxyObject = joinPoint.getThis();
log.info("====>JoinPoint 代理对象:{}", proxyObject);
// 封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
Signature signature = joinPoint.getSignature();
log.info("====>JoinPoint 封装的署名信息的对象:{}", JSONObject.toJSONString(signature));
String name = signature.getName();
log.info("====>JoinPoint signature 目标方法名:{}", name);
String declaringTypeName = signature.getDeclaringTypeName();
log.info("====>JoinPoint signature 目标方法所属类的类名:{}", declaringTypeName);
Class declaringType = signature.getDeclaringType();
log.info("====>JoinPoint signature 目标方法所属类的类型名:{}", declaringType);
String simpleName = declaringType.getSimpleName();
log.info("====>JoinPoint signature 目标方法所属类的简单类名:{}", simpleName);
int modifiers = declaringType.getModifiers();
log.info("====>JoinPoint signature 目标方法声明类型:{},{}", modifiers, Modifier.toString(modifiers));
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String url = request.getRequestURL().toString();
log.info("====>请求地址:{}", url);
String ip = request.getRemoteAddr();
log.info("====>请求ip:{}", ip);
}
@Around("logRecordPointCut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("====>日志记录 Around,环绕");
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
LogRecord logRecordAnnotation = method.getAnnotation(LogRecord.class);
String content = logRecordAnnotation.content();
log.info("====>日志记录 Around,注解:{},方法:{}, 注解内容:{}", logRecordAnnotation, method, content);
return joinPoint.proceed();
}
@After("logRecordPointCut()")
public void doAfter(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String classMethod = signature.getName();
log.info("====>日志记录 After,方法 {} 执行后", classMethod);
}
@AfterReturning(pointcut = "logRecordPointCut()", returning = "result")
public void doAfterReturning(JoinPoint joinPoint, Object result) {
Signature signature = joinPoint.getSignature();
String classMethod = signature.getName();
log.info("====>日志记录 AfterReturning,方法 {} 执行后,返回值:{}", classMethod, result);
}
}
5、效果
请求接口:http://127.0.0.1:8080/order/getOne?orderNo=M20230701001,日志打印效果如下
LogRecordAspect : ====>日志记录 Around,环绕
LogRecordAspect : ====>日志记录 Around,注解:@com.marui.common.LogRecord(content=查询订单信息),方法:public com.marui.common.ResultDTO com.marui.controller.OrderController.getOne(java.lang.String), 注解内容:查询订单信息
LogRecordAspect : ====>日志记录 doBefore
LogRecordAspect : ====>JoinPoint 获取参数:["M20230701001"]
LogRecordAspect : ====>JoinPoint 被代理目标对象:com.marui.controller.OrderController@45ffe7ed
LogRecordAspect : ====>JoinPoint 代理对象:com.marui.controller.OrderController@45ffe7ed
LogRecordAspect : ====>JoinPoint 封装的署名信息的对象:{"declaringType":"com.marui.controller.OrderController","declaringTypeName":"com.marui.controller.OrderController","exceptionTypes":[],"method":{"declaringClass":"com.marui.controller.OrderController","name":"getOne","parameterTypes":["java.lang.String"]},"modifiers":1,"name":"getOne","parameterNames":["orderNo"],"parameterTypes":["java.lang.String"],"returnType":"com.marui.common.ResultDTO"}
LogRecordAspect : ====>JoinPoint signature 目标方法名:getOne
LogRecordAspect : ====>JoinPoint signature 目标方法所属类的类名:com.marui.controller.OrderController
LogRecordAspect : ====>JoinPoint signature 目标方法所属类的类型名:class com.marui.controller.OrderController
LogRecordAspect : ====>JoinPoint signature 目标方法所属类的简单类名:OrderController
LogRecordAspect : ====>JoinPoint signature 目标方法声明类型:1,public
LogRecordAspect : ====>请求地址:http://127.0.0.1:8080/order/getOne
LogRecordAspect : ====>请求ip:127.0.0.1
OrderController : ====>/getOne 请求执行
LogRecordAspect : ====>日志记录 AfterReturning,方法 getOne 执行后,返回值:ResultDTO(code=000000, msg=Success, data=M20230701001)
LogRecordAspect : ====>日志记录 After,方法 getOne 执行后