4

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实现了接口 TargetServicetest()方法,所以能匹配到 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 参数。
  • 环绕通知必须有返回值,返回值即通知的目标方法的返回值。
  • 环绕通知中需要明确调用 ProceedingJoinPointproceed() 方法来执行被通知的目标方法
  • 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 接口异常码处理

我们在开发


一只小小鸟
144 声望25 粉丝

如何做一个深层次的思考者,从简单开始、从记录开始。