当前位置: 首页 > news >正文

Spring AOP 详解

一、什么是 AOP

AOP,即 Aspect Oriented Programming(面向切面编程),简单来说就是针对一类事情的集中处理。在我们之前讲到拦截器,就是针对用户登录进行了统一处理,统一数据返回、统一异常处理,就实现了 AOP。

上述的拦截器、统一数据返回、统一异常处理,虽然是 Spring 进行实现的,但并不代表 AOP 就是 Spring 的。也就是说,AOP 是一种思想,任何人都可以实现。常见的实现方式有 Spring AOP、AspectJ,CGLIB等。下面针对 Spring AOP 进行讲解。

二、使用 Spring AOP

下面我们来看一下如何使用 AOP 来进行编写代码。

现有下面场景,需要针对 controller 类中的每一个方法记录其运行时间,这段逻辑的代码虽然只有几行,但如果需要在每一个代码中进行实现,就使得代码变得冗余,这里我们就可以使用 Spring AOP 来进行开发。

在使用 Spring AOP 之前需要引入以下依赖:

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

记录方法耗时代码如下:

@Slf4j
@Aspect
@Component
public class TimeRecordAspect {/*** 记录 controller 下的方法运行的时间* @param pjp* @return* @throws Throwable*/@Around("execution(* com.gjm.demo.controller.*.*(..))")public Object timeRecord(ProceedingJoinPoint pjp) throws Throwable {log.info("开始记录时间");long begin = System.currentTimeMillis();Object proceed = pjp.proceed();long end = System.currentTimeMillis();log.info("方法耗时: " + (end - begin) + "ms");log.info("记录时间结束");return proceed;}
}

controller 测试代码如下:

@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {@RequestMapping("/method1")public void method1() {log.info("运行 method1");for (int i = 0; i < 1000000; i++) {}}
}

代码运行结果如下:

在这里我们看到,method1 方法的运行时间成功记录。下面针对 Spring AOP 进行详细讲解。

三、Spring AOP核心概念

核心概念有:切点、切面、通知、连接点。下面是这几个概念在具体的代码中表示哪一部分。

 1、切点

切点的作用就是告诉程序对哪些方法记录时间。

execution(* com.gjm.demo.controller.*.*(..))

上面这一段代码就是切点表达式,表示的意思是对于 controller 下的所有类、所有方法、不论参数个数均生效。切点表达式在下面会进行详细解释。 

2、连接点

连接点就是 AOP 程序对哪些方法生效,这些方法就是连接点。

连接点就是满足切点的各个元素,切点就是所有连接点的集合。

3、通知

通知就是 AOP 具体要执行的逻辑,也就是上面的记录方法耗时。

4、切面

切面 = 切点 + 通知,即当前 AOP 程序针对哪些方法,具体实行什么操作。

四、通知类型

通知类型一共有五种:

@Around : 环绕通知,在连接点前运行前后均会执行;

@Before : 前置通知,在连接前运行之前执行;

@After : 后置通知,在连接点运行之后执行,无论是否有异常都会执行;

@AfterReturning : 返回后通知,在连接点运行结束后执行,有异常就不会执行;

@AfterThrowing : 异常后通知,当连接点运行时发生异常时执行。

AOP 代码如下:

@Slf4j
@Aspect
@Component
public class TimeRecordAspect {/*** 记录 controller 下的方法运行的时间* @param pjp* @return* @throws Throwable*/@Around("execution(* com.gjm.demo.controller.*.*(..))")public Object timeRecord(ProceedingJoinPoint pjp) throws Throwable {log.info("开始记录时间");long begin = System.currentTimeMillis();Object proceed = pjp.proceed();long end = System.currentTimeMillis();log.info("方法耗时: " + (end - begin) + "ms");log.info("记录时间结束");return proceed;}@Before("execution(* com.gjm.demo.controller.*.*(..))")public void doBefore() {log.info("do before");}@After("execution(* com.gjm.demo.controller.*.*(..))")public void doAfter() {log.info("do after");}@AfterReturning("execution(* com.gjm.demo.controller.*.*(..))")public void doAfterReturning() {log.info("do afterReturning");}@AfterThrowing("execution(* com.gjm.demo.controller.*.*(..))")public void doAfterThrowing() {log.info("do afterThrowing");}
}

连接点代码如下:

@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {@RequestMapping("/method1")public void method1() {log.info("运行 method1");for (int i = 0; i < 1000000; i++) {}}@RequestMapping("/method2")public void method2() {log.info("运行 method2");int a = 1 / 0;}
}

method1 运行结果如下:

method2 运行结果如下:

 上面是程序正常运行和运行时发生异常的结果。

从第二个结果可以看出,当连接点发生异常时,@Around 后面的逻辑就不会执行,但 @After 会执行。

