[Spring 系列] Spring AOP 之详解与基础用法②

1,015 阅读10分钟

前言

Hi 我是来自西部大嫖客杰西

上篇文章我从代理模式的概念,慢慢引入了静态代理和动态代理,还讲了一些关于动态代理的分类及概念的特别说明。如果你没看到我上节的文章,我可以送一个传送门:

理解了设计模式,那么我们开始学习一下看 Spring 团队利用了代理模式糅合到 Spring AOP 当中去的。

AOP 介绍

什么是 AOP ? AOP 其实跟 OOP (面向对象编程)一样,都是一种编程思想。它的全称是 Aspect-oriented Programming,也就是面向切面编程。

在应用上,AOP 其实是 OOP 的一种补充。还记得,OOP 为什么这么流行?因为 OOP 的出现是可以让人们将复杂的问题抽象化,简单化;同时,OOP 的三大特性如封装/继承/多态,可以封装细节内容,使编程更加简单,更符合人类的思维。这些选项就已经优于古老的面向过程的思想(但是说面向过程并不优越,只是说明 OOP 更加受大众欢迎)。

但是为什么 AOP 是一个补充呢?因为虽然 OOP 已经是理想的编程模型,但是它任然有不足之处。在实际编程中,我们常常会遇到一些普通,通用但又是不可缺少的功能(代码)。

那怎么办?我们只能将涉及多业务流程的通用功能抽取并单独封装,形成独立的切面,在合适的时机将这些切面横向切入到业务流程指定的位置中。例如说,我们常常使用的日志功能,事务等等。

总的来说,AOP 其实是将散布于不同业务但功能通用的代码从业务逻辑中抽取出来,封装成独立的模块(切面)。同时,当业务逻辑中执行的过程中,AOP 把切面和关注点切入业务流程。

AOP 应用场景

切面应用场景大多数都会想到的是日志或者事务。在单体应用中使用切面实现日志是个不错的选择,但是分布式后就不一定了,当然这是题外话。

下面我说些常用的 AOP 场景。

  1. 缓存
  2. 异常处理,可以用来捕捉异常
  3. 调试,性能优化,计算某个方法执行的时间进行精准优化
  4. 持久化
  5. 事务,这个我们经常使用的 @Transactional 注解
  6. 日志
  7. 记录追踪,可以记录执行了哪些功能(本质上和日志差不多)

AOP 术语

想了解清楚 Spring AOP,那么了解它们的组成非常重要。这对接下来的源码分析也有一定的帮助。

名称 说明
Aspect 一个关注点的模块化。因为一个关注点可以横切多个对象,所以我们可以将它进行模块管理。事务管理(@Transactinal)就是一个很好的例子。在 Spring AOP 中,切面可以基于模式或者基于 @Aspect 注解的方式实现。
Join Point 程序执行过程中的一个点,如方法的执行或异常的处理。在 Spring AOP 中,连接点总是表示方法执行。
Advice 在切面的某个特定的连接点上的执行的动作
Introduction 代表类型声明其他方法或字段。Spring AOP 允许您将新的接口(和相应的实现)引入任何被建议的对象。例如,您可以使用一个介绍来让一个 bean 实现一个 IsModified 接口,以简化缓存。
Target Object 即将被代理的目标对象。
AOP proxy 为了实现 Aspect 契约(通知方法执行等)而由 AOP框架创建的对象。在Spring框架中,AOP代理是JDK动态代理或CGLIB代理。
Weave aspect 与其他应用程序类型或对象链接以创建 advised 对象。

关于 AOP 在 Spring 中的应用

我们都知道了,Spring AOP 基于动态代理的 AOP 框架。其中动态代理包含了两种模式:JDK Dynamic ProxyCglib Proxy

AspectJ 属于静态代理,因为它实际上是在编译期增强,与动态代理的编织时期不同,所以 Spring 虽然集成了 AspectJ,但 Spring 实际上仅仅用了 Aspect 提供的用于切入点解析和匹配的库来解释与 AspectJ5 相同的注解。但 AOP 运行时仍然纯 Spring AOP 并且不依赖 AspectJ 编译器或编织器。

但是实际上在 Spring 也可以使用原生的 AspectJ(文末有链接传送门)。 如果您的拦截需要包括目标类中的方法调用甚至构造函数,那么考虑使用 Spring 驱动的本机 AspectJ 编织,而不是 Spring 的基于代理的 AOP 框架。这构成了具有不同特征的 AOP 使用的不同模式,所以在做决定之前一定要熟悉编织。

举个栗子

我决定从一个例子开始,讲解 Spring AOP 在 Spring Boot 的常用实践。下面是步骤说明:

  1. 初始化 Spring Boot
  2. 引入依赖
  3. 编写 Controller
  4. 编写切面
  5. 测试

初始化 Spring Boot

初始化方法我一般用以下两种

  1. 通过 start.spring.io 自定义项目,然后下载/导入。
  2. 通过 IDEA 来新建 Spring Boot 项目。

引入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
  1. 因为我是通过 web 进行测试的,所以需要引入 web 依赖。
  2. 因为需要切面的依赖,所以引入了 aop

编写 Controller

demoApplication.java 写入代码

@SpringBootApplication
@RestController
public class DemoV2020012901Application {

    public static void main(String[] args) {
        SpringApplication.run(DemoV2020012901Application.class, args);
    }

    @RequestMapping("helloworld")
    public String helloworld() {
        System.out.println("hello world");
        return "helloworld";
    }

}

编写切面

