Spring AOP核心注解全解析:从原理到实战,彻底掌握切面编程

发布时间:2026/8/2 8:25:21

Spring AOP核心注解全解析:从原理到实战,彻底掌握切面编程
1. 项目概述为什么我们需要Spring AOP如果你写过一段时间的Java Web应用尤其是用过Spring框架那你肯定遇到过这样的场景你需要在几十个Service方法里都加上同样的日志记录逻辑或者你需要在所有涉及数据库操作的方法前后统一管理事务的开启与提交又或者你希望在不修改业务代码的前提下为某些接口添加权限校验。这时候如果你还在用“复制-粘贴”大法或者写一堆重复的样板代码那不仅效率低下代码也会变得臃肿且难以维护。Spring AOP面向切面编程就是为了解决这类“横切关注点”问题而生的利器。它允许你将那些分散在应用各处、与核心业务逻辑无关的公共行为如日志、事务、安全等模块化形成一个独立的“切面”然后通过声明的方式将这些行为“织入”到指定的目标方法中。简单说AOP让你能“优雅地”给代码“打补丁”而不用侵入原有的业务逻辑。今天要聊的这几个注解——Aspect、Pointcut、Before、Around、After、AfterReturning、AfterThrowing——就是Spring AOP这套“手术工具”的核心部件。它们共同定义了“在哪里切”切入点、“切了之后干什么”通知以及“怎么组织这些操作”切面。搞懂它们你就能从“被重复代码折磨”的程序员进阶为“优雅解耦”的架构师。这篇文章我就结合自己踩过的坑和实战经验带你彻底吃透这七个注解让你不仅能看懂更能用得溜。2. 核心概念与设计思想拆解在直接上手写代码之前我们必须先理解AOP背后的几个核心概念。这就像学开车你得先知道方向盘、油门、刹车是干嘛的而不是直接猛踩油门。2.1 横切关注点与核心概念横切关注点这个词听起来有点学术但其实很简单。它就是那些“贯穿”或“横跨”多个模块的功能需求。比如我们前面说的日志它可能需要在用户模块、订单模块、支付模块的许多方法中被调用。这个“记录日志”的需求就像一个刀片横向切过了我们纵向分层Controller, Service, Dao的应用程序。AOP的核心价值就是把这些“横向”的代码抽离出来集中管理。围绕这个核心思想AOP定义了几个关键角色切面 这就是抽离出来的那个“横向”模块的实体。它包含了通知和切入点。在Spring中一个用Aspect注解的类就是一个切面。你可以把它理解为一个“功能增强包”里面定义了要增强什么功能通知以及增强哪些地方切入点。连接点 这是程序执行过程中一个明确的点比如方法调用、异常抛出、字段修改等。在Spring AOP中连接点特指方法的执行。你可以理解为你的代码里到处都是潜在的“手术点位”。切入点 这是一个谓词表达式它用来匹配和筛选我们感兴趣的连接点。简单说它定义了“在哪里切”。Pointcut注解就是用来定义这个表达式的。比如“所有com.example.service包下以save开头的方法”就是一个切入点表达式。通知 这是切面在特定的切入点“处”执行的动作。也就是“切了之后要干什么”。Before、Around等注解就是用来标记通知方法的。通知定义了增强行为的具体逻辑比如打印日志、开启事务。它们之间的关系我画个简单的类比你的应用程序是一栋大楼核心业务连接点是每一扇门和窗户方法。切入点是你手里的一张图纸上面圈出了所有需要安装防盗网的门窗例如所有朝外的窗户。切面就是一个施工队队里既有图纸切入点也有具体的施工方案和材料通知。通知就是具体的施工动作比如“在安装前测量尺寸”Before、“把旧窗户拆下来装上新窗户再把旧窗户处理掉”Around。2.2 Spring AOP的实现原理与能力边界Spring AOP默认使用动态代理来实现。这很重要因为它直接决定了Spring AOP的能力范围和限制。对于实现了接口的类Spring会使用JDK动态代理来创建代理对象。对于没有实现接口的类Spring会使用CGLIB库来生成目标类的子类作为代理。这个代理对象会包裹着原始的目标对象。当你调用目标方法时实际上调用的是代理对象的方法。代理对象在调用目标方法的前后就有机会执行我们定义的切面逻辑通知。注意正因为是基于代理的Spring AOP只能拦截public方法的调用。对于同类内部的方法调用即一个方法调用同一个类里的另一个方法由于调用者this是目标对象本身而非代理对象切面是不会生效的。这是新手常踩的一个大坑。解决方案通常是将方法拆分到不同的类或者使用更底层的AspectJ。理解了这些我们再去看那七个注解就会清晰很多Aspect用来组建施工队Pointcut用来画施工图纸其余五个Xxx注解则是具体的施工工序。3. 核心注解深度解析与实操要点接下来我们逐个拆解这七个核心注解。我会先讲清楚每个注解是干什么的、怎么用然后立刻配上代码示例和最重要的注意事项与实操心得。3.1 Aspect声明切面组建你的“施工队”Aspect注解用于声明一个类是一个切面。Spring容器在扫描到带有Aspect注解的类时就知道这个类里定义了一组横切关注点的逻辑。import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; Aspect // 声明这是一个切面类 Component // 同时需要让Spring管理它的生命周期所以通常也加上Component或其衍生注解Service等 public class LoggingAspect { // 这里面会定义切入点和通知 }实操要点必须与Component等注解搭配使用 仅仅有AspectSpring是不会把它当成一个Bean来创建和管理的。你必须同时加上Component、Service等注解或者通过Bean方法在配置类中显式声明。切面Bean默认必须是单例的 切面通常是无状态的Spring默认将其作为单例Bean。如果你的切面中定义了有状态的成员变量比如一个计数器并且你的应用是多线程的那么你需要考虑线程安全问题或者调整Bean的作用域但这种情况很少见设计上应避免。3.2 Pointcut定义切入点绘制精准的“施工图纸”Pointcut注解用于定义一个可重用的切入点表达式。它的值是一个AspectJ的切入点表达式。定义好的切入点可以通过方法名来引用。Aspect Component public class LoggingAspect { // 定义一个切入点匹配com.example.demo.service包及其子包下所有类的所有方法 Pointcut(execution(* com.example.demo.service..*.*(..))) public void serviceLayer() {} // 方法体通常为空它只是一个标识符 // 定义另一个切入点匹配所有带有Transactional注解的方法 Pointcut(annotation(org.springframework.transaction.annotation.Transactional)) public void transactionalMethod() {} }切入点表达式语法详解 最常用的是execution指示器其语法为execution(修饰符? 返回类型 声明类型? 方法名(参数列表) 异常类型?)其中?表示可选部分。* com.example.demo.service..*.*(..)解读第一个* 匹配任意返回类型。com.example.demo.service.. 匹配service包及其所有子包。第二个* 匹配任意类名。第三个* 匹配任意方法名。(..) 匹配任意数量、任意类型的参数。其他常用指示器within 匹配指定类型内的方法。如within(com.example.service.*)。annotation 匹配带有指定注解的方法。如上例所示非常灵活。within 匹配带有指定注解的类里的所有方法。bean Spring特有的匹配Bean名称。如bean(*Service)匹配所有名字以Service结尾的Bean的所有方法。实操心得与避坑指南切入点表达式要尽可能精确 不要使用过于宽泛的表达式如execution(* *.*(..))匹配所有方法这会导致不必要的性能开销和意料之外的拦截。精确匹配你的目标包或注解。善用Pointcut组合 你可以使用与、||或、!非来组合多个切入点表达式实现更复杂的匹配逻辑。// 匹配service包下所有方法但排除掉get开头的方法 Pointcut(execution(* com.example.service..*.*(..)) !execution(* com.example.service..*.get*(..))) public void serviceLayerExcludingGetters() {}将通用切入点定义在公共类 如果多个切面需要使用相同的切入点可以将其定义在一个公共的Aspect类中其他切面通过全限定名来引用例如Before(com.example.aspect.CommonPointcuts.serviceLayer())这有利于维护。3.3 Before前置通知在目标方法“开工前”做准备Before注解声明一个前置通知。该通知会在匹配的切入点方法执行之前运行。它通常用于权限检查、参数校验、日志记录等场景。Aspect Component public class LoggingAspect { Pointcut(execution(* com.example.demo.service.*.*(..))) public void serviceMethod() {} Before(serviceMethod()) // 引用定义好的切入点 public void logBefore(JoinPoint joinPoint) { // JoinPoint对象包含了当前连接点的信息如方法名、参数、目标对象等 String methodName joinPoint.getSignature().getName(); Object[] args joinPoint.getArgs(); System.out.println([前置日志] 准备执行方法: methodName , 参数: Arrays.toString(args)); // 这里可以执行参数校验如果校验失败可以抛出异常来阻止目标方法执行 // if (args[0] null) { // throw new IllegalArgumentException(参数不能为空); // } } }关键对象JoinPoint 任何通知方法除了Around的第一个参数都可以声明为JoinPoint类型用于获取连接点的上下文信息。Around通知的参数是ProceedingJoinPoint它是JoinPoint的子类。注意事项无法阻止方法执行除非抛异常Before通知不能直接返回一个值来替代目标方法的执行。如果你想在某种条件下完全阻止目标方法运行只能在Before通知里抛出异常。更精细的控制如替换返回值、修改参数需要使用Around通知。执行顺序 如果多个切面都匹配了同一个切入点它们的Before通知执行顺序可以通过Order注解或实现Ordered接口来控制数字小的先执行。3.4 After后置通知在目标方法“结束后”进行清理After注解声明一个后置通知。该通知会在匹配的切入点方法执行之后运行无论方法是正常返回还是抛出异常。它类似于try-finally块中的finally部分常用于资源清理、释放锁等场景。Aspect Component public class ResourceCleanupAspect { Pointcut(execution(* com.example.demo.service.FileService.*(..))) public void fileServiceMethod() {} After(fileServiceMethod()) public void cleanupResource(JoinPoint joinPoint) { String methodName joinPoint.getSignature().getName(); System.out.println([资源清理] 方法 methodName 执行完毕正在清理临时文件或关闭流...); // 模拟清理操作在实际应用中可能是关闭IO流、数据库连接、删除临时文件等 // cleanupTemporaryFiles(); } }重要提示After通知无法知道目标方法是成功返回还是抛出了异常。如果你需要根据执行结果进行不同的处理应该使用AfterReturning或AfterThrowing。3.5 AfterReturning返回后通知只在“成功”时庆祝AfterReturning注解声明一个返回后通知。该通知只在匹配的切入点方法正常执行并返回后运行。它常用于记录成功的操作日志、缓存方法返回结果、发送成功通知等。Aspect Component public class MonitoringAspect { Pointcut(execution(* com.example.demo.service.OrderService.createOrder(..))) public void createOrderMethod() {} AfterReturning( pointcut createOrderMethod(), returning result // 绑定目标方法的返回值到通知方法的result参数 ) public void logSuccess(JoinPoint joinPoint, Object result) { // 返回值类型可以声明为具体类型如Order String methodName joinPoint.getSignature().getName(); System.out.println([成功监控] 方法 methodName 执行成功返回结果: result); // 可以将结果存入缓存或发送MQ消息 // cacheService.put(order_key, result); // mqProducer.sendSuccessMessage(result); } }核心参数returningreturning属性的值必须与通知方法中对应的参数名一致。通过它我们可以在通知方法里访问到目标方法的返回值。你可以将参数类型声明得更具体如Order result这样Spring会做类型检查如果类型不匹配该通知将不会被执行。实操心得可以修改返回值吗理论上在AfterReturning通知中修改result对象的内容是可能的如果它是可变对象。例如如果返回的是一个Map或List你可以往里面添加元素。但是你不能替换这个返回值对象本身即让方法返回一个全新的对象。如果你需要替换返回值必须使用Around通知。3.6 AfterThrowing异常通知只在“失败”时处理AfterThrowing注解声明一个异常通知。该通知只在匹配的切入点方法抛出异常后运行。它非常适合用于统一的异常日志记录、错误报警、事务回滚标记注意声明式事务通常由Transactional处理这里指其他资源操作等。Aspect Component public class ExceptionHandlingAspect { Pointcut(execution(* com.example.demo.service.*.*(..))) public void serviceMethod() {} AfterThrowing( pointcut serviceMethod(), throwing ex // 绑定抛出的异常到通知方法的ex参数 ) public void logException(JoinPoint joinPoint, Exception ex) { // 可以指定具体的异常类型如NullPointerException String methodName joinPoint.getSignature().getName(); String errorMsg String.format([异常警报] 服务层方法 %s 执行失败异常类型: %s, 异常信息: %s, methodName, ex.getClass().getSimpleName(), ex.getMessage()); System.err.println(errorMsg); // 发送报警邮件、短信或写入错误监控平台 // alertService.sendToDingTalk(errorMsg); // 注意这里不能“处理”掉异常异常仍然会向上传播 } }核心参数throwing 与returning类似throwing属性的值必须与通知方法中对应的参数名一致。你可以指定具体的异常类型如NullPointerException ex这样该通知就只捕获指定类型的异常。重要限制无法“吞掉”异常AfterThrowing通知执行完毕后原始异常仍然会继续向上抛出。你不能在这里通过catch来“处理”掉异常并让程序正常往下走。它的定位是“异常发生后的附加处理”而不是“异常处理”。如果你需要改变异常传播行为如转换异常类型、吞掉特定异常必须使用Around通知。3.7 Around环绕通知功能最强大的“总指挥”Around注解声明一个环绕通知。它是功能最强大的通知类型因为它完全控制了目标方法的调用过程。你可以在目标方法执行前、后插入逻辑甚至可以决定是否执行目标方法以及修改目标方法的参数和返回值。Aspect Component public class PerformanceAspect { Pointcut(annotation(com.example.demo.annotation.MonitorPerformance)) public void performanceMonitorPointcut() {} Around(performanceMonitorPointcut()) public Object measureExecutionTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long startTime System.currentTimeMillis(); String methodName proceedingJoinPoint.getSignature().getName(); Object result; try { // 1. 前置处理这里可以修改参数 Object[] args proceedingJoinPoint.getArgs(); // 可以对args进行修改例如解密、格式化等 // args[0] someProcessing(args[0]); // 2. 决定是否执行以及何时执行目标方法 // proceed()方法就是调用目标方法它的返回值就是目标方法的返回值 result proceedingJoinPoint.proceed(args); // 可以传入修改后的参数 // 3. 后置处理这里可以修改返回值 // result somePostProcessing(result); } finally { // 4. 最终处理无论成功失败都会执行适合做资源清理但注意它与After的区别 long endTime System.currentTimeMillis(); long duration endTime - startTime; System.out.println([性能监控] 方法 methodName 执行耗时: duration ms); if (duration 1000) { System.out.println([性能警告] 方法 methodName 执行过慢); } } // 5. 返回结果 return result; } }核心对象ProceedingJoinPoint 环绕通知方法的参数必须是ProceedingJoinPoint它是JoinPoint的子类。它的proceed()方法是整个环绕通知的核心调用它才会执行目标方法。环绕通知的完整控制权不调用proceed() 如果你永远不调用proceedingJoinPoint.proceed()那么目标方法就根本不会执行。你可以直接返回一个自定义的结果。这可以用于实现缓存、熔断、Mock等高级功能。调用多次proceed() 理论上你可以调用多次但这通常不是好主意除非你有特殊需求如重试逻辑。修改参数和返回值 你可以修改传入proceed()方法的参数数组也可以修改proceed()返回的结果对象。注意事项与最佳实践务必在finally块中处理耗时等最终逻辑 像计算耗时、资源清理这类无论成功失败都要执行的逻辑应该放在try-finally的finally块中。如果放在proceed()调用之后一旦目标方法抛出异常这部分逻辑就不会执行。谨慎使用 能力越大责任越大。Around太过强大滥用会导致程序流程难以理解。如果Before、AfterReturning等能满足需求就优先使用它们代码更清晰。处理异常 环绕通知需要声明抛出Throwable因为proceed()方法会抛出它。你可以在通知内部try-catch异常进行转换或处理然后选择是否重新抛出。4. 综合实战构建一个完整的日志与监控切面光说不练假把式。现在我们把所有注解组合起来实现一个在企业级应用中非常实用的综合切面它记录方法入参、出参、耗时并监控异常。package com.example.demo.aspect; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.Arrays; Aspect Component Slf4j // 使用Lombok的Slf4j注解自动生成log对象 public class ComprehensiveLoggingAspect { private final ObjectMapper objectMapper new ObjectMapper(); /** * 切入点匹配所有Controller层的方法。 * 这里使用within匹配带有RestController注解的类下的所有方法。 * 你也可以用execution(* com.example.demo.controller..*.*(..)) */ Pointcut(within(org.springframework.web.bind.annotation.RestController)) public void controllerLayer() {} /** * 环绕通知记录完整的请求-响应过程及耗时。 * 这是最核心的日志记录点。 */ Around(controllerLayer()) public Object logAround(ProceedingJoinPoint pjp) throws Throwable { long startTime System.currentTimeMillis(); // 获取请求信息在Web上下文中 String requestInfo ; try { ServletRequestAttributes attributes (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes ! null) { HttpServletRequest request attributes.getRequest(); requestInfo String.format([URL: %s, Method: %s, IP: %s], request.getRequestURL().toString(), request.getMethod(), request.getRemoteAddr()); } } catch (Exception e) { // 非Web环境或上下文不可用忽略 } String className pjp.getTarget().getClass().getSimpleName(); String methodName pjp.getSignature().getName(); Object[] args pjp.getArgs(); // 记录入参注意生产环境可能需要对敏感参数如密码进行脱敏 try { log.info({} -- {}.{} 方法开始执行入参: {}, requestInfo, className, methodName, Arrays.toString(args)); // 使用JSON序列化更清晰但注意性能和大对象 // log.info(入参详情: {}, objectMapper.writeValueAsString(args)); } catch (Exception e) { log.warn(记录入参日志时发生序列化异常, e); } Object result; try { // 执行目标方法 result pjp.proceed(); long endTime System.currentTimeMillis(); long duration endTime - startTime; // 记录出参和耗时同样注意敏感信息脱敏 try { log.info({} -- {}.{} 方法执行成功耗时: {}ms, 返回结果: {}, requestInfo, className, methodName, duration, result); } catch (Exception e) { log.warn(记录出参日志时发生序列化异常, e); } // 慢查询警告 if (duration 1000) { log.warn({} {}.{} 方法执行过慢耗时: {}ms, requestInfo, className, methodName, duration); } } catch (Throwable throwable) { // 记录异常和耗时 long endTime System.currentTimeMillis(); long duration endTime - startTime; log.error({} !!! {}.{} 方法执行异常耗时: {}ms异常信息: , requestInfo, className, methodName, duration, throwable); // 异常继续向上抛出由全局异常处理器处理 throw throwable; } return result; } /** * 返回后通知专门用于业务成功后的特定处理例如发送审计事件。 * 此处的处理不应影响主流程通常异步执行。 */ AfterReturning(pointcut controllerLayer(), returning result) public void auditLogOnSuccess(JoinPoint joinPoint, Object result) { // 例如将关键操作记录到审计日志表或发送到消息队列 // auditService.logOperation(joinPoint, result, SUCCESS); // log.debug(审计日志记录成功异步处理); } /** * 异常通知专门用于业务异常后的特定处理例如更新监控状态。 * 注意异常日志已在Around中记录此处做额外处理。 */ AfterThrowing(pointcut controllerLayer(), throwing ex) public void monitorOnFailure(JoinPoint joinPoint, Exception ex) { // 例如增加错误计数器用于熔断器或健康检查 // metricsService.incrementErrorCounter(joinPoint.getSignature().toShortString()); // log.debug(错误监控状态已更新); } }这个实战案例的要点解析切入点设计 我们使用within来匹配所有RestController下的方法这比用execution表达式更简洁且能自动适配所有Controller。日志脱敏 在记录入参和出参时我特别加了注释提醒敏感信息脱敏。在实际生产环境中像密码、身份证号、手机号等字段绝对不能直接打印到日志里。你需要编写自定义的序列化器或是在切面中过滤/替换这些字段。性能考量 使用ObjectMapper将对象序列化成JSON字符串再打印虽然清晰但对于大对象或高频调用方法会有性能损耗。生产环境需要权衡有时只记录关键字段或类名即可。异常处理 在Around中我们用try-catch包裹了proceed()这样就能在方法抛出异常时依然记录下耗时并把异常日志级别定为error。最后再将原异常throw出去保证业务异常的正常传播。职责分离 我们用了Around做核心日志用AfterReturning和AfterThrowing做额外的成功/失败后处理如审计、监控。这样的分离使得切面逻辑更清晰每个通知各司其职。Web上下文 通过RequestContextHolder获取当前HTTP请求信息这在Web环境下非常有用。但要注意在非Web上下文如定时任务、MQ监听器中调用时这部分代码会获取不到request所以需要做好判空处理。5. 高级技巧、常见问题与排查实录掌握了基本用法我们来看看一些进阶场景和那些年我踩过的坑。5.1 通知执行顺序当多个切面命中同一个方法如果一个方法被多个切面的多个通知匹配执行顺序由两点决定切面级别的顺序 在切面类上使用Order注解或让切面类实现Ordered接口。数字越小优先级越高越先执行。通知类型的默认顺序 在同一个切面内如果同一个切入点有多个不同类型的通知执行顺序是Around通知的开始部分Before通知目标方法执行Around通知的结束部分如果目标方法成功执行After通知无论成功失败AfterReturning通知仅成功或AfterThrowing通知仅失败一个常见的坑 你定义了一个事务切面Around和一个日志切面Before/After希望日志在事务内外都能记录。如果事务切面顺序更高那么日志切面实际上记录的是事务代理方法内部的执行情况。通常我们会把事务切面的顺序设得较高Order(Ordered.LOWEST_PRECEDENCE - 1)确保它在最内层执行。5.2 同类内部方法调用导致AOP失效这是Spring AOP基于代理机制带来的最经典问题。Service public class MyService { public void outerMethod() { System.out.println(outerMethod called); this.innerMethod(); // 这里调用的是this即目标对象本身不是代理对象 } Transactional // 假设这里有个事务注解 public void innerMethod() { System.out.println(innerMethod called); } }当你调用myService.outerMethod()时innerMethod()上的Transactional其本质也是一个AOP不会生效。解决方案推荐重构代码 将innerMethod抽取到另一个Service中然后通过注入调用。这样调用就通过代理进行了。自注入 在MyService中注入自己Autowired private MyService self然后通过self.innerMethod()调用。Spring会注入代理对象。但这种方法有点“黑魔法”影响代码可读性。使用AspectJ 将Spring AOP的代理模式切换为AspectJ的编译时或加载时织入LTW这样可以实现真正的字节码增强解决内部调用问题。但配置更复杂。5.3 切入点表达式匹配不上的排查步骤你写好了切面但日志就是不打印按以下步骤排查检查Bean是否被Spring管理 你的切面类有Component吗目标类被拦截的类是一个Spring Bean吗有Service,Controller等注解吗检查包扫描路径 切面类和目标类所在的包都在Spring的组件扫描范围内吗检查切入点表达式使用execution(* com.example..*.*(..))这种最宽泛的表达式试一下如果生效了说明是你的原表达式写得太窄。仔细核对包名、类名、方法名是否有拼写错误。注意访问修饰符。execution(public * ..*(..))只匹配public方法。检查同类调用问题 你是在外部调用还是在类的另一个方法内部调用开启Spring AOP调试日志 在application.properties中设置logging.level.org.springframework.aopDEBUGSpring会打印详细的代理创建和匹配信息。5.4 性能优化与生产建议切入点表达式要精确 避免使用*.*(..)。尽量限定到具体的包、类或注解。在环绕通知中谨慎进行IO操作和序列化 像上面的JSON序列化日志在高并发场景可能成为瓶颈。考虑使用异步日志如Logback的AsyncAppender或采样记录。避免在切面中执行耗时或不可靠的远程调用 例如不要在Before通知里调用一个慢速的身份验证服务这会拖慢所有业务方法。考虑异步或缓存。为切面编写单元测试 切面也是代码需要用测试来保证其正确性。你可以使用Spring Boot Test来启动一个轻量级上下文测试你的切面是否按预期织入。Spring AOP的这些注解就像一套精密的瑞士军刀每把都有其特定的用途。Pointcut是定位器Before、After等是执行不同阶段任务的工具而Around是那个功能最全的钳子。理解它们的本质——基于代理的拦截——是避免踩坑的关键。从简单的日志记录开始逐步尝试事务管理、缓存、权限校验等复杂场景你会越来越体会到AOP让代码变得清晰、简洁和强大的魔力。记住最好的学习方式就是动手创建一个测试项目把上面每个例子都敲一遍再试着改造一下你现有项目中的重复代码感受一下“切面”带来的整洁与高效。

