当前位置: 首页 > news >正文

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配置放在独立的配置类中,便于管理和维护。记得根据具体业务场景选择合适的通知类型和切入点表达式哦!✨

http://www.cnnetsun.cn/news/191607.html

相关文章:

  • sprintf 和 printf
  • 毕业季必看!9个AI写论文神器,1天生成25000字含真实参考文献
  • 【国产 OS 顶流实战】KylinOS V10 等保 2.0 三级合规 + MES 系统国产化迁移全案
  • Java基于springboot+vue的毕业生离校管理系统的设计与实现
  • 【毕业设计】基于springboot的旧物回收商城系统的设计与实现(源码+文档+远程调试,全bao定制等)
  • OpenMV中HOG特征提取全面讲解
  • 8个AI论文生成平台测评,降重与写作功能深度解析
  • 8个AI论文改写工具评测,降重与写作功能全面分析
  • Elasticsearch基本用法项目应用:分页与高亮处理
  • 基于proteus的4位数码管动态扫描实战案例
  • 全面讲解ESP32开发核心外设:GPIO控制基础教学
  • PaperzzAI PPT:别再熬夜做PPT了,让AI给你“一键生成高光时刻”——不是模板搬运工,是你的视觉导演+内容编剧
  • 图解说明Vitis使用教程:适合初学者的界面功能解析
  • 具身智能重构体验!CES Asia 2026:消费电子从“工具”变身“主动伙伴”
  • STM32-时钟树编程
  • Packet Tracer使用教程:OSPF基础配置图解说明
  • 批量部署USB转串口驱动的企业级Windows策略应用
  • 赋能成长型企业:SAP Business One与奥维奥的数字化共赢之道
  • 一文说清同步整流buck电路图及其工作原理
  • Packet Tracer下载步骤详解:适合初学者的系统学习
  • 2025年AI论文写作平台精选,集成LaTeX支持与智能格式检查
  • Hotkey Detective终极指南:3步解决Windows热键冲突难题
  • 【Mol Plant综述精读】植物中的染色质重塑:复合物组成、机制多样性及生物学功能
  • 基于GA-HIDMSPSO算法优化BP神经网络+NSGAII多目标优化算法工艺参数优化、工程设计优化(四目标优化案例)
  • 系统学习erase前必须知道的存储基础知识
  • 通俗解释定制ROM在2025机顶盒刷机中的作用机制
  • 【数据分析】基于逆向方法的新型神经网络的实现,以估计云杉音木薄板的材料特性附Matlab代码
  • 微信小程序二维码生成实战指南:3步实现个性化营销码
  • 终极指南:如何使用Keyboard Chatter Blocker解决机械键盘连击问题
  • Performance-Fish性能优化指南:让《环世界》告别卡顿的5大秘诀