添加文件夹 aspect,建立一个切面 DemoAspect.java

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/*
    @Component 注解是必须加的,因为 Spring 需要检测到这个实体类;
    仅仅一个 @Aspect 不足以让 Spring 检测得到。
    (XML 模式的话我们一般是用了 <aop:aspectj-autoproxy/> 让 Spring 检测的)
*/
@Aspect
@Component      
public class DemoAspect {

    @Pointcut("execution(* helloworld(..))") // 切点表达式,属于 AspectJ 5 标准表达式
    private void pointcut() {}   // 切点签名

    @Before("pointcut()")
    public void before() {
        System.out.println("before");
    }
}

启动测试

这时候我们就可以启动应用测试了。测试地址访问:http://localhost:8080/helloworld

控制台会输出

before
hello world

AspectJ 切入点指示器

在上面的例子当中,切点表达式我使用的是 execution。其实 Spring 还提供了许多切入点指示器,与强大的 AspectJ 的切点函数相匹配。

切入点类别
名称
入参
说明
方法 execution 方法匹配模式串 匹配满足某一模式的所有目标类方法的连接点
方法 @annotation 方法注解类名 表示标注了特定注解的目标类方法连接点
方法入参 args 类名 通过判别目标类方法运行时入参对象的类型定义指定连接点。如 args(com.jc.Student) 表示所有有且仅有一个类型匹配于 Student 入参方法。
方法入参 @args 类型注解类名 通过判断目标类方法运行时入参对象的类是否标准特定注解来指定连接点。如 @args(com.jc.Test) 表示任何这样的一个目标方法,它有一个入参对象类标注了 @Test 注解。
目标类 within 类名匹配串 表示特定领域下所有的连接点。如 within(com.jc.service.*) 是指匹配 service 包下的所有连接点,即所有类的所有方法。
目标类 target 类名 目标类按类型匹配于指定类,则目标类的所有连接点匹配这个切点。如 @target(com.jc.Student) 定义的切点,StudentStudent 实现类 MaleStudent 中的所有连接点都匹配切点
目标类 @within 类名注解类名 假如目标类按照类型匹配于某个类A,且类A标注了特定的注解,则目标类的所有连接点匹配这个切点。例如 @within(com.jc.Test),假设 Student 类标注了 @Test 注解,则 Student 及实现累 MaleStudent 的所有连接点都匹配这个切点
目标类 @target 类名注解类名 假如目标类标注了特定注解,则目标类的所有连接点都匹配该切点。如 @target(com.jc.Test)。如果 Student 类标注了 @Test,则 Student 的所有连接点都匹配这个切点。
代理类 this 类名 代理类按类型匹配于指定类,则被代理的目标类的所有连接点都匹配该切点

各种指示器都可以试一下,但是我们最常用的是 execution

Advice 的类型

Advice 的类型有五种,能满足大部分的企业应用需求,而且它们也非常好区分。以下就是它的类型:

名字
说明
Before advice 在连接点之前运行的通知。 Before advice 不能阻止连接点执行,除非代码抛出异常。
AfterReturning advice 在连接点正常完成后运行的通知(例如,如果一个方法没有抛出异常返回)。
After throwing advice 如果方法通过抛出异常退出,则执行通知。
After(finally) advice 不管连接点以何种方式退出(正常或异常返回),都要执行的通知
Around advice 围绕连接点(如方法调用)的通知。这是最强大的advice。Around通知可以在方法调用前后执行自定义行为。它还负责选择是继续到连接点,还是通过返回它自己的返回值或抛出异常来简化建议的方法执行。

❤️ 下面是不同类型的用法示例:

  • After Returning advice
    /* 1. @AfterReturning 有四个参数:value / pointcut / returning /        argNames
       2. 下面的 pointcut 填的也是切面表达式,如果与 value 同时存在,则会覆盖 value 
       3. retVal 指的是方法返回值的对象 
    */
    @AfterReturning(value = "pointcut()", returning = "retVal")
    public void afterReturning(Object retVal) {
        System.out.println(retVal);
    }
  • After throwing advice
    /*
      1. @AfterThrowing 有四个参数:value / pointcut / throwing /        argNames
      2. 使用 throwing 参数,这样可以限定异常才执行 advice。如果没设置则抛出的所有异常都会执行 advice
    */
    @AfterThrowing(pointcut = "pointcut()")
    public void AfterThrowing() {
        System.out.println("AfterThrowing");
    }
  • After(finally) advice
    /*
      1. @AfterThrowing 有二个参数:value / argNames
    */
    @After(value = "pointcut()")
    public void AfterThrowing() {
        System.out.println("AfterThrowing");
    }
  • Around advice
    /*
      1. @AfterThrowing 有二个参数:value / argNames
    */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }

❤️ 关于 Advice 的使用选型,官方建议使用最特定的通知类型可以提供更简单的编程模型,减少出错的可能性。例如,如果只需要用方法的返回值更新缓存,则最好实现 After return 通知,而不是 Around 通知,尽管 Around 通知可以完成相同的工作。

传送门

⭐️ 关于是选择 Spring AOP 还是 FULL AspectJ?官网文档告诉你: Spring AOP or Full AspectJ?

⭐️ 要想完全使用 AspectJ 的编织器,可以参考官方文档: Using AspectJ with Spring Applications

⭐️ 更加了解 Advice?官网文档告诉你: Declaring Advice

⭐️ 当然,假设你还在使用原生的 Spring XML 配置方式怎么办?一个例子带你飞: Schema-based AOP Support

⭐️ 想了解 Spring 基于代理是怎么实现 AOP 框架?想看官方是怎么讲解的: Proxying Mechanisms!++下文我会讲关于 AOP 在 Spring Boot 的源码解析哟++。