相关新闻

基于XIAO ESP32S3 Sense的嵌入式视觉开发:从图像采集到AI识别实战

基于XIAO ESP32S3 Sense的嵌入式视觉开发:从图像采集到AI识别实战

2026/8/2 8:25:21

1. 项目概述:解锁XIAO ESP32S3 Sense的视觉潜能 如果你手头有一块Seeed Studio XIAO ESP32S3 Sense开发板,却还只把它当做一个普通的Wi-Fi/蓝牙模块来用,那可就太“屈才”了。这块板子最吸引人的地方,就是它板载的那颗OV2640摄像头…

2026企业文件管理系统横向对比测评|通用选型标准与场景适配指南

2026企业文件管理系统横向对比测评|通用选型标准与场景适配指南

2026/8/2 8:25:21

随着企业数字化建设不断推进,企业产生的文档、素材、项目资料、业务数据体量持续增长。传统依靠本地文件夹、即时通讯传输、人工归档的文件管理方式,普遍存在文件分散、版本混乱、检索低效、协作困难、数据无沉淀、权限无管控等问题,难以适配…

5分钟彻底解决Mac鼠标滚轮卡顿:Mos平滑滚动工具的完整指南

5分钟彻底解决Mac鼠标滚轮卡顿:Mos平滑滚动工具的完整指南

