尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
微服务项目OpenFeign下全局异常处理器的正确使用方式,解决feign调用时全局异常处理器失效的问题
本方案适用于 Spring Cloud OpenFeign 统一响应体包装的微服务架构已在 Spring Boot 3.x JDK 17/21 环境下验证通过。默认你知晓单体架构下全局异常处理器的使用方式0前置需要首先得是个微服务项目要引入feign依赖1问题导入在拆单体架构的项目到微服务架构时自然会引入OpenFeign进行微服务互发请求而全局异常拦截器自然要移到common模块这样每个模块都有了全局异常处理器。但是。我们模拟一种情况前端打到网关转发到了一个微服务然后这个微服务进行了feign调用driverFeign.login(driverLoginRequestDTO);然后这个driverFeign对应的服务即driver模块里面出现了异常if(ObjectUtil.notEqual(code,s)){ throw new MyException(DriverManagerExceptionEnum.CODE_ERROR); }driver模块里面的全局异常处理器捕获到了异常ExceptionHandler(MyException.class) public ResponseEntityVoid handleMyWebException(MyWebException e){ log.warn(业务异常{}, e.getMsg()); return ResponseEntity.error(e.getMsg(),e.getCode()); }这样return了一个http响应体对象。但是return到哪里了呢你要知道feign调用本质上就是一次http请求调用driver模块然后返回数据。正常返回的是正常数据有异常被全局捕获然后返回了响应体对象之后就返回了响应体对象。如果你的feign返回的是数据如PostMapping(/driver/login) BaseLoginVODriverBaseInfoDTO login(RequestBody DriverLoginRequestDTO driverLoginRequestDTO);那将响应体对象ResponseEntity.error(e.getMsg(),e.getCode())转化为BaseLoginVODriverBaseInfoDTO的时候就会出现异常。导致这个BaseLoginVO为null或者不完整。问题的关键就是feign调用的时候没有处理异常响应体数据。2解决思路2.1❌️行不通的ResponseInterceptor方案我为了解决这个问题首先想到的就是定义一个Feign配置的ResponseInterceptor因为我的项目本来就配置了Bean public RequestInterceptor requestInterceptor(){ return new RequestInterceptor() { Override public void apply(RequestTemplate requestTemplate) { BaseInfoDTO baseInfoDTO holderUtil.get(); if(ObjectUtil.isEmpty(baseInfoDTO)){ return; } //feign传入json会出问题转为base64编码 String jsonStr JSONUtil.toJsonStr(baseInfoDTO); String decode Base64.encode(jsonStr.getBytes(StandardCharsets.UTF_8)); requestTemplate.header(base-info, decode); } }; }这个ResponseInterceptor响应拦截器实现的思路就是获取到invocationContext调用上下文的response就是执行结果判断是不是响应实体ResponseEntity如果是那就直接抛异常这样就可以解决feign调用下异常响应实体类丢失的问题。这边详细展开抛异常就可以解决这块我们知道web-driver模块调用driver模块之后driver模块抛了个异常响应了响应实体类然后feign拦截响应判断如果是普通数据(driver模块没有抛异常)feign拦截器判断无误之后就正常返回但是一旦抛了异常返回了响应体实体如public class ResponseEntityV { private int code; private String msg; private V data; ... }那就手动抛一个错误然后又会被web-driver模块的全局异常处理器捕获返回ResponseEntity.error()然后就可以返回前端写出了如下的代码Bean public ResponseInterceptor responseInterceptor(){ return new ResponseInterceptor() { Override public Object intercept(InvocationContext invocationContext, Chain chain) throws Exception { //feign返回的可能是普通对象和数据类型也可能是responseentity(如果经过了全局异常处理器) Response response invocationContext.response(); //获取请求体 Response.Body body response.body(); if(ObjectUtil.isEmpty(body)){ //如果没有返回值就继续 return proceed(); } String jsonStr JSONUtil.toJsonStr(Util.toString(body.asReader(StandardCharsets.UTF_8))); if (!JSONUtil.isTypeJSON(JSONUtil.toJsonStr(jsonStr))) { //返回如null、123、123 return proceed(); } JSONObject bodyJson JSONUtil.parseObj(jsonStr); //检查对象有无code和msg确认是不是entity String code bodyJson.getStr(code); if(ObjectUtil.isEmpty(code)){ return proceed(); } String msg bodyJson.getStr(msg); if(ObjectUtil.isEmpty(msg)){ return proceed(); } //大概率就是entity了 throw new MyWebException(Convert.toInt(code),msg); } }; }这边proceed进行的操作就是把流数据写回。但是基于FeignResponseInterceptor的这个方案有一个致命问题你要知道流数据只能读取一次如果没有写回再读取就会报stream is close错误而invocationContext并不能写回流所以这个方案只好out。2.2Decoder解码方案decoder是负责feign调用返回数据的解码的在解码数据的时候判断一下是否是ResponseEntity.error()如果是抛异常给本模块的异常处理器处理正常就正常处理先给出代码Bean public Decoder feignDecoder(ObjectProviderHttpMessageConverters messageConverters) { // 1. 拿到 Spring Boot 3 默认的“翻译官” (Spring 会自动帮你处理泛型和日期) Decoder defaultDecoder new ResponseEntityDecoder(new SpringDecoder(messageConverters::getIfAvailable)); ​ // 2. 返回我们的“拦截器” return (Response response, Type type) - { if (response.body() null) return defaultDecoder.decode(response, type); ​ // 3. 读流 - 校验 code,msg String bodyStr Util.toString(response.body().asReader(StandardCharsets.UTF_8)); ​ if (ObjectUtil.isNotEmpty(bodyStr) JSONUtil.isTypeJSON(bodyStr)) { JSONObject json JSONUtil.parseObj(bodyStr); String code json.getStr(code); String msg json.getStr(msg); ​ if (ObjectUtil.isNotEmpty(code)ObjectUtil.isNotEmpty(msg)) { // 4. 如果code不为空且msg也不为空肯定是响应实体了直接抛你的自定义异常 throw new MyWebException(Integer.parseInt(code), json.getStr(msg)); } } ​ // 5. 如果把流塞回去交给 Spring 默认的 Decoder 变成 Java 对象 return defaultDecoder.decode(response.toBuilder().body(bodyStr, StandardCharsets.UTF_8).build(), type); }; }然后解析一下。Spring需要一个我们自己书写的Decoder的Bean所以方法返回Decoder。ObjectProviderHttpMessageConverters messageConverters作为请求参数ObjectProvider是springboot3新的类他可以做到启动时不加载泛型中的类在调用的时候才加载这个泛型的HttpMessageConverters类messageConverters是HttpMessageConverters的对象而不是ObjectProvider的总之就是传入HttpMessageConverters转化器。然后是代码部分1拿到springboot自带的Decoder就是用默认的messageConverters定义了一个DecoderDecoder defaultDecoder new ResponseEntityDecoder(new SpringDecoder(messageConverters::getIfAvailable));2返回一个()-{}lambda表达式这是什么意思呢返回一个可执行的对象这段代码什么时候传入参数并调用完全由调用方说了算。3lambda参数传入的参数一个是Response一个是typeresponse大概就是feign调用返回的响应对象type就是方法的返回值类型。4逻辑解析先读流运用Feign包的Util把body里面的数据转化为string然后判断string是不是ResponseEntity对象如果是那就直接抛异常给本模块的异常处理器。最后需要把流写回response写入bodyStr再用默认的翻译器Decoder解码为type最后返回结束。2.3包装问题实际上采用自定义Decoderthrow的异常会被包装上一层DecoderException导致处理器先处理的是RuntimeException而不是我们的MyException所以要在处理器中加上ExceptionHandler({FeignException.class, DecodeException.class}) public ResponseEntityVoid handleDecodeException(Exception e){ Throwable cause e.getCause(); if(cause instanceof MyWebException myWebException){ log.warn(业务异常{}, myWebException.getMsg()); return ResponseEntity.error(myWebException.getMsg(),myWebException.getCode()); } if(cause instanceof MyException myException){ log.warn(业务异常{}, myException.getMsg()); return ResponseEntity.error(myException.getMsg(),myException.getCode()); } throw new RuntimeException(系统错误); }2.4总结实际上都是在feign调用之后解析/解码的时候做文章自定义加一个判断是否是异常的逻辑然后按照原样解码返回。3尾声希望这篇文章能帮到你能帮我点个赞吗谢谢你
RELATED

相关推荐

RUFI洗衣凝珠适合谁

RUFI洗衣凝珠适合谁

RUFI万花筒香氛洗衣凝珠更适合机洗多、想省步骤、洗衣房收纳空间有限的人。它的价值不在于“比洗衣液更高级”,而是在固定用量、拿取方便和多效洗护上更省心。适合怕麻烦的机洗场景凝珠的优势是不用每次估量倒多少,干手拿取也方便。RUFI万花筒香氛洗衣凝…

📅 2026/9/16 14:34:42
【C++重载操作符与转换】重载操作符的定义

【C++重载操作符与转换】重载操作符的定义

在C中,操作符重载(Operator Overloading)是一项强大的特性,它允许开发者为自定义类型(如类或结构体)重新定义内置操作符的行为。通过重载操作符,可以使自定义类型的对象表现得像内置类型一样自然…

📅 2026/8/22 16:59:01
STM32硬件I2C驱动OLED屏幕:从SSD1306原理到图形界面实战

STM32硬件I2C驱动OLED屏幕:从SSD1306原理到图形界面实战

1. 项目概述:为什么选择STM32驱动OLED?玩过单片机的朋友都知道,显示是人机交互最直观的窗口。从早期的数码管、LCD1602,到后来的TFT彩屏,显示技术在不断演进。而OLED,尤其是那种小巧的0.96寸或1.3寸模块&am…

📅 2026/8/22 16:59:01
MORE NEWS

更多资讯

📰

init INIT_PGD_PAGE_COUNT

INIT_PGD_PAGE_COUNT 是 Linux 内核 x86 架构启动过程中定义的一个宏,它规定了早期页表缓冲区(pgt_buf)可以容纳的页表页数量。这个缓冲区在系统启动早期、正式的内存分配器(如伙伴系统)尚未就绪时,为建立初…

📰

AI视频超分实战指南:免费本地化4K无损升级方案

1. 这不是“魔法”,而是可控的视频超分工程:为什么说它真正改变了内容复用逻辑你手头有一段2018年用手机拍的家庭聚会视频,分辨率只有720p,画面发灰、边缘模糊、字幕锯齿明显;或者你刚从老硬盘里翻出一段十年前的旅行V…

📰

SadTalker实战指南:用一张照片+语音生成唇形同步视频

1. 这不是魔法,是CVPR2023落地的“嘴型驱动”技术:一张照片真能开口说话?你有没有试过把家里老照片里爷爷奶奶的静态肖像,配上一段语音,让他们的嘴唇自然动起来,仿佛真的在对你说话?这不是电影特…

📰

GSQL 6.5.2.1:Windows域环境下SQL Server批量运维脚本框架

简介:GSQL 6.5.2.1 是一款专为SQL Server 2000环境定制的轻量级数据库管理工具精简版,面向个人开发者、测试工程师及数据库初学者,解决在低资源环境下快速部署、附加MDF/LDF数据库文件、执行T-SQL脚本及开展本地开发验证等核心需求。压缩包共…

📰

校园外卖小程序数据库设计:从表结构到Flask后端完整实战

简介:这是一份基于JavaScript与Python的微信小程序校园外卖系统,面向数据库课程设计场景,适合计算机相关专业学生作源码参考。系统完整覆盖学生、商家与配送员三类角色:学生可浏览商品、下单并查看订单状态(制作中、派…

📰

生产级记忆型Agent实战:AgentScope架构拆解与落地经验

做Agent这件事,真正难的不是“能跑起来”,而是“能不能一直稳定地跑在生产环境里”。AgentScope这个项目我关注了挺久,它最打动我的不是又多了一个AI Agent框架,而是它把“记忆型Agent”从demo级别拉到了生产级:会话记…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