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

Spring之AOP面向切面编程详解

Spring之AOP面向切面编程详解

    • 一、AOP的核心思想与优势
      • 1.1 什么是AOP?
        • 示例:日志功能的两种实现
      • 1.2 AOP的核心优势
    • 二、AOP的核心术语
      • 通知(Advice)的类型
    • 三、Spring AOP的实现方式
    • 四、Spring AOP实战:日志切面案例
      • 4.1 环境准备
      • 4.2 目标业务类
      • 4.3 定义切面(Aspect)
      • 4.4 配置类与测试
        • 4.4.1 Spring配置类
        • 4.4.2 测试代码
      • 4.5 执行结果与分析
    • 五、切入点表达式(Pointcut Expression)
      • 5.1 `execution`表达式语法
      • 5.2 常用表达式示例
      • 5.3 其他切入点表达式
    • 六、AOP的高级应用:环绕通知与事务管理
      • 6.1 环绕通知(@Around)的高级用法
        • 示例:实现方法重试(失败后重试)
      • 6.2 Spring事务管理(AOP的典型应用)
    • 七、常见问题与避坑指南
      • 7.1 切面不生效(通知未执行)
      • 7.2 自调用导致AOP失效
      • 7.3 环绕通知未执行目标方法

AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架的核心特性之一,它通过“横切”思想,将日志、事务、权限等通用功能从业务逻辑中分离,实现代码解耦与复用,掌握AOP是编写优雅Spring代码的关键。

一、AOP的核心思想与优势

1.1 什么是AOP?

AOP是一种编程范式,核心是将“横切关注点”(如日志、事务)与“核心业务逻辑”分离。传统OOP(面向对象)通过继承和组合纵向组织代码,而AOP通过“切面”横向切入多个类的通用功能。

示例:日志功能的两种实现
  • 传统方式:在每个业务方法中手动添加日志代码(冗余、耦合);
  • AOP方式:定义日志切面,通过配置指定需要添加日志的方法,无需修改业务代码。

1.2 AOP的核心优势

  1. 代码解耦:通用功能(如日志)与业务逻辑分离,业务类只关注核心逻辑;
  2. 代码复用:横切关注点只需实现一次,可应用到多个目标方法;
  3. 便于维护:修改通用功能(如日志格式)只需调整切面,无需修改所有业务类;
  4. 集中管控:如事务管理、权限校验等可通过AOP集中控制。

二、AOP的核心术语

理解AOP术语是学习的基础,核心术语如下:

术语说明示例
切面(Aspect)封装横切关注点的类(如日志切面、事务切面)LogAspect
连接点(JoinPoint)程序执行过程中的可切入点(如方法调用、异常抛出)所有方法的执行过程
切入点(Pointcut)被AOP选中的连接点(需通过表达式指定)execution(* com.example.service.*.*(..))(匹配service包下所有方法)
通知(Advice)切面在切入点执行的操作(如日志打印)前置通知(方法执行前)、后置通知(方法执行后)
目标对象(Target)被切入的业务对象(如UserService)UserService的实例
代理对象(Proxy)AOP生成的目标对象的代理,用于执行切面逻辑Spring通过JDK动态代理或CGLIB生成的代理对象

通知(Advice)的类型

Spring AOP提供5种通知类型,覆盖方法执行的全生命周期:

  1. 前置通知(@Before):目标方法执行前执行;
  2. 后置通知(@After):目标方法执行后执行(无论是否抛出异常);
  3. 返回通知(@AfterReturning):目标方法正常返回后执行;
  4. 异常通知(@AfterThrowing):目标方法抛出异常后执行;
  5. 环绕通知(@Around):包裹目标方法,可控制目标方法的执行(最强大)。

三、Spring AOP的实现方式

Spring AOP基于动态代理实现,支持两种代理方式:

  • JDK动态代理:默认方式,代理接口(目标对象需实现接口);
  • CGLIB代理:目标对象无接口时使用,通过继承目标类实现代理。