2026/8/2 8:15:21

5分钟彻底解决Mac鼠标滚轮卡顿:Mos平滑滚动工具的完整指南 【免费下载链接】Mos 一个用于在 macOS 上平滑你的鼠标滚动效果或单独设置滚动方向的小工具, 让你的滚轮爽如触控板 | A lightweight tool used to smooth scrolling and set scroll direction independent…

CM4S核心板深度解析:RK3568工业模块与可插拔eMMC设计实战

CM4S核心板深度解析:RK3568工业模块与可插拔eMMC设计实战

2026/8/2 9:25:24

1. 项目概述:CM4S是什么,以及为什么它值得关注 最近在折腾嵌入式开发板的朋友圈里,CM4S这个词的热度有点高。乍一看,很多人会把它和树莓派基金会那款经典的Compute Module 4(CM4)搞混,但实际上&…

ESD防护中静电电容选型与PCB布局实战指南

ESD防护中静电电容选型与PCB布局实战指南

2026/8/2 9:25:23

1. 项目概述:静电电容,EMC难题的“守门员” 做硬件,尤其是涉及接口、电源、高速信号的产品,最头疼也最绕不开的问题之一就是EMC(电磁兼容性)。产品功能跑得再溜,一到实验室做静电放电&#xff0…

3分钟搞定:让Switch手柄在PC上玩转所有游戏的终极指南