通过上面的结果可以看出,我们可以将 @Before,@After,@AfterRunning,@AfterThrowing 中的逻辑移至 @Around 中 :

“do before” 就相当于 “开始记录时间”;

“do after”、“do afterRunning” 就相当于 “记录时间结束”;

Object proceed = pjp.proceed();

这一段代码就相当于执行连接点方法,我们可以加上 try - catch,这样就能捕获住连接点运行时发生的异常,但必须使用 Throwable 进行捕获,于是,“do afterThrowing” 就相当于 try - catch,改动过的代码如下:

@Slf4j
@Aspect
@Component
public class TimeRecordAspect {@Around("execution(* com.gjm.demo.controller.*.*(..))")public Object timeRecord(ProceedingJoinPoint pjp) throws Throwable {log.info("do before");long begin = System.currentTimeMillis();Object proceed = null;try {proceed = pjp.proceed();} catch (Throwable e){log.info("do afterThrowing");return proceed;}long end = System.currentTimeMillis();log.info("方法耗时: " + (end - begin) + "ms");log.info("do after");return proceed;}}

代码运行结果如下:

根据上面的运行结果,可以看出:@Around 可以代替 @Before、@After、@AfterRunning、@AfterThrowing。

 五、@Pointcut

在上面的代码中,当我们使用了多个相同的切点表达式时,就会显得代码过于冗余,这样我们就可以将切点表达式单独定义出来,代码如下:

@Slf4j
@Aspect
@Component
public class TimeRecordAspect {@Pointcut("execution(* com.gjm.demo.controller.*.*(..))")private void pt() {}@Around("pt()")public Object timeRecord(ProceedingJoinPoint pjp) throws Throwable {log.info("do before");long begin = System.currentTimeMillis();Object proceed = null;try {proceed = pjp.proceed();} catch (Throwable e){log.info("do afterThrowing");return proceed;}long end = System.currentTimeMillis();log.info("方法耗时: " + (end - begin) + "ms");log.info("do after");return proceed;}@Before("pt()")public void doBefore() {log.info("do before");}@After("pt()")public void doAfter() {log.info("do after");}@AfterReturning("pt()")public void doAfterReturning() {log.info("do afterReturning");}@AfterThrowing("pt()")public void doAfterThrowing() {log.info("do afterThrowing");}
}

如果在别的类中也想使用这个切点时,就需要将 pt 设为 public,引用方式为:全限定类名 . 方法名(),代码如下:

@Slf4j
@Aspect
@Component
public class AspectDemo {@Pointcut("com.gjm.demo.aspect.TimeRecordAspect.pt()")public void pt(){}@Before("pt()")public void doBefore() {log.info("do before2");}@After("com.gjm.demo.aspect.TimeRecordAspect.pt()")public void doAfter() {log.info("do after2");}
}

可以重新定义一个切点 pt ,也可以在注解中直接使用 com.gjm.demo.aspect.TimeRecordAspect.pt() 。

六、切面优先级

当我们定义了多个切点类时,这些切点类的执行顺序时怎样的呢?

现有如下代码:

@Slf4j
@Aspect
@Component
public class AspectDemo1 {@Before("execution(* com.gjm.demo.controller.*.*(..))")public void doBefore() {log.info("do before1");}@After("execution(* com.gjm.demo.controller.*.*(..))")public void doAfter() {log.info("do after1");}
}

另外还有三个与之相同的类,类名依次为AspectDemo2,AspectDemo3,AspectDemo4,代码运行结果如下:

这里我们可以看到,依次执行了 AspectDemo1,AspectDemo2,AspectDemo3,AspectDemo4。这是 Spring 给我们排的序,那如果我们想要自定义切面类的执行顺序呢?

可以使用 @Order 注解,这是类注解,注解中的数字越小,切面的优先级越高。

现将 AspectDemo1 的 @Order 设为4,AspectDemo2 的 @Order 设为3,AspectDemo3 的 @Order 设为2,AspectDemo4 的 @Order 设为1,代码运行结果如下:

这样就依次执行了AspectDemo4,AspectDemo3, AspectDemo2, AspectDemo1。

代码执行图如下:

七、切点表达式

在上面的代码中,我们使用切点表达式来描述 AOP 作用与哪些连接点,常用的切点表达式有两种,分别为:

execution:根据方法名进行匹配;

@annotation:根据注解匹配。

下面进行一一介绍。

1、execution

execution 的标准语法如下:

execution(<访问修饰符> <返回类型> <包名.类名.⽅法(⽅法参数)> <异常>)

上面的代码中我们使用的切点表达式如下:

execution(* com.gjm.demo.controller.*.*(..))

二者的关系如下图所示:

访问修饰符:public、private、protected、default,可省略;

返回类型:连接点方法的返回类型;

包名:连接点所在的包的全限定名;

类名:连接点所在的类;

方法:即连接点;

异常:连接点所抛出的异常,可省略。

