news 2026/7/29 5:20:05

微服务项目OpenFeign下全局异常处理器的正确使用方式,解决feign调用时全局异常处理器失效的问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微服务项目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 ResponseEntity<Void> handleMyWebException(MyWebException e){ log.warn("业务异常:{}", e.getMsg()); return ResponseEntity.error(e.getMsg(),e.getCode()); }

这样return了一个http响应体对象。

但是,return到哪里了呢?你要知道feign调用本质上就是一次http请求,调用driver模块,然后返回数据。

正常返回的是正常数据,有异常被全局捕获然后返回了响应体对象之后,就返回了响应体对象

如果你的feign返回的是数据,如:

@PostMapping("/driver/login") BaseLoginVO<DriverBaseInfoDTO> login(@RequestBody DriverLoginRequestDTO driverLoginRequestDTO);

那将响应体对象ResponseEntity.error(e.getMsg(),e.getCode())转化为BaseLoginVO<DriverBaseInfoDTO>的时候,就会出现异常。导致这个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 ResponseEntity<V> { 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(ObjectProvider<HttpMessageConverters> 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。

ObjectProvider<HttpMessageConverters> messageConverters作为请求参数,ObjectProvider是springboot3新的类,他可以做到启动时不加载泛型中的类,在调用的时候才加载这个泛型的HttpMessageConverters类,messageConverters是HttpMessageConverters的对象而不是ObjectProvider的!总之就是传入HttpMessageConverters,转化器。

然后是代码部分:

1,拿到springboot自带的Decoder,就是用默认的messageConverters定义了一个Decoder
Decoder defaultDecoder = new ResponseEntityDecoder(new SpringDecoder(messageConverters::getIfAvailable));
2,返回一个()->{}lambda表达式,这是什么意思呢?

返回一个可执行的对象,这段代码什么时候传入参数并调用,完全由调用方说了算。

3,lambda参数

传入的参数一个是Response一个是type,response大概就是feign调用返回的响应对象,type就是方法的返回值类型。

4,逻辑解析

先读流,运用Feign包的Util把body里面的数据转化为string,然后判断string是不是ResponseEntity对象,如果是,那就直接抛异常给本模块的异常处理器。

最后,需要把流写回,response写入bodyStr,再用默认的翻译器Decoder解码为type,最后返回结束。

2.3包装问题

实际上,采用自定义Decoder,throw的异常,会被包装上一层DecoderException导致处理器先处理的是RuntimeException而不是我们的MyException,所以要在处理器中加上:

@ExceptionHandler({FeignException.class, DecodeException.class}) public ResponseEntity<Void> 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,尾声

希望这篇文章能帮到你

能帮我点个赞吗,谢谢你

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

STM32与A5000安全芯片的物联网设备开发实践

1. 硬件选型与安全架构设计在物联网设备开发中&#xff0c;选择STM32F100ZE作为主控芯片搭配A5000安全芯片的方案&#xff0c;主要基于以下考量&#xff1a;STM32F100ZE作为Cortex-M3内核微控制器&#xff0c;具有以下关键特性&#xff1a;72MHz主频和16KB SRAM&#xff0c;满足…

作者头像 李华
网站建设 2026/7/29 5:14:41

智能车编码器里程计:从硬件连接到软件实现的精准测距指南

1. 项目概述&#xff1a;为什么智能车需要精确的“里程计”&#xff1f;在智能车竞赛中&#xff0c;无论是摄像头循迹、电磁导航还是其他组别&#xff0c;让车“知道自己走了多远”都是一个基础且核心的需求。你可能遇到过这样的场景&#xff1a;车子在直道上跑得飞快&#xff…

作者头像 李华
网站建设 2026/7/29 5:13:53

DIY桌面级3D激光扫描仪:从硬件搭建到点云生成全流程解析

1. 项目概述&#xff1a;从零打造一台桌面级3D激光扫描仪几年前&#xff0c;当我第一次接触3D打印时&#xff0c;一个念头就挥之不去&#xff1a;如果能把自己手边的实物&#xff0c;比如一个精巧的模型、一件有纪念意义的小物件&#xff0c;轻松地转换成数字模型&#xff0c;那…

作者头像 李华
网站建设 2026/7/29 5:11:55

PTFT图像加密算法原理与Matlab实现详解

1. 项目概述在数字图像安全领域&#xff0c;相位截断傅立叶变换(PTFT)加密算法因其独特的非线性特性正成为研究热点。这个项目实现了基于双随机相位编码和相位截断操作的图像加密方案&#xff0c;通过Matlab完整复现了加密/解密流程。不同于传统傅立叶变换加密&#xff0c;PTFT…

作者头像 李华
网站建设 2026/7/29 5:09:32

Formlabs软件更新深度解析:草稿模式与智能支撑如何提升3D打印效率

1. 项目概述&#xff1a;一次软件更新如何重塑硬件性能最近&#xff0c;Formlabs发布了一次软件更新&#xff0c;在圈内引起了不小的讨论。如果你手头正好有Formlabs的机器&#xff0c;比如Form 3或Form 3L&#xff0c;这次更新绝对值得你立刻去操作一下。官方宣称这次更新能“…

作者头像 李华
网站建设 2026/7/29 5:07:08

Claude Cowork AI协作平台:代码审查与文档生成实战指南

这次我们来看一个能帮你"上班"的AI工具——Claude Cowork。这个由Anthropic开发的AI协作平台最近在技术圈热度很高&#xff0c;核心卖点是能让Claude AI深度集成到你的工作流中&#xff0c;处理日常重复性任务。从实际使用角度看&#xff0c;Claude Cowork最值得关注…

作者头像 李华