3分钟搞定:让Switch手柄在PC上玩转所有游戏的终极指南

2026/8/2 9:25:23

3分钟搞定:让Switch手柄在PC上玩转所有游戏的终极指南 【免费下载链接】BetterJoy Allows the Nintendo Switch Pro Controller, Joycons and SNES controller to be used with CEMU, Citra, Dolphin, Yuzu and as generic XInput 项目地址: https://gitcode.com/…

ATL Saathi:解析印度STEM教育创新生态系统的架构与落地

ATL Saathi:解析印度STEM教育创新生态系统的架构与落地

2026/8/2 9:25:23

1. 项目概述:ATL Saathi的使命与愿景 最近在关注全球教育科技领域的朋友,可能都注意到了“ATL Saathi”这个项目。乍一看标题,它像是一个针对印度市场的本地化教育工具,但当你深入拆解,会发现它远不止于此。这是一个旨…

呼和浩特冰煮羊推荐,萫尔创新多味锅底吃出汤清肉嫩

呼和浩特冰煮羊推荐,萫尔创新多味锅底吃出汤清肉嫩

2026/8/2 9:25:23

呼和浩特冰煮羊,萫尔这口“汤清肉嫩”有点东西 要说内蒙古独有的火锅品类,冰煮羊绝对算得上是一张特色名片。跟热油翻滚的川渝火锅不同,冰煮羊从端上桌的那一刻起,就带着一股子草原的清凉劲儿。在呼和浩特,想寻一处既…

