Spring 5: 使用RestTemplate 将POST 多个文件与POJO; MediaType.MULTIPART_FORM_DATA 与 LinkedMultiValueMap

Spring 5: use RestTemplate to POST multiple files with POJO; MediaType.MULTIPART_FORM_DATA with LinkedMultiValueMap

版本:

Java 11
Spring 5.3.9
Jackson 2.13
org.apache.httpcomponents:httpclient: 4.5.13

我认为这里的问题是开箱即用,RestTemplate 无法处理 HttpEntity 其值本身就是 MultiValueMap with (String, Resource) 对。如何解决?我想规范的用例是支持通过 HTML 形式同时上传多个文件以及元数据。详情如下。

消息转换器如下:

  private List<HttpMessageConverter<?>> getMessageConverters()
  {
    List<MediaType> mediaTypes = new ArrayList<>();
    mediaTypes.add(MediaType.TEXT_HTML);
    mediaTypes.add(MediaType.APPLICATION_JSON);
    mediaTypes.add(MediaType.TEXT_PLAIN);

    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    converter.setSupportedMediaTypes(mediaTypes);

    List<MediaType> formMediaTypes = new ArrayList<>();
    formMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
    formMediaTypes.add(MediaType.MULTIPART_FORM_DATA);

    FormHttpMessageConverter formConverter = new FormHttpMessageConverter();
    formConverter.setSupportedMediaTypes(formMediaTypes);
    formConverter.addPartConverter(new MappingJackson2HttpMessageConverter());
    formConverter.addPartConverter(new ResourceHttpMessageConverter());

    StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
    stringConverter.setSupportedMediaTypes(formMediaTypes);

    List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
    messageConverters.add(converter);
    messageConverters.add(formConverter);
    messageConverters.add(stringConverter);
    return messageConverters;
  }

并且:

    RestTemplate restTemplate = new RestTemplate(requestFactory);
    restTemplate.setMessageConverters(getMessageConverters());

然后我用这些文件创建一个 HttpEntity(在这个例子中,我只 POST 创建一个文件):

    ByteArrayResource bas = new ByteArrayResource(labxReport.getPDFFile().getBytes()) {
      @Override public String getFilename() { return reportFilename; }
    };
    MultiValueMap<String, Object> reportFiles = new LinkedMultiValueMap<String, Object>();
    reportFiles.add(reportFilename, bas);

    HttpHeaders reportFilesReqHeaders = new HttpHeaders();
    reportFilesReqHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
    HttpEntity<MultiValueMap<String, Object>> reportFilesEntity = new HttpEntity<>(reportFiles, reportFilesReqHeaders);

对于 POJO(这里是 ReportInfo 的一个实例),我创建了一个单独的 HttpEntity,如下所示:

    HttpHeaders reportInfoReqHeaders = new HttpHeaders();
    reportInfoReqHeaders.setContentType(MediaType.APPLICATION_JSON);
    HttpEntity<ReportInfo> reportInfoEntity = new HttpEntity<>(reportInfo, reportInfoReqHeaders);

然后我为我的主要 POST 请求拼凑 HttpEntity,如下所示:

    MultiValueMap<String, Object> postParams = new LinkedMultiValueMap<String, Object>();
    postParams.set("files", reportFilesEntity);
    postParams.set("data", reportInfoEntity);

    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
    httpHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));

    HttpEntity<MultiValueMap<String, Object>> requestPOST = new HttpEntity<>(postParams, httpHeaders);

最后,我提出 POST 请求:

    ResponseEntity<String> response = restTemplate.exchange(PORTAL_URL, HttpMethod.POST, requestPOST, String.class);

这导致以下堆栈跟踪:

org.springframework.http.converter.HttpMessageNotWritableException: Could not write request: no suitable HttpMessageConverter found for request type [org.springframework.util.LinkedMultiValueMap]
        at org.springframework.http.converter.FormHttpMessageConverter.writePart(FormHttpMessageConverter.java:532) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        at org.springframework.http.converter.FormHttpMessageConverter.writeParts(FormHttpMessageConverter.java:503) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        at org.springframework.http.converter.FormHttpMessageConverter.writeMultipart(FormHttpMessageConverter.java:483) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        at org.springframework.http.converter.FormHttpMessageConverter.write(FormHttpMessageConverter.java:360) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        at org.springframework.http.converter.FormHttpMessageConverter.write(FormHttpMessageConverter.java:156) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        at org.springframework.web.client.RestTemplate$HttpEntityRequestCallback.doWithRequest(RestTemplate.java:950) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:735) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:672) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:581) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        ...

谢谢。

我解决这个问题的方法如下getMessageConverters所示。当时的想法是 files 表单参数本身就是一种 form-based 消息(即具有 key/value 对),因此需要 FormHttpMessageConverter.[=14 的另一个实例=]

这是对我有用的逻辑:

  private List<HttpMessageConverter<?>> getMessageConverters()
  {
    List<MediaType> mediaTypes = new ArrayList<>();
    mediaTypes.add(MediaType.TEXT_HTML);
    mediaTypes.add(MediaType.APPLICATION_JSON);
    mediaTypes.add(MediaType.TEXT_PLAIN);

    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    converter.setSupportedMediaTypes(mediaTypes);

    List<MediaType> formMediaTypes = new ArrayList<>();
    formMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
    formMediaTypes.add(MediaType.MULTIPART_FORM_DATA);

    FormHttpMessageConverter formConverter = new FormHttpMessageConverter();
    formConverter.setSupportedMediaTypes(formMediaTypes);
    formConverter.addPartConverter(new MappingJackson2HttpMessageConverter());

    FormHttpMessageConverter multifileConverter = new FormHttpMessageConverter();
    multifileConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_OCTET_STREAM));
    multifileConverter.addPartConverter(new ResourceHttpMessageConverter());
    formConverter.addPartConverter(multifileConverter);

    StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
    stringConverter.setSupportedMediaTypes(formMediaTypes);

    List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
    messageConverters.add(converter);
    messageConverters.add(formConverter);
    messageConverters.add(stringConverter);
    return messageConverters;
  }