spring boot中aop配置
- pom.xml 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
- application.yml 配置
spring:
# AOP 配置
aop:
auto: true #相当于标注了 @EnableAspectJAutoProxy
proxy-target-class: true #开启 cglib 代理
基本概念
说到spring aop
大家都知道是面向切面编程,本文就不在啰嗦的介绍什么是面向切面编程,本文重点是编码过程中如何使用spring aop
,首先要理解一下几个概念。
切面(Aspect)
切面简单理解就是一个类,在这个类里面定义了通知与切点,通知用@Aspect 和 @Component
注解标注该类。
通知(advice)
spring aop中的五种通知方式:
- @Before:前置通知,在目标方法被调用之前调用通知功能
- @After:后置通知,在目标方法执行结束后,无论执行结果如何都执行通知定义的任务
- @After-returning:后置通知,在目标方法执行结束后,如果执行成功,则执行通知定义的任务
- @After-throwing:异常通知,如果目标方法执行过程中抛出异常,则执行通知定义的任务
- @Around:环绕通知,在目标方法执行前和执行后,都需要执行通知定义的任务。
这五种通知类型注解都有value属性,该属性就是我们的切点表达式,其格式:
@Before(value = "切点表达式")
切点(PointCut)
切点就是告诉程序要在执行哪些类、方法时执行我们自定义的各种通知。切点如何定义呢?我们通常是使用 Aspectj 的切点表达式语言来定义切点。所以需要了解一下 spring aop 所支持的 Aspectj 切点表达式。
方法切点表达式
execution()
-
execution(* com.sff.aspect.service.AspectDemoService.sayHello(..))
: 第一个*
表示方法的任意返回值类型,..
表示方法使用参数任意 -
execution(* com.sff.aspect.service.AspectDemoService.*(..))
: 第一个*
表示方法的任意返回值类型,第二个*
表示 AspectDemoService类的任意方法,..
表示方法使用参数任意 -
execution(* com.sff.aspect.service.*.*(..))
: : 第一个*
表示方法的任意返回值类型,第二个*.*
表示com.sff.aspect.service包下的任意类的任意方法,..
表示方法使用参数任意
-
@annotation 匹配带指定注解的连接点
通常使用方式就是将这个自定义注解标注到某个方法之上
//自定义注解
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetAnnotation {
}
//注解使用
@Service
public class TargetServiceImpl {
@TargetAnnotation
public void test() {
System.out.println("test");
}
}
//定义切面
@Aspect
@Component
public class DemoAspect {
@After("@annotation(com.sff.aspect.annotations.TargetAnnotation)")
public void target() {
System.out.println("aspect target :");
}
}
方法入参切点表达式
到底干什么用的呢?就是能够在我们的切面中捕获我们传给业务的方法的某个参数
args()
@Before("execution(* com.sff.aspect.service.AspectDemoService.count(..)) && args(count)")
public void countTime(long count) {
System.out.println("切面:" + count);//获取业务方法的参数
}
传给 AspectDemoService 的 count(long count) 方法参数在我们的切面方法中捕获。注意这里的 countTime 的方法参数 、args(count) 的参数必须和业务方法参数名、类型完全相同。
目标类的参切点表达式
AspectJ指示器 | 作用 | 示例 |
---|---|---|
within() | 匹配指定类型内的方法都执行 | within(com.sff.aspect.service.within.*) : 该包下的所有类都能匹配到通知; within(com.sff.aspect.service.within..*) : 该包下的所有类以及子包下所有的类都能匹配到通知;within(com.sff.aspect.service..WithInService) : 该包以及它的所有子包下WithInService类型的任何方法都能匹配到通知; |
@within() | 匹配指定注解标注的类内的方法都执行 | @within(com.sff.aspect.annotations.WithAnnotation) : 此时WithAnnotation 注解标注的类的方法都会通知到 |
target() | 匹配指定的类方法 | target(com.sff.aspect.service.TargetServiceImpl) : TargetServiceImpl 实现了接口 TargetService 的test() 方法,所以能匹配到 TargetServiceImpl 类的 test() 方法 |
注意target中使用的表达式必须是类型全限定名,不支持通配符;
切点独立命令
上面讲解的切点表达式都是直接在切面中定义的,我们把切点直接配置到切点表达式函数里,这种切点叫做匿名切点。但是在实际开发中并不这么做,我们可以通过@Pointcut
注解来单独命名切点,这样可以在切面中复用相同功能的切点,提高重用性和降低维代码护成本。
- 定义独立切点
public class AspectPointcut {
@Pointcut(value = "execution(* com.sff.aspect.service.AspectDemoService.sayHello(..))")
public void sayHello() {
}
@Pointcut("@annotation(com.sff.aspect.annotations.TargetAnnotation)")
public void target() {
}
}
- 切面中使用独立切点
@Aspect
@Component
public class ServiceAspect {
@Before(value = "com.sff.aspect.aop.demo.AspectPointcut.sayHello()")
public void beforeSayHello() {
System.out.println("say hello");
}
}
通知中的业务参数
如果切面所通知的方法有自己的参数,在切面中如何访问和使用该参数呢?
使用 agrs()
使用ProceedingJoinPoint
获取业务参数
ProceedingJoinPoint
只针对于环绕通知 @Around
使用,总结一下节点:
- 环绕通知必须要要有
ProceedingJoinPoint
参数。 - 环绕通知必须有返回值,返回值即通知的目标方法的返回值。
- 环绕通知中需要明确调用
ProceedingJoinPoint
的proceed()
方法来执行被通知的目标方法 -
ProceedingJoinPoint
使用例子
/**
* 切点定义
*/
public class AspectPointcut {
@Pointcut(value = "execution(* com.sff.aspect.service.AspectDemoService.sayHello(..))")
public void sayHello() {
}
}
/**
* 定义切面
*/
@Aspect
@Component
public class ServiceAspect {
//环绕通知
@Around(value = "com.sff.aspect.aop.demo.AspectPointcut.sayHello()")
public void beforeSayHello(ProceedingJoinPoint joinPoint) {
try {
//获取业务请求参数
Object[] pointArgs = joinPoint.getArgs();
System.out.println("目标参数: " + Arrays.toString(pointArgs));
//执行目标方法
Object result = joinPoint.proceed();
//获取目标方法结果
System.out.println("执行结果:" + result);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
灵活的切点表达式
&&
、||
、!
关系操作符也同样适用于切点表达式。
-
execution(* com.sff.aspect.service.AspectDemoService.count(..)) && args(count)
:通知 count 方法并且在切面中获取业务参数 -
execution(* com.sff.aspect.service.impl.*.*(..)) && !@annotation(com.sff.aspect.aspect.annotation.NonServiceReport)"
:通知com.sff.aspect.service.impl
包下的所有方法,除了带有NonServiceReport
注解的方法
实战案例
rpc、http 接口异常码处理
我们在开发