1. 为什么AOP是Spring里最值得先啃透的一块硬骨头刚接触Spring那会儿我最先学会的是IoC把对象交给容器管用的时候Autowired一注就完事确实省心。但真正让我觉得Spring“有点东西”的是AOP。原因很简单IoC解决的是对象怎么来的问题而AOP解决的是“一堆方法里重复横切的逻辑怎么抽出去”的问题。日志、事务、权限校验、接口耗时统计、缓存、限流这些代码如果每个方法都手写一遍项目很快就会变成一锅粥。AOP就是把这些“跟业务无关但每个业务都要沾一点”的逻辑从业务代码里剥出来统一织进去。这篇内容我打算把Spring中AOP的基本使用从头到尾捋一遍。不是那种只贴两行Aspect就结束的教程而是把“为什么这么设计”“注解背后发生了什么”“实际项目里怎么用才不出事”讲清楚。适合已经会写Spring基础代码、能跑起来一个Spring Boot项目、但对AOP还停留在“听说过、抄过、没真正理解”阶段的同学。看完之后你应该能独立写出可用的切面知道什么时候该用AOP、什么时候不该用以及踩坑时往哪个方向排查。先说清楚AOP到底是个啥。AOP全称Aspect Oriented Programming面向切面编程。你可以把它理解成给方法“套壳”原本你调用的是userService.save()AOP可以在不修改save()源码的前提下在它执行前、执行后、抛异常时、甚至环绕整个执行过程插入额外逻辑。这个“壳”就是切面插入的位置叫连接点插入的时机叫通知。Spring AOP底层用的是动态代理这点非常关键后面会展开讲因为它直接决定了哪些方法能被增强、哪些不能。我见过太多人AOP用不明白根子都在没搞懂代理机制。比如为什么private方法加了注解不生效为什么同一个类里this.xxx()调用不走切面为什么final方法切不到这些问题不是配置写错了而是动态代理的天然限制。所以这篇我会把原理和实操绑在一起讲让你不光会抄还能自己判断。2. AOP核心概念一次讲透别再被术语绕晕2.1 五个核心术语用生活场景类比AOP的术语第一次看确实劝退什么切点、连接点、通知、切面、织入一堆名词。我用“公司门禁”这个场景给你类比一下一下就通了。连接点JoinPoint程序执行过程中所有“可以被插入逻辑的点”。类比公司里每一扇门理论上都可以装门禁。切点Pointcut从所有连接点里用规则筛选出“真正要插入逻辑的点”。类比你规定“只有财务室和机房的门装门禁”这个筛选规则就是切点。通知Advice在切点选中的位置上具体要执行的逻辑以及执行的时机。类比门禁刷卡这个动作以及是进门刷、出门刷、还是进出都刷。切面Aspect切点加通知的组合体也就是“在哪、什么时候、干什么”的完整封装。类比整套门禁方案。织入Weaving把切面逻辑真正塞进目标方法的过程。类比施工队把门禁设备装到门上。Spring AOP里的织入是运行期完成的靠动态代理实现不需要特殊的编译器。这点和AspectJ的编译期织入不一样也是Spring AOP能力边界更窄的原因。2.2 五种通知类型时机决定用途通知类型决定了你的逻辑在目标方法的哪个位置执行选错了时机逻辑就会出问题。下面这张表是我自己整理的高频对照建议收藏。通知类型注解执行时机典型用途前置通知Before目标方法执行前参数校验、权限预检、日志入参后置返回通知AfterReturning目标方法正常返回后记录返回值、缓存写入后置异常通知AfterThrowing目标方法抛异常后异常告警、事务回滚标记后置最终通知After目标方法结束后无论成败资源释放、耗时统计收尾环绕通知Around包裹整个目标方法耗时统计、事务、缓存、限流这里有个特别容易搞混的点After和AfterReturning的区别。After相当于finally不管方法正常返回还是抛异常都会执行AfterReturning只在正常返回时执行。很多人做资源清理用了AfterReturning结果方法一抛异常资源就泄漏了这就是时机选错的典型后果。2.3 切点表达式写对了才切得准切点表达式是AOP里最容易写错的部分。Spring AOP用的是AspectJ的切点表达式语法最常用的就是execution。它的完整格式是execution(修饰符 返回类型 包名.类名.方法名(参数类型) throws 异常)实际写的时候大部分可以省略但省略的规则要清楚。举几个我项目里最常用的例子// 匹配 com.example.service 包下所有类的所有public方法 execution(* com.example.service.*.*(..)) // 匹配任意包下 Service 结尾的类的所有方法 execution(* com.example..*Service.*(..)) // 匹配指定注解标注的方法 annotation(com.example.annotation.LogRecord) // 匹配指定注解标注的类里的所有方法 within(com.example.annotation.LogRecord)..表示任意层级包或任意参数*表示任意返回值或任意方法名。我个人的经验是切点表达式尽量写精确不要图省事用execution(* *(..))这种全匹配否则连框架内部方法都可能被切到性能和安全都是隐患。提示切点表达式写完后强烈建议先开DEBUG日志观察实际匹配了哪些方法Spring会打印Applying pointcut相关的日志确认无误再往下写通知逻辑。3. 从零搭一个可运行的AOP示例3.1 依赖引入与基础配置Spring Boot项目里用AOP只需要加一个starter不需要额外配置类。这是Spring Boot最舒服的地方。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency这个starter会自动引入aspectjweaver并且通过AopAutoConfiguration自动开启EnableAspectJAutoProxy。如果你用的是纯Spring非Boot项目那就得手动在配置类上加EnableAspectJAutoProxy否则切面注解全部不生效。我早期踩过这个坑注解写得漂漂亮亮结果一个通知都不触发排查半天才发现是没开自动代理。关于EnableAspectJAutoProxy还有两个参数值得说proxyTargetClass默认false表示优先用JDK动态代理设为true则强制用CGLIB。Spring Boot 2.x之后默认就是true了。exposeProxy默认false设为true后可以通过AopContext.currentProxy()拿到当前代理对象用来解决自调用不走切面的问题。3.2 第一个切面接口耗时统计我拿“接口耗时统计”作为第一个例子因为它足够实用而且能覆盖环绕通知的完整写法。Aspect Component public class CostTimeAspect { private static final Logger log LoggerFactory.getLogger(CostTimeAspect.class); Around(annotation(com.example.annotation.CostTime)) public Object around(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); String method pjp.getSignature().toShortString(); try { Object result pjp.proceed(); return result; } finally { long cost System.currentTimeMillis() - start; log.info(方法 {} 耗时 {} ms, method, cost); } } }配套的自定义注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface CostTime { }用的时候直接在方法上加CostTime就行。这里有几个细节值得展开。第一pjp.proceed()必须调用而且只能调用一次除非你有意重试。环绕通知里如果不调用proceed()目标方法根本不会执行这是新手最常见的错误之一。第二耗时统计放在finally里保证方法抛异常时也能记录。如果放在proceed()后面直接写异常一抛就跳过了。第三pjp.getSignature().toShortString()拿到的是“类名.方法名(参数类型)”的简写比toString()干净日志里更好看。3.3 切点表达式的三种常用写法对比实际项目里切点定义方式主要有三种各有适用场景我做了个对比。写法示例优点缺点注解匹配annotation(com.example.CostTime)精确可控按需开启需要额外定义注解包路径匹配execution(* com.example.service..*.*(..))批量生效无需改业务代码范围难控容易误伤方法名匹配execution(* com.example..*Service.save*(..))针对性强命名规范依赖高我个人的偏好是能用注解匹配就用注解匹配。原因很实在——注解匹配是“显式声明”谁被增强了、为什么被增强看代码一目了然包路径匹配是“隐式生效”新人接手项目时经常一脸懵不知道这个方法为什么突然多了日志。当然像全局异常处理、统一日志这种确实需要批量生效的场景包路径匹配更合适。3.4 切面的执行顺序怎么控制一个方法上如果挂了多个切面执行顺序就成了问题。比如日志切面和事务切面谁先谁后Spring里控制顺序有两种方式实现Ordered接口重写getOrder()方法在切面类上加Order(数字)注解数字越小优先级越高。对于环绕通知优先级高的在外层优先级低的在内层。也就是说Order(1)的切面会先进入、后退出像洋葱一样包住Order(2)的切面。Aspect Component Order(1) public class LogAspect { } Aspect Component Order(2) public class TransactionAspect { }这里有个经验事务切面的优先级要设得比较低数字大让它尽量贴近目标方法。因为事务的开启和提交应该紧贴业务逻辑如果日志切面在事务外层日志记录的时间点可能和事务边界对不上排查问题时容易误判。4. 动态代理机制AOP能不能生效全看它4.1 JDK动态代理与CGLIB的区别Spring AOP的底层就两种代理方式理解它们的差异AOP的很多“玄学问题”就都有答案了。JDK动态代理基于接口要求目标类必须实现至少一个接口生成的代理类和目标类实现同一接口。CGLIB基于继承通过生成目标类的子类来实现代理所以不能代理final类和final方法。对比项JDK动态代理CGLIB代理基础接口继承目标类要求必须实现接口不能是final类方法要求接口中声明的方法非final、非private方法性能创建快调用稍慢创建慢调用快Spring Boot默认否是Spring Boot 2.x开始spring.aop.proxy-target-class默认是true也就是默认走CGLIB。这个默认值的变化主要是为了解决“注入接口时拿不到实现类特有方法”的问题。但不管用哪种代理private方法、static方法、final方法都无法被增强这是硬性限制。4.2 自调用失效问题与三种解法自调用失效是AOP最经典的坑没有之一。看这段代码Service public class OrderService { public void createOrder() { // 这里调用本类的另一个方法 this.sendNotify(); } CostTime public void sendNotify() { // 发送通知逻辑 } }createOrder()里调用sendNotify()CostTime不会生效。原因是this指向的是原始对象不是代理对象方法调用根本没经过代理。这个问题有三种解法我按推荐程度排序。解法一拆到另一个Bean里。把sendNotify()挪到独立的NotifyService里通过注入调用。这是最干净的做法符合单一职责也避免了代理问题。我现在的项目基本都这么干。解法二注入自己。在OrderService里注入OrderService自身用注入的实例调用方法。Spring能处理这种循环依赖默认允许但看起来有点怪。Service public class OrderService { Autowired private OrderService self; public void createOrder() { self.sendNotify(); } }解法三开启exposeProxy用AopContext.currentProxy()。需要在启动类或配置类上加EnableAspectJAutoProxy(exposeProxy true)然后public void createOrder() { ((OrderService) AopContext.currentProxy()).sendNotify(); }这种方式代码侵入性强而且强转容易出错我不太推荐。除非是老项目改造实在没法拆Bean才考虑它。4.3 代理对象与原始对象的区分有个细节很多人没注意注入到其他Bean里的永远是代理对象而this永远是原始对象。你可以写个测试验证一下Autowired private OrderService orderService; Test public void testProxy() { System.out.println(orderService.getClass().getName()); // 输出类似 com.example.OrderService$$EnhancerBySpringCGLIB$$xxx }看到$$EnhancerBySpringCGLIB$$或者$Proxy前缀就说明拿到的是代理对象。这个技巧在排查“切面为什么不生效”时特别有用——先确认你调用的到底是不是代理。注意如果你在切面里用pjp.getTarget()拿到的是原始对象用pjp.getThis()拿到的是代理对象。这两个方法在需要反射获取注解时经常用到别搞混。5. AOP在真实项目里的高频使用场景5.1 统一日志记录日志是AOP最普遍的应用。但我要提醒一句不要无脑记录所有方法的入参和返回值。我见过一个项目切面把每个方法的参数都JSON.toJSONString打出来结果大对象序列化直接把接口响应时间拉高了几十毫秒日志文件一天涨几个G。合理的做法是只对关键业务方法记录并且对参数做长度截断。Around(annotation(com.example.annotation.LogRecord)) public Object logAround(ProceedingJoinPoint pjp) throws Throwable { MethodSignature signature (MethodSignature) pjp.getSignature(); Method method signature.getMethod(); LogRecord annotation method.getAnnotation(LogRecord.class); String desc annotation.value(); Object[] args pjp.getArgs(); String argsStr truncate(Arrays.toString(args), 500); long start System.currentTimeMillis(); try { Object result pjp.proceed(); log.info([{}] 入参{} 结果{} 耗时{}ms, desc, argsStr, truncate(String.valueOf(result), 500), System.currentTimeMillis() - start); return result; } catch (Throwable e) { log.error([{}] 入参{} 异常{}, desc, argsStr, e.getMessage(), e); throw e; } }truncate方法自己实现超过长度就截断加省略号。这个细节看着小但在生产环境里能救命。5.2 声明式事务的AOP本质Transactional本质上就是一个AOP切面这个认知很重要。理解了它你就能明白为什么事务会失效。事务失效的常见原因几乎都能用AOP原理解释自调用this调用不走代理事务不生效。方法非publicCGLIB虽然理论上能代理protected但Spring的事务切面只对public方法生效。异常类型不匹配默认只对RuntimeException和Error回滚受检异常不回滚需要rollbackFor指定。类没被Spring管理new出来的对象没有代理自然没有事务。Transactional(rollbackFor Exception.class) public void transfer(Long from, Long to, BigDecimal amount) { // 扣款、入账逻辑 }我个人的习惯是Transactional一律加上rollbackFor Exception.class避免受检异常不回滚的坑。这个习惯是从一次线上事故里学来的——当时一个受检异常导致部分数据没回滚对账对了一整晚。5.3 接口限流与幂等控制限流和幂等也是AOP的经典场景。以幂等为例思路是用注解标记需要幂等的方法切面里根据业务key去Redis查是否已处理。Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface Idempotent { String key() default ; int expire() default 10; }切面里用Around在proceed()之前用SETNX抢占key抢不到就抛重复提交异常抢到了执行完再决定是否释放。这里有个细节如果业务执行失败应该释放key允许重试如果成功则保留key到过期。这个逻辑用try-catch配合就能实现但要注意释放key的操作本身也可能失败得加兜底。5.4 权限校验与数据脱敏权限校验用AOP也很自然。定义一个RequirePermission(user:delete)注解切面里从当前登录上下文取用户权限比对不通过就抛异常。数据脱敏则是反过来在返回结果上做处理比如手机号中间四位打码、身份证号只留前后。脱敏切面有个坑如果返回的是分页对象或者嵌套结构直接改返回值可能改不动。因为很多返回对象是不可变的或者嵌套层级很深。我的做法是脱敏逻辑尽量放在序列化层比如Jackson的自定义序列化器AOP只负责标记哪些字段需要脱敏不直接改对象。这样职责更清晰也不容易出并发问题。6. 常见问题排查与避坑清单6.1 切面不生效的排查思路切面不生效是最高频的问题我整理了一套排查顺序按这个走基本能定位。排查项检查内容常见原因1. 依赖是否引入aop starter漏加依赖2. 注解切面类是否有Aspect和Component漏加Component导致切面没被扫描3. 自动代理是否开启EnableAspectJAutoProxy纯Spring项目漏配4. 切点表达式是否匹配到目标方法包路径写错、方法名写错5. 代理调用的是不是代理对象自调用、new出来的对象6. 方法方法是否public、非final、非static代理限制我遇到最多的是第2项和第5项。第2项是因为很多人以为Aspect就够了其实它只是个标记还得让Spring扫描到所以Component不能少。第5项就是自调用前面讲过解法了。6.2 环绕通知里异常处理的坑环绕通知里如果自己catch了异常不往外抛上层就感知不到事务也不会回滚。看这段代码Around(annotation(...)) public Object around(ProceedingJoinPoint pjp) throws Throwable { try { return pjp.proceed(); } catch (Exception e) { log.error(异常, e); return null; // 危险异常被吞了 } }这样写目标方法抛异常后返回null调用方以为成功了事务也不会回滚。正确做法是记录日志后原样抛出} catch (Exception e) { log.error(异常, e); throw e; }如果确实需要转换异常类型也要抛出一个新的异常而不是返回null。这个坑我在早期项目里踩过导致一个订单状态没回滚排查了很久。6.3 切面性能与循环依赖切面本身有性能开销尤其是环绕通知每次调用都要走代理。如果切点范围过大比如切了整个service包那每个方法调用都要过一遍切面逻辑。我的建议是切点尽量精确别用全匹配。切面逻辑尽量轻量别在切面里做重IO操作。高频调用的方法比如缓存读取慎用切面。另外切面和循环依赖有时会互相影响。如果两个Bean互相注入其中一个还被AOP代理Spring在创建代理时可能拿不到完整的依赖导致启动报错。这种情况通常需要调整依赖关系或者用Lazy延迟注入来打破循环。6.4 一个容易被忽略的细节切面里的this在切面类里this指向的是切面对象本身不是目标对象。如果你想在切面里获取目标对象的信息要用pjp.getTarget()。想获取目标方法的注解要用MethodSignatureMethodSignature signature (MethodSignature) pjp.getSignature(); Method method signature.getMethod(); MyAnnotation ann method.getAnnotation(MyAnnotation.class);注意这里拿到的method是接口方法还是实现类方法取决于代理方式。JDK代理拿到的是接口方法CGLIB拿到的是实现类方法。如果注解加在实现类方法上而用的是JDK代理可能拿不到注解。这也是为什么Spring Boot默认改用CGLIB的原因之一。7. 我个人的一些使用心得AOP这东西用好了是利器用滥了是灾难。我给自己定了几条规矩分享出来供参考。第一条切面只做横切关注点不碰业务逻辑。日志、事务、权限、限流、缓存这些是切面的地盘业务规则、数据计算、流程编排这些老老实实写在Service里。我见过有人在切面里改业务参数、拼装返回值结果代码逻辑散落在各处维护起来极其痛苦。第二条注解匹配优先于包路径匹配。注解是显式的谁被增强一目了然包路径是隐式的容易误伤也容易漏。除非是全局性的需求否则我基本都用注解。第三条切面逻辑要能扛住异常。切面里的代码一旦抛异常可能直接影响主流程。所以切面里的日志、监控、缓存操作都要做好异常兜底不能让切面的问题变成业务的问题。第四条写完切面一定要写测试。AOP的生效依赖代理单元测试里直接new对象是测不出切面的。要用SpringBootTest启动容器注入代理对象来测。我一般会写一个测试验证通知确实执行了比如在切面里往一个List里塞标记测试里断言这个List非空。最后再分享一个小技巧调试切面时可以在切面里打印pjp.getSignature().toLongString()它会输出完整的方法签名包括返回类型、包名、参数类型。排查切点匹配问题时把这个输出和切点表达式对照一眼就能看出哪里对不上。AOP的进阶方向还有很多比如多个切面的顺序编排、切面与响应式编程的结合、自定义切点注解的元注解处理等等。但把这篇里的基础打牢后面那些都是水到渠成的事。真正把AOP用顺手之后你会发现很多原本要写一堆重复代码的地方现在一个注解就解决了那种感觉还是挺爽的。
企业数字化 ERP 产品动态
相关推荐
钧瓷匮乏症:历史创伤与现代商业的碰撞 1. 钧瓷匮乏症:一个时代的心理烙印第一次听到"钧瓷匮乏症"这个词,是在禹州一位老匠人的作坊里。老人摩挲着手中残缺的钧瓷片,突然叹了口气:"我们这代人啊,就像这窑变的瓷器,看着光彩&#x… · 2026/9/24 0:32:42
聚氨酯密封胶疲劳测试与性能优化实践 1. 项目背景与核心问题在工业制造领域,密封胶的性能稳定性直接关系到设备使用寿命和安全系数。我们团队最近完成了一项长达6个月的密封胶疲劳测试项目,累计循环次数达到120万次。这个数字在业内属于较高标准的测试强度,相当于模拟产品在实际使… · 2026/9/24 0:32:42
RenderDoc 完全上手指南:基于帧捕获的开源图形调试器的支持范围、获取方式与源码构建 开发工具调试器图形学GPU 【免费下载链接】renderdoc RenderDoc is a stand-alone graphics debugging tool. 项目地址: https://gitcode.com/gh_mirrors/re/renderdoc 点击查看 免费下载 RenderDoc 是一款基于帧捕获(frame-capture)理念的开… · 2026/9/24 1:10:01
杰理701N按键配置全流程:从可视化SDK到代码层对接与避坑指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 1:10:01
频率准确度与稳定度:晶振选型中的关键参数解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 1:09:55
华为鸿蒙免费的主题壁纸APP—小羊免费壁纸 夜里亮一下屏,锁屏还是那张旧图;想换,又怕先撞上半屏广告和会员墙。有人问我:有没有安静一点的?有——鸿蒙上的 小羊免费壁纸。印象就一句:按氛围挑一张,看好了再存。它干什么用?帮你… · 2026/9/24 1:09:43
首行缩进与悬挂缩进的区别及Word/WPS设置技巧 很多人在排文档的时候都遇到过这种困惑:明明都是“缩进”,首行缩进和悬挂缩进怎么长得完全不一样?更麻烦的是,段落对话框里这两个选项挨在一起,选错之后整篇文档的行列关系全乱,尤其是做参考文献列表、项目… · 2026/9/24 1:09:37
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程 简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13
1D-CNN时间序列建模实战:从Conv1d原理到工业落地 简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26
柔软的L:汉语语流中被忽视的舌肌张力控制 1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44