使用 feign ErrorDecoder 统一多个第三方接口的错误响应
使用 feign ErrorDecoder 统一多个第三方接口的错误响应
ErrorDecoder
HTTP 响应状态码用来表明特定 HTTP 请求是否成功完成。 响应被归为以下五大类:
feign 的 ErrorDecoder 用于处理 Response#status() 大于等于 300 的响应(经测试不会处理 1xx),将其转化为相应的异常,应用可根据自己的需要转化成应用特定的异常,比如业务异常。
public interface ErrorDecoder {
// 返回值必须是 Exception
public Exception decode(String methodKey, Response response);
}
404 (Not Found) 的特殊处理
但 404 (Not Found) 是个例外,因为 404 可以表示某些资源不存在,比如订单不存在、用户不存在。若将 dismiss404 (老版本为 decode404)设为 false 才会被 ErrorDecoder 处理,默认值就是 false。设为 true 则不经过 ErrorDecoder 直接返回对应类型的空对象,依赖于返回值的类型,由 feign.Util#emptyValueOf 实现:
public static Object emptyValueOf(Type type) {
return EMPTIES.getOrDefault(Types.getRawType(type), () -> null).get();
}
private static final Map<Class<?>, Supplier<Object>> EMPTIES;
static {
final Map<Class<?>, Supplier<Object>> empties = new LinkedHashMap<>();
empties.put(boolean.class, () -> false);
empties.put(Boolean.class, () -> false);
empties.put(byte[].class, () -> new byte[0]);
empties.put(Collection.class, Collections::emptyList);
empties.put(Iterator.class, Collections::emptyIterator);
empties.put(List.class, Collections::emptyList);
empties.put(Map.class, Collections::emptyMap);
empties.put(Set.class, Collections::emptySet);
empties.put(Optional.class, Optional::empty);
empties.put(Stream.class, Stream::empty);
EMPTIES = Collections.unmodifiableMap(empties);
}
默认的 ErrorDecoder 实现
默认实现是根据 http status 创建不同的 FeignException 的子类。同时处理重试。
public class Default implements ErrorDecoder {
private final RetryAfterDecoder retryAfterDecoder = new RetryAfterDecoder();
private Integer maxBodyBytesLength;
private Integer maxBodyCharsLength;
public Default() {
this.maxBodyBytesLength = null;
this.maxBodyCharsLength = null;
}
public Default(Integer maxBodyBytesLength, Integer maxBodyCharsLength) {
this.maxBodyBytesLength = maxBodyBytesLength;
this.maxBodyCharsLength = maxBodyCharsLength;
}
@Override
public Exception decode(String methodKey, Response response) {
// 根据 http status 创建不同的 FeignException 的子类
FeignException exception =
error(methodKey, response, maxBodyBytesLength, maxBodyCharsLength);
// 重试处理,从 Retry-After header 中获取下一次执行的毫秒时间戳
Long retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER));
if (retryAfter != null) {
// 抛出 RetryableException 异常来准备下一次重试
return new RetryableException(
response.status(),
exception.getMessage(),
response.request().httpMethod(),
exception,
retryAfter,
response.request());
}
return exception;
}
private <T> T firstOrNull(Map<String, Collection<T>> map, String key) {
if (map.containsKey(key) && !map.get(key).isEmpty()) {
return map.get(key).iterator().next();
}
return null;
}
}
feign/FeignException.java
分别处理了 4xx、5xx 和其他 status:
private static FeignException errorStatus(
int status,
String message,
Request request,
byte[] body,
Map<String, Collection<String>> headers) {
// 4xx,返回 FeignClientException 的子类
if (isClientError(status)) {
return clientErrorStatus(status, message, request, body, headers);
}
// 5xx,返回 FeignServerException 的子类
if (isServerError(status)) {
return serverErrorStatus(status, message, request, body, headers);
}
// 其他,返回 FeignException 异常
return new FeignException(status, message, request, body, headers);
}
// 4xx
private static boolean isClientError(int status) {
return status >= 400 && status < 500;
}
private static FeignClientException clientErrorStatus(
int status,
String message,
Request request,
byte[] body,
Map<String, Collection<String>> headers) {
switch (status) {
case 400:
return new BadRequest(message, request, body, headers);
case 401:
return new Unauthorized(message, request, body, headers);
case 403:
return new Forbidden(message, request, body, headers);
case 404:
return new NotFound(message, request, body, headers);
case 405:
return new MethodNotAllowed(message, request, body, headers);
case 406:
return new NotAcceptable(message, request, body, headers);
case 409:
return new Conflict(message, request, body, headers);
case 410:
return new Gone(message, request, body, headers);
case 415:
return new UnsupportedMediaType(message, request, body, headers);
case 429:
return new TooManyRequests(message, request, body, headers);
case 422:
return new UnprocessableEntity(message, request, body, headers);
default:
return new FeignClientException(status, message, request, body, headers);
}
}
// 5xx
private static boolean isServerError(int status) {
return status >= 500 && status <= 599;
}
private static FeignServerException serverErrorStatus(
int status,
String message,
Request request,
byte[] body,
Map<String, Collection<String>> headers) {
switch (status) {
case 500:
return new InternalServerError(message, request, body, headers);
case 501:
return new NotImplemented(message, request, body, headers);
case 502:
return new BadGateway(message, request, body, headers);
case 503:
return new ServiceUnavailable(message, request, body, headers);
case 504:
return new GatewayTimeout(message, request, body, headers);
default:
return new FeignServerException(status, message, request, body, headers);
}
}
使用 ErrorDecoder 统一多个第三方接口的错误响应
有一个 app A,需要调用两个第三方的接口 app B、app C,三个服务的统一返回值各不相同,如下:
@Data
@Builder
public class ResultA<T> {
private Integer code;
private String message;
private T data;
}
@Data
public class ResultB<T> {
private final Integer status;
private final String message;
private final T data;
}
@Data
public class ResultC<T> {
private final Integer code;
private final String msg;
private final T data;
}
在 app A 中通过 feign 调用 app B、app C:
AController.java
@RequiredArgsConstructor
@RestController
@RequestMapping("/a")
public class AController {
private final BFeignClient bFeignClient;
private final CFeignClient cFeignClient;
@RequestMapping("/test-b")
public String testB() {
return bFeignClient.test();
}
@RequestMapping("/test-c")
public String testC() {
return cFeignClient.test();
}
}
BFeignClient.java
@FeignClient(name = "app-b", url = "http://localhost:8080/b")
public interface BFeignClient {
@GetMapping
String test();
}
CFeignClient.java
@FeignClient(name = "app-c", url = "http://localhost:8080/c")
public interface CFeignClient {
@GetMapping
String test();
}
app B 时报错如下:
feign.FeignException$BadRequest: [400] during [GET] to [http://localhost:8080/b] [BFeignClient#test()]: [{"status":-1,"message":"参数错误","data":null}]
at feign.FeignException.clientErrorStatus(FeignException.java:243) ~[feign-core-13.6.jar:na]
at feign.FeignException.errorStatus(FeignException.java:223) ~[feign-core-13.6.jar:na]
at feign.FeignException.errorStatus(FeignException.java:213) ~[feign-core-13.6.jar:na]
at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:103) ~[feign-core-13.6.jar:na]
at feign.InvocationContext.decodeError(InvocationContext.java:133) ~[feign-core-13.6.jar:na]
at feign.InvocationContext.proceed(InvocationContext.java:80) ~[feign-core-13.6.jar:na]
at feign.ResponseHandler.handleResponse(ResponseHandler.java:69) ~[feign-core-13.6.jar:na]
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:109) ~[feign-core-13.6.jar:na]
...
app C 报错如下:
feign.FeignException$Forbidden: [403] during [GET] to [http://localhost:8080/c] [CFeignClient#test()]: [{"code":-1,"msg":"无权操作","data":null}]
at feign.FeignException.clientErrorStatus(FeignException.java:247) ~[feign-core-13.6.jar:na]
at feign.FeignException.errorStatus(FeignException.java:223) ~[feign-core-13.6.jar:na]
at feign.FeignException.errorStatus(FeignException.java:213) ~[feign-core-13.6.jar:na]
at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:103) ~[feign-core-13.6.jar:na]
at feign.InvocationContext.decodeError(InvocationContext.java:133) ~[feign-core-13.6.jar:na]
at feign.InvocationContext.proceed(InvocationContext.java:80) ~[feign-core-13.6.jar:na]
at feign.ResponseHandler.handleResponse(ResponseHandler.java:69) ~[feign-core-13.6.jar:na]
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:109) ~[feign-core-13.6.jar:na]
...
需求:将第三方的 4xx 报错信息透传到前端
两个三方服务的返回结构是不一样的,最简单粗暴的方式就是在 AController 中使用 try caught 然后解析 response body,然后返回 new ResultA(...)。
{"status":-1,"message":"参数错误","data":null}
{"code":-1,"msg":"服务不可用","data":null}
如果有很多第三方接口要处理,以上方案会产生很多的重复代码,可维护性较差。如果使用 @ExceptionHandler 全局异常处理器又不好判断是否是 app B、app C 抛出的异常。
使用 @ExceptionHandler 全局异常处理器 + ErrorDecoder 便能很好的解决这个问题。首先分别为两个三方服务定义自己的 ErrorDecoder,令其返回相同的异常,比如 ThirdException,然后在全局异常处理器中将 ThirdException 转换成 ResultA。
以下两个 ErrorDecoder 只处理了 4xx 的情况,其他情况委托给 feign.codec.ErrorDecoder.Default 处理。
BFeignErrorDecoder
package com.example.feigntest.a;
import com.example.feigntest.JsonUtils;
import com.example.feigntest.b.ResultB;
import feign.Response;
import feign.codec.ErrorDecoder;
import org.springframework.http.HttpStatus;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class BFeignErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultErrorDecoder = new Default();
@Override
public Exception decode(String methodKey, Response response) {
// 处理 4xx
if (HttpStatus.valueOf(response.status()).is4xxClientError()) {
try {
ResultB<?> result = JsonUtils.parse(response.body().asReader(StandardCharsets.UTF_8), ResultB.class);
// 返回 ThirdException
return new ThirdException(result.getMessage());
} catch (IOException _) {
// ignore
}
}
return defaultErrorDecoder.decode(methodKey, response);
}
}
CFeignErrorDecoder
package com.example.feigntest.a;
import com.example.feigntest.JsonUtils;
import com.example.feigntest.c.ResultC;
import feign.Response;
import feign.codec.ErrorDecoder;
import org.springframework.http.HttpStatus;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class CFeignErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultErrorDecoder = new Default();
@Override
public Exception decode(String methodKey, Response response) {
// 处理 4xx
if (HttpStatus.valueOf(response.status()).is4xxClientError()) {
try {
ResultC<?> result = JsonUtils.parse(response.body().asReader(StandardCharsets.UTF_8), ResultC.class);
// 返回 ThirdException
return new ThirdException(result.getMsg());
} catch (IOException _) {
// ignore
}
}
return defaultErrorDecoder.decode(methodKey, response);
}
}
application.yaml
spring:
cloud:
openfeign:
client:
config:
app-b:
error-decoder: com.example.feigntest.a.BFeignErrorDecoder
app-c:
error-decoder: com.example.feigntest.a.CFeignErrorDecoder
或者直接在 @FeignClient 里配置:
@FeignClient(name = "app-b", url = "http://localhost:8080/b", configuration = BFeignErrorDecoder.class)
public interface BFeignClient {...}
@FeignClient(name = "app-c", url = "http://localhost:8080/c", configuration = CFeignErrorDecoder.class)
public interface CFeignClient {...}
ExceptionHandlerA.java
全局异常处理,将 ThirdException 转换成 ResultA。
package com.example.feigntest.a;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class ExceptionHandlerA {
@ExceptionHandler(ThirdException.class)
public ResultA<?> handleException(ThirdException e) {
return ResultA.error(e.getMessage());
}
}
返回值如下:
{
"code": -1,
"data": null,
"message": "参数错误"
}
{
"code": -1,
"data": null,
"message": "无权操作"
}
更多推荐



所有评论(0)