likes
comments
collection
share

SpringCloud OpenFeign 自定义响应解码器

作者站长头像
站长
· 阅读数 12

为什么自定义响应解码器

工作中有些时候服务提供者的接口实现并不是我们写的,那么就可能出现远程服务的响应数据结构和我们预期的不一致。例如可能我们想要的是原生 Object ,而远程接口返回的是包装后的

{
    "success": true,
    "code": "0",
    "message": "success",
    "data": Object
}

如果我们一味迁就,我们的 Feign 接口就会变成这样

@FeignClient(value = "xxx")
public interface XxxClient {
  @GetMapping("/{id}/info")
  Response<RecordPersonalInfoResponse> info(@PathVariable("id") Long id);
}

业务代码就会变成这样

RecordPersonalInfoResponse response  = client.info(1L).getData();

不知道大家是什么感觉,反正我有强迫症,看到多余的代码我是浑身难受......每个接口返回值定义都要多一个 Response<T>,每个 client 调用结果都要多一个 getData()。其实这样还不是最离谱的,我最怕的是同事这样写代码

CouponUseResponse response = couponClient.use(request.getCustomerId(), order.getLoanId(), coupon.getCode());
if (Objects.isNull(response) || !response.getResult()) {
  log.error( "xxx customerId:{}, code:{}", request.getCustomerId(), coupon.getCode());
  throw new AppException(CouponErrorCode.ERR_REC_COUPON_USED_FAILED);
}

这特么......

SpringCloud OpenFeign 自定义响应解码器

我们来分析一下这段代码,如果能走到 if 里面说明什么?说明券服务代码报错了对吧,然后你在券服务捕捉了 throw 出来的 Exception ,然后用下面这个结构包装了一下返回给了调用方。

{
    "success": false,
    "code": "1",
    "message": "error",
    "data": null
}

然后你特喵的又在当前服务去判断这个 success = false 再去把异常抛出去,那他喵我为啥不在券服务那边就不去捕捉让它自己抛出去不完事儿了?何必多此一举呢。

其实上面的问题都是受到一些上古项目遗留的影响,后面也没人主动去更新优化,更有很多人都被误导认为这种结构就是正确的做法,导致后面为了兼容这种格式不得不做出各种沙雕操作。当然说到这可能会跳出很多朋友来 diss 了,这个问题就上升到了 到底使用标准 HttpCode 还是自定义响应 Code?

十年前为了解决无良运营商把 404 页面劫持成广告页,上古程序员发明了无论接口成功失败一律返回 HttpCode = 200。结果一直被沿用至今,不遵守标准规范。推荐大家读一下大牛陈皓的文章 我做系统架构的一些原则。行了不扯那么多跑题的了,继续说我们怎么自定义解码器。

自定义解码器

查询官网发现了这样一段 Decoder feignDecoder: ResponseEntityDecoder (which wraps a SpringDecoder) 。它说 Feign 的解码是用一个包含了 SpringDecoderResponseEntityDecoder 类。

我们点进去发现 ResponseEntityDecoder 实现了接口 feign.codec.Decoder, 对ResponseEntityDecoder 类源码进行 debug。发现对于普通的 Feign 接口解码的时候方法调用顺序是 OptionalDecoder、ResponseEntityDecoder、SpringDecoder

观察源码发现 SpringDecoder 是底层的解码工具,那么显然我们要做的是实现一个在 ResponseEntityDecoderSpringDecoder 中间的 Decoder 实现。

/**
 * 自定义解码,实在不想看到遍地的 Response<T>
 */
public class ResponseDecoder implements Decoder {
    private final SpringDecoder decoder;

    public ResponseDecoder(SpringDecoder decoder) {
        this.decoder = decoder;
    }

    @Override
    public Object decode(Response response, Type type) throws IOException, FeignException {
        Method method = response.request().requestTemplate().methodMetadata().method();
        //如果Feign接口的返回值不是 Response{code:0,...} 结构类型,并且远程响应又是这个结构
        boolean notTheSame = method.getReturnType() != com.feign.test.client.Response.class;
        if (notTheSame) {
            //构造一个这个结构类型
            Type newType =
                    new ParameterizedType() {
                        @Override
                        public Type[] getActualTypeArguments() {
                            return new Type[]{type};
                        }
                        @Override
                        public Type getRawType() {
                            return com.feign.test.client.Response.class;
                        }
                        @Override
                        public Type getOwnerType() {
                            return null;
                        }
                    };
            com.feign.test.client.Response<?> result = (com.feign.test.client.Response<?>) this.decoder.decode(response, newType);
            //只返回data
            return result.getData();
        }
        return this.decoder.decode(response, type);
    }
}

这样我们自己的解码器就定义好了,现在只要把它应用到我们的 FeignClient 上即可,我们可以通过代码自定义一个配置

public class FeignClientDecodeConfiguration {
  @Bean
  public Decoder feignDecoder(ObjectProvider<HttpMessageConverters> messageConverters) {
    return new OptionalDecoder((new ResponseEntityDecoder(new ResponseDecoder(new SpringDecoder(messageConverters)))));
  }
}

注意不要加 @Configuration 注解,然后在 @FeignClient 上使用

@FeignClient(value = "xxx",configuration = FeignClientDecodeConfiguration.class)
public interface XxxClient {}

结语

我个人强烈反对不使用 httpCode 的做法......等后面几篇文章你就知道无论接口成功失败都返回 200 的做法会让 OpenFeign 的熔断、降级、重试都很麻烦......

如果这篇文章对你有帮助,记得点赞加关注!你的支持就是我继续创作的动力!