Spring AOP
作为和 Spring IOC
平起平坐的大兄弟,在日常开发中还是经常会用到的,通过本文记录一下相关的概念和常见的使用场景。
什么是 AOP
AOP 全称为 Aspect Oriented Programing,翻译为「面向切面编程」,指的是将一定的切面逻辑按照一定的方式编织到指定的业务模块中,从而将这些业务模块的调用包裹起来,使得业务逻辑各部分之间的耦合度降低,提高代码的可重用性,提高开发效率。
核心概念
切面(Aspect)
切面由切点和通知组成,Spring AOP
中通过 @Aspect
注解标注的类就是一个切面。
连接点(join point)
所有我们能够将通知应用到的地方都是连接点,在 Spring 中,我们可以认为连接点就是所有的方法(构造函数除外)。连接点没啥实际意义,这个概念的提出只是为了更好的说明切点。
切点(point cut)
切点就是那些我们想要应用通知的连接点。
通知(advice)
就是我们想在连接点附近想要实现的功能。
目标对象(target)
就是要被通知的对象,也就是切点所对应的实际业务逻辑所在的对象。
代理对象(target)
目标对象织入切面后所产生的代理对象,即包含通知的对象。
织入(Weaving)
把切面应用的目标对象来创建新的代理对象的过程。织入方式有三种:
- 编译时织入(特殊编译器实现)
- 加载时织入(特使的类加载器实现)
- 运行时织入,通常 Spring AOP 就是通过这种方式实现织入
Spring 中如何使用 AOP
1.开启 AOP 支持
在配置类上加上 @EnableAspectJAutoProxy
注解即自动开启 AOP 的支持,这个注解有两个属性如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AspectJAutoProxyRegistrar.class})
public @interface EnableAspectJAutoProxy {
//是否使用CGLIB代理,默认false,使用JDK动态代理
boolean proxyTargetClass() default false;
//是否将代理类作为线程本地变量(threadLocal)暴露(可以通过AopContext访问)
//主要设计的目的是用来解决内部调用的问题
boolean exposeProxy() default false;
}
2.声明切面
@Aspect // 申明是一个切面
@Component // 切记,一定要将切面交由Spring管理,否则不起作用
public class WycAnnotationAspect {
//......
}
3.声明切点
一个切点有两部分组成,一个方法签名和一个切点表达式。切点表达时又分为切点标志符和操作参数。常用的切点表达式有 excecution
@annotation
within
@within
等。
@Aspect
@Component
public class WycAnnotationAspect {
@Pointcut("execution(public * *(..))")
private void executionPointcut() {}
@Pointcut("@annotation(com.spring.study.aop.annotation.WycAnnotation)")
private void annotationPointcut() { }
// 可以组合使用定义好的切点
// 表示同时匹配满足两者
@Pointcut("executionPointcut() && annotationPointcut()")
private void annotationPointcutAnd() {}
// 满足其中之一即可
@Pointcut("executionPointcut() || annotationPointcut()")
private void annotationPointcutOr() {}
// 不匹配即可
@Pointcut("!executionPointcut()")
private void annotationPointcutNot() {}
}
4.声明通知
常用的 advice
类型有以下几种
@Before
在切点之前执行@AfterReturning
在切点正常返回后执行@AfterThrowing
在切点抛出异常后执行@After
在方法执行结束之后执行,不管是正常返回还是抛出异常都会执行@Around
最强大的通知类型,可以包裹目标方法
@Aspect
@Component
public class WycAspect {
// 申明的切点
@Pointcut("execution(public * *(..))")
private void executionPointcut() {}
@Pointcut("@annotation(com.spring.study.aop.annotation.WycAnnotation)")
private void annotationPointcut() {}
// 前置通知,在目标方法前调用
@Before("executionPointcut()")
public void executionBefore() {
System.out.println("execution aspect Before invoke!");
}
// 后置通知,在目标方法返回后调用
@AfterReturning("executionPointcut()")
public void executionAfterReturning() {
System.out.println("execution aspect AfterReturning invoke!");
}
// 最终通知,正常的执行时机在AfterReturning之前
@After("executionPointcut()")
public void executionAfter() {
System.out.println("execution aspect After invoke!");
}
// 异常通知,发生异常时调用
@AfterThrowing("executionPointcut()")
public void executionAfterThrowing() {
System.out.println("execution aspect AfterThrowing invoke!");
}
// 环绕通知,方法调用前后都能进行处理
@Around("executionPointcut()")
public void executionAround(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("execution aspect Around(before) invoke!");
System.out.println(pjp.proceed());
System.out.println("execution aspect Around(after) invoke!");
}
}
通知中的参数
在上面应用的例子中,只有在「环绕通知」的方法上添加了一个 ProceedingJoinPoint
类型的参数。这个 ProceedingJoinPoint
意味着当前执行中的方法,它继承了 JoinPoint
接口。
JoinPonit
可以再任意方法上作为第一个参数声明,代表的是当前通知所应用的切点(即目标类中的方法),提供了以下几个方法:
getArgs()
返回当前切点的参数getThis()
返回代理对象getTarget()
返回目标对象getSignature()
返回当前切点的描述信息,比如修饰符、名称等
ProceedingJoinPoint
在JoinPonit
的基础上多提供了两个方法:
proceed()
直接执行当前的方法,基于此,我们可以在方法的执行前后直接加入对应的业务逻辑proceed(Object[] args)
可以改变当前执行方法的参数,然后用改变后的参数执行这个方法
通知的排序
当我们对于一个切点定义了多个通知时,例如,在一个切点上同时定义了两个before类型的通知。这个时候,为了让这两个通知按照我们期待的顺序执行,我们需要在切面上添加 @Order
注解或者让切面实现 org.springframework.core.Ordered
接口,重写 getOrder()
方法。例如:
@Aspect
@Component
@Order(-1)
public class WycFirstAspect {
// ...
}
@Aspect
@Component
@Order(0)
public class WycSecondAspect {
// ...
}
AOP 的应用
- 监控方法执行时长,记录出入参
- 对外接口的统一验签、权限控制
- 缓存优化
- 事务支持
下面举一个AOP 结合自定义注解完成接口限流的例子
@Slf4j
@Aspect
@Configuration
public class AopAdviseDefine {
@Around("execution(* com.wyc1856.assistant.controller..*.*(..)) && @annotation(com.wyc1856.assistant.annotation.Limit)")
public Object checkLimit(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Limit annotation = method.getAnnotation(Limit.class);
LimitTypeEnum limitType = annotation.limitType();
String key = "";
String prefix = annotation.prefix();
switch (limitType) {
case CUSTOMER:
key = annotation.key();
break;
case IP:
key = NetUtil.getIpAddress();
break;
default:
break;
}
if (StringUtils.isBlank(key)) {
key = method.getName();
}
ImmutableList<String> keys = ImmutableList.of(StringUtils.join(prefix, key));
try {
String luaScript = buildLuaScript();
DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number inCount = limitRedisTemplate.execute(redisScript, keys, annotation.count(), annotation.period());
if (inCount == null || inCount.intValue() > annotation.count()) {
throw new LimitException("请求太频繁了,请稍后再试");
}
return joinPoint.proceed();
} catch (LimitException e) {
throw e;
} catch (Throwable th) {
throw new RuntimeException("限流处理异常: " + th.getLocalizedMessage());
}
}
}