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