切点表达式支持通配符表达。

* :匹配任意字符,只匹配一个元素

包名使用 * 表示匹配任意包,一个包名使用一个 *;

类名使用 * 表示任意类,一个 * 可以匹配多个类;

返回值使用 * 表示任意类型的返回值;

方法名使用 * 表示任意方法;

参数使用 * 表示一个任意类型的参数,只匹配一个参数。

. . :匹配多个连续的 * 

使用 . .  配置包名,即匹配该包下的多个子包;

使用 . .  配置参数,即匹配多个任意类型的参数。

2、@annotation

execution 适用与匹配有规则的连接点,例如一个类下的多个方法,但如果想要匹配两个类下的多个方法,就需要使用注解来完成。

这个注解不是 Spring 为我们提供的,而是我们自己创造的。

下面是自定义注解的方法:

① 先声明自定义的注解,需表明该注解的作用对象(使用 @Target 注解)、生命周期(使用 @Retention 注解)。

常见的作用对象有以下几个:

ElementType.TYPE :描述类、接口或 enum 声明;

ElementType.METHOD :描述方法;

ElementType.PARAMETER :描述参数

ElementType.TYPE_USE :描述任意类型

在这里我们使用 @Target(ElementType.METHOD)。

常见的声明周期有以下几个:

RetentionPolicy.SOURCE:注解仅存在于源代码中,编译后会被丢弃

RetentionPolicy.CLASS:编译时注解,仅存在于源代码和字节码中,运行时会被丢弃

RetentionPolicy.RUNTIME:运行时注解,存在于源代码、字节码和运行中

在这里我们使用 @Retention(RetentionPolicy.RUNTIME)。

注解声明代码如下:

@Target(ElementType.METHOD) //作用范围
@Retention(RetentionPolicy.RUNTIME) //生命周期
public @interface MyAspect {
}

② 定义该注解的用途,即加上这个注解后需要做什么。

在这里,我们需要记录方法运行的时间,代码如下:

@Slf4j
@Aspect
@Component
public class MyTimeRecordAspect {@Around("@annotation(com.gjm.demo.aspect.MyAspect)")public Object timeRecord(ProceedingJoinPoint pjp) throws Throwable {log.info("do before");long begin = System.currentTimeMillis();Object proceed = null;try {proceed = pjp.proceed();} catch (Throwable e){log.info("do afterThrowing");return proceed;}long end = System.currentTimeMillis();log.info("方法耗时: " + (end - begin) + "ms");log.info("do after");return proceed;}
}

在 @Around 注解中需要使用 @annotation + 自定义注解的全限定类名,也可以使用 @Before 等注解,代码运行结果如下:

 

当然,在 @annotation 后面也可以使用别的注解的全限定类名,这样在使用别的该注解时就可以执行下面的逻辑。

八、Spring AOP 的实现方式(常见面试题)

1、基于 @Aspect 进行实现,即 @Around、@Before、@After、@AfterRunning、@AfterThrowing;

2、基于自定义注解,即 @annotation;

3、基于 Spring API(少见);

4、基于代理模式(更少见)。

相关文章:

  • ARCGIS PRO 在地图中飞行
  • (done) 吴恩达版提示词工程 9. 总结 (就是复述一遍前面的内容,以及建议你基于LLM开发应用程序)
  • 8、HTTPD服务--CGI机制
  • linux两个特殊的宏 _RET_IP_ 和_THIS_IP_ 实现
  • 第15节:传统分类模型-K近邻(KNN)算法
  • 【文献速递】snoRNA-SNORD113-3/ADAR2通过对PHKA2的A-to-I编辑影响胶质母细胞瘤糖脂代谢
  • Nginx配置文件介绍
  • 创建一个springboot的项目-简洁步骤
  • 【前端基础】viewport 元标签的详细参数解析与实战指南
  • 【项目实训个人博客】multi-agent调研(1)
  • DES密码系统的差分分析
  • DLNA 功能
  • LINUX427 冒险位 粘滞位 chmod 权限
  • 杭州小红书代运营公司-品融电商:专业赋能品牌社交增长
  • Leetcode837.新21点
  • OpenCV彩色图像分割
  • 突破常规:探索无 get 方法类设计的独特魅力
  • 互联网大厂Java面试实录:从Spring Boot到微服务架构的技术问答
  • 硬件工程师面试常见问题(9)
  • 使用 Cherry Studio 调用高德 MCP 服务
  • 古籍新书·2025年春季|中国土司制度史料集成
  • 现场|西岸美术馆与蓬皮杜启动新五年合作,新展今开幕
  • 美加征“对等关税”后,调研显示近半外贸企业将减少对美业务
  • 科学时代重读“老子”的意义——对谈《老子智慧八十一讲》
  • 我国首个大型通用光谱望远镜JUST在青海启动建设
  • 梅花画与咏梅诗