Spring AOP 介绍

AOP 简介

AOP (Aspect Orient Programming),直译过来就是面向切面编程。AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。

在 OOP 中,模块化的基本单位是类,而 AOP 中模块化的基本单位是切面。

Spring 中关于 AOP 的介绍:5. Aspect Oriented Programming with Spring

AOP 相关术语

术语中文翻译说明
Aspect切面切面是 PointcutAdvice 的集合,一般单独作为一个类。在 Spring AOP 中,使用 @Aspect 注解来实现切面。
Joinpoint连接点程序执行过程的一个点,例如一个方法的执行或一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法的执行。
Advice增强切面在连接点执行的动作。不同增强类型包含 “around”、”before”、”after”等增强。
Pointcut切点可以插入增强的连接点。
Introduction引入为一个类添加额外的方法或属性。Spring AOP 允许我们为目标对象引入新的接口(及对应的实现)。
Target object目标对象由一个或多个切面增强的对象。也称为 “advised object”。由于 Spring AOP 是使用运行时代理来实现的,所以这个对象始终是一个被代理的对象
AOP proxyAOP代理AOP 创建的对象,用于实现切面规约(增强方法执行等)。在 Spring 中,AOP 代理是一个 JDK 动态代理或 CGLIB 代理。
Weaving织入创建一个被增强对象的过程。

通过注解来配置 Spring AOP

Spring AOP 支持的切入点指示器

指示符说明
execution用于匹配方法执行连接点
within限制匹配特定类型的连接点
this限制匹配特定的连接点,Spring AOP 代理的 Bean 是指定类型的实例
target限制匹配特定的连接点,其中目标对象(被代码的应用对象)是指定类型的实例
args限制匹配参数为指定类型的连接点
@target限制匹配连接点特定的执行对象,这些对象对应的类要具备指定类型的注解
@args限制匹配传递的参数类型为指定类型的注解
@within限制匹配到指定注解所标注的类型中的连接点
@annotation限制匹配指定注解的连接点

例如,以下是使用 execution 指示器的一个例子:

execution(* com.lanweihong.aop.service.IEatService.eat(..))

使用 execution 指示器选择 IEatServiceeat 方法,当 eat 方法执行时触发增强,方法表达式以 * 开头,表示返回任意类型的值。然后指定了全限定类名和方法 com.lanweihong.aop.service.IEatService.eat(),对于方法参数,使用 .. 表示任意参数。

以下是一些通用切入点表达式用法的例子:

// 任意公共方法执行:
execution(public * *(..))

// 任何一个以 "set" 开头的方法执行:
execution(* set*(..))

// AccountService 接口定义的任意方法执行:
execution(* com.xyz.service.AccountService.*(..))

// 在 service 包中定义的任意方法执行:
execution(* com.xyz.service.*.*(..))

// 在 service 包及其子包中定义的任意方法执行:
execution(* com.xyz.service..*.*(..))

// 在 service 包的任意连接点(在Spring AOP中只是方法执行):
within(com.xyz.service.*)

// 在 service 包及其子包中的任意连接点(在Spring AOP中只是方法执行):
within(com.xyz.service..*)

// 实现了 AccountService 接口的代理对象的任意连接点(在Spring AOP中只是方法执行):
this(com.xyz.service.AccountService)

// 实现 AccountServic 接口的目标对象的任意连接点(在Spring AOP中只是方法执行):
target(com.xyz.service.AccountService)

// 任何一个只接受一个参数,并且运行时所传入的参数是 Serializable 接口的连接点(在Spring AOP中只是方法执行):
args(java.io.Serializable)

// 目标对象中有一个 `@Transactional` 注解的任意连接点 (在Spring AOP中只是方法执行):
@target(org.springframework.transaction.annotation.Transactional)

// 任何一个目标对象声明的类型有一个 @Transactional 注解的连接点 (在Spring AOP中只是方法执行):
@within(org.springframework.transaction.annotation.Transactional)

// 任何一个执行的方法有一个 @Transactional 注解的连接点 (在Spring AOP中只是方法执行):
@annotation(org.springframework.transaction.annotation.Transactional)

// 任何一个只接受一个参数,并且运行时所传入的参数类型具有 @Classified 注解的连接点(在Spring AOP中只是方法执行):
@args(com.xyz.security.Classified)

// 任何一个在名为 "tradeService" 的 Spring Bean 之上的连接点 (在Spring AOP中只是方法执行):
bean(tradeService)

// 任何一个在名字匹配通配符表达式 "*Service" 的 Spring Bean 之上的连接点 (在Spring AOP中只是方法执行):
bean(*Service)

对于多个匹配,我们可以使用连接符 &&||! 来表示 「且」、「或」、「非」的关系。对于使用 XML 配置时,使用 「and」、「or」、「not」 来表示。

示例:

// 匹配以 Service 或 ServiceImpl 结尾的 bean
bean(*Service || *ServiceImpl)

// 匹配名字以 Service 结尾, 并且在包 com.lanweihong.service 中的 bean
bean(*Service) && within(com.lanweihong.service.*)

// 匹配注解为 @AuthCheck 且在 service 包中的任意方法
annotation(com.lanweihong.annotation.AuthCheck) && execution(* com.lanweihong.service.*.*(..))

Spring AOP 中的 5 种增强类型

