欢迎光临
我们一直在努力

FeignClient 中,方法返回值为 void 不进 Decoder 的原因及解决办法

基于 SynchronousMethodHandler 处理器,来看下执行 http 请求的过程和decode逻辑(仅保留关键代码

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    // 创建请求
    Request request = targetRequest(template);

    // 发起请求,并构建响应
    Response response = client.execute(request, options);
    response = response.toBuilder()
          .request(request)
          .requestTemplate(template)
          .build();

    // decoder != null,执行 decoder 逻辑
    if (decoder != null) {
        return responseInterceptor.aroundDecode(new InvocationContext(decoder, metadata.returnType(), response));
    }

    // 否则执行 asyncResponseHandler
    CompletableFuture<Object> resultFuture = new CompletableFuture<>();
    asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response,
        metadata.returnType(), elapsedTime);
}

关于 decoder 属性来自于构造注入,如下:

private SynchronousMethodHandler(Target<?> target, Client client, Retryer retryer,
      List<RequestInterceptor> requestInterceptors, ResponseInterceptor responseInterceptor,
      Logger logger, Logger.Level logLevel, MethodMetadata metadata,
      RequestTemplate.Factory buildTemplateFromArgs, Options options,
      Decoder decoder, ErrorDecoder errorDecoder, boolean dismiss404,
      boolean closeAfterDecode, ExceptionPropagationPolicy propagationPolicy,
      boolean forceDecoding) {

    // 。。。省略N行

    if (forceDecoding) {
      // internal only: usual handling will be short-circuited, and all responses will be passed to
      // decoder directly!
      this.decoder = decoder;
      this.asyncResponseHandler = null;
    } else {
      this.decoder = null;
      this.asyncResponseHandler = new AsyncResponseHandler(logLevel, logger, decoder, errorDecoder,
          dismiss404, closeAfterDecode, responseInterceptor);
    }
  }

常规情况下,使用 Feign.Builder 初始化 SynchronousMethodHandler 时,forceDecoding 传入为 false,也就是说构造参数中的 decoder 即使有值,也没有直接赋值给 this.decoder,而是传入到了 new AsyncResponseHandler。所以在上面执行 #executeAndDecode 方法时,实际上是调用了 asyncResponseHandler.handleResponse

void handleResponse(CompletableFuture<Object> resultFuture, String configKey, Response response, Type returnType, long elapsedTime) {
    // 。。。省略N行
    else if (response.status() >= 200 && response.status() < 300) {
        if (isVoidType(returnType)) {
            resultFuture.complete(null);
        } else {
            final Object result = decode(response, returnType);
            shouldClose = closeAfterDecode;
            resultFuture.complete(result);
        }
    }
}

当http状态码在 [200, 300) 中间时(常规我们都是 200正常 状态码)

则进行 isVoidType(returnType) 判断,如果是 void 返回值,则 resultFuture.complete(null); 就完事了,不会进入 decode !!!


解决方案:

1)返回值不使用 void 【不推荐】

返回值不使用void,而是提供一个void包装类,在decoder中针对包装类直接跳过

不推荐原因:对于新项目而言还可以,老项目已经存在的void方法,还需要改造成本

2)强制启用 forceDecoding【不推荐】

在创建代理对象前,可以执行自定义,如下:

// customizers
applyBuildCustomizers(context, builder);

private void applyBuildCustomizers(FeignContext context, Feign.Builder builder) {
    Map<String, FeignBuilderCustomizer> customizerMap = 
                  context.getInstances(contextId, FeignBuilderCustomizer.class);
    if (customizerMap != null) {
       customizerMap.values().stream()
              .sorted(AnnotationAwareOrderComparator.INSTANCE)
              .forEach(feignBuilderCustomizer -> feignBuilderCustomizer.customize(builder));
    }
    additionalCustomizers.forEach(customizer -> customizer.customize(builder));
}

也就是说,实现 FeignBuilderCustomizer 接口,即可在创建代理对象前拿到 builder

/**
 * Internal - used to indicate that the decoder should be immediately called
 */
Builder forceDecoding() {
    this.forceDecoding = true;
    return this;
}

由于 forceDecoding() 非 public,因此需要使用反射来设置值

public class PilotFeignDecodeBuilderCustomizer implements FeignBuilderCustomizer {
    @Override
    public void customize(Feign.Builder builder) {
        ReflectUtil.setFieldValue(builder, "forceDecoding", true);
    }
}

不推荐原因:asyncResponseHandler 无法被执行,则其中的日志记录、body.close() 等都需要decoder来做

3、代理 Contract【推荐】

1)返回值不使用 void 的思想一致,只不过对于 void 或者 Void 的返回值,做了一层自动包装

原理:在 Feign.Builder 中存在 Contract 属性,创建 FeignContractHolder 对 Contract 做代理,这样在创建 FeignClient Proxy 过程中,对于 target 的解析工作则交给 FeignContractHolder 来做,在FeignContractHolder 中仍旧使用原来的Contract 进行解析,FeignContractHolder 则对解析结果做遍历,如果存在 void 或者 Void,则设定为包装类

第一步:创建一个 FeignContractHolder,用来做 contract 代理,如下:

import feign.Contract;

@AllArgsConstructor
public class FeignContractHolder implements Contract {
    // 代理对象
    private final Contract delegate;

    @Override
    public List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType) {
        List<MethodMetadata> metadatas = delegate.parseAndValidateMetadata(targetType);

        // 循环遍历处理,如果是 void 或者 Void,则包装一下
        if (CollUtil.isNotEmpty(metadatas)) {
            metadatas.forEach(item -> {
                if (item.returnType() == void.class || item.returnType() == Void.class) {
                    item.returnType(VoidWrapper.class);
                }
            });
        }

        return metadatas;
    }
}

第二步:在 FeignBuilderCustomizer 中创建PilotFeignContractHolder


import feign.Contract;
import feign.Feign;
import org.springframework.cloud.openfeign.FeignBuilderCustomizer;

/**
 * Feign Builder Customizer.
 * <p>
 * 1、启用 PilotFeignContractHolder,用来代理 Contract
 */
public class FeignDecodeVoidBuilderCustomizer implements FeignBuilderCustomizer {
    @Override
    public void customize(Feign.Builder builder) {
        // 反射拿到 contract
        Contract originalContract = (Contract) ReflectUtil.getFieldValue(builder, "contract");
        // 创建 PilotFeignContractHolder,用于代理 originalContract
        builder.contract(new PilotFeignContractHolder(originalContract));
    }
}
赞(0)
LiuYD's 个人技术分享 » FeignClient 中,方法返回值为 void 不进 Decoder 的原因及解决办法

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址