文章目录
-
- 一、什么是AOP
- [二、Spring AOP 使用入门](#二、Spring AOP 使用入门)
-
- [2.1 引入AOP依赖](#2.1 引入AOP依赖)
- [2.2 编写AOP程序](#2.2 编写AOP程序)
- [三、Spring AOP 详解](#三、Spring AOP 详解)
-
- [3.1 Spring AOP核心概念](#3.1 Spring AOP核心概念)
-
- [3.1.1 切点(Pointcut)](#3.1.1 切点(Pointcut))
- [3.1.2 连接点(Join Point)](#3.1.2 连接点(Join Point))
- [3.1.3 通知(Advice)](#3.1.3 通知(Advice))
- [3.1.4 切面(Aspect)](#3.1.4 切面(Aspect))
- [3.2 通知类型](#3.2 通知类型)
- [3.3 @PointCut](#3.3 @PointCut)
- [3.4 切面优先级 @Order](#3.4 切面优先级 @Order)
- [3.5 切点表达式](#3.5 切点表达式)
-
- [3.5.1 execution表达式](#3.5.1 execution表达式)
- [3.5.2 @annotation](#3.5.2 @annotation)

一、什么是AOP
AOP是Spring框架的第二大核心(第一大核心是 IoC)
什么是AOP?
Aspect Oriented Programming(面向切面编程)
-
面向切面编程:切面就是指某一类特定问题,所以AOP也可以理解为面向特定方法编程。
-
面向特定方法编程:比如"登录校验",就是一类特定问题。登录校验拦截器,就是对"登录校验"这类问题的统一处理。所以,拦截器也是AOP的一种应用。AOP是一种思想,拦截器是AOP思想的一种实现。Spring框架实现了这种思想,提供了拦截器技术的相关接口。
简单来说:AOP是一种思想,是对某一类事情的集中处理。
什么是Spring AOP?
AOP是一种思想,它的实现方法有很多,有Spring AOP,也有AspectJ、CGLIB等。Spring AOP是其中的一种实现方式。
AOP作用的维度更加细致(可以根据包、类、方法名、参数等进行拦截),能够实现更加复杂的业务逻辑。
举个例子:
现在有一个项目,项目中开发了很多的业务功能。现在有一些业务的执行效率比较低,耗时较长,我们需要对接口进行优化。第一步就需要定位出执行耗时比较长的业务方法,再针对该业务方法来进行优化。
java
public void function1(){
test1();
}
public void function1(){
Long startTime = System.currentTimeMillis();//记录开始时间
test1();
Long endTime = System.currentTimeMillis();//记录结束时间
Log.info("function1执行耗时:" + (endTime - startTime) + "ms");//记录方法执行耗时
}
这种方法是可以解决问题的,但一个项目中会包含很多业务模块,每个业务模块又有很多接口,一个接口又包含很多方法,如果要在每个业务方法中都记录方法的耗时,会增加很多的工作量。AOP就可以做到在不改动这些原始方法的基础上,针对特定的方法进行功能的增强。
AOP的作用: 在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性:解耦)
二、Spring AOP 使用入门
有如下需求:统计各个接口方法的执行时间。
2.1 引入AOP依赖
在pom.xml文件中添加配置
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2 编写AOP程序
记录Controller中每个方法的执行时间
java
@Slf4j
@Aspect
@Component
public class TimeAspect {
/**
* 记录方法耗时
*/
@Around("execution(* com.example.demo.controller.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//记录方法执行开始时间
long begin = System.currentTimeMillis();
//执行原始方法
Object result = pjp.proceed();
//记录方法执行结束时间
long end = System.currentTimeMillis();
//记录方法执行耗时
log.info(pjp.getSignature() + "执行耗时: {}ms", end - begin);
return result;
}
}
对程序进行简单的讲解
- @Aspect:标识这是一个切面类。
- @Around:环绕通知,在目标方法的前后都会被执行。后面的表达式表示对哪些方法进行增强。
- ProceedingJoinPoint.proceed():让原始方法执行。
整个代码划分为三部分:

通过上面的程序,可以感受到AOP面向切面编程的一些优势:
- 代码无侵入:不修改原始的业务方法,就可以对原始的业务方法进行了功能的增强或者是功能的改变。
- 减少了重复代码
- 提高开发效率
- 维护方便
三、Spring AOP 详解
Spring AOP 主要是以下几部分:
- Spring AOP中涉及的核心概念
- Spring AOP通知类型
- 多个AOP程序的执行顺序
3.1 Spring AOP核心概念
3.1.1 切点(Pointcut)
Pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述),告诉程序对哪些方法来进行功能增强。

上面的表达式 execution(* com.example.demo.controller.*.*(..)) 就是切点表达式。
3.1.2 连接点(Join Point)
满足切点表达式规则的方法,就是连接点。也就是可以被AOP控制的方法。
以上面的程序举例,所有 com.example.demo.controller 路径下的方法,都是连接点。
java
@RequestMapping("/book")
@RestController
public class BookController {
@RequestMapping("/addBook")
public Result addBook(BookInfo bookInfo) {
//...代码省略
}
@RequestMapping("/queryBookById")
public BookInfo queryBookById(Integer bookId) {
//...代码省略
}
@RequestMapping("/updateBook")
public Result updateBook(BookInfo bookInfo) {
//...代码省略
}
}
上述BookController 中的方法都是连接点。
切点和连接点的关系: 连接点是满足切点表达式的元素。切点可以看做是保存了众多连接点的一个集合。
3.1.3 通知(Advice)
通知就是具体要做的工作,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)。比如上述程序中记录业务方法的耗时时间,就是通知。

在AOP面向切面编程当中,j将这部分重复的代码逻辑抽取出来单独定义,这部分代码就是通知的内容。
3.1.4 切面(Aspect)
切面(Aspect)= 切点(Pointcut)+ 连接点(Join Point)+ 通知(Advice)
通过切面就能够描述当前AOP程序需要针对于哪些方法,在什么时候执行什么样的操作。
切面既包含了通知逻辑的定义,也包括了连接点的定义。
切面所在的类,我们一般称为切面类(被@Aspect注解标识的类)
3.2 通知类型
通知的类型:@Around 就是其中一种通知类型,表示环绕通知。Spring中AOP的通知类型有以下几种:
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行
- @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行
代码例子:
java
@Slf4j
@Aspect
@Component
public class AspectDemo {
//前置通知
@Before("execution(* com.whx.aop.demo.controller.*.*(..))")
public void doBefore() {
log.info("执行 Before 方法");
}
//后置通知
@After("execution(* com.whx.aop.demo.controller.*.*(..))")
public void doAfter() {
log.info("执行 After 方法");
}
//返回后通知
@AfterReturning("execution(* com.whx.aop.demo.controller.*.*(..))")
public void doAfterReturning() {
log.info("执行 AfterReturning 方法");
}
//抛出异常后通知
@AfterThrowing("execution(* com.whx.aop.demo.controller.*.*(..))")
public void doAfterThrowing() {
log.info("执行 doAfterThrowing 方法");
}
//添加环绕通知
@Around("execution(* com.whx.aop.demo.controller.*.*(..))")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Around 方法开始执行");
Object result = joinPoint.proceed();
log.info("Around 方法结束执行");
return result;
}
}
测试程序:
java
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1() {
return "t1";
}
@RequestMapping("/t2")
public boolean t2() {
int a = 10 / 0;
return true;
}
}
正常运行的情况:
观察日志:

程序正常运行的情况下,@AfterThrowing 标识的通知方法不会执行。
从日志可以看出来,@Around 标识的通知方法包含两部分,一个"前置逻辑",一个"后置逻辑"。其中"前置逻辑"会先于@Before 标识的通知方法执行,"后置逻辑"会晚于@After 标识的通知方法执行。
异常时的情况:
观察日志:

程序发生异常的情况下:
- @AfterReturning 标识的通知方法不会执行
- @AfterThrowing 标识的通知方法执行了
- @Around 环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会再执行了(因为原始方法调用出异常了)
注意事项
- @Around 环绕通知需要调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行。
- @Around 环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。
- 一个切面类可以有多个切点。
3.3 @PointCut
上面代码存在一个问题,就是存在大量重复的切点表达式 execution(* com.whx.aop.demo.controller.*.*(..)),Spring提供了@PointCut 注解,把公共的切点表达式提取出来,需要用到时引用该切入点表达式即可。
上述代码就可以修改为:
java
@Slf4j
@Aspect
@Component
public class AspectDemo {
//定义切点(公共的切点表达式)
@Pointcut("execution(* com.whx.aop.demo.controller.*.*(..))")
private void pt(){}
//前置通知
@Before("pt()")
public void doBefore() {
//...代码省略
}
//后置通知
@After("pt()")
public void doAfter() {
//...代码省略
}
//返回后通知
@AfterReturning("pt()")
public void doAfterReturning() {
//...代码省略
}
//抛出异常后通知
@AfterThrowing("pt()")
public void doAfterThrowing() {
//...代码省略
}
//添加环绕通知
@Around("pt()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
//...代码省略
}
}
当切点定义使用private修饰时,仅能在当前切面类中使用,当其他切面类也要使用当前切点定义时,就需要把private改为public。引用方式为:全限定类名.方法名()
java
@Slf4j
@Aspect
@Component
public class AspectDemo2 {
//前置通知
@Before("com.example.demo.aspect.AspectDemo.pt()")
public void doBefore() {
log.info("执行 AspectDemo2 -> Before 方法");
}
}
3.4 切面优先级 @Order
当定义了多个切面类时,并且这些切面类的多个切入点都匹配到了同一个目标方法。当目标方法运行的时候,这些切面类中的通知方法都会执行,那么这几个通知方法的执行顺序是什么。
当存在多个切面类时,默认按照切面类的类名字母排序:
- @Before 通知:字母排名靠前的先执行
- @After 通知:字母排名靠前的后执行
但实际开发中类名不会按照字母顺序来定义,此时会不方便管理。
Spring 给我们提供了一个新的注解,来控制这些切面通知的执行顺序:@Order
使用方式如下:
java
@Aspect
@Component
@Order(2)
public class AspectDemo2 {
//...代码省略
}
@Aspect
@Component
@Order(1)
public class AspectDemo3 {
//...代码省略
}
@Aspect
@Component
@Order(3)
public class AspectDemo4 {
//...代码省略
}
使用了 @Order 注解标识的切面类,执行顺序如下:
- @Before 通知:数字越小先执行
- @After 通知:数字越大先执行
@Order 控制切面的优先级,先执行优先级较高的切面,再执行优先级较低的切面,最终执行目标方法。
3.5 切点表达式
切点表达式常见有两种表达方式:
- execution(... ):根据方法的签名来匹配
- @annotation(... ):根据注解匹配
3.5.1 execution表达式
execution() 是最常用的切点表达式,用来匹配方法,语法为:
execution(<访问修饰符> <返回类型> <包名.类名.方法(方法参数)> <异常>)
其中:访问修饰符和异常可以省略

切点表达式支持通配符表达:
*:匹配任意字符,只匹配一个元素(返回类型、包、类名、方法或者方法参数)
包名使用*表示任意包(一层包使用一个*)
类名使用*表示任意类
返回值使用*表示任意返回值类型
方法名使用*表示任意方法
参数使用*表示一个任意类型的参数..:匹配多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
使用..配置包名,标识此包以及此包下的所有子包
可以使用..配置参数,任意个任意类型的参数
切点表达式示例表:
| 匹配场景 | execution 切点表达式 |
|---|---|
| TestController 下public修饰、返回String、方法名t1、无参方法 | execution(public String com.example.demo.controller.TestController.t1()) |
| 省略访问修饰符(同上述方法,无public限定) | execution(String com.example.demo.controller.TestController.t1()) |
| 匹配TestController下t1方法、任意返回类型 | execution(* com.example.demo.controller.TestController.t1()) |
| 匹配TestController下的所有无参方法 | execution(* com.example.demo.controller.TestController.*()) |
| 匹配TestController下的所有方法(任意参数) | execution(* com.example.demo.controller.TestController.*(..)) |
| 匹配controller包下所有类的所有方法 | execution(* com.example.demo.controller.*.*(..)) |
| 匹配所有包下的TestController类的所有方法 | execution(* com..TestController.*(..)) |
| 匹配com.example.demo包及子孙包下所有类的所有方法 | execution(* com.example.demo..*(..)) |
3.5.2 @annotation
execution表达式更适用有规则的,如果要匹配多个无规则的方法,比如:TestController中的t1() 和UserController中的u1()这两个方法。
这个时候使用execution这种切点表达式来描述就不是很方便了。
可以借助自定义注解的方式以及另一种切点表达式@annotation 来描述这一类的切点。
实现步骤:
- 编写自定义注解
- 使用@annotation 表达式来描述切点
- 在连接点的方法上添加自定义注解
准备测试代码:
java
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1() {
return "t1";
}
@RequestMapping("/t2")
public boolean t2() {
return true;
}
}
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/u1")
public String u1() {
return "u1";
}
@RequestMapping("/u2")
public String u2() {
return "u2";
}
}
- 自定义注解 @MyAspect:创建一个注解类(和创建Class文件一样的流程,选择Annotation)
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {
}
- 切面类:使用@annotation 切点表达式定义切点,只对 @MyAspect 生效
切面类代码如下:
java
@Slf4j
@Component
@Aspect
public class MyAspectDemo {
//前置通知
@Before("@annotation(com.example.demo.aspect.MyAspect)")
public void before() {
log.info("MyAspect -> before ...");
}
//后置通知
@After("@annotation(com.example.demo.aspect.MyAspect)")
public void after() {
log.info("MyAspect -> after ...");
}
}
- 添加自定义注解:在TestController中的t1()和UserController中的u1()这两个方法上添加自定义注解 @MyAspect,其他方法不添加
接口代码:
java
@MyAspect
@RequestMapping("/t1")
public String t1() {
return "t1";
}
@MyAspect
@RequestMapping("/u1")
public String u1() {
return "u1";
}
同理,不仅可以自定义注解,还可以对已有的注解进行生效,比如接口的@RequestMapping注解,使得对所有添加了@RequestMapping注解的接口都生效。