news 2026/10/14 3:14:10

Spring AOP 从入门到原理:一文搞懂面向切面编程与动态代理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring AOP 从入门到原理:一文搞懂面向切面编程与动态代理

一. 为什么需要AOP?

首先,我们先来用一个例子来说明 AOP 存在的意义。就简单地以统计各个接口方法的执行耗时为例,根据我们之前学习的知识来说,我们需要在每个业务方法前后分别统计开始时间、结束时间并计算出所使用的时间(即开始时间 - 结束时间),并在日志中打印出来。但是,这样子实现这个功能的话就会引入一系列相应的问题:在每个接口方法中都存在有大量的重复代码,写起来吃力不说,后续如果要修改的话还需每个接口方法都要进行修改,十分费力。为了解决这个问题,我们就要引入 AOP 的思想。

二. AOP 概述

2.1 什么是 AOP

AOP,全称为 : Aspect Oriented Programming,意为面向切面编程。要理解 AOP 这个思想,我们首先要先明白切面是什么,切面指的就是抽出来的公共模块,也可以理解为某一类特定的问题,比如说 用户登录验证啦、方法耗时统计啦等等,都可以被认为是一个切面。而 AOP 就是针对相同类型的切面进行一些统一的处理的思想,而前文中所提到的拦截器就是 AOP 思想的一种体现,简单来说,AOP 就是一种针对某一类事情进行统一处理的一种思想。

2.2 AOP 的作用与优势

1. 可以在不修改原代码的基础上对已有的接口方法进行处理

2. 可以降低代码的耦合度,无侵入性

3. 减少了重复代码,提高了开发效率,代码维护起来方便

2.3 Spring AOP 是什么

上文中我们已经提到过,AOP 是一种思想,那么对于这个思想就会有很多实现方式,就比如说 Spring AOP 、AspectJ 和 CGLIB 就是几个实现了 AOP 思想的方式。而 Spring AOP 又是 Spring 框架的两大核心之一(IOC 和 Spring AOP),自然也是十分重要的部分,接下来,我们就来介绍如何使用 Spring AOP

三. Spring AOP 快速入门

在本小节中,我们将使用统一统计方法耗时为例来让我们对 Spring AOP 进行一个快速的入门

3.1 引入 AOP 依赖

要想在 Spring 项目中使用 Spring AOP,我们首先就要引入相应的依赖,所需的依赖如下所示:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc-test</artifactId> <scope>test</scope> </dependency>

直接将以上内容复制到Spring 项目中的 pom 文件当中并刷新 Maven 即可

3.2 编写 AOP 程序

在编写 AOP 程序时,我们会用到以下几个核心注解 : @Aspect、@Around,接下来,我们先来介绍这几个注解的作用

@Aspect :标记当前这个 Bean 是一个 AOP 切面类

@Around:环绕通知

切点表达式:"execution(* com.example.springaopdemo.controller.*.*(..))"

在此小节中,我们暂不用深究其概念问题,先理解代码的书写,在下一小节中我们会依次来讲解着其中的概念

该计时器的代码如下所示

@Slf4j @Aspect @Component public class AspectDemo1 { @Around("execution(* com.example.springaopdemo.controller.*.*(..))") public Object recordTime(ProceedingJoinPoint pjp) { log.info("目标方法执行前...."); //执行目标方法 Object result = null; try { result = pjp.proceed(); } catch (Throwable e) { log.error("do Around throwing...."); } log.info("目标方法执行后...."); return result; } }

代码分析:

方法执行前的逻辑:记录开始时间

目标方法执行的时机:pjp.proceed()

方法执行后的逻辑:记录结束时间 、计算出耗时并打印日志

当我们完成了这个代码后,只要是符合切点表达式的接口方法执行时,就会统一执行上述的计时器代码,这也体现了 AOP 的优点 : 无侵入、大大减少了重复代码 和 维护起来方便

四. Spring AOP 的核心概念

4.1 切点(Pointcut)

  • 也称之为 “切入点”,提供一组规则,告诉程序对哪些方法进行功能增强

  • 例如:"execution(* com.example.springaopdemo.controller.*.*(..))"

4.2 连接点(JoinPoint)

