Spring Boot 学习笔记(十三) 整合 AOP


0. 前言

AOP 全称 Aspect Oriented Programming ,即面向切面编程。通俗来说,我们可以在一个方法执行之前做一些操作,比如修改一下参数,在一个方法之后做一些操作,比如修改一下返回值。我们可以把这些要做的操作,放到一个类,或者是方法中,这个类或者方法,可以称作一个切面。

面向切面编程:即我们需要针对这些切面编程。编程的关注点不在于方法的逻辑而在于方法的执行前,执行后的逻辑。

一个切面大体包括两个部分,切点和在切点处要做的操作(官方叫:通知。一直没有理解这个叫法的含义 ^_^)。

切点可以以是一个或者一些具体的方法。

在切点处的操作又分为下面几种情况:

  • 方法执行前的操作(前置通知)
  • 方法执行后的操作(后置通知)
  • 方法抛出异常后 catche 块中的操作(异常通知)
  • 方法抛出异常后 finally 块中的操作(后置最终通知)
  • 完全控制方法的操作(环绕通知)

1. 创建一个传统的 Service 服务

创建一个 AopService 接口,实现类的接口如下:

@Service
public class AopServiceImpl implements AopService {
    @Override
    public AccountInfo aopHello(AccountInfo accountInfo) {
        accountInfo.setPwd("123");
        return accountInfo;
    }
}

为了方便测试,我们为这个 Service 创建一个 Controller,代码如下:

@RestController
public class AopController {
    private static Logger logger = LoggerFactory.getLogger(AopController.class);

    @Autowired
    private AopService aopService;

    @GetMapping("/helloAop/{name}")
    public AccountInfo helloAop(@PathVariable("name") String name) {
        logger.info("AOP 接口入参:{}", name);
        AccountInfo accountInfo = new AccountInfo();
        accountInfo.setName(name);

        accountInfo = aopService.aopHello(accountInfo);

        logger.info("AOP 接口出参:{}", accountInfo);
        return accountInfo;
    }
}

下面我们会为 aopHello 这个方法添加切面。对方法的执行前,执行后做一些处理。

2. 创建一个切面

首先需要添加 AOP 的依赖:

<!-- 引入 aop 支持 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

然后创建一个切面。新建 AopAspect 类,代码如下:

import org.aspectj.lang.annotation.*;

@Aspect
@Component
public class AopAspect {
    private static Logger logger = LoggerFactory.getLogger(AopAspect.class);
}

好了,这个切面是创建好了,现在我们还没有为其指定具体的切点以及切点处的操作

3. 指定切点

我们把这个切点指定到上面创建的 AopServiceImpl 的 aopHello 方法上:

    /**
     * 类 AopServiceImpl 下的 aopHello 方法为切入点
     */
    @Pointcut("execution(public * com.zdran.springboot.service.impl.AopServiceImpl.aopHello(..))")
    public void pointCut() {}

切点表达式。切点表达式可以清晰的表示一个或者一些方法。

格式: execution([可见性] 返回类型 [声明类型].方法名(参数) [异常])

支持以下通配符:

  • ‘*’ 匹配任意字符
  • ‘+’ 匹配一个或多个字符。一般用于表示某个类的所有子类
  • ‘..’:一般用于匹配多个包,多个参数

现在我们为这个切面指定了切点。下面我们定义一些在这个切点处的操作。

4. 前置通知

我们在这个方法执行前做一些操作,国际惯例,先打印个 hello aop

    /**
     * 在方法执行之前执行
     *
     * @param joinPoint
     */
    @Before(value = "pointCut()")
    public void doBefore(JoinPoint joinPoint) {
        logger.info("doBefore run: hello aop");
    }

你也可以将 Before 注解里的 value 值换成 execution 表达式。 意思是这个操作指定在某个切点上。

代码里的这种做法是把切点抽出来了,你可以理解为把这个切点定义成了一个变量,而不是每次使用的时候直接使用字符串了。