增强(Advice)说明
Before在连接点(方法)之前执行
AfterReturning在连接点(方法)成功执行之后执行
AfterThrowing在连接点(方法)抛出异常后执行
After在连接点(方法)之后执行,无论方法执行成功或抛出异常
Around在连接点(方法)调用的前后执行,就是将切入点包起来执行。这是一种最强大的增强类型,它可以在方法调用前后执行自定义行为。

Spring 5.2.7 后,Advice 执行的顺序如下图所示:

注解使用

新增切面类,用于测试:

@Aspect
@Component
@Slf4j
public class EatAspect {

    /**
     * 定义切入点
     */
    @Pointcut("execution(* com.lanweihong.aop.service.IEatService.eat(..))")
    public void eat() {
    }

    @Around("eat()")
    public Object doAround(ProceedingJoinPoint pjp) {
        log.info("进入 @Around......");
        Object result = null;
        try {
            result = pjp.proceed();

        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        log.info("退出 @Around......");
        return result;
    }

    /**
     * Before 增强,指定切入点 eat()
     */
    @Before("eat()")
    public void doBefore() {
        log.info("进入 @Before......");
    }

    @AfterReturning("eat()")
    public void doAfterReturning() {
        log.info("进入 @AfterReturning......");
    }

    @AfterThrowing(pointcut = "eat()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
        log.error("进入 @AfterThrowing......错误:" + e.getMessage());
    }

    @After("eat()")
    public void doAfter() {
        log.info("进入 @After......");
    }

}

EatServiceImpl

@Service
@Slf4j
public class EatServiceImpl implements IEatService {

    @Override
    public void eat() {
        log.info("正在吃饭......");
    }
}

执行 eat() 方法,正常时输出如下:

2020-07-16 01:29:02.752  INFO 42784 --- [nio-8022-exec-5] com.lanweihong.aop.aspect.EatAspect      : 进入 @Around......
2020-07-16 01:29:02.753  INFO 42784 --- [nio-8022-exec-5] com.lanweihong.aop.aspect.EatAspect      : 进入 @Before......
2020-07-16 01:29:02.753  INFO 42784 --- [nio-8022-exec-5] c.l.aop.service.impl.EatServiceImpl      : 正在吃饭......
2020-07-16 01:29:02.753  INFO 42784 --- [nio-8022-exec-5] com.lanweihong.aop.aspect.EatAspect      : 进入 @AfterReturning......
2020-07-16 01:29:02.753  INFO 42784 --- [nio-8022-exec-5] com.lanweihong.aop.aspect.EatAspect      : 进入 @After......
2020-07-16 01:29:02.753  INFO 42784 --- [nio-8022-exec-5] com.lanweihong.aop.aspect.EatAspect      : 退出 @Around......

发送异常时输出如下:

2020-07-16 01:34:16.072  INFO 49188 --- [nio-8022-exec-2] com.lanweihong.aop.aspect.EatAspect      : 进入 @Around......
2020-07-16 01:34:16.072  INFO 49188 --- [nio-8022-exec-2] com.lanweihong.aop.aspect.EatAspect      : 进入 @Before......
2020-07-16 01:34:16.075 ERROR 49188 --- [nio-8022-exec-2] com.lanweihong.aop.aspect.EatAspect      : 进入 @AfterThrowing......错误:/ by zero
2020-07-16 01:34:16.075  INFO 49188 --- [nio-8022-exec-2] com.lanweihong.aop.aspect.EatAspect      : 进入 @After......
2020-07-16 01:34:16.077  INFO 49188 --- [nio-8022-exec-2] com.lanweihong.aop.aspect.EatAspect      : 退出 @Around......

从输出结果看,Advice 执行的顺序和上图描述一致。

其他

当开发企业级应用的时候,我们可能会想要从几个切面引用模块化应用和特定操作的集合。推荐定义一个 SystemArchitecture 的切面来统一配置通用的切入点表达式,类似如下:

package com.lanweihong.aop.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

/**
 * 统一切入点配置
 */
@Aspect
public class SystemArchitecture {

    /**
     * 在 com.lanweihong.aop.controller 包下及其子包的连接点
     */
    @Pointcut("within(com.lanweihong.aop.controller..*)")
    public void inControllerLayer() {
    }

    /**
     * 在 com.lanweihong.aop.service 包下及其子包的连接点
     */
    @Pointcut("within(com.lanweihong.aop.service..*)")
    public void inServiceLayer() {
    }

    /**
     * 在 com.lanweihong.aop.dao 包下及其子包的连接点
     */
    @Pointcut("within(com.lanweihong.aop.dao..*)")
    public void inDaoLayer() {
    }

    // 更多的切入点定义......
}

基于注解使用:

@Before("com.lanweihong.aop.aspect.SystemArchitecture.inDaoLayer()")
public void doBefore() {
}

基于 XML 使用:

<aop:config>
  <aop:advisor 
      pointcut="com.lanweihong.aop.aspect.SystemArchitecture.inServiceLayer()"
      advice-ref="tx-advice"/>
</aop:config>

<tx:advice id="tx-advice">
  <tx:attributes>
    <tx:method name="*" propagation="REQUIRED"/>
  </tx:attributes>
</tx:advice>

总结

本文详细介绍了 Spring AOP 的相关概念、特性术语、支持的切入点指示器使用方法及基于注解的使用教程。更多的介绍及用法请查看官方文档。

参考文档

  1. Aspect Oriented Programming with Spring
  2. Spring AOP——Spring 中面向切面编程
文章目录