CloudCompare点云选择工具:从原理到实战的精准数据提取指南

CloudCompare点云选择工具:从原理到实战的精准数据提取指南

2026/8/2 9:15:23

1. 项目概述:为什么点云选择是三维数据处理的门槛 在三维激光扫描、摄影测量或者任何涉及三维数据处理的领域,拿到一堆密密麻麻的点云数据后,第一件头疼的事往往不是怎么分析,而是“怎么从这几十万甚至上亿个点里,把我…

ncmdumpGUI:一键解锁网易云音乐ncm文件的终极解决方案

ncmdumpGUI:一键解锁网易云音乐ncm文件的终极解决方案

2026/8/2 0:04:43

ncmdumpGUI:一键解锁网易云音乐ncm文件的终极解决方案 【免费下载链接】ncmdumpGUI C#版本网易云音乐ncm文件格式转换,Windows图形界面版本 项目地址: https://gitcode.com/gh_mirrors/nc/ncmdumpGUI 你是否曾经从网易云音乐下载了心爱的歌曲&am…

分布式配置中心选型实战:Nacos与Consul在创业场景下的对比

分布式配置中心选型实战:Nacos与Consul在创业场景下的对比

2026/8/2 0:04:43

分布式配置中心选型实战:Nacos与Consul在创业场景下的对比工程导读:本文深入讨论 分布式配置中心选型实战:Nacos与Consul在创业场景下的对比 在生产工程实践中的核心落地方案。基于 分布式架构与微服务设计 视角,剖析实际痛点、架…