Spring会自动选择代理方式,开发者无需手动干预。

四、Spring AOP实战:日志切面案例

以“日志切面”为例,演示Spring AOP的完整使用流程(基于注解配置)。

4.1 环境准备

添加Spring AOP依赖(Maven):

<dependencies><!-- Spring核心 --><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.3.20</version></dependency><!-- Spring AOP --><dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>5.3.20</version></dependency>
</dependencies>

4.2 目标业务类

定义一个Service作为目标对象(被切入的类):

package com.example.service;import org.springframework.stereotype.Service;@Service
public class UserService {// 目标方法1:查询用户public String getUserById(Integer id) {System.out.println("执行getUserById:查询ID为" + id + "的用户");return "用户" + id;}// 目标方法2:新增用户(可能抛出异常)public void addUser(String username) {if (username == null || username.isEmpty()) {throw new IllegalArgumentException("用户名不能为空");}System.out.println("执行addUser:新增用户" + username);}
}

4.3 定义切面(Aspect)

创建日志切面类,实现日志记录功能:

package com.example.aspect;import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;// 1. 标记为切面(@Aspect)和Spring组件(@Component)
@Aspect
@Component
public class LogAspect {// 2. 定义切入点(Pointcut):匹配UserService的所有方法@Pointcut("execution(* com.example.service.UserService.*(..))")public void userServicePointcut() {} // 切入点签名(无实际逻辑)// 3. 前置通知:目标方法执行前打印请求参数@Before("userServicePointcut()")public void beforeAdvice(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName(); // 获取方法名Object[] args = joinPoint.getArgs(); // 获取方法参数System.out.println("[前置通知] " + methodName + " 方法参数:" + Arrays.toString(args));}// 4. 返回通知:目标方法正常返回后打印返回值@AfterReturning(pointcut = "userServicePointcut()",returning = "result" // 绑定返回值)public void afterReturningAdvice(JoinPoint joinPoint, Object result) {String methodName = joinPoint.getSignature().getName();System.out.println("[返回通知] " + methodName + " 方法返回值:" + result);}// 5. 异常通知:目标方法抛出异常后打印异常信息@AfterThrowing(pointcut = "userServicePointcut()",throwing = "ex" // 绑定异常对象)public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {String methodName = joinPoint.getSignature().getName();System.out.println("[异常通知] " + methodName + " 方法抛出异常:" + ex.getMessage());}// 6. 后置通知:目标方法执行后(无论是否异常)打印结束信息@After("userServicePointcut()")public void afterAdvice(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();System.out.println("[后置通知] " + methodName + " 方法执行结束");}// 7. 环绕通知:包裹目标方法,可控制执行时机(最灵活)@Around("userServicePointcut()")public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {String methodName = joinPoint.getSignature().getName();long startTime = System.currentTimeMillis();try {// 执行目标方法(必须调用,否则目标方法不执行)Object result = joinPoint.proceed(); long endTime = System.currentTimeMillis();System.out.println("[环绕通知] " + methodName + " 方法执行耗时:" + (endTime - startTime) + "ms");return result; // 返回目标方法结果} catch (Throwable e) {// 可处理异常throw e; // 抛出异常,让异常通知捕获}}
}

4.4 配置类与测试

