Spring AOP 底层动态代理 + 实战:事务、日志切面、权限拦截、接口限流

发布时间:2026/10/15 4:02:17
Spring AOP 底层动态代理 + 实战:事务、日志切面、权限拦截、接口限流
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后置通知目标方法无论正常 / 异常都会执行类似 finallyAround环绕通知功能最强可以控制目标方法什么时候执行是否执行捕获入参、返回值、异常。切面 Aspect Pointcut Advice。使用Aspect注解定义切面类。目标对象 Target原始业务对象没有被增强的对象。代理对象 ProxySpring 基于目标对象动态生成的代理对象对外暴露调用方法时会执行切面增强逻辑。织入 Weaving把切面增强逻辑嵌入到目标对象的过程。Spring AOP 是运行期织入在 IOC 容器创建 Bean 的时候动态生成代理。重点区分AspectJ 是编译期织入Spring AOP 是运行时动态代理织入轻量集成在 Spring 容器。二、底层原理两种动态代理Spring AOP 底层依靠两种动态代理技术自动选择使用哪一种JDK 动态代理前提目标类实现了接口原理JDK 在运行时动态生成一个接口的代理类代理类实现目标对象的所有接口代理对象调用方法会进入 InvocationHandler 的 invoke 方法在这里执行切面逻辑再调用目标对象方法。限制只能拦截接口中定义的方法目标类没有接口无法使用 JDK 代理。CGLIBCode Generation Library动态代理前提目标类没有实现接口原理ASM 字节码框架运行时动态生成目标类的子类重写目标类非 private 方法调用方法时先执行切面增强逻辑再调用父类原始对象方法。限制不能代理 final 类、final 方法无法继承、重写。SpringBoot2.x 之后默认使用 CGLIB 代理SpringBoot1.x 默认 JDK 代理。可以在 yml 配置修改切换。spring: aop: proxy-target-class: true # true 使用CGLIBfalse JDK代理核心坑点AOP 什么时候失效只有从 IOC 容器拿到代理对象调用方法切面逻辑才生效。 如果是同一个类内部方法自调用this.xxx ()this 指向原始目标对象不是代理对象切面逻辑不会执行这是面试最高频的坑。三、通知执行顺序正常无异常流程Around 前置逻辑 → Before → 目标方法执行 → AfterReturning → After → Around 后置逻辑目标方法抛出异常流程Around 前置逻辑 → Before → 目标方法抛异常 → AfterThrowing → AfterAfterReturning 不会执行四、四大业务场景完整实战代码实战 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(); // 从上下文获取当前登录用户权限集合 SetString userPerms getCurrentUserPerms(); if(userPerms null || !userPerms.contains(needPerm)){ throw new RuntimeException(权限不足无法访问该接口); } return jp.proceed(); } // 模拟从ThreadLocal获取当前用户权限 private SetString getCurrentUserPerms(){ // 实际项目从token解析用户信息 return new HashSet(); } } // 使用方式直接加到Controller方法 GetMapping(/user/list) RequiresPermission(user:query) public Result getUserList(){ return Result.success(); }实战 3AOP 实现接口限流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(); }实战 4Spring 事务底层原理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()否则目标方法不会执行。七、问答汇总Q1JDK 动态代理和 CGLIB 动态代理区别AJDK 动态代理要求目标类实现接口代理实现相同接口CGLIB 动态代理生成目标类子类重写非私有方法。SpringBoot2 默认 CGLIB。Q2Around 环绕通知和其他通知的区别A环绕通知功能最强可以控制目标方法执行时机捕获返回值、捕获异常可以选择不执行目标方法。其余通知只能做增强不能控制目标方法是否执行。Q3为什么同一个类里面方法 A 调用本类带 Transactional 注解的方法 B事务会失效A方法 A 内部调用 B 的时候使用 this 对象this 是原始目标对象不是 IOC 容器中的代理对象AOP 切面无法拦截这个方法调用事务切面不会执行。Q4Spring AOP 和 AspectJ 的区别AAspectJ 是完整 AOP 规范编译期 / 加载期织入Spring AOP 只实现部分 AOP 功能运行时动态代理织入只支持方法拦截不需要额外编译插件。Q5多个切面同时作用在同一个方法执行顺序怎么控制A使用Order(int)注解数值越小优先级越高。前置通知order 小先执行后置通知顺序相反。结语AOP 是 Spring 框架极具魅力的特性核心价值就是解耦横切通用逻辑和业务代码。 很多开发只停留在复制日志切面代码当遇到 AOP 失效、事务不回滚这类线上 bug就无从下手。吃透底层动态代理原理就能理解各类 AOP 失效场景同时可以灵活开发权限、限流、审计等通用组件减少项目重复代码提升工程化能力。 AOP 建立在 IOC 容器之上代理对象最终也要存入 IOC 容器。理解 IOC 和 AOPSpring 全家桶的底层逻辑就打通了后续学习 Spring 事务、SpringBoot 自动配置会轻松很多。