  • 满足切点表达式规则的方法,就是连接点,也就是可以被 AOP 控制的方法

4.3 通知(Advice)

  • 具体要做的工作,指重复的逻辑,也就是共性功能,最终体现为一个方法

4.4 切面(Aspect)

  • 切面 = 切点 + 通知

  • 切面所在类一般称为切面类,被 @Aspect 注解标识

4.5 切点与连接点的关系

  • 连接点是满足切点表达式的元素

  • 切点可以看做是保存了众多连接点的一个集合

五. Spring AOP 的通知类型

5.1 五种通知类型

  • @Around:环绕通知,目标方法前后都执行(分执行前的逻辑 和 执行后的逻辑)

  • @Before:前置通知,目标方法前执行

  • @After:后置通知,目标方法后执行,无论是否有异常都会执行

  • @AfterRunning:返回后通知,目标方法后执行,有异常不会执行

  • @AfterThrowing:异常后通知,目标方法发生异常后执行

5.2 未出现异常的通知的执行逻辑

@Around 前置逻辑 → @Before → 目标方法 → @AfterRunning → @After → @Around 后置逻辑

我们可以通过打印的日志内容来验证上述内容

5.3 出现异常的通知的执行逻辑

Around 前置逻辑 → @Before → @AfterThrowing → @After

同样的,我们也可以通过打印的日志内容来验证上述内容

六、切点表达式与 @Pointcut

6.1 execution 表达式

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

其中,访问修饰符 和 异常 这两部分可以省略

所支持的通配符:* 匹配一个元素 ,.. 匹配多个连续的任意符号

1.*

  • a. 包名使用*:表示任意包(一层包使用一个*)
  • b. 类名使用*:表示任意类
  • c. 返回值使用*:表示任意返回值类型
  • d. 方法名使用*:表示任意方法
  • e. 参数使用*:表示一个任意类型的参数

2...

  • a. 使用..配置包名:标识此包以及此包下的所有子包
  • b. 可以使用..配置参数:任意个任意类型的参数

示例:execution(* com.example.springaopdemo.controller.*.*(..))

6.2 @Pointcut 提取公共切点表达式

定义方式:在 @Pointcut 注解中加入 execution 表达式

例如:@Pointcut("execution(* com.example.springaopdemo.controller.*.*(..))")

public void pt(){};

后续在需要用到 切点表达式 的地方直接用 pt 代替即可

具体代码演示:

@Slf4j @Aspect @Component public class AspectDemo1 { //将切点表达式提取出来 @Pointcut("execution(* com.example.springaopdemo.controller.*.*(..))") public void pt(){}; @Around("pt()") public Object recordTime(ProceedingJoinPoint pjp) throws Throwable{ log.info("目标方法执行前...."); Object result = pjp.proceed(); log.info("目标方法执行后...."); return result; } }

6.3 @annotation 与自定义注解

上文中讲到的 execution 切点表达式适合用于匹配有规则的方法(比如说是都在 xx 包下的方法),而如果我们要匹配的是无规则的方法时,execution 切点表达式就不太适用了。为了解决这个问题,spring 引入了@annotation 注解 和 自定义注解

适用场景:匹配多个无规则的方法

实现方式:编写自定义注解 → 使 @annotation 表达式描述切点 → 在连接点方法上添加自定义注解

具体代码示例如下所示

自定义注解:

@Target(ElementType.METHOD) //注解生效的地方 @Retention(RetentionPolicy.RUNTIME) //注解生效的时机 //适用于无规则的情况 public @interface MyAspect { }

描述切点 和 要执行的逻辑

@Slf4j @Aspect @Component public class MyAspectDemo { @Around("@annotation(com.example.springaopdemo.aspect.MyAspect)") public Object recordTime(ProceedingJoinPoint pjp) { log.info("目标方法执行前...."); //执行目标方法 Object result = null; try { result = pjp.proceed(); } catch (Throwable e) { log.error("do Around throwing...."); } //记录方法执行结束时间 log.info("目标方法执行后...."); return result; } }

其中,@annotation 表达式 所描述的是 具有 @MyAspect 注解的这个方法就为目标方法,当我们完成了以上两个类之后,在其它类的方法中添加上 @MyAspect 注解,那么每当这个方法执行时,就会统一执行上述的通知逻辑

七、多个切面的执行顺序:@Order

在一个 Spring 项目中,我们可能会同时存在有多个切面,而切面之间是存在 执行顺序的,默认是按照切面类的类名字母排序,当然,Spring 也提供了设置各个切面的执行顺序的 注解 @Order,在 该注解后添加的数字越小,代表着该切面的执行顺序越快(添加的数字可以为负数)

八、Spring AOP 的实现方式

Spring AOP 共有四种实现方式,分别是:

