AOP
理解AOP
面向切面编程(AOP)补充了面向对象编程(OOP) 提供了另一种思考程序结构的方式。模块化的关键单元 在OOP中是类,而在AOP中,模块化的单位是切面。它允许开发人员在不修改原始代码的情况下,通过在方法执行前、后或异常抛出时执行某些操作来改变程序的行为。
Aspect:切面是一个横切关注点的模块化,它封装了通知和切入点,这些通知和切入点应用于目标对象以修改其行为。简单来说,切面是一个可重用的类,其中包含通知(在应用程序执行期间的特定点运行的代码)和切入点(定义哪些方法应该被拦截并应用通知的表达式)。在Aspect类中,您需要定义一个或多个通知方法和一个切入点表达式。通知方法将在满足切入点表达式时被执行。
Join Point:程序执行期间的一个点,例如方法执行、异常抛出等。
Pointcut(切点):描述哪些Join Point如何被拦截应用与切面,通过表达式定义。可以基于方法名、参数、注解等条件进行匹配。
Advice:它定义了在Join Point执行时要执行的操作。Advice可以在Join Point之前、之后或之前和之后执行,以提供不同的行为。Spring Boot AOP中有以下五种Advice类型:
-
Before Advice:在Join Point之前执行的Advice。
-
After Returning Advice:在Join Point成功返回后执行的Advice。
-
After Throwing Advice:在Join Point抛出异常后执行的Advice。
-
After Advice:在Join Point之后执行的Advice,无论成功与否。
-
Around Advice:在Join Point之前和之后执行的Advice,它可以控制是否调用Join Point以及如何处理它的结果。
Springboot中如何去使用AOP
1、类加上@Aspect注解,定义为一个切面类,加上@Component注入道IOC容器
2、使用@Pointcut注解定义切点
- 通过方法名定义切点
例:这个切点匹配了 com.example.aop.controller 包下的每一个控制器方法
@Pointcut("execution(* com.example.aopdemo.controller.*.*(..))")
public void firstPoint() {}
- 通过注解定义切点
例:这个切点定义匹配了所有使用 @EnumChange 注解的方法
@Pointcut("@annotation(com.example.aopdemo.annotation.EnumChange)")
public void enumChangePoint() {}
- 使用正则表达式定义切点
例:这个切点定义匹配了 com.example.demo.service 包下的所有方法,方法参数为Long类型且参数名为id
@Pointcut("execution(* com.example.demo.service.*.*(Long)) && args(id)")
public void serviceLayerExecutionWithId(Long id) {}
- 使用逻辑运算符定义切点
例:这个切点定义匹配了 com.example.demo.service 包下的所有方法,但不包括UserService类中的方法
@Pointcut("execution(* com.example.demo.service.*.*(..)) && !execution(* com.example.demo.service.UserService.*(..))")
public void serviceLayerExecutionWithoutUserService() {}
3、定义Advice方法
@Before:使用@Before注解来定义Before Advice,它会在切点方法执行之前执行。如下面例子可以在切点方法执行之前,我们就成功地修改了它的参数。(需要注意的是,修改参数的行为可能会对应用程序的正确性产生影响,因此需要谨慎使用。)
@Component
@Aspect
public class PageAspect {
@Pointcut("execution(public * com.example.demo_knife4jv2.controller.*.*(Integer,Integer)) && args(pageSize,pageNum)")
public void checkPagePointCut(Integer pageSize, Integer pageNum) {}
@Before("checkPagePointCut(pageSize,pageNum)")
public void beforePiontCut(JoinPoint joinPoint, Integer pageSize, Integer pageNum) {
if (pageSize < 0) {
pageSize = 1;
}
if (pageSize > 10000) {
pageSize = 10000;
}
}
}
@AfterReturning:在Join Point成功返回后执行的Advice。可以通过Join Point参数和Advice方法参数来访问返回值
@Component
@Aspect
public class ResultAspect {
@Pointcut("execution(* com.example.demo_knife4jv2.controller.*.*(..))")
public void checkResultPointCut() {}
@AfterReturning(value = "checkResultPointCut()", returning = "result")
public Object afterPiontCut(JoinPoint joinPoint, Object result) {
// 如果result为空,返回特定的返回结构 Result.success()是自定义的方法
if (result == null) {
result = (Object) Result.success("操作成功");
}
return result;
}
}
@AfterThrowing:After Throwing Advice在Join Point抛出异常后执行的Advice。可以通过Join Point参数和Advice方法参数来访问异常信息
@Component
@Aspect
@Slf4j
public class ExceptionLogAspect {
@Pointcut("execution(* com.example.demo_knife4jv2.controller.*.*(..))")
public void exceptionLogPointCut() {}
@AfterThrowing(value = "exceptionLogPointCut()", throwing = "exception")
public void afterThrowingPiontCut(JoinPoint joinPoint, Exception exception) {
log.error("exception is happen the message is " + exception.getMessage());
}
}
@After:After Advice在Join Point之后执行的Advice,无论成功与否。可以通过Join Point参数来访问方法信息
@Component
@Aspect
@Slf4j
public class LogAspect {
@Pointcut("execution(* com.example.demo_knife4jv2.controller.*.*(..))")
public void finalLogPointCut() {}
@After("finalLogPointCut()")
public void afterPiontCut(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
log.info("Method " + methodName + " executed finally!");
}
}
@Around:Around Advice 用于在方法执行前后执行操作。它允许您通过修改方法参数或返回值来控制方法执行流程。Around Advice 的使用场景包括性能监控、日志记录、事务管理等。
如下例子是一个通过Around修改返回值,转换枚举值的方法。
@Component
@Aspect
@Slf4j
public class EnumAspect {
@Pointcut("@annotation(com.example.demo_knife4jv2.annotation.EnumChange)")
public void EnumAspect() {}
@Around("EnumAspect()")
public Object around(ProceedingJoinPoint jp) throws Throwable {
Result result = (Result) jp.proceed();
log.info(result.toString());
Class resultClass = result.getData().getClass();
Field[] fields = resultClass.getDeclaredFields();
JSONObject resultJson = (JSONObject) JSON.toJSON(result.getData());
for (Field field : fields) {
if (field.isAnnotationPresent(EnumMessage.class)) {
Enum[] enums = (Enum[]) field.getType().getEnumConstants();
Enum enumOptional = Arrays.stream(enums).map(s -> {
if (s.name().equals(resultJson.getString(field.getName()))) {
return s;
}
return null;
}).findFirst().get();
IEnum iEnum = (IEnum) enumOptional;
String message = iEnum.getMessage();
resultJson.put(field.getName() + "_message", message);
}
}
result.setData(resultJson);
return result;
}
}
4、添加@Order注解设置Aspect类顺序,在一个项目中有多个Aspect类时,可能会因为顺序问题导致与实际效果不符,这时可以在Aspect类上添加一个@Order注解来设置Aspect类的顺序,数值越小的Aspect越先执行。如果没有指定@Order注解,则Aspect的执行顺序将由Spring决定
参考资料:https://docs.spring.io/spring-framework/reference/core/aop.html