openfeign同时上传文件和表单

一、问题描述

1、服务端feign接口。

/**
 * 上链接口
 */
@PostMapping(value = STORE_TO_CHAIN)
R<Void> storeDataToBlockChain(StoreDataToChainParamDTO param, @RequestPart(value = "files",required = false) List<MultipartFile> files);

2、客户端调用接口,服务端并未收到 StoreDataToChainParamDTO 对象的值。

================  Request Start  ================
===> POST: /client/chain/store-to-chain Parameters: {"param":{"enterpriseSocialCode":null,"enterpriseId":null,"tenantId":null,"enterpriseName":null,"name":null,"certCardNumber":null,"industryType":null,"industryName":null,"region":null,"pluginType":null,"timestamp":null,"refSerialNumber":null,"type":null,"dataList":null},"files":null}
===Headers===  Accept: */*
===Headers===  Connection: Keep-Alive
===Headers===  User-Agent: okhttp/4.9.3
===Headers===  Host: 2.0.0.1:9500
===Headers===  Accept-Encoding: gzip
===Headers===  Content-Length: 2
===Headers===  Content-Type: application/json
================   Request End   ================

二、原因分析

1、类 FeignInvocationHandler:调用feign接口的方法,实际调用的就是这个动态代理对象的 invoke() 方法。

FeignInvocationHandler.java

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  if ("equals".equals(method.getName())) {
    try {
      Object otherHandler =
          args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
      return equals(otherHandler);
    } catch (IllegalArgumentException e) {
      return false;
    }
  } else if ("hashCode".equals(method.getName())) {
    return hashCode();
  } else if ("toString".equals(method.getName())) {
    return toString();
  }
  // 基于dispatch 找到对应的 MethodHandler,调用invoke方法
  return dispatch.get(method).invoke(args);
}

2、dispatch.get(method).invoke(args) 调用类 SynchronousMethodHandler 的 invoke() 方法。

SynchronousMethodHandler.java

@Override
public Object invoke(Object[] argv) throws Throwable {
  // 基于入参,构建出 http request template
  RequestTemplate template = buildTemplateFromArgs.create(argv);
  Options options = findOptions(argv);
  Retryer retryer = this.retryer.clone();
  while (true) {
    try {
      // 请求和解码
      return executeAndDecode(template, options);
    } catch (RetryableException e) {
      try {
        retryer.continueOrPropagate(e);
      } catch (RetryableException th) {
        Throwable cause = th.getCause();
        if (propagationPolicy == UNWRAP && cause != null) {
          throw cause;
        } else {
          throw th;
        }
      }
      if (logLevel != Logger.Level.NONE) {
        logger.logRetry(metadata.configKey(), logLevel);
      }
      continue;
    }
  }
}

3、buildTemplateFromArgs.create(argv) 调用类 BuildTemplateByResolvingArgs 的 create() 方法。

BuildTemplateByResolvingArgs.java

@Override
public RequestTemplate create(Object[] argv) {
  RequestTemplate mutable = RequestTemplate.from(metadata.template());
  mutable.feignTarget(target);
  if (metadata.urlIndex() != null) {
    int urlIndex = metadata.urlIndex();
    checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
    mutable.target(String.valueOf(argv[urlIndex]));
  }
  Map<String, Object> varBuilder = new LinkedHashMap<String, Object>();
  for (Entry<Integer, Collection<String>> entry : metadata.indexToName().entrySet()) {
    int i = entry.getKey();
    Object value = argv[entry.getKey()];
    if (value != null) { // Null values are skipped.
      if (indexToExpander.containsKey(i)) {
        value = expandElements(indexToExpander.get(i), value);
      }
      for (String name : entry.getValue()) {
        varBuilder.put(name, value);
      }
    }
  }

  // 核心:组装template
  RequestTemplate template = resolve(argv, mutable, varBuilder);
  if (metadata.queryMapIndex() != null) {
    // add query map parameters after initial resolve so that they take
    // precedence over any predefined values
    Object value = argv[metadata.queryMapIndex()];
    Map<String, Object> queryMap = toQueryMap(value);
    template = addQueryMapQueryParameters(queryMap, template);
  }

  if (metadata.headerMapIndex() != null) {
    template =
        addHeaderMapHeaders((Map<String, Object>) argv[metadata.headerMapIndex()], template);
  }

  return template;
}

4、1)类BuildTemplateByResolvingArgs、BuildFormEncodedTemplateFromArgs、BuildEncodedTemplateFromArgs 都实现了 resolve(argv, mutable, varBuilder) 方法;
2)走哪个实现类,是在生成代理对象的时候判断(类 ParseHandlersByName 的 apply() 方法);
3)根据 apply() 方法,resolve(argv, mutable, varBuilder) 会调用 BuildFormEncodedTemplateFromArgs 的 resolve() 方法,入参 argv 不会填充到 formVariables ,导致客户端的参数没有发送到服务端。

ParseHandlersByName.java

public Map<String, MethodHandler> apply(Target target) {
  List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());
  Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
  for (MethodMetadata md : metadata) {
    BuildTemplateByResolvingArgs buildTemplate;
    if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
      buildTemplate =
          new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
    } else if (md.bodyIndex() != null || md.alwaysEncodeBody()) {
      buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
    } else {
      buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);
    }
    if (md.isIgnored()) {
      result.put(md.configKey(), args -> {
        throw new IllegalStateException(md.configKey() + " is not a method handled by feign");
      });
    } else {
      result.put(md.configKey(),
          factory.create(target, md, buildTemplate, options, decoder, errorDecoder));
    }
  }
  return result;
}
BuildFormEncodedTemplateFromArgs.java

@Override
  protected RequestTemplate resolve(Object[] argv,
                                    RequestTemplate mutable,
                                    Map<String, Object> variables) {
    Map<String, Object> formVariables = new LinkedHashMap<String, Object>();
    for (Entry<String, Object> entry : variables.entrySet()) {
      if (metadata.formParams().contains(entry.getKey())) {
        formVariables.put(entry.getKey(), entry.getValue());
      }
    }
    try {
      encoder.encode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable);
    } catch (EncodeException e) {
      throw e;
    } catch (RuntimeException e) {
      throw new EncodeException(e.getMessage(), e);
    }
    return super.resolve(argv, mutable, variables);
  }
}
BuildEncodedTemplateFromArgs.java

@Override
protected RequestTemplate resolve(Object[] argv,
                                  RequestTemplate mutable,
                                  Map<String, Object> variables) {

  boolean alwaysEncodeBody = mutable.methodMetadata().alwaysEncodeBody();

  Object body = null;
  if (!alwaysEncodeBody) {
    body = argv[metadata.bodyIndex()];
    checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex());
  }

  try {
    if (alwaysEncodeBody) {
      body = argv == null ? new Object[0] : argv;
      encoder.encode(body, Object[].class, mutable);
    } else {
      encoder.encode(body, metadata.bodyType(), mutable);
    }
  } catch (EncodeException e) {
    throw e;
  } catch (RuntimeException e) {
    throw new EncodeException(e.getMessage(), e);
  }
  return super.resolve(argv, mutable, variables);
}

三、解决方案

1、把文件对象封装起来,resolve(argv, mutable, varBuilder) 会调用 BuildEncodedTemplateFromArgs 的 resolve() 方法。

@PostMapping(value = STORE_TO_CHAIN, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
R<Void> storeDataToBlockChain(StoreDataToChainParamDTO param);

2、序列化时list转换失败问题解决(客户端)

@Configuration
public class MultipartSupportConfig {
    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;
          
    @Bean
    public Encoder feignFormEncoder() {
        return new SpringFormEncoder(new SpringEncoder(messageConverters));
    }  
 } 

3、解码(服务端)

public class StringToListMapConverter implements Converter<String, List<LinkedHashMap<String,Object>>> {
    @Override
    public List<LinkedHashMap<String, Object>> convert(String source) {
        return JsonUtil.parse(source, new TypeReference<List<LinkedHashMap<String, Object>>>() {});
    }
}
@Configuration
public class ConvertConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToListMapConverter());
    }
}
  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值