4.4.1 Spring配置类
package com.example.config;import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;@Configuration
@ComponentScan("com.example") // 扫描组件(包括切面和Service)
@EnableAspectJAutoProxy // 开启AOP注解支持
public class SpringConfig {
}
4.4.2 测试代码
package com.example;import com.example.config.SpringConfig;
import com.example.service.UserService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class AopTest {public static void main(String[] args) {// 加载Spring配置,启动容器AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);UserService userService = context.getBean(UserService.class);System.out.println("===== 测试正常方法 =====");userService.getUserById(1); // 调用无异常的方法System.out.println("\n===== 测试异常方法 =====");try {userService.addUser(null); // 调用会抛出异常的方法} catch (Exception e) {// 捕获异常(不影响程序执行)}context.close();}
}

4.5 执行结果与分析

===== 测试正常方法 =====
[前置通知] getUserById 方法参数:[1]
执行getUserById:查询ID为1的用户
[返回通知] getUserById 方法返回值:用户1
[后置通知] getUserById 方法执行结束
[环绕通知] getUserById 方法执行耗时:5ms===== 测试异常方法 =====
[前置通知] addUser 方法参数:[null]
执行addUser:新增用户null
[异常通知] addUser 方法抛出异常:用户名不能为空
[后置通知] addUser 方法执行结束

结果分析

  • 所有通知按预期执行,日志成功记录;
  • getUserById正常执行:触发前置→目标方法→返回→后置→环绕(耗时统计);
  • addUser抛出异常:触发前置→目标方法→异常→后置(无返回通知,因方法未正常返回)。

五、切入点表达式(Pointcut Expression)

切入点表达式用于指定“哪些方法需要被切入”,是AOP的核心配置,Spring AOP支持多种表达式,最常用的是execution

5.1 execution表达式语法

execution(修饰符? 返回值类型 包名.类名.方法名(参数类型) 异常类型?)
  • ?表示可选;
  • *表示任意(如任意返回值、任意方法名);
  • ..表示任意子包或任意参数。

5.2 常用表达式示例

表达式说明
execution(* com.example.service.*.*(..))匹配com.example.service包下所有类的所有方法
execution(public * com.example..*Service.*(..))匹配com.example及其子包中所有以Service结尾的类的public方法
execution(* com.example.service.UserService.get*(Integer))匹配UserService中以get开头、参数为Integer的方法
execution(* com.example.service.UserService.*(String, ..))匹配UserService中第一个参数为String的方法

5.3 其他切入点表达式

  • @annotation:匹配标注特定注解的方法(如@Transactional);
    @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
    
  • within:匹配特定包或类的所有方法;
    @Pointcut("within(com.example.service..*)") // 匹配service包及其子包的所有类
    
  • args:匹配参数类型符合指定条件的方法;
    @Pointcut("args(Integer, String)") // 匹配第一个参数为Integer、第二个为String的方法
    

六、AOP的高级应用:环绕通知与事务管理

6.1 环绕通知(@Around)的高级用法

环绕通知是最灵活的通知类型,可控制目标方法的执行(如超时控制、重试机制)。

示例:实现方法重试(失败后重试)
@Around("userServicePointcut()")
public Object retryAdvice(ProceedingJoinPoint joinPoint) throws Throwable {int maxRetry = 3; // 最大重试次数int retryCount = 0;while (retryCount < maxRetry) {try {return joinPoint.proceed(); // 执行目标方法} catch (Exception e) {retryCount++;if (retryCount >= maxRetry) {throw e; // 达到最大次数,抛出异常}System.out.println("方法执行失败,第" + retryCount + "次重试...");}}throw new RuntimeException("重试次数耗尽");
}

6.2 Spring事务管理(AOP的典型应用)

Spring的声明式事务(@Transactional)本质是AOP的应用:

  • 切面:Spring内置的事务切面;
  • 切入点:标注@Transactional的方法;
  • 通知:事务切面在目标方法执行前开启事务,执行后提交/回滚。
@Service
public class OrderService {@Autowiredprivate OrderMapper orderMapper;// 事务管理(AOP自动切入)@Transactionalpublic void createOrder(Order order) {orderMapper.insert(order); // 插入订单orderMapper.updateStock(order.getProductId()); // 更新库存// 若任意操作失败,AOP会自动回滚事务}
}

七、常见问题与避坑指南

7.1 切面不生效(通知未执行)

原因

  • 切面类未添加@Component(未被Spring扫描);
  • 未添加@EnableAspectJAutoProxy(未开启AOP支持);
  • 切入点表达式错误(未匹配到目标方法);
  • 目标类未被Spring管理(如手动new的对象,非容器中的Bean)。

解决方案

  • 确保切面类有@Aspect@Component
  • 配置类添加@EnableAspectJAutoProxy
  • 通过org.springframework.aop的DEBUG日志排查切入点匹配情况。

7.2 自调用导致AOP失效

问题:目标类内部方法调用(自调用)时,AOP通知不执行。

@Service
public class UserService {public void methodA() {methodB(); // 自调用,AOP不生效}@Transactional // 事务AOP在自调用时不生效public void methodB() { ... }
}

原因:AOP通过代理对象生效,自调用是目标对象内部调用,未经过代理。

解决方案

  • 避免自调用,或通过容器获取代理对象调用;
  • 配置exposeProxy=true,通过AopContext.currentProxy()获取代理对象。

7.3 环绕通知未执行目标方法

问题:环绕通知未调用proceed(),导致目标方法不执行。

@Around("userServicePointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) {// 错误:未调用joinPoint.proceed()System.out.println("环绕通知");return null; // 目标方法未执行
}

解决方案:环绕通知必须调用joinPoint.proceed(),否则目标方法会被拦截。

总结:AOP的核心要点与最佳实践
AOP通过“横切”思想解决了通用功能与业务逻辑的耦合问题,核心要点在于分离关注点
Spring AOP的最佳实践:

  1. 合理设计切面:一个切面专注一个功能(如日志切面、事务切面),避免大而全的切面;
  2. 精准切入点:切入点表达式尽量精确(如限定包、类、方法名),避免过度切入;
  3. 选择合适通知类型
  • 日志记录:前置+返回/异常通知;
  • 性能监控:环绕通知(需统计耗时);
  • 资源清理:后置通知(无论是否异常都需执行);
  1. 注意代理限制:避免自调用,确保目标对象是Spring容器管理的Bean;
  2. 结合注解使用:通过@annotation切入点,实现灵活的注解驱动AOP(如自定义@Log注解标记需要日志的方法)。

若这篇内容帮到你,动动手指支持下!关注不迷路,干货持续输出!
ヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノ

http://www.dtcms.com/a/288948.html

相关文章:

  • 软件工程学概述:从危机到系统化工程的演进之路
  • MySQL详解三
  • Java 字符集(Charset)详解:从编码基础到实战应用,彻底掌握字符处理核心机制
  • 文件编码概念|文件的读取操作|文件读取的课后练习讲解
  • 数据治理,治的是什么?
  • 0719代码调试记录
  • 【星海出品】python安装调试篇
  • 网络安全隔离技术解析:从网闸到光闸的进化之路
  • Spring Boot总结
  • RabbitMQ核心组件浅析:从Producer到Consumer
  • 深入理解设计模式:访问者模式详解
  • 深入理解浏览器解析机制和XSS向量编码
  • Java中List<int[]>()和List<int[]>[]的区别
  • React-Native开发环境配置-安装工具-创建项目教程
  • 数据并表技术全面指南:从基础JOIN到分布式数据融合
  • Pinia 核心知识详解:Vue3 新一代状态管理指南
  • 六边形滚动机器人cad【7张】三维图+设计书明说
  • [数据库]Neo4j图数据库搭建快速入门
  • 反激电源中的Y电容--问题解答
  • Python类中方法种类与修饰符详解:从基础到实战
  • linux shell从入门到精通(一)——为什么要学习Linux Shell
  • MybatisPlus-14.扩展功能-DB静态工具-练习
  • 0401聚类-机器学习-人工智能
  • VSCode中Cline无法正确读取终端的问题解决
  • Github 贪吃蛇 主页设置
  • hot100——第八周
  • 【文件IO】认识文件描述符和内核缓冲区
  • docker Neo4j
  • 【论文阅读笔记】RF-Diffusion: Radio Signal Generation via Time-Frequency Diffusion
  • Vue3虚拟滚动实战:从固定高度到动态高度,告别列表卡顿