
Spring Boot异常处理别被RestControllerAdvice“坑”了事故引入支付异常却显示成功上周四下午我正在工位上悠哉悠哉地喝着咖啡享受着片刻的宁静。突然手机铃声大作打破了这份惬意。电话那头传来财务小姐姐焦急的声音“不好啦有用户反馈支付成功了但钱却没到账已经有好几个用户都这么说了” 我心里 “咯噔” 一下暗叫不好赶紧打开后台日志查看。经过一番排查很快定位到了问题所在。原来是支付回调接口出了岔子代码是这样写的PostMapping(/pay/callback)publicStringhandlePayCallback(RequestBodyPayRequestrequest){payService.process(request);// 这里抛了异常returnsuccess;// 但前端收到这个}从代码里可以看出payService.process(request)这行代码抛出了异常可由于使用了RestControllerAdvice全局捕获异常返回给前端的竟然是 “success”支付平台也以为支付成功了结果用户被扣了钱订单却没生成。这可真是个大乌龙这次事故给公司带来了不小的损失也让我深刻认识到 Spring Boot 异常处理可不能掉以轻心。今天我就把自己总结的 3 个 Spring Boot 异常处理的隐藏陷阱分享给大家希望能帮助大家在开发中少踩坑。陷阱 1RestControllerAdvice 一锅端协议不兼容错误示范在 Spring Boot 开发中很多人习惯使用RestControllerAdvice进行全局异常处理代码类似这样RestControllerAdvicepublicclassGlobalExceptionHandler{ExceptionHandler(Exception.class)publicResult?handleException(Exceptione){log.error(系统异常,e);returnResult.error(系统繁忙);}}看起来很完美所有异常都被统一处理了。但问题在于这种 “一锅端” 的方式没有考虑到不同接口协议的特殊性。比如支付回调接口要求失败时返回特定字符串像支付宝回调失败需返回fail字符串微信支付失败要返回 XML 格式如return_codeFAIL/return_code邮件 Webhook 失败则需返回 HTTP 5xx 状态码 。而上述全局异常处理器统一返回 JSON 格式数据和 HTTP 200 状态码完全不符合第三方平台要求。产生后果在实际业务中这种错误处理方式会引发严重问题。就拿开篇提到的支付回调事故来说由于异常被全局处理器捕获后返回了 HTTP 200 和错误 JSON支付平台误判支付成功用户被扣钱但订单却未生成给公司造成直接经济损失。再比如短信回调接口如果因为异常处理不当返回错误信息可能导致验证码失效影响用户正常注册登录 。Webhook 接口若返回错误格式也会导致数据传输异常影响业务流程的正常运转。解决方案为了避免这种情况我们需要采用按路径 / 协议分治的生产级方案。针对不同类型接口创建不同的异常处理器// 1. 普通API异常处理器返回JSONRestControllerAdvice(basePackagescom.example.api)publicclassApiExceptionHandler{ExceptionHandler(ServiceException.class)publicResult?handleServiceException(ServiceExceptione){returnResult.error(e.getCode(),e.getMessage());}}// 2. 支付回调专用处理器返回字符串RestControllerAdvice(assignableTypesPayCallbackController.class)publicclassPayCallbackExceptionHandler{ExceptionHandler(Exception.class)publicResponseEntityStringhandlePayException(Exceptione){log.error(支付回调异常,e);// 支付宝/微信要求失败返回failreturnResponseEntity.status(200).body(fail);}}// 3. Webhook专用处理器返回5xxRestControllerAdvice(assignableTypesWebhookController.class)publicclassWebhookExceptionHandler{ExceptionHandler(Exception.class)publicResponseEntityVoidhandleWebhookException(){// 明确返回500触发第三方重试returnResponseEntity.status(500).build();}}这里用到两个关键注解basePackages按包路径隔离适用于对某一路径下的接口进行统一异常处理assignableTypes按 Controller 类隔离更加精准地针对特定 Controller 处理异常 。验证时可以用 Postman 调支付回调接口故意传错参数检查是否返回fail而非 JSON以此确保异常处理器按预期工作。陷阱 2吞掉异常不记录排查困难错误示范再来看第二个陷阱捕获异常后不记录日志。很多人在处理异常时只想着返回给前端一个友好的错误提示却忽略了记录日志的重要性。比如这样的代码ExceptionHandler(BusinessException.class)publicResult?handleBusiness(BusinessExceptione){// 只返回错误不记日志returnResult.error(e.getMessage());}这段代码捕获了业务异常BusinessException然后直接返回错误信息给前端没有记录任何日志。看似简单方便实则隐藏着巨大的隐患。产生后果在实际业务中这种做法会让排查问题变得异常困难。想象一下用户反馈 “下单失败”但运维去查看日志时却发现没有任何相关记录CPU、内存、DB 全正常。最后只能靠用户截图才发现是库存不足异常。这时候老板就会灵魂拷问“为什么没告警为什么没日志” 你只能哑口无言因为异常被 “静默” 处理了。没有日志就像在黑暗中摸索根本不知道问题出在哪里更别说快速解决问题了。解决方案为了解决这个问题我们需要采用分级日志 关键字段脱敏的生产级方案。具体代码如下ExceptionHandler(BusinessException.class)publicResult?handleBusiness(BusinessExceptione,HttpServletRequestrequest){//1. 记录WARN日志业务异常需关注log.warn(业务异常 | URI:{} | User:{} | Error:{},request.getRequestURI(),getCurrentUserId(),// 从Token解析e.getMessage());returnResult.error(e.getCode(),e.getMessage());}ExceptionHandler(Exception.class)publicResult?handleSystem(Exceptione,HttpServletRequestrequest){//2. 记录ERROR日志系统异常需告警StringtraceIdMDC.get(TRACE_ID);// 链路IDlog.error(系统异常 | TraceId:{} | URI:{} | Params:{},traceId,request.getRequestURI(),maskSensitiveParams(request.getParameterMap())// 脱敏,e);//3. 触发告警可选alertService.send(系统异常,e.getMessage());returnResult.error(系统繁忙请稍后重试);}// 敏感参数脱敏工具privateStringmaskSensitiveParams(MapString,String[]params){MapString,StringsafeParamsnewHashMap();for(Stringkey:params.keySet()){if(key.contains(password)||key.contains(card)){safeParams.put(key,******);}else{safeParams.put(key,String.join(,,params.get(key)));}}returnJsonUtil.toJson(safeParams);}这里对业务异常和系统异常进行了分级处理业务异常记录 WARN 日志因为业务异常通常是可预期的如库存不足、参数错误等只需要关注即可系统异常记录 ERROR 日志并触发告警因为系统异常往往是严重的问题如数据库连接失败、空指针异常等需要及时处理 。同时记录了关键信息如用户 ID从 Token 解析获取用于快速定位问题用户、链路 ID通过 MDC 获取用于全链路追踪并对敏感参数进行了脱敏处理防止密码、银行卡号等敏感信息泄露 。在实际应用中一定要注意日志的配置确保日志能够正确记录并保存同时要定期清理日志防止日志文件过大影响系统性能。陷阱 3返回码混乱前端痛苦错误示范在处理异常返回码时很多开发者没有统一的规范导致返回码混乱不堪。比如下面这段代码ExceptionHandler(NotFoundException.class)publicResult?handleNotFound(){returnResult.error(404,资源未找到);}ExceptionHandler(BusinessException.class)publicResult?handleBusiness(BusinessExceptione){// 这里业务异常也返回404returnResult.error(404,e.getMessage());}ExceptionHandler(AnotherException.class)publicResult?handleAnother(){// 又返回个无规则的1001returnResult.error(1001,其他错误);}在这个例子中NotFoundException和BusinessException都返回了 404 错误码这显然是不合理的。而且AnotherException返回的 1001 错误码毫无规律让人摸不着头脑。产生后果返回码混乱会给前端开发带来极大的困扰。前端需要根据返回码来进行不同的处理比如显示不同的错误提示、进行页面跳转等 。但如果返回码没有规律前端就难以统一处理错误增加了开发和维护的成本。更严重的是这会影响用户体验导致用户在使用产品时遇到问题却得不到准确的提示降低用户对产品的信任度。解决方案为了解决这个问题我们需要制定统一的错误码规范。可以参考 HTTP 状态码的设计思路对错误码进行分段比如1XX信息一般用于临时响应在 Web 开发中较少使用自定义此范围错误码。2XX成功200 表示成功这是大家最熟悉的。3XX重定向如 301 永久重定向、302 临时重定向 在微服务内部调用中不常用来自定义错误码。4XX客户端错误400 表示参数错误401 表示未认证403 表示禁止访问404 表示资源未找到 。5XX服务器错误500 表示服务器内部错误502 表示网关错误503 表示服务不可用 。同时针对业务异常可以在 HTTP 状态码的基础上进行扩展比如 10001 表示用户模块的用户不存在20001 表示订单模块的订单不存在等 。后端在返回错误时严格按照这个规范来返回前端就可以根据返回码进行针对性的处理。例如// 统一错误码定义publicinterfaceErrorCode{intSUCCESS200;intBAD_REQUEST400;intUNAUTHORIZED401;intFORBIDDEN403;intNOT_FOUND404;intINTERNAL_SERVER_ERROR500;// 业务异常扩展intUSER_NOT_FOUND10001;intORDER_NOT_FOUND20001;}ExceptionHandler(NotFoundException.class)publicResult?handleNotFound(){returnResult.error(ErrorCode.NOT_FOUND,资源未找到);}ExceptionHandler(BusinessException.class)publicResult?handleBusiness(BusinessExceptione){// 根据业务异常类型返回对应的错误码if(einstanceofUserNotFoundException){returnResult.error(ErrorCode.USER_NOT_FOUND,用户不存在);}elseif(einstanceofOrderNotFoundException){returnResult.error(ErrorCode.ORDER_NOT_FOUND,订单不存在);}returnResult.error(ErrorCode.INTERNAL_SERVER_ERROR,e.getMessage());}这样前端只需要根据返回的错误码就可以知道是哪类错误从而进行相应的处理大大提高了开发效率和用户体验。总结在 Spring Boot 开发中异常处理是保障系统稳定运行的关键环节。本文通过支付回调异常的实际案例深入剖析了使用RestControllerAdvice进行全局异常处理时可能遇到的 3 个隐藏陷阱协议不兼容、吞掉异常不记录、返回码混乱 。这些陷阱看似微小却可能引发严重的生产事故给公司带来经济损失影响用户体验 。针对这些陷阱我们提出了相应的生产级解决方案按路径 / 协议分治处理异常确保不同接口协议得到正确处理采用分级日志 关键字段脱敏准确记录异常信息便于排查问题制定统一的错误码规范避免返回码混乱降低前端开发难度 。希望大家在开发中能够重视异常处理避免陷入这些陷阱写出更加健壮、稳定的代码。如果在实际开发中遇到了其他关于异常处理的问题欢迎在评论区留言分享让我们一起学习进步