ErrorDecoder

HTTP 响应状态码用来表明特定 HTTP 请求是否成功完成。 响应被归为以下五大类:

  1. 信息响应 (100199)
  2. 成功响应 (200299)
  3. 重定向消息 (300399)
  4. 客户端错误响应 (400499)
  5. 服务端错误响应 (500599)

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": "无权操作"
}
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