一、引言

在软件开发的世界中,代码的复杂性往往随着业务逻辑的扩展而增长。为了管理这种复杂性,传统的面向对象编程(OOP)提供了模块化的代码结构,但在处理跨越多个模块的通用功能时,OOP 的局限性显露无遗。于是,面向切面编程(AOP)应运而生。AOP 通过将关注点分离,将那些常见的非核心功能,如日志记录、安全检查、事务管理等,独立出来,从而简化了核心业务逻辑的开发和维护。

在这篇博客中,我们将深入探讨 AOP 的核心思想、应用场景以及如何利用它解锁更高效、更清晰的编程方式。通过这次 "AOP 革命",你将掌握解锁面向切面编程终极奥义的关键,进一步提升你的开发效率与代码质量。


二、AOP 概述

2.1、什么是 AOP

AOP 的全称叫做 Aspect Oriented Programming(面向切面编程),这里的切面可不是在数学中的那种切面,这里的切面就是指某⼀类特定问题,所以 AOP 也可以理解为面向特定方法编程。什么是面向特定方法编程呢?比如 "登录校验"、"返回值统一处理" 就是⼀类特定问题。登录校验拦截器,就是对 "登录校验" 这类问题的统⼀处理。所以,拦截器也是 AOP 的⼀种应用。AOP 是⼀种思想,拦截器是 AOP 思想的⼀种实现。Spring 框架实现了这种思想,提供了拦截器技术的相关接口。同样的,统⼀数据返回格式和统⼀异常处理,也是 AOP 思想的⼀种实现。

简单来说:AOP 是⼀种思想,是对某⼀类事情的集中处理。


三、使用 AOP

3.1、引入依赖

在 "pom.xml" 文件中添加配置

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

3.2、编写 AOP 程序

在正式编写 AOP 的程序之前,我们首先要知道如何告诉 Spring 我们是在编写一个 AOP 的东西呢?AOP 革命:解锁面向切面编程的终极奥义_切面类的执行顺序这就得用到我们的 Aspect 注解,声明我们这个类是切面类了,程序如下:

@Slf4j
@Aspect
@Component
public class AspectTest {
    @Around("execution(* com.zmbdp.aopdemo.blogtest.*.*(..))")// 这是环绕执行的注解
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("doAround 环绕执行前...");
        Object result = joinPoint.proceed();// 执行原本要执行的方法
        log.info("doAround 环绕执行后...");
        return result;
    }
}

然后我们 Controller 的代码和正常的一样:

@RestController
@RequestMapping("/bc")
public class BlogController {
    @RequestMapping("/around")
    public String doAround() {
        return "doAround Run...";
    }
}

按照道理来说,如果这里没有切面类,那我们的程序在控制台中,什么都不会打印,然后网页正常显示 "doAround Run...",但是加了切面类,控制台变成了这样:

AOP 革命:解锁面向切面编程的终极奥义_自定义切面类_02

所以,这个切面类更贴切的一种理解是这样的:本来通过 URL 是直接和我们所对应的方法连接的,但是这个切面类给这种连接切断了,从中间插了一脚,然后通过这个切面类访问的目标类,有点像代理模式,但是不仅限于代理模式,添加了一些其他的东西,它允许开发者将影响多个应用程序部分的问题(如日志记录、事务管理等)集中处理,而不是分散在多处代码中。按照我的个人理解是 AOP 的实现原理是基于代理模式的,但不限于此。

3.3、Spring AOP 核心概念

a)切点

切点,也称之为 "切入点",切点的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述),告诉程序对哪些方法来进行功能增强。也就是我们使用的 @Around 注解括号后面的 execution(* com.zmbdp.aopdemo.blogtest.*.*(..)) 就是切点表达式。

b)连接点

连接点就是原本要执行的方法,比如说上面写的切点代码,因为后面是 * ,blogtest 这个包的类的所有方法就是连接点。

c)通知

通知就是具体要做的工作,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)比如上述程序中打印的日志,就是通知。

d)切面

切面 = 切点 + 通知

图解如下:

AOP 革命:解锁面向切面编程的终极奥义_切面类的执行顺序_03

3.4、通知类型

上⾯我们讲了什么是通知, 接下来学习通知的类型.

@Around 就是其中⼀种通知类型,表示环绕通知。Spring 中 AOP 的通知类型有以下几种:

  • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  • @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  • @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  • @AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行

这些方法的执行顺序呢是 @Around前@Before > @AfterReturning@After > @Around后,如果说有异常,那么 @AfterReturning 和 @Around后 就不会执行,@AfterReturning 就会被替换成 @AfterThrowing

3.5、切面优先级

假设我们这时候定义了多个切面,比如:

@Slf4j
@Aspect
@Component
public class AspectTest1 {
    //前置通知
    @Before("execution(* com.zmbdp.aopdemo.blogtest.*.*(..))")
    public void doBefore() {
        log.info("执⾏ AspectTest1 -> Before ⽅法");
    }
    //后置通知
    @After("execution(* com.zmbdp.aopdemo.blogtest.*.*(..))")
    public void doAfter() {
        log.info("执⾏ AspectTest1 -> After ⽅法");
    }
}

@Slf4j
@Aspect
@Component
public class AspectTest2 {
    //前置通知
    @Before("execution(* com.zmbdp.aopdemo.blogtest.*.*(..))")
    public void doBefore() {
        log.info("执⾏ AspectTest2 -> Before ⽅法");
    }
    //后置通知
    @After("execution(* com.zmbdp.aopdemo.blogtest.*.*(..))")
    public void doAfter() {
        log.info("执⾏ AspectTest2 -> After ⽅法");
    }
}

