Netflix Feign - 通过微服务传播状态和异常

26

我正在使用Netflix Feign,将Microservice A的一个操作调用到Microservice B的另一个操作,后者使用Spring Boot验证代码。

如果验证失败,则Microservice B的操作会抛出异常。然后我在微服务中进行处理,并返回HttpStatus.UNPROCESSABLE_ENTITY (422),如下所示:

@ExceptionHandler({
       ValidateException.class
    })
    @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
    @ResponseBody
    public Object validationException(final HttpServletRequest request, final validateException exception) {
        log.error(exception.getMessage(), exception);
        error.setErrorMessage(exception.getMessage());
        error.setErrorCode(exception.getCode().toString());
        return error;
    }

因此,当微服务A在接口中调用B时,如下所示:

@Headers("Content-Type: " + MediaType.APPLICATION_JSON_UTF8_VALUE)
@RequestLine("GET /other")
void otherOperation(@Param("other")  String other );

@Headers("Content-Type: " + MediaType.APPLICATION_JSON_UTF8_VALUE)
@RequestLine("GET /code/validate")
Boolean validate(@Param("prefix") String prefix);

static PromotionClient connect() {

    return Feign.builder()
        .encoder(new GsonEncoder())
        .decoder(new GsonDecoder())
        .target(PromotionClient.class, Urls.SERVICE_URL.toString());
}

当验证失败时,它会返回一个内部错误500,并显示以下消息:

and the validations fails it returns a internal error 500 with next message:

{
  "timestamp": "2016-08-05T09:17:49.939+0000",
  "status": 500,
  "error": "Internal Server Error",
  "exception": "feign.FeignException",
  "message": "status 422 reading Client#validate(String); content:\n{\r\n  \"errorCode\" : \"VALIDATION_EXISTS\",\r\n  \"errorMessage\" : \"Code already exists.\"\r\n}",
  "path": "/code/validate"
}

但我需要返回与微服务操作B相同的内容。

使用Netflix Feign在微服务中传播状态和异常的最佳方法或技术是什么?

6个回答

26

您可以使用Feign的ErrorDecoder来处理自定义错误。

https://github.com/OpenFeign/feign/wiki/Custom-error-handling

以下是一个示例:

public class MyErrorDecoder implements ErrorDecoder {

    private final ErrorDecoder defaultErrorDecoder = new Default();

    @Override
    public Exception decode(String methodKey, Response response) {
        if (response.status() >= 400 && response.status() <= 499) {
            return new MyBadRequestException();
        }
        return defaultErrorDecoder.decode(methodKey, response);
    }

}

为了让Spring捕获ErrorDecoder,您需要将其放在ApplicationContext上:

@Bean
public MyErrorDecoder myErrorDecoder() {
  return new MyErrorDecoder();
}

1
我怎么知道错误是来自 otherOperation() 还是 validate()?我正在共享 Feign.builder(),所以我会有相同的 ErrorDecoder,我的代码和消息也不一样 :( - Pau
1
我认为你可以使用decode方法的methodKey参数。根据API文档,它应该包含调用请求的Java方法的{@link feign.Feign#configKey}。例如:{@code IAM#getUser()}。这样可以给你提供上下文的线索。 - Mathias Dpunkt
有一个针对404错误的特殊标志,将decode404 = true传递给Feign.builder()将仅返回404而不是其他任何内容。 - Tito

4
使用反射动态重新抛出基于响应体返回的错误代码的已检查异常(如果它们在Feign接口上是未检查的,则也会抛出)的小型库。有关更多信息,请参见自述文件: https://github.com/coveo/feign-error-decoder

4
OpenFeign的FeignException不会绑定到特定的HTTP状态(即不使用Spring的@ResponseStatus注释),这使得当面临FeignException时,Spring默认为500。这是可以接受的,因为FeignException可能有许多原因,与特定的HTTP状态无关。

但是,您可以更改Spring处理FeignExceptions的方式。只需定义一个ExceptionHandler来处理您需要的FeignException(请参见此处):

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(FeignException.class)
    public String handleFeignStatusException(FeignException e, HttpServletResponse response) {
        response.setStatus(e.status());
        return "feignError";
    }

}

这个例子使得Spring返回与Microservice B相同的HTTP状态。你还可以进一步返回原始响应主体:
response.getOutputStream().write(e.content());

handleFeignStatusException 方法的返回类型应该是什么?返回“feignError”将作为响应体发送“feignError”。此外,当异常状态码为401时,似乎没有响应主体。 - Juan Rojas
@JuanRojas 请参考 ExceptionHandler文档 查看返回类型的详细信息。如果您希望返回类型为响应正文,则可以使用 @ResponseBody - Moritz
Feign异常状态始终为500,我无法在调试监视中看到其他微服务返回的初始HTTP状态。 - Zon

1
编写您自己的异常映射器并进行注册。您可以定制响应。
完整示例在此处
public class GenericExceptionMapper implements ExceptionMapper<Throwable> {

    @Override
    public Response toResponse(Throwable ex) {
        return Response.status(500).entity(YOUR_RETURN_OBJ_HERE).build();
    }

}

1

自2017年以来,我们创建了一个库,可以通过注释实现此功能(这使得像请求/等等一样编写代码变得相当容易)。

它基本上允许您按以下方式编写错误处理代码:

@ErrorHandling(codeSpecific =
    {
        @ErrorCodes( codes = {401}, generate = UnAuthorizedException.class),
        @ErrorCodes( codes = {403}, generate = ForbiddenException.class),
        @ErrorCodes( codes = {404}, generate = UnknownItemException.class),
    },
    defaultException = ClassLevelDefaultException.class
)
interface GitHub {

    @ErrorHandling(codeSpecific =
        {
            @ErrorCodes( codes = {404}, generate = NonExistentRepoException.class),
            @ErrorCodes( codes = {502, 503, 504}, generate = RetryAfterCertainTimeException.class),
        },
        defaultException = FailedToGetContributorsException.class
    )
    @RequestLine("GET /repos/{owner}/{repo}/contributors")
    List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
}

你可以在OpenFeign组织中找到它: https://github.com/OpenFeign/feign-annotation-error-decoder 免责声明:我是feign的贡献者和该错误解码器的主要开发人员。

0
我们所做的如下:
使用包含异常的共同jar文件与两个微服务共享。
1.) 在微服务A中,将异常转换为DTO类,例如ErrorInfo。它将包含自定义异常的所有属性以及一个字符串异常类型,其中包含异常类名。
2.) 当它在微服务B中接收到时,它将由微服务B中的ErrorDecoder处理,并尝试从exceptionType创建异常对象,如下所示:
@Override
public Exception decode(String methodKey, Response response) {       

ErrorInfo errorInfo = objectMapper.readValue(details, ErrorInfo.class);
Class exceptionClass;

Exception decodedException;

try {

    exceptionClass = Class.forName(errorInfo.getExceptionType());  

    decodedException = (Exception) exceptionClass.newInstance();

    return decodedException;

 }

 catch (ClassNotFoundException e) {

    return new PlatformExecutionException(details, errorInfo);

 }
  return defaultErrorDecoder.decode(methodKey, response);
 }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接