Openfeign 错误处理
前言
Openfeign 让我们可以通过声明式的方式调用远程接口,这种方式让我们调用远程接口变得非常简单,但是如果远程接口发生错误了,或者远程接口返回的数据解码失败了,openfeign 是如何处理的?我们该如何应对呢?本文将会讲述 openfeign 的错误处理。
Openfeign 默认错误处理
在开始之前,假设我们需要通过 openfeign 调用订单服务的订单详情接口,接口定义如下:
@FeignClient(contextId = "remoteOrderService", value = "order-service")
public interface RemoteOrderService {
@GetMapping("order")
public R<Order> queryOrder(@RequestParam("orderId") String orderId);
}
注意 @FeignClient
注解没有 fallbackFactory 和 fallback 属性。因为我们这里讨论的是 openfeign 默认异常处理方式。现在我们调用 queryOrder 的时候可能会发生以下三类错误:
- remoteOrderService 服务故障,例如宕机;
- remoteOrderService 服务正常,但是 queryOrder 抛出了异常;
- remoteOrderService 服务正常,queryOrder 也正常返回了,但是返回的响应在调用端解码失败。
注:调用端解码失败是有可能会发生的,举个例子,调用端希望的数据格式是 json,但是服务端返回的却是 xml,这显然会导致解码失败,当然这个发生的可能性比较小。即便服务端返回的数据格式和调用端期望的数据格式相同,服务端返回的数据结构可能也会和客户端期望的不同,例如服务端返回的是数组但是客户端期望的却是单个元素。
假设调用方的代码如下,我们分别讨论发生上述三类错误时,客户端收到的响应,以及为什么会收到这样的响应。
@GetMapping("/public/queryOrder")
public R<Order> queryOrder() {
return remoteFileService.queryOrder("123456");
}
remoteOrderService 服务故障
当 remoteOrderService 发生故障,例如宕机时,打印异常堆栈如下:
feign.FeignException$ServiceUnavailable: [503] during [GET] to [http://order-service/queryOrder?orderId=123456] [RemoteOrderService#queryOrder(String)]: [Load balancer does not contain an instance for the service order-service]
at feign.FeignException.serverErrorStatus(FeignException.java:256)
at feign.FeignException.errorStatus(FeignException.java:197)
at feign.FeignException.errorStatus(FeignException.java:185)
at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:92)
at feign.AsyncResponseHandler.handleResponse(AsyncResponseHandler.java:96)
...
收到的响应为:
{
"timestamp": "2023-01-15T13:30:12.546+08:00",
"status": 500,
"error": "Internal Server Error",
"path": "/public/queryOrder"
}
从异常我们可以看出,是由于 remoteOrderService 服务不用导致的,且异常的类型为 FeignException。但如果仅从收到的响应却看不出是异常的真正原因,关于这个响应是怎么来的,可以参考Spring Boot 404 没那么简单一文中的“未捕获异常处理”部分。
queryOrder 抛出了异常
当 remoteOrderService 服务正常,但是 queryOrder 抛出了异常,此时调用端的异常堆栈如下:
feign.FeignException$InternalServerError: [500] during [GET] to [http://order-service/queryOrder?orderId=123456] [RemoteOrderService#queryOrder(String)]: [{"timestamp":"2023-01-15T06:50:41.033+00:00","status":500,"error":"Internal Server Error","path":"/queryOrder"}]
at feign.FeignException.serverErrorStatus(FeignException.java:250)
at feign.FeignException.errorStatus(FeignException.java:197)
at feign.FeignException.errorStatus(FeignException.java:185)
at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:92)
...
异常大类型和前一个相同,都是 FeignException,‘[500] during [GET] ’ 则告诉我们异常是由于服务端接口异常。收到的响应和前面的完全相同,就不贴了。
解码失败
当 remoteOrderService 服务正常,queryOrder 也正常返回了,但是响应为 <>
,此时调用端的异常堆栈如下:
feign.codec.DecodeException: Error while extracting response ...
at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 31] (through reference chain: com.ruoyi.common.core.domain.R["data"])
at feign.AsyncResponseHandler.decode(AsyncResponseHandler.java:119)
at feign.AsyncResponseHandler.handleResponse(AsyncResponseHandler.java:87)
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:138)
异常类型为 DecodeException,DecodeException 也是继承自 FeignException,但是 DecodeException 更好的表达了是由于解码失败导致的异常。收到的响应仍然和第一种情况相同。
Openfeign 默认错误处理小结
通过 Openfeign 调用接口如果发生异常,异常的大类型均为 FeignException,但是具体的异常类型根据实际情况而定,例如解码异常时异常类型为 DecodeException。
Openfeign 响应处理流程
Openfeign 响应处理位于 AsyncResponseHandler#handleResponse 方法中,方法的代码有点长,在不考虑 returnType(returnType 为 feign 接口的返回值类型,对应我们这里类型为 R<Order>
。) 为 Response 以及 IOException 的情况下,可将代码精简如下:
void handleResponse(CompletableFuture<Object> resultFuture,
String configKey,
Response response,
Type returnType,
long elapsedTime) {
if (response.status() >= 200 && response.status() < 300) {
if (isVoidType(returnType)) {
resultFuture.complete(null);
} else {
// 如果响应状态码在[200,300)之间,调用 decoder 解码响应
final Object result = decode(response, returnType);
}
} else if (decode404 && response.status() == 404 && !isVoidType(returnType)) {
// 如果设置了解码404响应,且当前响应状态码为404,则将404当成正常响应处理,调用 decoder 解码响应
final Object result = decode(response, returnType);
resultFuture.complete(result);
} else {
// 其他响应状态码,都调用 errorDecoder 解码响应
resultFuture.completeExceptionally(errorDecoder.decode(configKey, response));
}
}
注:所谓的解码就是将响应的内容转换为 returnType 类型的一个对象。
从上面的代码可以看出,可见整个大的逻辑是根据服务端响应的状态码决定处理流程。 正常状态码调用 decoder 解码响应,异常状态码调用 errorDecoder 解码响应。异常状态码默认的 decoder 为 ErrorDecoder.Default
,解码方法如下:
@Override
public Exception decode(String methodKey, Response response) {
FeignException exception = errorStatus(methodKey, response);
Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER));
if (retryAfter != null) {
return new RetryableException(
response.status(),
exception.getMessage(),
response.request().httpMethod(),
exception,
retryAfter,
response.request());
}
return exception;
}
可见默认的解码器就是返回一个 FeignException,RetryableException 也是 FeignException 的子类。exception 中记录了响应的状态吗,响应的 body 等信息。
decoder 和 errorDecoder
上一节中提到了 decoder 和 errorDecoder,他们是哪里蹦出来的,默认的 decoder 是在 FeignClientsConfiguration 中注册的:
@Configuration(proxyBeanMethods = false)
public class FeignClientsConfiguration {
@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder(ObjectProvider<HttpMessageConverterCustomizer> customizers) {
return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(messageConverters, customizers)));
}
}
默认的 errorDecoder 则是硬编码在 Feign.Builder 中的:
public abstract class Feign {
public static class Builder {
private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
}
}
不论是 decoder 还是 errorDecoder 都可以定制,但是要彻底搞懂定制方式却没有那么简单,参考Feign Client 配置优先级了解更多内容。
如何处理错误
我们知道不论是 decoder 还是 errorDecoder,在发生异常的情况下都会抛出 FeignException,因此我们可以使用全局异常处理来处理 FeignException。
@RestControllerAdvice
public class GlobalExceptionHandler
@ExceptionHandler(FeignException.class)
public Result handleFeignException(FeignException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生未知异常.", requestURI, e);
return Result.error(e.getMessage());
}
}
当然这只是提供一种异常处理的思路,其实 openfeign 常常和 hystrix 和 sentinel 集成使用已达到容错的目的,在这种情况下,我们可以借助于 fallback 或者 fallbackFactory 属性来处理异常。
注:当 openfeign 集成了 sentinel 之后,如果发生了限流或熔断降级且我们没有注册 fallback 和 fallbackFactory 时,抛出的异常类型不再是 FeignException,而是 sentinel 的 FlowException/DegradeException。这是由于发生熔断或者限流时,请求被拦截,没有请求自然也没有响应的处理过程。
@FeignClient(contextId = "remoteOrderService", value = "order-service", fallbackFactory = RemoteOrderFallbackFactory.class)
public interface RemoteOrderService {
@GetMapping("order")
public R<Order> queryOrder(@RequestParam("orderId") String orderId);
}
IOException 异常处理
上面分析了响应处理过程中的异常处理,但是还有一种情况没有说明,如果压根没有响应,或者响应没能在我们设置的超时时间内返回,feign 又是如何处理的呢?处理代码如下:
SynchronousMethodHandler#executeAndDecode
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
Request request = targetRequest(template);
Response response;
long start = System.nanoTime();
try {
response = client.execute(request, options);
// ensure the request is set. TODO: remove in Feign 12
response = response.toBuilder()
.request(request)
.requestTemplate(template)
.build();
} catch (IOException e) {
// 如果超时或者其他原因发生 io 异常,则抛出异常,不会走到下面的 handleResponse
// 异常类型为 RetryableException 也是 FeignException 的子类
throw errorExecuting(request, e);
}
// ...
asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response,
metadata.returnType(),
elapsedTime);
// ...
}
温馨提示:反馈需要登录