【问题标题】:Spring Web Reactive clientSpring Web 反应式客户端
【发布时间】:2018-04-22 10:07:25
【问题描述】:

我正在尝试使用 Spring Reactive WebClient 将文件上传到 spring 控制器。控制器非常简单,看起来像这样:

@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> uploadFile(
        @RequestParam("multipartFile") MultipartFile multipartFile,
        @RequestParam Map<String, Object> entityRequest
        ) {
    entityRequest.entrySet().forEach(System.out::println);
    System.out.println(multipartFile);
    return ResponseEntity.ok("OK");
}

当我将此控制器与 cURL 一起使用时,一切正常

curl -X POST http://localhost:8080/upload -H 'content-type: multipart/form-data;' -F fileName=test.txt -F randomKey=randomValue -F multipartFile=@document.pdf

multipartFile 进入正确的参数,其他参数进入 Map。

当我尝试从 WebClient 执行相同操作时,我被卡住了。我的代码如下所示:

    WebClient client = WebClient.builder().baseUrl("http://localhost:8080").build();

    MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
    map.set("multipartFile", new ByteArrayResource(Files.readAllBytes(Paths.get("/path/to/my/document.pdf"))));
    map.set("fileName", "test.txt");
    map.set("randomKey", "randomValue");
    String result = client.post()
            .uri("/upload")
            .contentType(MediaType.MULTIPART_FORM_DATA)
            .syncBody(map)
            .exchange()
            .flatMap(response -> response.bodyToMono(String.class))
            .flux()
            .blockFirst();
    System.out.println("RESULT: " + result);

这会导致 400 错误

{
  "timestamp":1510228507230,
  "status":400,
  "error":"Bad Request",
  "message":"Required request part 'multipartFile' is not present",
  "path":"/upload"
}

有人知道如何解决这个问题吗?

【问题讨论】:

  • 你可以尝试使用@RequestPart for MultipartFile multipartFile
  • 我已经这样做了,但这并没有什么不同。我找到了另一个解决方案,我会发布它。

标签: java spring spring-mvc spring-boot spring-webflux


【解决方案1】:

所以我自己找到了解决方案。事实证明,Spring 确实需要 Content-Disposition 标头包含一个文件名,以便将上传序列化为 Controller 中的 MultipartFile。

为此,我必须创建一个支持设置文件名的 ByteArrayResource 子类

public class MultiPartResource extends ByteArrayResource {

  private String filename;

  public MultiPartResource(byte[] byteArray) {
    super(byteArray);
  }

  public MultiPartResource(byte[] byteArray, String filename) {
    super(byteArray);
    this.filename = filename;
  }

  @Nullable
  @Override
  public String getFilename() {
    return filename;
  }

  public void setFilename(String filename) {
    this.filename = filename;
  }
}

然后可以在客户端中使用此代码

WebClient client = WebClient.builder().baseUrl("http://localhost:8080").build();

MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();

map.set("fileName", "test.txt");
map.set("randomKey", "randomValue");
ByteArrayResource resource = new MultiPartResource(Files.readAllBytes(Paths.get("/path/to/my/document.pdf")), "document.pdf");

String result = client.post()
        .uri("/upload")
        .contentType(MediaType.MULTIPART_FORM_DATA)
        .body(BodyInserters.fromMultipartData(map))
        .exchange()
        .flatMap(response -> response.bodyToMono(String.class))
        .flux()
        .blockFirst();
System.out.println("RESULT: " + result);

【讨论】:

【解决方案2】:

您需要在文件部分包含文件名才能上传成功,并结合asyncPart()以避免缓冲所有文件内容,然后您可以编写如下代码:

WebClient client = WebClient.builder().baseUrl("http://localhost:8080").build();

Mono<String> result = client.post()
        .uri("/upload")
        .contentType(MediaType.MULTIPART_FORM_DATA)
        .body((outputMessage, context) ->
                Mono.defer(() -> {
                    MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder();

                    Flux<DataBuffer> data = DataBufferUtils.read(
                            Paths.get("/tmp/file.csv"), outputMessage.bufferFactory(), 4096);

                    bodyBuilder.asyncPart("file", data, DataBuffer.class)
                            .filename("filename.csv");

                    return BodyInserters.fromMultipartData(bodyBuilder.build())
                            .insert(outputMessage, context);
                }))
        .exchange()
        .flatMap(response -> response.bodyToMono(String.class));

System.out.println("RESULT: " + result.block());

【讨论】:

    【解决方案3】:

    提供 Content-Disposition 的更简单方法

    MultipartBodyBuilder builder = new MultipartBodyBuilder();
    String header = String.format("form-data; name=%s; filename=%s", "paramName", "fileName.pdf");
    builder.part("paramName", new ByteArrayResource(<file in byte array>)).header("Content-Disposition", header);
    
    // in the request use
    webClient.post().body(BodyInserters.fromMultipartData(builder.build()))
    

    【讨论】:

      【解决方案4】:

      在这种情况下使用ByteArrayResource 效率不高,因为整个文件内容都将加载到内存中。

      使用带有"file:" 前缀的UrlResourceClassPathResource 应该可以解决这两个问题。

      UrlResource resource = new UrlResource("file:///path/to/my/document.pdf");
      

      【讨论】:

      • 这只是这里的代码示例。在实际代码中,资源已经是来自内容存储库的字节数组。不是文件系统中的文件。
      【解决方案5】:

      我尝试使用 @Ozzie 的方法无法使其与 List 一起使用,有什么提示吗?

      @PostMapping(
              value = "uploadReport",
              consumes = {MediaType.MULTIPART_FORM_DATA_VALUE},
              produces = MediaType.APPLICATION_JSON_VALUE
      )
      public ResponseEntity<Mono<Report>> postUploadReport(@RequestPart(name = "titleReport")List<FilePart> titleReport){}
      

      测试代码:

          MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder();
      
          List<byte[]> listOfTitleReport = new ArrayList<>();
          var file = Files.readAllBytes(new ClassPathResource("test.pdf").getFile().toPath());
          listOfTitleReport.add(file);
      
          bodyBuilder.part("titleReport", listOfTitleReport).header("Content-Type", MediaType.MULTIPART_FORM_DATA_VALUE);
          MultiValueMap<String, HttpEntity<?>> body = bodyBuilder.build();
      

      【讨论】:

        猜你喜欢
        • 2021-04-26
        • 2021-08-24
        • 1970-01-01
        • 2021-07-20
        • 2020-10-17
        • 1970-01-01
        • 1970-01-01
        • 2019-06-01
        • 2022-01-02
        相关资源
        最近更新 更多