建设商城购物网站,外贸网站优化价格,网站 备案 名称,企业邮箱多少钱一年Spring AOP 面向切面编程完全指南 #x1f680;
一、什么是 AOP#xff1f; #x1f914;
面向切面编程#xff08;AOP#xff09;是 Spring 框架的核心功能之一#xff0c;它允许开发者将横切关注点#xff08;如日志记录、事务管理、安全控制等#xff09;从业务逻…Spring AOP 面向切面编程完全指南 一、什么是 AOP 面向切面编程AOP是 Spring 框架的核心功能之一它允许开发者将横切关注点如日志记录、事务管理、安全控制等从业务逻辑中分离出来实现代码的模块化和复用。就像电影特效团队他们专注于特效制作而不需要关心剧本内容二、快速开始 ⚡1. 导入依赖 !-- Spring Boot AOP 依赖 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-aop/artifactId/dependency2. 第一个 AOP 程序方法执行时间监控 ⏱️Slf4jAspect// 声明为切面类 Component// 交给 IOC 容器管理 publicclassRecordTimeAspect{/** * 切入点表达式监控 com.example.service 包下所有类的所有方法 */Around(execution(* com.example.service.*.*(..)))publicObjectrecordTime(ProceedingJoinPointpjp)throwsThrowable{// 记录开始时间longbeginTimeSystem.currentTimeMillis();// 执行原始方法 ♂️Objectresultpjp.proceed();// 记录结束时间并计算耗时longendTimeSystem.currentTimeMillis();longcostTimeendTime-beginTime;// 获取方法签名信息StringmethodNamepjp.getSignature().getName();StringclassNamepjp.getTarget().getClass().getSimpleName();log.info( [{}] - [{}] 执行耗时: {}ms,className,methodName,costTime);returnresult;}}三、AOP 核心概念详解 1. 核心术语 概念说明类比表情连接点 (JoinPoint)程序执行过程中可以插入切面的点电影中的每个场景通知 (Advice)切面在特定连接点执行的动作特效团队的工作切入点 (Pointcut)匹配连接点的谓词需要加特效的场景切面 (Aspect)通知和切入点的组合完整的特效设计方案目标对象 (Target)被一个或多个切面所通知的对象原始电影片段2. AOP 底层原理动态代理 ♂️Spring AOP 默认使用两种代理方式JDK 动态代理基于接口实现 ✨CGLIB 代理基于继承实现 四、通知类型详解 1. 五种通知类型 ️AspectComponentpublicclassAllAdviceExample{/** * 1. 环绕通知 - 最强大的通知类型 */Around(execution(* com.example.service.*.*(..)))publicObjectaroundAdvice(ProceedingJoinPointpjp)throwsThrowable{log.info( Around - 方法执行前);try{Objectresultpjp.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(valueexecution(* com.example.service.*.*(..)),returningresult)publicvoidafterReturningAdvice(JoinPointjoinPoint,Objectresult){log.info(✅ AfterReturning - 方法正常返回结果: {},result);}/** * 5. 异常通知 - 在目标方法抛出异常后执行 ❌ */AfterThrowing(valueexecution(* com.example.service.*.*(..)),throwingex)publicvoidafterThrowingAdvice(JoinPointjoinPoint,Exceptionex){log.error(❌ AfterThrowing - 方法执行异常: {},ex.getMessage());}}2. 通知执行顺序 默认执行顺序同一切面内 Around前半部分 →⬆️ Before→ 目标方法→ Around后半部分 →✅ AfterReturning/❌ AfterThrowing→⬇️ After五、切入点表达式 ✨1. execution 表达式 AspectComponentpublicclassPointcutExamples{// 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)// 运行时生效publicinterfaceLog{Stringvalue()default;booleanrecordParams()defaulttrue;booleanrecordResult()defaulttrue;}六、实战操作日志记录到数据库 1. 创建操作日志实体 DataTableName(t_operate_log)// MyBatis-Plus 注解publicclassOperateLog{TableId(typeIdType.AUTO)privateLongid;privateLongoperateUserId;// 操作人ID privateStringoperateUserName;// 操作人姓名JsonFormat(patternyyyy-MM-dd HH:mm:ss)privateLocalDateTimeoperateTime;// 操作时间 ⏰privateStringclassName;// 类名privateStringmethodName;// 方法名TableField(typeHandlerJacksonTypeHandler.class)privateObjectmethodParams;// 方法参数JSON格式 TableField(typeHandlerJacksonTypeHandler.class)privateObjectreturnValue;// 返回值JSON格式 privateBooleansuccess;// 操作是否成功 ✅privateStringerrorMessage;// 错误信息 ❌privateLongcostTime;// 耗时毫秒 ⏱️privateStringclientIp;// 客户端IP privateStringrequestUri;// 请求URI }2. Mapper 接口 MapperpublicinterfaceOperateLogMapperextendsBaseMapperOperateLog{// 这里可以添加自定义查询方法}3. 用户上下文工具类ThreadLocal ComponentpublicclassUserContext{privatestaticfinalThreadLocalCurrentUserUSER_CONTEXTnewThreadLocal();/** * 设置当前用户信息 */publicstaticvoidsetCurrentUser(CurrentUseruser){USER_CONTEXT.set(user);}/** * 获取当前用户ID */publicstaticLonggetCurrentUserId(){CurrentUseruserUSER_CONTEXT.get();returnuser!null?user.getId():null;}/** * 获取当前用户信息 */publicstaticCurrentUsergetCurrentUser(){returnUSER_CONTEXT.get();}/** * 清除用户信息防止内存泄漏 */publicstaticvoidclear(){USER_CONTEXT.remove();}DatapublicstaticclassCurrentUser{privateLongid;privateStringusername;privateStringname;privateStringrole;}}4. 切面类实现 ️Slf4jAspectComponentpublicclassOperateLogAspect{AutowiredprivateOperateLogMapperoperateLogMapper;AutowiredprivateHttpServletRequestrequest;/** * 基于注解的切入点 */Around(annotation(logAnnotation))publicObjectlogOperate(ProceedingJoinPointpjp,LoglogAnnotation)throwsThrowable{// 记录开始时间 ⏰longstartTimeSystem.currentTimeMillis();// 创建日志对象 OperateLogoperateLognewOperateLog();operateLog.setOperateTime(LocalDateTime.now());operateLog.setSuccess(true);// 获取方法信息 MethodSignaturesignature(MethodSignature)pjp.getSignature();Methodmethodsignature.getMethod();// 设置基本信息operateLog.setClassName(pjp.getTarget().getClass().getName());operateLog.setMethodName(method.getName());// 获取当前用户 LongcurrentUserIdUserContext.getCurrentUserId();operateLog.setOperateUserId(currentUserId);// 获取请求信息 if(request!null){operateLog.setClientIp(getClientIp(request));operateLog.setRequestUri(request.getRequestURI());}// 记录方法参数根据配置 if(logAnnotation.recordParams()){Object[]argspjp.getArgs();String[]paramNamessignature.getParameterNames();MapString,ObjectparamsnewHashMap();for(inti0;iargs.length;i){// 过滤掉敏感参数 if(!isSensitiveParam(paramNames[i])){params.put(paramNames[i],args[i]);}}operateLog.setMethodParams(params);}Objectresultnull;try{// 执行目标方法 ♂️resultpjp.proceed();// 记录返回值根据配置 ✅if(logAnnotation.recordResult()){operateLog.setReturnValue(result);}// 计算耗时 ⏱️longendTimeSystem.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){Stringiprequest.getHeader(X-Forwarded-For);if(ipnull||ip.length()0||unknown.equalsIgnoreCase(ip)){iprequest.getHeader(Proxy-Client-IP);}if(ipnull||ip.length()0||unknown.equalsIgnoreCase(ip)){iprequest.getHeader(WL-Proxy-Client-IP);}if(ipnull||ip.length()0||unknown.equalsIgnoreCase(ip)){iprequest.getRemoteAddr();}returnip;}/** * 判断是否为敏感参数 */privatebooleanisSensitiveParam(StringparamName){ListStringsensitiveParamsArrays.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 AOPHappy coding! 小提示在实际项目中建议将AOP配置放在独立的配置类中便于管理和维护。记得根据具体业务场景选择合适的通知类型和切入点表达式哦✨