📌 写在前面
初学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()返回值:在
@AfterReturning的returning属性中指定变量名
Q4:多个切面的执行顺序?
默认顺序不确定
使用
@Order注解或实现Ordered接口,数字越小优先级越高(越先执行前置通知,越后执行后置通知)
避坑指南
不能代理private方法,因为代理类无法访问父类的私有方法。
final类无法被CGLIB代理,因为CGLIB需要生成子类。
不要滥用AOP,过度使用会让代码难以调试(“魔法”太多)。
环绕通知记得调用
proceed(),否则目标方法不会执行。
8️⃣ 总结:AOP的“灵魂三问”
如果一个Service类中的方法A调用了同一个类中的方法B,且方法B上标注了@Cacheable,那么调用方法A时,方法B的缓存能生效吗?为什么?如何解决?