AOP面向切面(方法)编程
快速入门:以下示例是计算DeptServiceImpl每一个方法执行的时间
package com.example.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect 标记这是一个切面类
@Component
public class MyAspect {
// 提取公共的切入点表达式,这里表明DeptServiceImpl类中的所有方法
@Pointcut("execution(* com.example.service.impl.DeptServiceImpl.*(..))")
private void pt() {
}
@Around("pt()")
public Object TimeAspect(ProceedingJoinPoint joinPoint) throws Throwable {
long begin = System.currentTimeMillis();
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
log.info(joinPoint.getSignature() + "执行耗时:{}", end - begin);
return result;
}
}
效果如下
我们创建一个切面类,当
目标对象(使用@Pointcut指定的对象)
的方法被调用时,AOP框架(如Spring AOP)会生成目标对象的代理对象(Proxy)
。这个代理对象会拦截对目标对象方法的调用,然后执行这个代理对象中的函数(称为通知
),这个代理对象中的函数就是我们在切面类中定义的函数(例如这里的TimeAspect
),当通过代理对象调用方法时,代理对象会先执行切面类中定义的通知(如前置通知
、后置通知
,环绕通知
等),这里的@Around
就是环绕通知,然后再执行目标对象的原方法。而不是直接调用我们原来的函数,这样就可以在原来的函数执行前后插入我们自己的代码,这就是AOP的原理
执行流程:调用者 -> 代理对象 -> 切面类中的通知 -> 原方法 -> 切面类中的通知 -> 返回结果
再例如下面这些通知
package com.example.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect // 标记这是一个切面类
@Component
public class AopTest{
// 提取公共的切入点表达式,这里表明DeptServiceImpl类中的所有方法
@Pointcut("execution(* com.example.service.impl.DeptServiceImpl.*(..))")
private void pointcut() {
}
@Around("pointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("around环绕通知");
return joinPoint.proceed();
}
@Before("pointcut()")
public void beforeAdvice(JoinPoint joinPoint) {
// 方法执行前的逻辑
log.info("before前置通知");
}
@After("pointcut()")
public void afterAdvice(JoinPoint joinPoint) {
// 方法执行后的逻辑
log.info("after后置通知");
}
@AfterReturning("pointcut()")
public void afterReturningAdvice(JoinPoint joinPoint) {
// 方法返回后的逻辑
log.info("afterReturning返回通知");
}
@AfterThrowing("pointcut()")
public void afterThrowingAdvice(JoinPoint joinPoint) {
// 方法抛出异常后的逻辑
log.info("afterThrowing异常通知");
}
}
一、通知类型
AOP提供了五种通知方式,分别是:
@Around
:环绕通知,在目标方法执行前后都执行,这里的前后通知不是执行两次,而是在Object result = joinPoint.proceed()前后都能添加逻辑,比较特殊@Before
:前置通知,在目标函数执行前被执行@After
:后置通知,在目标函数执行后执行,不论目标方法是否正常返回或抛出异常都会执行。@AfterReturning
:后置通知,在目标函数正常返回时(不报错)才执行,如果目标函数报错了就不执行@AfterThrowing
: 后置通知,只有在目标函数报错时才执行,如果不报错就不执行,与@AfterReturning
相反
二、通知顺序
如果有多个切面类,则按切面类名排序
- 前置通知:字母排序靠前的先执行
- 后置通知:字母靠前的后执行
可以这么理解,这就是一个栈,排序靠前的先进栈,然后后出站,先进后出
- 使用
@Order(数字)
放在切面类上来控制顺序
@Order(2)
@Slf4j
@Aspect // 标记这是一个切面类
@Component
public class AopTest
@Order(1)
@Slf4j
@Aspect // 标记这是一个切面类
@Component
public class BopTest
三、切入点表达式
- 基本概念
使用示例:
@Pointcut("execution(* com.example.service.*.*(..))")
public void allMethodsInServicePackage() {
// 切入点表达式匹配 com.example.service 包下的所有类的所有方法
}
@Pointcut("execution(* com.example.service.MyService.*(..))")
public void allMethodsInMyService() {
// 切入点表达式匹配 com.example.service.MyService 类中的所有方法
}
@Pointcut("execution(* com.example.service.MyService.myMethod(..))")
public void specificMethod() {
// 切入点表达式匹配 com.example.service.MyService 类中的 myMethod 方法
}
@Pointcut("execution(* com.example.service.MyService.myMethod(String, ..))")
public void specificMethodWithStringParam() {
// 切入点表达式匹配 com.example.service.MyService 类中的 myMethod 方法,
// 该方法的第一个参数类型是 String,其他参数任意
}
@Pointcut("execution(* com.example.service.*.*(..)) && @annotation(org.springframework.transaction.annotation.Transactional)")
public void allTransactionalMethodsInServicePackage() {
// 切入点表达式匹配 com.example.service 包下的所有标注了 @Transactional 注解的方法
}
- 注意事项
注意,切入点表达式尽量缩小范围,范围过大会导致程序运行效率较低
通过上述切入点表达式,我们会发现execution
在指定特定的多个方法时就比较麻烦,需要使用&&
,||
等,不利于使用,下面介绍更利于特定方法使用的方式
- 使用自定义注释
package com.example.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLog {
}
创建方式:
我们只需要在目标方法上使用自定义注解,就能使用AOP代理了
- 在切面类中指定自定义注释的全类名
package com.example.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect // 标记这是一个切面类
@Component
public class MyAspect {
@Around("@annotation(com.example.anno.MyLog)")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = joinPoint.proceed();
return result;
}
}
- 添加注释
public class MyService {
@MyLog
public void method1() {
// 方法实现
}
@MyLog
public void method2() {
// 方法实现
}
}
四、连接点
由于@Around的使用比较特殊,只能通过ProceedingJoinPoint
对象获取相关信息,而其他通知只能使用JoinPoint
来获取
- ProceedingJoinPoint
@Around("execution(* com.itheima.service.DeptService.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取目标类名
String className = joinPoint.getTarget().getClass().getName();
System.out.println("Target Class: " + className);
// 获取目标方法签名
Signature signature = joinPoint.getSignature();
System.out.println("Method Signature: " + signature);
// 获取目标方法名
String methodName = joinPoint.getSignature().getName();
System.out.println("Method Name: " + methodName);
// 获取目标方法运行参数
Object[] args = joinPoint.getArgs();
System.out.println("Method Arguments: " + Arrays.toString(args));
// 执行原始方法,获取返回值
Object res = joinPoint.proceed();
System.out.println("Method Result: " + res);
return res;
}
- JoinPoint
@Before("execution(* com.itheima.service.DeptService.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
// 获取目标类名
String className = joinPoint.getTarget().getClass().getName();
System.out.println("Target Class: " + className);
// 获取目标方法签名
Signature signature = joinPoint.getSignature();
System.out.println("Method Signature: " + signature);
// 获取目标方法名
String methodName = joinPoint.getSignature().getName();
System.out.println("Method Name: " + methodName);
// 获取目标方法运行参数
Object[] args = joinPoint.getArgs();
System.out.println("Method Arguments: " + Arrays.toString(args));
}
对比
JoinPoint
适用于所有类型的通知(前置通知、后置通知、返回通知、异常通知),但它没有proceed()
方法,因此无法控制目标方法的执行。ProceedingJoinPoint
继承自JoinPoint
,仅适用于环绕通知。它包含proceed()
方法,可以在通知中执行目标方法,并且在方法执行的前后插入逻辑。
总结起来,JoinPoint
和 ProceedingJoinPoint
都可以用来获取目标方法的各种信息,但只有 ProceedingJoinPoint
可以控制目标方法的执行。
使用示例:
- 自定义注释
package com.example.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 指定什么时候有效
@Retention(RetentionPolicy.RUNTIME)
// 指定作用在方法上
@Target(ElementType.METHOD)
public @interface MyLog {
}
- 记录员工操作日记
package com.example.aop;
import com.alibaba.fastjson.JSONObject;
import com.example.mapper.OperateLogMapper;
import com.example.pojo.OperateLog;
import com.example.utils.JwtUtils;
import io.jsonwebtoken.Claims;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Arrays;
@Slf4j
@Aspect // 标记这是一个切面类
@Component
public class MyAspect {
@Autowired
private HttpServletRequest request;
@Autowired
private OperateLogMapper operateLogMapper;
@Around("@annotation(com.example.anno.MyLog)")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取token
String token = request.getHeader("token");
// 解析token
Claims claims = JwtUtils.parseJWT(token);
// 获取用户id
Integer operateUser = (Integer) claims.get("id");
// 当前操作时间
LocalDateTime operateTime = LocalDateTime.now();
//操作的类名
String className = joinPoint.getTarget().getClass().getName();
//操作的方法名
String methodName = joinPoint.getSignature().getName();
//操作方法的参数
Object[] args = joinPoint.getArgs();
String methodParams = Arrays.toString(args);
// 调用目标方法
long begin = System.currentTimeMillis();
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
//操作的返回值
String returnValue = JSONObject.toJSONString(result);
//操作的耗时
long costTime = end - begin;
//记录日志
OperateLog log = new OperateLog(null, operateUser, operateTime, className, methodName, methodParams, returnValue, costTime);
operateLogMapper.insert(log);
return result;
}
}
效果图