【问题标题】:Netflix Feign - Propagate Status and Exception through MicroservicesNetflix Feign - 通过微服务传播状态和异常
【发布时间】:2016-12-11 16:34:24
【问题描述】:

我正在使用Netflix Feign 调用微服务 A 的一个操作来调用微服务 B 的其他其他操作,该操作使用 Spring Boot 验证代码。

如果验证失败,微服务 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 和下一条消息:

{
  "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 通过微服务传播状态和异常的最佳方式或技术是什么?

【问题讨论】:

    标签: java spring spring-boot microservices feign


    【解决方案1】:

    你可以使用伪装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();
    }
    

    【讨论】:

    • 我怎么知道错误是来自 otherOperation() 还是 validate()?我正在共享 Feign.builder() 所以我会有相同的 ErrorDecoder 并且我的代码和消息不会相同:(
    • 我认为你可以使用decode方法的methodKey参数。根据 API 文档,它应该包含 {@link feign.Feign#configKey} of the java method that invoked the request. ex. {@code IAM#getUser()} 这样可以为您提供有关上下文的提示。
    • 对于 404 错误有一个特殊情况标志,在 Feign.builder() 中传递 decode404 = true 将返回 404 而没有别的。
    【解决方案2】:

    我做的一个小库的无耻插件,它使用反射根据响应正文中返回的错误代码动态重新抛出已检查的异常(如果它们在 Feign 接口上则未检查)。

    有关自述文件的更多信息: https://github.com/coveo/feign-error-decoder

    【讨论】:

      【解决方案3】:

      OpenFeign 的 FeignException 不绑定到特定的 HTTP 状态(即不使用 Spring 的 @ResponseStatus 注释),这使得 Spring 在遇到 FeignException 时默认为 500。没关系,因为 FeignException 可能有许多与特定 HTTP 状态无关的原因。

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

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

      此示例使 Spring 返回与您从微服务 B 收到的相同 HTTP 状态。您可以更进一步,还返回原始响应正文:

      response.getOutputStream().write(e.content());
      

      【讨论】:

      • handleFeignStatusException 方法的返回类型应该是什么?返回“feignError”正在发送“feignError”作为响应正文。当异常是 401 状态代码时,似乎也没有正文。
      • @JuanRojas 请参阅ExceptionHandler documentation 了解返回类型的详细信息。如果您希望返回类型为响应正文,则可以使用 @ResponseBody
      • Feign 异常状态始终为 500,我在调试观察中看不到其他微服务返回的初始 HTTP 状态。
      【解决方案4】:

      编写您的自定义异常映射器并注册它。您可以自定义响应。

      完整的例子是here

      public class GenericExceptionMapper implements ExceptionMapper<Throwable> {
      
          @Override
          public Response toResponse(Throwable ex) {
              return Response.status(500).entity(YOUR_RETURN_OBJ_HERE).build();
          }
      
      }
      

      【讨论】:

        【解决方案5】:

        自 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 的贡献者,也是该错误解码器的主要开发者。

        【讨论】:

          【解决方案6】:

          我们的做法如下:

          与两个微服务共享包含异常的通用 jar。

          1.) 在微服务中,将异常转换为 DTO 类可以说是 ErrorInfo。 其中将包含您的自定义异常的所有属性,带有 String exceptionType,其中将包含异常类名称。

          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);
           }
          

          【讨论】:

            猜你喜欢
            • 2017-05-21
            • 2015-07-20
            • 1970-01-01
            • 2019-02-11
            • 2019-12-02
            • 2017-11-20
            • 2017-04-14
            • 1970-01-01
            • 2020-02-29
            相关资源
            最近更新 更多