AOP(面向切面编程)是 Spring 两大核心之一,和 IOC 容器相辅相成。很多后端开发日常使用 AOP 做日志、鉴权、限流,但对 AOP 底层动态代理机制、通知执行顺序、代理失效场景理解模糊。Spring 声明式事务底层就是 AOP。本文从设计思想、核心术语、底层原理、完整实战代码、踩坑总结,全方位拆解 AOP。
前言
在业务开发中,总会存在一类通用逻辑:接口请求日志记录、接口权限校验、访问限流、全局事务、接口埋点、异常统一处理。这类代码不属于业务核心逻辑,但是几乎每个接口都要写。 如果直接写在业务方法内,会造成代码侵入、大量重复代码、业务代码臃肿,后期难以维护。
AOP 面向切面编程思想,就是把这类横切逻辑抽离出来,做成独立的切面。在不修改原有业务代码的前提下,对目标方法进行增强。
OOP 面向对象是纵向的类继承;AOP 是横向的切面增强,互补关系。 Spring AOP 不是完整 AOP 规范实现,它只支持方法级别的切面增强,不支持构造器、字段拦截。
一、AOP 核心术语
- 连接点 JoinPoint:程序执行过程中,可以被 AOP 拦截的点。Spring AOP 仅支持方法执行作为连接点。
- 切入点 Pointcut:匹配规则,用来筛选哪些连接点需要被切面增强。只有匹配切入点表达式的方法,才会执行切面增强逻辑。 常用两种写法:
execution()按方法签名匹配;@annotation()自定义注解匹配。 - 通知 Advice:切面里要执行的增强逻辑,分为 5 种类型:
- @Before:前置通知,目标方法执行之前执行
- @AfterReturning:返回通知,目标方法正常执行成功返回后执行;方法抛异常不会触发
- @AfterThrowing:异常通知,目标方法抛出异常时才执行
- @After:后置通知,目标方法无论正常 / 异常,都会执行(类似 finally)
- @Around:环绕通知,功能最强,可以控制目标方法什么时候执行,是否执行,捕获入参、返回值、异常。
- 切面 Aspect= Pointcut + Advice。使用
@Aspect注解定义切面类。 - 目标对象 Target:原始业务对象,没有被增强的对象。
- 代理对象 Proxy:Spring 基于目标对象动态生成的代理对象,对外暴露,调用方法时会执行切面增强逻辑。
- 织入 Weaving:把切面增强逻辑,嵌入到目标对象的过程。Spring AOP 是运行期织入,在 IOC 容器创建 Bean 的时候动态生成代理。
重点区分:AspectJ 是编译期织入;Spring AOP 是运行时动态代理织入,轻量集成在 Spring 容器。
二、底层原理:两种动态代理
Spring AOP 底层依靠两种动态代理技术,自动选择使用哪一种:
- JDK 动态代理
- 前提:目标类实现了接口
- 原理:JDK 在运行时动态生成一个接口的代理类,代理类实现目标对象的所有接口;代理对象调用方法,会进入 InvocationHandler 的 invoke 方法,在这里执行切面逻辑,再调用目标对象方法。
- 限制:只能拦截接口中定义的方法;目标类没有接口,无法使用 JDK 代理。
- CGLIB(Code Generation Library)动态代理
- 前提:目标类没有实现接口
- 原理:ASM 字节码框架,运行时动态生成目标类的子类,重写目标类非 private 方法;调用方法时,先执行切面增强逻辑,再调用父类(原始对象)方法。
- 限制:不能代理 final 类、final 方法(无法继承、重写)。
SpringBoot2.x 之后,默认使用 CGLIB 代理;SpringBoot1.x 默认 JDK 代理。可以在 yml 配置修改切换。
spring: aop: proxy-target-class: true # true 使用CGLIB;false JDK代理核心坑点:AOP 什么时候失效?
只有从 IOC 容器拿到代理对象调用方法,切面逻辑才生效。 如果是同一个类内部方法自调用(this.xxx ()),this 指向原始目标对象,不是代理对象,切面逻辑不会执行!这是面试最高频的坑。
三、通知执行顺序
正常无异常流程
@Around 前置逻辑 → @Before → 目标方法执行 → @AfterReturning → @After → @Around 后置逻辑
目标方法抛出异常流程
@Around 前置逻辑 → @Before → 目标方法抛异常 → @AfterThrowing → @After
@AfterReturning 不会执行!
四、四大业务场景完整实战代码
实战 1:统一请求日志切面(记录入参、出参、耗时,线上排查利器)
适合:所有 Controller 接口,自动打印请求参数、响应结果、接口耗时,减少重复日志代码。
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.Arrays; @Aspect @Component public class ApiLogAspect { private static final Logger log = LoggerFactory.getLogger(ApiLogAspect.class); // 切入点:拦截所有@RestController注解类下全部方法 @Pointcut("within(@org.springframework.web.bind.annotation.RestController *)") public void apiPointcut(){} @Around("apiPointcut()") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); String methodName = joinPoint.getSignature().toShortString(); Object[] args = joinPoint.getArgs(); log.info("【接口请求】方法:{},请求入参:{}", methodName, Arrays.toString(args)); Object result; try { // 执行目标方法 result = joinPoint.proceed(); long cost = System.currentTimeMillis() - start; log.info("【接口响应】方法:{},耗时{}ms,返回结果:{}", methodName, cost, result); return result; } catch (Throwable e) { long cost = System.currentTimeMillis() - start; log.error("【接口异常】方法:{},耗时{}ms,异常信息:{}", methodName, cost, e.getMessage(), e); throw e; // 异常继续向外抛出,交给全局异常处理器 } } }实战 2:权限拦截切面(自定义注解实现接口鉴权)
适合:在接口上加注解,声明需要的权限,自动拦截校验,不用每个接口写权限判断代码。
// 自定义注解 @Target({java.lang.annotation.ElementType.METHOD}) @Retention(java.lang.annotation.RetentionPolicy.RUNTIME) public @interface RequiresPermission { String value(); } // 切面类 @Aspect @Component public class PermissionAspect { @Pointcut("@annotation(requiresPermission)") public void permPointcut(RequiresPermission requiresPermission){} @Around("permPointcut(requiresPermission)") public Object checkPerm(ProceedingJoinPoint jp, RequiresPermission requiresPermission) throws Throwable{ String needPerm = requiresPermission.value(); // 从上下文获取当前登录用户权限集合 Set<String> userPerms = getCurrentUserPerms(); if(userPerms == null || !userPerms.contains(needPerm)){ throw new RuntimeException("权限不足,无法访问该接口"); } return jp.proceed(); } // 模拟从ThreadLocal获取当前用户权限 private Set<String> getCurrentUserPerms(){ // 实际项目:从token解析用户信息 return new HashSet<>(); } } // 使用方式,直接加到Controller方法 @GetMapping("/user/list") @RequiresPermission("user:query") public Result getUserList(){ return Result.success(); }实战 3:AOP 实现接口限流(Redis + 自定义注解,防刷接口)
适合:对外接口防刷,限制指定时间窗口内最大访问次数。
// 自定义注解 @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimit { int limit() default 10; //最大访问次数 int window() default 60; //时间窗口,单位秒 } @Aspect @Component public class RateLimitAspect { @Autowired private StringRedisTemplate redisTemplate; @Pointcut("@annotation(rateLimit)") public void limitPointcut(RateLimit rateLimit){} @Around("limitPointcut(rateLimit)") public Object limit(ProceedingJoinPoint jp, RateLimit rateLimit) throws Throwable{ // 可以改成根据用户IP/用户ID作为key,实现用户级限流 String key = "rate:limit:" + jp.getSignature().getName(); Long count = redisTemplate.opsForValue().increment(key,1); if(count ==1){ redisTemplate.expire(key, rateLimit.window(), TimeUnit.SECONDS); } if(count > rateLimit.limit()){ throw new RuntimeException("访问过于频繁,请稍后重试"); } return jp.proceed(); } } // 使用 @GetMapping("/submit") @RateLimit(limit = 5, window = 10) //10秒最多访问5次 public Result submit(){ return Result.success(); }实战 4:Spring 事务底层原理(AOP 经典落地场景)
@Transactional本质就是 Spring 内置的 AOP 切面。
- 在目标方法执行之前,切面拦截,获取数据库连接,关闭自动提交,开启事务;
- 调用目标业务方法;
- 如果方法正常执行完毕,切面执行事务提交;
- 如果抛出指定异常,切面捕获异常,执行事务回滚,最后释放连接。
经典坑:@Transactional 失效,根源大多是 AOP 代理失效。比如同类内方法调用、私有方法、异常被 try-catch 吃掉,切面无法捕获异常。
五、AOP 典型业务应用场景汇总
1、统一接口日志记录、请求埋点、链路耗时统计
2、接口权限校验、登录拦截、数据权限过滤
3、接口限流、防重复提交、幂等性校验
4、声明式事务管理(Spring 内置 AOP 切面)
5、全局异常捕获、统一响应封装
6、接口监控、慢接口告警、审计日志
六、AOP 高频踩坑汇总
- 同类内部方法调用,切面失效:this 调用,使用原始对象,不是代理对象。解决方案:自己注入自己,使用代理对象调用。
@Service public class OrderService{ @Autowired private OrderService self; //注入代理对象 public void test(){ self.createOrder(); // 使用代理对象,AOP生效 } @Transactional public void createOrder(){} }- private 方法无法被 AOP 增强:CGLIB 无法重写私有方法,切入点无法匹配 private 方法。
- final 方法 /final 类不能被 CGLIB 代理。
- 切面的顺序问题:多个切面叠加,可以用
@Order注解指定执行优先级,数字越小优先级越高。 - 环绕通知必须手动调用
joinPoint.proceed(),否则目标方法不会执行。
七、问答汇总
Q1:JDK 动态代理和 CGLIB 动态代理区别?
A:JDK 动态代理,要求目标类实现接口,代理实现相同接口;CGLIB 动态代理,生成目标类子类,重写非私有方法。SpringBoot2 默认 CGLIB。
Q2:@Around 环绕通知和其他通知的区别?
A:环绕通知功能最强,可以控制目标方法执行时机,捕获返回值、捕获异常,可以选择不执行目标方法。其余通知只能做增强,不能控制目标方法是否执行。
Q3:为什么同一个类里面,方法 A 调用本类带 @Transactional 注解的方法 B,事务会失效?
A:方法 A 内部调用 B 的时候使用 this 对象,this 是原始目标对象,不是 IOC 容器中的代理对象,AOP 切面无法拦截这个方法调用,事务切面不会执行。
Q4:Spring AOP 和 AspectJ 的区别?
A:AspectJ 是完整 AOP 规范,编译期 / 加载期织入;Spring AOP 只实现部分 AOP 功能,运行时动态代理织入,只支持方法拦截,不需要额外编译插件。
Q5:多个切面同时作用在同一个方法,执行顺序怎么控制?
A:使用@Order(int)注解,数值越小,优先级越高。前置通知:order 小先执行;后置通知顺序相反。
结语
AOP 是 Spring 框架极具魅力的特性,核心价值就是解耦横切通用逻辑和业务代码。 很多开发只停留在复制日志切面代码,当遇到 AOP 失效、事务不回滚这类线上 bug,就无从下手。吃透底层动态代理原理,就能理解各类 AOP 失效场景,同时可以灵活开发权限、限流、审计等通用组件,减少项目重复代码,提升工程化能力。 AOP 建立在 IOC 容器之上,代理对象最终也要存入 IOC 容器。理解 IOC 和 AOP,Spring 全家桶的底层逻辑就打通了,后续学习 Spring 事务、SpringBoot 自动配置会轻松很多。