news 2026/6/10 12:19:00

Spring AOP 面向切面编程完全指南 [特殊字符]

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring AOP 面向切面编程完全指南 [特殊字符]

Spring AOP 面向切面编程完全指南 🚀

一、什么是 AOP? 🤔

面向切面编程(AOP)是 Spring 框架的核心功能之一,它允许开发者将横切关注点(如日志记录、事务管理、安全控制等)从业务逻辑中分离出来,实现代码的模块化和复用。就像电影特效团队🎬,他们专注于特效制作,而不需要关心剧本内容!

二、快速开始 ⚡

1. 导入依赖 📦

<!-- Spring Boot AOP 依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>

2. 第一个 AOP 程序:方法执行时间监控 ⏱️

@Slf4j@Aspect// 声明为切面类 🎯@Component// 交给 IOC 容器管理 🏭publicclassRecordTimeAspect{/** * 切入点表达式:监控 com.example.service 包下所有类的所有方法 */@Around("execution(* com.example.service.*.*(..))")publicObjectrecordTime(ProceedingJoinPointpjp)throwsThrowable{// 记录开始时间longbeginTime=System.currentTimeMillis();// 执行原始方法 🏃‍♂️Objectresult=pjp.proceed();// 记录结束时间并计算耗时longendTime=System.currentTimeMillis();longcostTime=endTime-beginTime;// 获取方法签名信息StringmethodName=pjp.getSignature().getName();StringclassName=pjp.getTarget().getClass().getSimpleName();log.info("🎯 [{}] - [{}] 执行耗时: {}ms",className,methodName,costTime);returnresult;}}

三、AOP 核心概念详解 🎓

1. 核心术语 📚

概念说明类比表情
连接点 (JoinPoint)程序执行过程中可以插入切面的点电影中的每个场景🎬
通知 (Advice)切面在特定连接点执行的动作特效团队的工作🎨
切入点 (Pointcut)匹配连接点的谓词需要加特效的场景📍
切面 (Aspect)通知和切入点的组合完整的特效设计方案📋
目标对象 (Target)被一个或多个切面所通知的对象原始电影片段🎥

2. AOP 底层原理:动态代理 🧙‍♂️

Spring AOP 默认使用两种代理方式:

  • JDK 动态代理:基于接口实现 ✨
  • CGLIB 代理:基于继承实现 🔄

四、通知类型详解 🎪

1. 五种通知类型 🖐️