@Slf4j
@Aspect
@Component
public class AspectTest3 {
    //前置通知
    @Before("execution(* com.zmbdp.aopdemo.blogtest.*.*(..))")
    public void doBefore() {
        log.info("执⾏ AspectTest3 -> Before ⽅法");
    }
    //后置通知
    @After("execution(* com.zmbdp.aopdemo.blogtest.*.*(..))")
    public void doAfter() {
        log.info("执⾏ AspectTest3 -> After ⽅法");
    }
}

运行结果如下:

AOP 革命:解锁面向切面编程的终极奥义_自定义切面类_04

由此我们能判断这个切面执行的顺序是根据切面类名字的 ASCll 值来判定的,并且限制性的切面类后结束。那我们程序猿肯定不会计算他的 ASCll 值啊,所以我们可以使用 @Order 这个注解来自定义优先级,如下:

@Slf4j
@Aspect
@Order(3)
@Component
public class AspectTest1 {
    //前置通知
    @Before("execution(* com.zmbdp.aopdemo.blogtest.*.*(..))")
    public void doBefore() {
        log.info("执⾏ AspectTest1 -> Before ⽅法");
    }
    //后置通知
    @After("execution(* com.zmbdp.aopdemo.blogtest.*.*(..))")
    public void doAfter() {
        log.info("执⾏ AspectTest1 -> After ⽅法");
    }
}

@Slf4j
@Aspect
@Order(1)
@Component
public class AspectTest2 {
    //前置通知
    @Before("execution(* com.zmbdp.aopdemo.blogtest.*.*(..))")
    public void doBefore() {
        log.info("执⾏ AspectTest2 -> Before ⽅法");
    }
    //后置通知
    @After("execution(* com.zmbdp.aopdemo.blogtest.*.*(..))")
    public void doAfter() {
        log.info("执⾏ AspectTest2 -> After ⽅法");
    }
}

@Slf4j
@Aspect
@Order(2)
@Component
public class AspectTest3 {
    //前置通知
    @Before("execution(* com.zmbdp.aopdemo.blogtest.*.*(..))")
    public void doBefore() {
        log.info("执⾏ AspectTest3 -> Before ⽅法");
    }
    //后置通知
    @After("execution(* com.zmbdp.aopdemo.blogtest.*.*(..))")
    public void doAfter() {
        log.info("执⾏ AspectTest3 -> After ⽅法");
    }
}

执行结果如下:

AOP 革命:解锁面向切面编程的终极奥义_自定义切面类_05

3.6、自定义 AOP

execution 表达式更适用有规则的,如果说我们要匹配多个无规则的方法呢?

这时候 execution 就只能在类上使用了,当我们自定义一个注解的时候,我们就可以设置它是能在方法上用的注解了,如下:

package com.zmbdp.aopdemo.blogtest;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {
}
==============================================================
package com.zmbdp.aopdemo.blogtest;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class MyAspectDemo {
    // 自定义注解的全限定名称
    @Before("@annotation(com.zmbdp.aopdemo.blogtest.MyAspect)")
    public void doBefore() {
        log.info("MyAspectDemo.doBefore: 前置通知");
    }

    @After("@annotation(com.zmbdp.aopdemo.blogtest.MyAspect)")
    public void doAfter() {
        log.info("MyAspectDemo.doAfter: 后置通知");
    }
}

然后我们的要使用的方法加上 @MyAspect 这个注解就行了,运行结果如下:

AOP 革命:解锁面向切面编程的终极奥义_AOP_06


四、总结

在本文中,我们深入探讨了面向切面编程(AOP)的核心思想、应用场景及其在 Spring 框架中的具体实现。AOP 通过分离关注点,将诸如日志记录、安全检查、事务管理等非核心业务逻辑从核心代码中抽离出来,从而简化了代码结构,提高了代码的可维护性和开发效率。

我们首先从 AOP 的定义入手,明确了 AOP 是一种编程思想,它关注于对某类特定问题的集中处理。随后,我们详细介绍了如何在Spring框架中引入 AOP 依赖、编写 AOP 程序,并通过实际代码示例展示了如何使用 @Aspect 注解来定义切面,以及如何通过切点表达式来指定哪些方法需要被增强。

在深入 AOP 的核心概念时,我们解释了切点、连接点、通知和切面等关键术语,并通过图解的方式帮助读者更好地理解它们之间的关系。此外,我们还详细讲解了不同类型的通知(如环绕通知、前置通知、后置通知、返回后通知和异常后通知),并讨论了这些通知的执行顺序。

对于切面优先级的问题,我们介绍了如何通过 @Order 注解来自定义切面的执行顺序,从而解决了多个切面同时存在时可能出现的执行顺序问题。

最后,我们探讨了如何自定义 AOP,通过定义一个自定义注解,并基于该注解来指定哪些方法需要被增强,从而实现了对无规则方法的匹配和处理。

五、结语

通过本文的学习,相信读者已经对 AOP 有了更深入的理解,并掌握了在 Spring 框架中使用 AOP 的基本方法。AOP 作为一种强大的编程范式,不仅能够帮助我们更好地管理代码的复杂性,还能提高代码的重用性和可维护性。在未来的软件开发过程中,我们可以更加灵活地运用 AOP 思想来优化我们的代码结构,提升开发效率。

希望本文能够成为你掌握 AOP 的得力助手,也期待你在未来的软件开发中能够灵活运用 AOP,创造出更加高效、清晰的代码。让我们一起在 AOP 的 "革命" 中,解锁更高效、更清晰的编程方式吧!下篇博客见。

AOP 革命:解锁面向切面编程的终极奥义_切面类的执行顺序_07