news 2026/4/22 14:24:59

一文搞懂Spring AOP:面向切面编程的核心理念与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一文搞懂Spring AOP:面向切面编程的核心理念与实战

📌 写在前面

初学Spring时,AOP(Aspect Oriented Programming,面向切面编程)是一个听起来很厉害但实际用得很少的概念。我知道它能在不修改源码的情况下给方法增加功能,比如记录日志、测量执行时间。但除了这些“经典案例”,我在项目中似乎很少主动使用AOP。

直到后来维护一个老项目,看到几百个Service方法里都有一模一样的log.info("开始执行xxx")log.info("执行结束"),我才意识到:这种横切逻辑(Cross-Cutting Concerns)完全可以用AOP统一处理。另外,在微服务中做分布式锁时,用AOP封装@RedisLock注解,比在每个方法里写锁逻辑清爽得多。

AOP的思想其实不难:把业务无关的公共逻辑抽出来,动态织入到目标方法中。但切点表达式、通知类型、代理方式这些细节,确实容易混淆。这篇笔记,我从初学者的视角,带你系统理解Spring AOP的核心概念、实战用法和底层原理,顺便搞定面试高频题。

1️⃣ 为什么需要AOP?—— 从重复代码说起

假设你有一个计算器服务,需要给每个方法加上日志和耗时统计:

@Service public class Calculator { public int add(int a, int b) { System.out.println("【开始】add方法"); long start = System.currentTimeMillis(); int result = a + b; long end = System.currentTimeMillis(); System.out.println("【结束】add方法,耗时:" + (end - start) + "ms"); return result; } public int divide(int a, int b) { System.out.println("【开始】divide方法"); // ... 同样的代码 } // 每个方法都要重复写这些“非业务”代码 }

这种代码侵入性强难以维护无法复用。AOP的核心思想就是:将这些横切关注点(日志、性能、事务)从业务逻辑中抽取出来,动态地织入到目标方法中。

2️⃣ AOP核心概念(先理解这几个名词)

3️⃣ Spring AOP实战:五种通知类型

Spring AOP支持五种通知,以@Aspect注解为例(最常用方式)。

第一步:引入依赖

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

第二步:定义切面类

@Aspect @Component public class LogAspect { // 定义切点:匹配com.example.service包下所有类的所有方法 @Pointcut("execution(* com.example.service.*.*(..))") public void servicePointcut() {} // 前置通知 @Before("servicePointcut()") public void logBefore(JoinPoint joinPoint) { System.out.println("【前置】开始执行:" + joinPoint.getSignature().getName()); } // 后置通知(方法正常返回后执行) @AfterReturning(pointcut = "servicePointcut()", returning = "result") public void logAfterReturning(JoinPoint joinPoint, Object result) { System.out.println("【后置】方法返回:" + result); } // 异常通知 @AfterThrowing(pointcut = "servicePointcut()", throwing = "error") public void logAfterThrowing(JoinPoint joinPoint, Throwable error) { System.out.println("【异常】方法异常:" + error.getMessage()); } // 最终通知(类似finally) @After("servicePointcut()") public void logFinally(JoinPoint joinPoint) { System.out.println("【最终】方法执行完毕"); } // 环绕通知(最强大,可控制是否执行目标方法) @Around("servicePointcut()") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); System.out.println("【环绕前】方法开始"); Object result = joinPoint.proceed(); // 执行目标方法 long end = System.currentTimeMillis(); System.out.println("【环绕后】方法耗时:" + (end - start) + "ms"); return result; } }

切点表达式详解(面试重点)

格式:execution(修饰符? 返回类型 包名.类名.方法名(参数) 异常?)

4️⃣ 基于注解的AOP(更灵活)

很多时候,我们只想给特定方法加增强,而不是某个包下的所有方法。这时候可以自定义注解。

自定义注解

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RedisLock { String key(); long expire() default 3000; }

切面实现

@Aspect @Component public class RedisLockAspect { @Around("@annotation(redisLock)") public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable { String key = redisLock.key(); // 尝试加锁逻辑... if (tryLock(key, redisLock.expire())) { try { return joinPoint.proceed(); } finally { unlock(key); } } else { throw new RuntimeException("获取锁失败"); } } }

使用

@Service public class OrderService { @RedisLock(key = "#orderId", expire = 5000) public void processOrder(Long orderId) { // 业务逻辑,自动加锁 } }

这种方式的优点:声明式编程,业务代码完全无侵入。

5️⃣ AOP底层原理:动态代理

Spring AOP基于动态代理实现。当目标类实现了接口时,使用JDK动态代理;否则使用CGLIB生成子类代理。

JDK动态代理 vs CGLIB

强制使用CGLIB(Spring Boot默认是JDK代理):

spring.aop.proxy-target-class=true

重要概念:织入时机

Spring AOP是运行时织入(通过代理),而非编译时或类加载时。所以不能代理private方法,也不能代理final类。

6️⃣ 常见应用场景(不止日志)

7️⃣ 面试高频题与避坑指南

Q1:Spring AOP和AspectJ有什么区别?