@Aspect@ComponentpublicclassAllAdviceExample{/** * 1. 环绕通知 - 最强大的通知类型 💪 */@Around("execution(* com.example.service.*.*(..))")publicObjectaroundAdvice(ProceedingJoinPointpjp)throwsThrowable{log.info("🔄 @Around - 方法执行前");try{Objectresult=pjp.proceed();// 必须显式调用原始方法log.info("✅ @Around - 方法执行后");returnresult;}catch(Exceptione){log.error("❌ @Around - 方法执行异常");throwe;}}/** * 2. 前置通知 - 在目标方法执行前执行 ⬆️ */@Before("execution(* com.example.service.*.*(..))")publicvoidbeforeAdvice(JoinPointjoinPoint){log.info("⬆️ @Before - 方法执行前");}/** * 3. 后置通知 - 在目标方法执行后执行(无论是否异常) ⬇️ */@After("execution(* com.example.service.*.*(..))")publicvoidafterAdvice(JoinPointjoinPoint){log.info("⬇️ @After - 方法执行后(总执行)");}/** * 4. 返回后通知 - 在目标方法正常返回后执行 ✅ */@AfterReturning(value="execution(* com.example.service.*.*(..))",returning="result")publicvoidafterReturningAdvice(JoinPointjoinPoint,Objectresult){log.info("✅ @AfterReturning - 方法正常返回,结果: {}",result);}/** * 5. 异常通知 - 在目标方法抛出异常后执行 ❌ */@AfterThrowing(value="execution(* com.example.service.*.*(..))",throwing="ex")publicvoidafterThrowingAdvice(JoinPointjoinPoint,Exceptionex){log.error("❌ @AfterThrowing - 方法执行异常: {}",ex.getMessage());}}

2. 通知执行顺序 📊

默认执行顺序(同一切面内):
🔄 @Around(前半部分) →⬆️ @Before🎯 目标方法🔄 @Around(后半部分) →✅ @AfterReturning/❌ @AfterThrowing⬇️ @After

五、切入点表达式 ✨

1. execution 表达式 🎯

@Aspect@ComponentpublicclassPointcutExamples{// 1. 抽取公共切入点表达式 📝@Pointcut("execution(* com.example.service.*.*(..))")publicvoidserviceLayer(){}// 2. 精确匹配方法 🎯@Pointcut("execution(public String com.example.service.UserService.getUserById(Integer))")publicvoidspecificMethod(){}// 3. 匹配包下所有方法 📁@Pointcut("execution(* com.example.service..*.*(..))")// ..表示子包publicvoidallServiceMethods(){}// 4. 匹配特定注解的方法 🏷️@Pointcut("@annotation(com.example.annotation.Log)")publicvoidlogAnnotation(){}// 5. 组合切入点表达式 🔗@Pointcut("serviceLayer() && !specificMethod()")publicvoidserviceButNotSpecific(){}@Around("serviceLayer()")// 引用切入点表达式publicObjectadviceMethod(ProceedingJoinPointpjp)throwsThrowable{// 业务逻辑returnpjp.proceed();}}

2. @annotation 表达式(基于注解) 🏷️

/** * 自定义日志注解 */@Target(ElementType.METHOD)// 只能用在方法上@Retention(RetentionPolicy.RUNTIME)// 运行时生效public@interfaceLog{Stringvalue()default"";booleanrecordParams()defaulttrue;booleanrecordResult()defaulttrue;}

六、实战:操作日志记录到数据库 💾

1. 创建操作日志实体 📋

@Data@TableName("t_operate_log")// MyBatis-Plus 注解publicclassOperateLog{@TableId(type=IdType.AUTO)privateLongid;privateLongoperateUserId;// 操作人ID 👤privateStringoperateUserName;// 操作人姓名@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")privateLocalDateTimeoperateTime;// 操作时间 ⏰privateStringclassName;// 类名privateStringmethodName;// 方法名@TableField(typeHandler=JacksonTypeHandler.class)privateObjectmethodParams;// 方法参数(JSON格式) 📄@TableField(typeHandler=JacksonTypeHandler.class)privateObjectreturnValue;// 返回值(JSON格式) 📋privateBooleansuccess;// 操作是否成功 ✅privateStringerrorMessage;// 错误信息 ❌privateLongcostTime;// 耗时(毫秒) ⏱️privateStringclientIp;// 客户端IP 🌐privateStringrequestUri;// 请求URI 🔗}

2. Mapper 接口 🔧

@MapperpublicinterfaceOperateLogMapperextendsBaseMapper<OperateLog>{// 这里可以添加自定义查询方法}

3. 用户上下文工具类(ThreadLocal) 🧵

@ComponentpublicclassUserContext{privatestaticfinalThreadLocal<CurrentUser>USER_CONTEXT=newThreadLocal<>();/** * 设置当前用户信息 👤 */publicstaticvoidsetCurrentUser(CurrentUseruser){USER_CONTEXT.set(user);}/** * 获取当前用户ID 🔑 */publicstaticLonggetCurrentUserId(){CurrentUseruser=USER_CONTEXT.get();returnuser!=null?user.getId():null;}/** * 获取当前用户信息 📋 */publicstaticCurrentUsergetCurrentUser(){returnUSER_CONTEXT.get();}/** * 清除用户信息(防止内存泄漏) 🧹 */publicstaticvoidclear(){USER_CONTEXT.remove();}@DatapublicstaticclassCurrentUser{privateLongid;privateStringusername;privateStringname;privateStringrole;}}

4. 切面类实现 🛡️

@Slf4j@Aspect@ComponentpublicclassOperateLogAspect{@AutowiredprivateOperateLogMapperoperateLogMapper;@AutowiredprivateHttpServletRequestrequest;/** * 基于注解的切入点 🎯 */@Around("@annotation(logAnnotation)")publicObjectlogOperate(ProceedingJoinPointpjp,LoglogAnnotation)throwsThrowable{// 记录开始时间 ⏰longstartTime=System.currentTimeMillis();// 创建日志对象 📝OperateLogoperateLog=newOperateLog();operateLog.setOperateTime(LocalDateTime.now());operateLog.setSuccess(true);// 获取方法信息 🔍MethodSignaturesignature=(MethodSignature)pjp.getSignature();Methodmethod=signature.getMethod();// 设置基本信息operateLog.setClassName(pjp.getTarget().getClass().getName());operateLog.setMethodName(method.getName());// 获取当前用户 👤LongcurrentUserId=UserContext.getCurrentUserId();operateLog.setOperateUserId(currentUserId);// 获取请求信息 🌐if(request!=null){operateLog.setClientIp(getClientIp(request));operateLog.setRequestUri(request.getRequestURI());}// 记录方法参数(根据配置) 📄if(logAnnotation.recordParams()){Object[]args=pjp.getArgs();String[]paramNames=signature.getParameterNames();Map<String,Object>params=newHashMap<>();for(inti=0;i<args.length;i++){// 过滤掉敏感参数 🚫if(!isSensitiveParam(paramNames[i])){params.put(paramNames[i],args[i]);}}operateLog.setMethodParams(params);}Objectresult=null;try{// 执行目标方法 🏃‍♂️result=pjp.proceed();// 记录返回值(根据配置) ✅if(logAnnotation.recordResult()){operateLog.setReturnValue(result);}// 计算耗时 ⏱️longendTime=System.currentTimeMillis();operateLog.setCostTime(endTime-startTime);log.info("✅ 操作日志记录成功: {}.{}",operateLog.getClassName(),operateLog.getMethodName());}catch(Throwablethrowable){// 记录异常信息 ❌operateLog.setSuccess(false);operateLog.setErrorMessage(throwable.getMessage());operateLog.setCostTime(System.currentTimeMillis()-startTime);log.error("❌ 操作执行失败: {}",throwable.getMessage());throwthrowable;}finally{// 异步保存日志到数据库 💾saveLogAsync(operateLog);}returnresult;}/** * 异步保存日志 🚀 */privatevoidsaveLogAsync(OperateLogoperateLog){CompletableFuture.runAsync(()->{try{operateLogMapper.insert(operateLog);log.debug("💾 操作日志已保存到数据库");}catch(Exceptione){log.error("❌ 保存操作日志失败: {}",e.getMessage());}});}/** * 获取客户端IP地址 🌐 */privateStringgetClientIp(HttpServletRequestrequest){Stringip=request.getHeader("X-Forwarded-For");if(ip==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){ip=request.getHeader("Proxy-Client-IP");}if(ip==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){ip=request.getHeader("WL-Proxy-Client-IP");}if(ip==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){ip=request.getRemoteAddr();}returnip;}/** * 判断是否为敏感参数 🚫 */privatebooleanisSensitiveParam(StringparamName){List<String>sensitiveParams=Arrays.asList("password","token","secret");returnsensitiveParams.stream().anyMatch(paramName::contains);}}

七、最佳实践与注意事项 ⚠️

1. 性能优化建议 🚀

  • 合理使用切入点表达式:避免过于宽泛的匹配
  • 异步处理耗时操作:如数据库日志保存
  • 缓存频繁访问的数据:如方法签名信息

2. 常见陷阱 🕳️

  • 自调用问题:同一个类中方法互相调用,AOP不会生效
  • 异常处理:确保异常被正确处理和传播
  • 内存泄漏:ThreadLocal使用后必须清理

3. 调试技巧 🔧

// 在通知中打印详细信息@Before("execution(* com.example..*.*(..))")publicvoiddebugAdvice(JoinPointjoinPoint){log.debug("🎯 目标类: {}",joinPoint.getTarget().getClass().getName());log.debug("🔧 方法名: {}",joinPoint.getSignature().getName());log.debug("📋 参数: {}",Arrays.toString(joinPoint.getArgs()));}

八、总结 🎉

Spring AOP 是一个强大的面向切面编程框架,它通过动态代理机制实现了横切关注点的模块化。掌握 AOP 可以让你的代码更加:

  • 干净整洁- 分离关注点
  • 易于维护- 集中化管理
  • 高度复用- 一处定义,多处使用
  • 灵活扩展- 非侵入式增强

希望这篇指南能帮助你更好地理解和使用 Spring AOP!Happy coding! 😊👨‍💻👩‍💻


小提示:在实际项目中,建议将AOP配置放在独立的配置类中,便于管理和维护。记得根据具体业务场景选择合适的通知类型和切入点表达式哦!✨

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

RISC理念在ARM中的体现:通俗解释

RISC为何能“四两拨千斤”&#xff1f;ARM的底层逻辑全解析你有没有想过&#xff0c;为什么一部轻薄的iPad可以流畅剪辑4K视频&#xff0c;而功耗却远低于一台高性能游戏本&#xff1f;为什么苹果M1芯片能在性能不输AMD Ryzen的同时&#xff0c;把笔记本的续航轻松做到20小时&a…

作者头像 李华
网站建设 2026/6/8 17:28:53

LC.173 | 二叉搜索树迭代器 | 树 | 中序展开/栈模拟

输入&#xff1a; BST 根节点 root&#xff0c;构造 BSTIterator。 要求&#xff1a; 实现一个按中序遍历输出 BST 的迭代器&#xff1a; next()&#xff1a;返回下一个最小值hasNext()&#xff1a;是否还有下一个元素 输出&#xff1a; 按题意实现类方法&#xff08;next/hasN…

作者头像 李华
网站建设 2026/6/9 23:52:51

8个顶尖AI论文写作平台功能对比,支持降重与改写

AI论文工具的选择需结合降重、降AIGC率及写作需求进行综合评估。根据实测数据与用户反馈&#xff0c;主流工具在效率、准确性和易用性方面表现各异&#xff0c;例如部分平台擅长语义重构降低重复率&#xff0c;而另一些则通过算法优化减少AI生成痕迹。实际应用中需优先匹配核心…

作者头像 李华
网站建设 2026/6/10 12:20:34

高效论文生成工具盘点,涵盖AI降重与自动写作功能

AI论文工具的选择需要结合降重、降AIGC率、写作效率等核心需求。通过实测数据和用户反馈综合评估&#xff0c;目前主流的8款工具中&#xff0c;排名靠前的平台在准确性、易用性和处理速度上表现突出&#xff0c;尤其擅长智能改写、降低AI生成痕迹以及辅助论文创作&#xff0c;能…

作者头像 李华