news 2026/10/21 20:32:36

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring AOP 底层动态代理 + 实战:事务、日志切面、权限拦截、接口限流

AOP(面向切面编程)是 Spring 两大核心之一,和 IOC 容器相辅相成。很多后端开发日常使用 AOP 做日志、鉴权、限流,但对 AOP 底层动态代理机制、通知执行顺序、代理失效场景理解模糊。Spring 声明式事务底层就是 AOP。本文从设计思想、核心术语、底层原理、完整实战代码、踩坑总结,全方位拆解 AOP。

前言

在业务开发中,总会存在一类通用逻辑:接口请求日志记录、接口权限校验、访问限流、全局事务、接口埋点、异常统一处理。这类代码不属于业务核心逻辑,但是几乎每个接口都要写。 如果直接写在业务方法内,会造成代码侵入、大量重复代码、业务代码臃肿,后期难以维护。

AOP 面向切面编程思想,就是把这类横切逻辑抽离出来,做成独立的切面。在不修改原有业务代码的前提下,对目标方法进行增强。

OOP 面向对象是纵向的类继承;AOP 是横向的切面增强,互补关系。 Spring AOP 不是完整 AOP 规范实现,它只支持方法级别的切面增强,不支持构造器、字段拦截。

一、AOP 核心术语

  1. 连接点 JoinPoint:程序执行过程中,可以被 AOP 拦截的点。Spring AOP 仅支持方法执行作为连接点。
  2. 切入点 Pointcut:匹配规则,用来筛选哪些连接点需要被切面增强。只有匹配切入点表达式的方法,才会执行切面增强逻辑。 常用两种写法:execution()按方法签名匹配;@annotation()自定义注解匹配。
  3. 通知 Advice:切面里要执行的增强逻辑,分为 5 种类型:
    • @Before:前置通知,目标方法执行之前执行
    • @AfterReturning:返回通知,目标方法正常执行成功返回后执行;方法抛异常不会触发
    • @AfterThrowing:异常通知,目标方法抛出异常时才执行
    • @After:后置通知,目标方法无论正常 / 异常,都会执行(类似 finally)
    • @Around:环绕通知,功能最强,可以控制目标方法什么时候执行,是否执行,捕获入参、返回值、异常。
  4. 切面 Aspect= Pointcut + Advice。使用@Aspect注解定义切面类。
  5. 目标对象 Target:原始业务对象,没有被增强的对象。
  6. 代理对象 Proxy:Spring 基于目标对象动态生成的代理对象,对外暴露,调用方法时会执行切面增强逻辑。
  7. 织入 Weaving:把切面增强逻辑,嵌入到目标对象的过程。Spring AOP 是运行期织入,在 IOC 容器创建 Bean 的时候动态生成代理。

重点区分:AspectJ 是编译期织入;Spring AOP 是运行时动态代理织入,轻量集成在 Spring 容器。

二、底层原理:两种动态代理

Spring AOP 底层依靠两种动态代理技术,自动选择使用哪一种:

  1. JDK 动态代理
    • 前提:目标类实现了接口
    • 原理:JDK 在运行时动态生成一个接口的代理类,代理类实现目标对象的所有接口;代理对象调用方法,会进入 InvocationHandler 的 invoke 方法,在这里执行切面逻辑,再调用目标对象方法。
    • 限制:只能拦截接口中定义的方法;目标类没有接口,无法使用 JDK 代理。
  2. 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 切面。

  1. 在目标方法执行之前,切面拦截,获取数据库连接,关闭自动提交,开启事务;
  2. 调用目标业务方法;
  3. 如果方法正常执行完毕,切面执行事务提交;
  4. 如果抛出指定异常,切面捕获异常,执行事务回滚,最后释放连接。

经典坑:@Transactional 失效,根源大多是 AOP 代理失效。比如同类内方法调用、私有方法、异常被 try-catch 吃掉,切面无法捕获异常。

五、AOP 典型业务应用场景汇总

1、统一接口日志记录、请求埋点、链路耗时统计

2、接口权限校验、登录拦截、数据权限过滤

3、接口限流、防重复提交、幂等性校验

4、声明式事务管理(Spring 内置 AOP 切面)

5、全局异常捕获、统一响应封装

6、接口监控、慢接口告警、审计日志

六、AOP 高频踩坑汇总

  1. 同类内部方法调用,切面失效:this 调用,使用原始对象,不是代理对象。解决方案:自己注入自己,使用代理对象调用。
@Service public class OrderService{ @Autowired private OrderService self; //注入代理对象 public void test(){ self.createOrder(); // 使用代理对象,AOP生效 } @Transactional public void createOrder(){} }
  1. private 方法无法被 AOP 增强:CGLIB 无法重写私有方法,切入点无法匹配 private 方法。
  2. final 方法 /final 类不能被 CGLIB 代理。
  3. 切面的顺序问题:多个切面叠加,可以用@Order注解指定执行优先级,数字越小优先级越高。
  4. 环绕通知必须手动调用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 自动配置会轻松很多。

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

多模态模型的“思考”方式:视觉、通用音频与语音的 CoT 技术路线

AudioCC交我学在文本大模型中&#xff0c;思维链&#xff08;Chain-of-Thought&#xff0c;CoT&#xff09;通常意味着模型先写出若干推理步骤&#xff0c;再给出答案。一旦输入变成图像、环境声或连续语音&#xff0c;关键问题就从“是否逐步推理”进一步转向“用什么承载和更…

作者头像 李华
网站建设 2026/10/21 10:57:52

如何写 Skill 第4课,脚本不进门

按系列惯例&#xff0c;文中自家项目的 skill 脱敏成「评审类 skill A」「测试类 skill B」&#xff0c;其余标本一律实名。skill-audit 是这一课现搓的&#xff0c;经允许&#xff0c;你可以照着复现。 体检的活&#xff0c;我手搓了三课 前三课攒下一份检查单&#xff1a;wc …

作者头像 李华
网站建设 2026/10/19 14:48:41

Halcon 定位技术(Positioning)详解与算子说明

Halcon 定位技术(Positioning)详解与算子说明 参考来源:Halcon 17.12 Operator Reference — Matching、2D Metrology、XLD Contours 章节 适用版本:Halcon 17.12(主基准)/ 20.11 / 22.11 / 24.05 官方示例路径:Applications/Matching/、Applications/Measuring-2D/、ex…

作者头像 李华
网站建设 2026/10/18 8:00:12

两节串联锂电池主动均衡方案:MP2642GR-Z与STM32L021K4协同设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华