MoneyPrinterPlus实战指南:AI视频批量生成与自动化发布完整解决方案

MoneyPrinterPlus实战指南:AI视频批量生成与自动化发布完整解决方案

2026/8/2 0:04:43

MoneyPrinterPlus实战指南:AI视频批量生成与自动化发布完整解决方案 【免费下载链接】MoneyPrinterPlus AI一键批量生成各类短视频,自动批量混剪短视频,自动把视频发布到抖音,快手,小红书,视频号上,赚钱从来没有这么容易过! 支持本地语音模型chatTTS,fasterwhisper,…

ncmdumpGUI:一键解锁网易云音乐ncm文件的终极解决方案

ncmdumpGUI:一键解锁网易云音乐ncm文件的终极解决方案

2026/8/2 0:04:43

ncmdumpGUI:一键解锁网易云音乐ncm文件的终极解决方案 【免费下载链接】ncmdumpGUI C#版本网易云音乐ncm文件格式转换,Windows图形界面版本 项目地址: https://gitcode.com/gh_mirrors/nc/ncmdumpGUI 你是否曾经从网易云音乐下载了心爱的歌曲&am…

分布式配置中心选型实战:Nacos与Consul在创业场景下的对比

分布式配置中心选型实战:Nacos与Consul在创业场景下的对比

