Клиент Spring Web Reactive

Я пытаюсь использовать 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 [email protected]

MultipartFile переходит к правильному параметру, а другие параметры - к карте.

Когда я пытаюсь сделать то же самое из 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"
}

Кто-нибудь знает, как решить эту проблему?


person Ozzie    schedule 09.11.2017    source источник
comment
Можете ли вы попробовать использовать @RequestPart для MultipartFile multipartFile   -  person Yogi    schedule 09.11.2017
comment
Я уже сделал это, но это не имеет значения. Я нашел другое решение и опубликую его.   -  person Ozzie    schedule 09.11.2017


Ответы (5)


Так что я сам нашел решение. Оказывается, Spring действительно нуждается в заголовке Content-Disposition, чтобы включить имя файла для загрузки, которая будет сериализована в 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);
person Ozzie    schedule 09.11.2017
comment
Не забывайте звонок map.set("multipartFile", resource) - person A. Masson; 22.10.2019
comment
Должно быть проще согласно github.com/spring-projects/spring -framework / issues / - person aholub7x; 22.10.2019
comment
Это обязательно следует отметить как решение. Официальная документация docs .spring.io / spring / docs / current / spring-framework-reference / не упоминает об этом. Кроме того, простой способ, упомянутый aholub7x, отлично работает. - person BarbosSergos; 19.02.2020

Вам нужно указать имя файла для части файла для успешной загрузки в сочетании с 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());
person Manh Tai    schedule 13.03.2020

Более простой способ предоставить 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()))
person V.Aggarwal    schedule 16.04.2020
comment
это сработало для меня, спасибо :-) - person alphabytes; 14.05.2021

Использование ByteArrayResource в этом случае неэффективно, так как все содержимое файла будет загружено в память.

Использование UrlResource с префиксом "file:" или ClassPathResource должно решить обе проблемы.

UrlResource resource = new UrlResource("file:///path/to/my/document.pdf");
person Brian Clozel    schedule 10.11.2017
comment
Это было только для примера кода здесь. В фактическом коде ресурс уже представляет собой массив байтов из репозитория контента. Не файл из файловой системы. - person Ozzie; 13.11.2017

Я пробовал использовать метод @Ozzie, не могу заставить его работать со списком, какие-нибудь подсказки?

@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();
person alphabytes    schedule 13.05.2021