本方案适用于 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尾声希望这篇文章能帮到你能帮我点个赞吗谢谢你
微服务项目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尾声希望这篇文章能帮到你能帮我点个赞吗谢谢你