下面我们对请求参数进行修改。

    /**
     * 在方法执行之前执行
     *
     * @param joinPoint
     */
    @Before(value = "pointCut()")
    public void doBefore(JoinPoint joinPoint) {
        logger.info("doBefore run");
        AccountInfo accountInfo = (AccountInfo) joinPoint.getArgs()[0];
        logger.info("AOP:{}", accountInfo.toString());
        accountInfo.setName("aop");
    }

joinPoint.getArgs() 返回的是一个数组,我们取第一个参数,强转成 AccountInfo 类型,并且修改参数值。

可以访问我们之前写的 Controller 测试一下。

5. 后置通知

后置通知是指在方法执行后做的一些操作,代码如下:


    /**
     * 在方法之后执行,可以对方法返回值进行修改
     *
     * @param point
     * @param returnValue
     */
    @AfterReturning(value = "pointCut()", returning = "returnValue")
    public void doAfterReturning(JoinPoint point, AccountInfo returnValue) {
        logger.info("doAfterReturning:{}", returnValue);
        returnValue.setPwd("doAfterReturning");
    }

AfterReturning 注解的 returning 属性不是必须的,如果你不需要对方法的返回值进行操作,的话,可以不要这个属性。

同样的,方法签名里的第二个参数也不是必须的。但是,方法签名里的第二个参数,是与 returning 属性绑定的,所以属性的值和参数名称必须保持一致。

方法中的第二个参数就是返回值,我们可以直接修改。

6. 后置最终通知

这个通知是在 方法之外 的 finally 块中的操作,所以这个操作的执行顺序是在 AfterReturning 之后执行的。

    /**
     * 在方法执行之后执行
     *
     * @param joinPoint
     */
    @After(value = "pointCut()")
    public void doAfter(JoinPoint joinPoint) {
        logger.info("doAfter run");
    }

与 AfterReturning 的最大区别可能就是这个通知不能修改返回值。

6. 异常通知

异常通知,是指当在执行方法时,抛出异常后的操作,或者说是 方法之外 的 catche 块中的操作。代码如下:

    /**
     * 在方法抛出异常时执行,执行顺序在 After 之后
     *
     * @param ex
     */
    @AfterThrowing(value = "pointCut()", throwing = "ex")
    public void doAfterThrowing(Throwable ex) {
        logger.info("doAfterThrowing run");
        logger.error("doAfterThrowing:", ex);
    }

需要注意的是,这个注解的方法是在 After 之后执行的。异常通知不能对返回值做任何操作。

##7. 环绕通知

环绕通知相比于上面几个通知是最强大的一个通知。它不仅可以修改参数,修改返回值,还可以决定要不要调用切点处的方法。

需要注意的是,这个环绕通知是在 前置通知之前执行的。

    /**
     * 环绕通知
     *
     * @param joinPoint
     */
    @Around(value = "pointCut()")
    public AccountInfo doAround(ProceedingJoinPoint joinPoint) {
        logger.info("doAround run");
        AccountInfo accountInfo = (AccountInfo) joinPoint.getArgs()[0];
        //在方法被执行前,修改参数
        accountInfo.setBalance(123);
        try {
            //执行的实际方法
            joinPoint.proceed();
        } catch (Throwable throwable) {
            return null;
        }
        //在方法执行后修改返回值
        accountInfo.setName("around");
        return accountInfo;
    }

joinPoint.proceed(); 是实际要执行的方法,即我们 AopServiceImpl.aopHello() 方法,如果你不调用 proceed() 方法就不会执行 aopHello(),这样我们就可以控制到底要不要执行切点处的方法。甚至,我们可以在切点处执行别的方法。

8. 执行顺序

通过上面的一些实例我们可以简单的整理一下这些通知的执行顺序:

        //@Around
        try {
            try {
                //@Before
                method.invoke(..);
            } finally {
                //@After
            }
            //@AfterReturning
        } catch (Exception e) {
            //@AfterThrowing
        }

完整代码见: Spring Boot 学习笔记 源码地址

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
本文链接:https://zdran.com/20190418.html