首页 归档 标签 关于

Spring Cloud Feign 去除全局包装、统一异常处理

2022 年关于 Spring Cloud 服务间调用组件 Feign 配置的总结。

Maven 依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

1. 全局响应包装的拆解

一般来说,我们服务接口响应内容都有一层全局的包装,比如:

{
  "code": 0,
  "message": "成功",
  "data": {
      ...
  }
}

在服务间调用时如果在每个 Feign 的接口处定义同样的类比如 Result<XxVO>getData() 不是不行,但是比较繁琐,而且在获取想要的数据前可能需要根据 codemessage 判断得到的结果是否正确。通过自定义 decoder 可以比较好地解决前一个问题:

// 注入 Jackson 的 ObjectMapper,如果不喜欢 Lombok 可以自行修改
@RequiredArgsConstructor
public class UnwrapDecoder implements Decoder {
    private final ObjectMapper objectMapper;

    @SneakyThrows
    @Override
    public Object decode(Response response, Type type) {
        Reader reader = response.body().asReader(Charset.defaultCharset());
        Result<?> result = objectMapper.readValue(reader, Result.class);
        // 根据 code 判断操作是否成功
        if (ResultCode.isSuccess(result.getCode())) {
            Object data = result.getData();
            JavaType javaType = TypeFactory.defaultInstance().constructType(type);
            return objectMapper.convertValue(data, javaType);
        }
        // 若不成功,抛出业务异常,注意此处的异常会在 DecodeException 中被捕获,后文会处理
        throw new BusinessException(result.getCode(), result.getMessage());
    }
}

使用方式:

@FeignClient(name = "serviceA", configuration = {UnwrapDecoder.class})
...
@GetMapping("/xx")
// 无需使用 Result<XxVO> 泛型包装,业务中直接调用
XxVO getXxVO();
...

2. 上游异常请求统一处理

Feign 在收到响应 HTTP 状态码为 40X、50X 等时会进入ErrorDecoder,我们可以自定义处理器:

@Configuration
@RequiredArgsConstructor
@Slf4j
public class FeignClientErrorDecoder implements ErrorDecoder {
    private final ObjectMapper objectMapper;

    @Override
    public Exception decode(String methodKey, Response response) {
        try {
            Reader reader = response.body().asReader(Charset.defaultCharset());
            Result<?> result = objectMapper.readValue(reader, Result.class);
            // 如果上游服务响应符合全局包装约定,再次抛出即可
            return new BusinessException(result.getCode(), result.getMessage());
        } catch (Exception e) {
            log.error("Response 转换异常: ", e);
            return new BusinessException(ResultCode.INTERFACE_INNER_INVOKE_ERROR);
        }
    }
}

3. 关于 Feign 的全局异常处理

前文提到,UnwrapDecoder 中抛出的业务异常会被捕获并以 DecodeException 抛出,因此在全局异常中单独处理(此类可以与常规的全局异常处理类分开共存):

@RestControllerAdvice
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE) // 优先级
@ResponseStatus(code = HttpStatus.BAD_REQUEST) // 统一 HTTP 状态码
@RequiredArgsConstructor
public class FeignExceptionHandler {
    /**
     * 拦截 FeignException 异常,Jackson 处理失败等情况会进入
     */
    @ExceptionHandler(FeignException.class)
    public Result<?> handleFeignException(FeignException e) {
        log.error("FeignException: ", e);
        return Result.errorResult(ResultCode.INTERFACE_INNER_INVOKE_ERROR, e.getMessage());
    }

    /**
     * 拦截 DecodeException 异常,decoder 中抛出的自定义全局异常会进入此处
     */
    @ExceptionHandler(DecodeException.class)
    public Result<?> handleDecodeException(DecodeException e) {
        Throwable cause = e.getCause();
        if (cause instanceof BusinessException) {
            BusinessException businessException = (BusinessException) cause;
            // 上游符合全局响应包装约定的再次抛出即可
            return Result.errorResult(businessException.getCode(), businessException.getMessage());
        }
        log.error("DecodeException: ", e);
        return Result.errorResult(ResultCode.INTERFACE_INNER_INVOKE_ERROR, e.getMessage());
    }
}

至此,框架自动转交上游服务的业务异常,业务中不必关注全局包装的拆解问题,完美!


· 转载请注明 https://kytrun.com/spring-cloud-feign-unwrap-and-exception/