  • 基于注解 @Aspect

  • 基于自定义注解

  • 基于 Spring API,通过 XML 配置

  • 基于代理来实现

九、Spring AOP 原理与动态代理

9.1 代理模式

定义:为其他对象提供一种代理以用来控制对这个对象的访问。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。在某些情况下,一个对象并不适合直接去调用另一个类中的方法,而是要通过调用代理类来间接调用目标类中的方法,而代理类在其中就扮演了一个中介的角色。

主要角色:Subject(业务接口类)、RealSubject(业务实现类)、Proxy(代理类)

Subject(业务接口类):可以是抽象类或接口(不一定存在)

RealSubject(业务实现类):具体的业务逻辑的实现,也就是被代理的对象

Proxy(代理类):RealSubject 的代理类

意义:代理模式可以在不修改被代理对象的基础上,通过扩展代理类进行功能附加与增强

9.2 静态代理

创建时机:程序运行前,代理类的 .class 文件就已经存在

缺点:代码写死,不够灵活

代码示例

接下来,我们就以房东让中介代理 出售/出租 房子为例来演示一个简单的静态代理代码逻辑

首先,我们先来定义Subject(业务接口类),房东和中介都要实现这个接口,具体代码如下所示

public interface HouseSubject { void saleHouse(); void rentHouse(); }

然后,我们要去完成 RealSubject(业务实现类),也就是房东类,该类实现了 Subject(业务接口类)并重写了 出售/出租 房子这两个方法,具体代码如下所示

public class RealHouseSubject implements HouseSubject{ @Override public void saleHouse() { System.out.println("我是房东, 我卖房"); } @Override public void rentHouse() { System.out.println("我是房东, 我出租房子"); } }

接下来,我们就要接着去完成 Proxy(代理类),也就是中介类,对于中介类来说,他需要帮助房东去 出售/出租 房子,那么,首先在 中介类 中就要创建有一个 房东对象 来存储 其代理的房东是谁,并在后续的 出售/出租 房子中进行代理。具体代码如下所示

public class HouseProxy implements HouseSubject{ private HouseSubject realHouseSubject; public HouseProxy(HouseSubject realHouseSubject) { this.realHouseSubject = realHouseSubject; } @Override public void saleHouse() { System.out.println("我是中介, 我帮房东开始代理"); realHouseSubject.saleHouse(); System.out.println("我是中介, 我帮房东结束代理"); } @Override public void rentHouse() { System.out.println("我是中介, 我帮房东开始代理"); realHouseSubject.rentHouse(); System.out.println("我是中介, 我帮房东结束代理"); } }

通过以上的三个代码,我们就简单地完成了一个静态代理的逻辑

9.3 动态代理

动态代理有两种实现方式,分别是 JDK 动态代理 和 CGLIB 动态代理

9.3.1 JDK 动态代理

接下来,我们来通过一个具体的代码示例(房东将房子代理给中介进行 出售/出租)来理解 JDK 动态代理(理解即可,无需太过注意代码内容)

要使用 JDK 动态代理,我们要按照以下两个步骤来使用

1. 编写“中介”的办事逻辑

该中介要实现InvocationHandler 接口

public class JDKInvocation implements InvocationHandler { //目标对象, 被代理对象 private Object target; public JDKInvocation(Object target) { this.target = target; } //调用目标方法 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("我开始代理..."); Object invoke = method.invoke(target, args); System.out.println("我结束代理"); return invoke; } }

invoke 方法:

这是整个 JDK 动态代理最核心的方法。 当代理对象(中介)的任何方法被调用时,请求都会被转发到这个 invoke 方法里

参数:proxy 表示的是生成的代理对象,method 表示的是目标方法,args 表示的是目标方法执行所需要的参数

method.invoke 方法:

这是“中介”真正干活的地方。method 是接口的方法,target 是真实的房东。这句话的意思是:“执行 target 对象上的 method 方法,参数是 args”。如果没有这一步,代理就只是空壳,无法执行真实的业务逻辑

参数:target 表示的是被代理的对象(也就是来执行目标方法的对象) ,args 表示的是目标方法执行所需要的参数

2. 动态生成中介并进行调用
public class Main { public static void main(String[] args) { HouseSubject target = new RealHouseSubject(); //JDK动态代理 //只能代理接口,不能代理类 //动态创建一个代理类 HouseSubject houseSubject = (HouseSubject) Proxy.newProxyInstance(target.getClass().getClassLoader(), new Class[]{HouseSubject.class},new JDKInvocation(target)); houseSubject.saleHouse(); } }

Proxy.newProxyInstance 方法:

在程序运行时动态生成代理类和代理对象实例,返回值为所生成的代理对象

参数:target.getClass().getClassLoader() 表示的是代理对象的类加载器,new Class[]{HouseSubject.class} 表示的是你要代理的接口是哪个,new JDKInvocation(target) 表示的是要执行的逻辑是什么

注意:JDK 动态代理只能够代理接口,不能够代理类 !!!

9.3.2 CGLIB 动态代理

JDK动态代理有⼀个最致命的问题是其只能代理实现了接口的类。但在有些场景下,我们的业务代码是直接实现的,并没有接口定义。为了解决这个问题,我们可以使用第三方实现的 CGLIB 动态代理机制来解决。

还是和之前讲解 JDK 动态代理时一样,我们还是通过一个具体的代码示例(房东将房子代理给中介进行 出售/出租)来理解 CGLIB 动态代理

要使用 JDK 动态代理,我们要按照以下三个步骤来使用

1. 引入依赖

由于 CGLIB 动态代理 是由第三方实现的,我们想要在 Spring 项目中使用前要先引入一个第三方的依赖,引入的依赖如下所示

<dependency>

<groupId>cglib</groupId>

<artifactId>cglib</artifactId>

<version>3.3.0</version>

</dependency>

值得注意的是,CGLIB 动态代理有多个实现方式,比如说 Spring 和 第三方等等,在我们使用第三方实现的 CGLIB 动态代理时,由于在 JDK 9 之后加了一些模块的原因,有一些方法不能再被反射访问,我们需要给 GVM 配置一个启动参数,如果使用的是 Spring 实现的动态代理则无需此步

添加方式如下图所示

添加的启动参数为

--add-opens java.base/java.lang=ALL-UNNAMED

2. 编写“中介”的办事逻辑
public class CGlibMethodInterceptor implements MethodInterceptor { private Object target; public CGlibMethodInterceptor(Object target) { this.target = target; } @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("我开始代理"); Object invoke = method.invoke(target, args); System.out.println("我结束代理"); return invoke; } }

CGLIB 动态代理 在这个代码示例上是和 JDK 动态代理十分相似的,其中 CGLIB 动态代理中的 intercept 方法也是和 JDK 动态代理 invoke 方法类似,参数方面只是多了一个 method 的代理对象 MethodProxy proxy,在执行目标方法是时可以通过 method.invoke 或 proxy.invoke 来执行

3. 动态生成中介并进行调用
public class Main { public static void main(String[] args) { HouseSubject target = new RealHouseSubject(); HouseSubject houseSubject = (HouseSubject) Enhancer.create(target.getClass(), new CGlibMethodInterceptor(target)); houseSubject.rentHouse(); } }

9.4 Spring AOP 的代理选择

再讲解 Spring AOP 的代理选择前,我们要先知道 JDK 动态代理 和 CGLIB 动态代理的区别: JDK 动态代理只能够代理接口而不能代理类,CGLIB 则既可以代理接口也可以代理类

而 Spring AOP 是选择了哪个代理呢?答案是,两个都要。那么,这就会出现一个新的问题,什么时候应该使用 JDK 动态代理?什么时候应该使用 CGLIB 动态代理呢?

为了解决这个问题,我们就要去探究 Spring AOP 的源码,在源码中我们可以发现,在代理工厂中有一个重要的属性——proxyTargetClass,其默认值为 false (这是针对在 Spring Framework 的场景下,在 Spring Boot 2.X 中默认值也为 false,而在 Spring Boot 3.X 中默认值则为 true,即默认使用 CGLIB 动态代理)。该属性与Spring AOP 所选择的代理模式的对应关系如下图所示

简单来说,Spring AOP 的代理选择可以概括为 proxyTargetClass = false 时,如果目标类实现了接口,优先使用 JDK 动态代理;如果目标类没有接口,则使用 CGLIB 动态代理。proxyTarget = true时,则强制使用 CGLIB 动态代理。在 Spring Boot 3.x 中,默认值为true,也就是默认使用 CGLIB 动态代理。理解这一点,也就能理解为什么 Spring AOP 既能代理接口,也能代理普通类

十、总结

到这里,Spring AOP 的核心内容就基本介绍完了。本文从“为什么需要 AOP”开始,以接口耗时统计为例,说明了 AOP 是为了解决横切逻辑重复、分散、难以维护的问题;然后介绍了 Spring AOP 的快速入门,包括 @Aspect、@Around和切点表达式;接着梳理了切点、连接点、通知、切面等核心概念,以及五种通知类型、通知执行顺序、@Pointcut、@annotation 自定义注解和 @Order;最后深入到代理模式,分析了静态代理、JDK 动态代理、CGLIB 动态代理,以及 Spring AOP 在两者之间的选择。

一句话总结:AOP 是一种“把公共逻辑抽出来,在合适的位置统一织入”的思想;Spring AOP 则是基于动态代理对这一思想的落地实现。它让我们在不修改原有业务代码的前提下,完成日志、耗时统计、权限校验、事务管理等统一处理

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 3:14:02

AI预览版与前沿隐藏能力:普通从业者如何用出专家级效果

前段时间&#xff0c;一位常年泡在 AI 前沿一线的研究者在公开场合说了句话&#xff0c;大意是&#xff1a;外界看到的 AI 能力&#xff0c;其实只是一段“预览版”的演示&#xff1b;真正的前沿系统&#xff0c;早就在处理那些连专业人才都要耗上数年才能完成的活了。这句话让…

作者头像 李华
网站建设 2026/10/14 3:13:21

C++冒泡排序从入门到优化:原理、实现与性能对比

1. 为什么冒泡排序值得你花时间搞懂很多人第一次接触排序算法&#xff0c;学的就是冒泡排序。但也正因为它是"第一课"&#xff0c;不少人学完就扔&#xff0c;觉得这玩意儿除了教学演示&#xff0c;实际开发中根本用不上。我一开始也这么想&#xff0c;直到后来带新人…

作者头像 李华
网站建设 2026/10/14 3:11:11

IT疑难杂症诊疗室:从故障现象到根因定位的实战指南

一、引言&#xff1a;为什么需要一间“IT疑难杂症诊疗室”在IT运维与开发工作中&#xff0c;我们常常会遇到一些看似无解、排查成本极高的“疑难杂症”——它们可能表现为偶发性的服务抖动、难以复现的内存泄漏、诡异的网络超时&#xff0c;或是环境差异导致的“在我机器上明明…

作者头像 李华
网站建设 2026/10/14 3:10:17

STEPX首款大模型原生智能体手机STEPX Neo正式发布

2026年10月13日&#xff0c;大模型原生AI终端品牌STEPX&#xff08;阶跃终端&#xff09;在上海举办“Ready Builder One”主题发布会&#xff0c;旗下首款大模型原生智能体手机STEPX Neo正式发布&#xff0c;并在京东、天猫、抖音等电商平台同步开售。STEPX Neo先行版搭载了智…

作者头像 李华