  • Spring AOP:基于动态代理,运行时织入,只支持方法级别的连接点,性能较好,使用简单。

  • AspectJ:基于字节码增强(编译时/类加载时织入),支持字段、构造器等更细粒度的连接点,功能更强大,但配置复杂。

Spring Boot默认使用Spring AOP,如果要用AspectJ需额外配置。

Q2:同一个类内部调用被AOP增强的方法,为什么增强不生效?

原因:AOP基于代理,内部调用是通过this直接调用目标对象方法,而不是通过代理对象。解决方案:

  • 把方法移到另一个Bean中

  • 从IoC容器获取自己的代理:((YourService) AopContext.currentProxy()).method()

  • 使用@EnableAspectJAutoProxy(exposeProxy = true)

Q3:如何获取被增强方法的参数和返回值?

  • 参数:JoinPoint.getArgs()

  • 返回值:在@AfterReturningreturning属性中指定变量名

Q4:多个切面的执行顺序?

  • 默认顺序不确定

  • 使用@Order注解或实现Ordered接口,数字越小优先级越高(越先执行前置通知,越后执行后置通知)

避坑指南

  1. 不能代理private方法,因为代理类无法访问父类的私有方法。

  2. final类无法被CGLIB代理,因为CGLIB需要生成子类。

  3. 不要滥用AOP,过度使用会让代码难以调试(“魔法”太多)。

  4. 环绕通知记得调用proceed(),否则目标方法不会执行。

8️⃣ 总结:AOP的“灵魂三问”

如果一个Service类中的方法A调用了同一个类中的方法B,且方法B上标注了@Cacheable,那么调用方法A时,方法B的缓存能生效吗?为什么?如何解决?

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

ESP32网络调试与数据转发实战:利用AP+STA与NAT构建本地测试网关

ESP32网络调试与数据转发实战&#xff1a;利用APSTA与NAT构建本地测试网关 在物联网设备开发过程中&#xff0c;网络调试和数据转发是工程师们经常面临的挑战。想象一下这样的场景&#xff1a;你正在开发一款智能传感器设备&#xff0c;需要频繁进行固件升级测试和数据上报验证…

作者头像 李华
网站建设 2026/4/22 14:19:32

从GLUT到GLFW:为什么现代OpenGL项目都换成了它?

从GLUT到GLFW&#xff1a;现代OpenGL项目的窗口管理库演进之路 在计算机图形学领域&#xff0c;OpenGL作为跨平台的图形API标准已经存在了近三十年。然而&#xff0c;许多开发者可能没有意识到&#xff0c;支撑OpenGL应用运行的窗口管理库同样经历了显著的代际演进。本文将深入…

作者头像 李华
网站建设 2026/4/22 14:19:31

华为eNSP链路聚合实战:巧设优先级与负载分担策略优化网络性能

1. 链路聚合基础与企业网络痛点 企业网络中最常见的带宽瓶颈往往出现在核心交换机之间的互联链路上。想象一下&#xff0c;当两个部门之间频繁传输大文件时&#xff0c;单条千兆链路很容易被占满&#xff0c;导致视频会议卡顿、OA系统响应缓慢。这时候就需要链路聚合&#xff0…

作者头像 李华
网站建设 2026/4/22 14:18:33

FireRedASR-AED-L实战分享:集成钉钉/微信实现语音办公

FireRedASR-AED-L实战分享&#xff1a;集成钉钉/微信实现语音办公 1. 项目背景与价值 在日常办公场景中&#xff0c;语音交互正逐渐成为提升效率的关键手段。想象这样的场景&#xff1a;领导在出差途中收到紧急文件&#xff0c;只需对着手机说"同意审批"就能完成流…

作者头像 李华
网站建设 2026/4/22 14:18:27

MySQL 数据分片策略与方案

MySQL数据分片策略与方案&#xff1a;构建高扩展性数据库架构 随着数据量的爆炸式增长&#xff0c;单机MySQL数据库在存储容量和性能上面临严峻挑战。数据分片&#xff08;Sharding&#xff09;作为分布式数据库的核心技术&#xff0c;通过将数据水平拆分到多个节点&#xff0…

作者头像 李华
网站建设 2026/4/22 14:18:25

python inflect

# Python Inflect 漫谈&#xff1a;让代码学会“说话”的艺术 在编程的世界里&#xff0c;我们常常需要处理文本的生成和格式化。有时候&#xff0c;一个简单的数字需要转换成对应的英文单词&#xff0c;比如把“1”变成“one”&#xff1b;有时候&#xff0c;我们需要根据数量…

作者头像 李华