2026/8/2 0:04:43

分布式配置中心选型实战:Nacos与Consul在创业场景下的对比工程导读:本文深入讨论 分布式配置中心选型实战:Nacos与Consul在创业场景下的对比 在生产工程实践中的核心落地方案。基于 分布式架构与微服务设计 视角,剖析实际痛点、架…

MoneyPrinterPlus实战指南:AI视频批量生成与自动化发布完整解决方案

MoneyPrinterPlus实战指南:AI视频批量生成与自动化发布完整解决方案

2026/8/2 0:04:43

MoneyPrinterPlus实战指南:AI视频批量生成与自动化发布完整解决方案 【免费下载链接】MoneyPrinterPlus AI一键批量生成各类短视频,自动批量混剪短视频,自动把视频发布到抖音,快手,小红书,视频号上,赚钱从来没有这么容易过! 支持本地语音模型chatTTS,fasterwhisper,…

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

2026/8/1 0:03:03

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具,覆盖选题构思、文献整理、内容生成、格式排版等核心场景,真正帮你高效搞定论文难题。 一、全流程王者:一站式搞定论文全链路(一天定稿首…

导师推荐!2026最新AI论文工具测评与实用推荐

导师推荐!2026最新AI论文工具测评与实用推荐

2026/8/2 5:08:03

2026年真正好用的AI论文工具,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

告别游戏崩溃:XCOM 2模组管理器的智能革命

告别游戏崩溃:XCOM 2模组管理器的智能革命

2026/8/2 1:50:52

告别游戏崩溃:XCOM 2模组管理器的智能革命 【免费下载链接】xcom2-launcher The Alternative Mod Launcher (AML) is a replacement for the default game launchers from XCOM 2 and XCOM Chimera Squad. 项目地址: https://gitcode.com/gh_mirrors/xc/xcom2-lau…