Контроллер загрузки Spring - загрузка Multipart и JSON в DTO

Я хочу загрузить файл внутри формы в конечную точку Spring Boot API.

Пользовательский интерфейс написан на React:

export function createExpense(formData) {
  return dispatch => {
    axios.post(ENDPOINT,
      formData, 
      headers: {
        'Authorization': //...,
        'Content-Type': 'application/json'
      }
      ).then(({data}) => {
        //...
      })
      .catch(({response}) => {
        //...
      });
    };
}

  _onSubmit = values => {
    let formData = new FormData();
    formData.append('title', values.title);
    formData.append('description', values.description);
    formData.append('amount', values.amount);
    formData.append('image', values.image[0]);
    this.props.createExpense(formData);
  }

Это сторонний код Java:

@RequestMapping(path = "/{groupId}", method = RequestMethod.POST)
public ExpenseSnippetGetDto create(@RequestBody ExpensePostDto expenseDto, @PathVariable long groupId, Principal principal, BindingResult result) throws IOException {
   //..
}

Но я получаю это исключение на стороне Java:

org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'multipart/form-data;boundary=----WebKitFormBoundaryapHVvBsdZYc6j4Af;charset=UTF-8' not supported

Как мне решить эту проблему? Аналогичные конечные точки API и сторонний код JavaScript уже работают.

Примечание

Я видел решение, в котором предполагается, что тело запроса должно иметь 2 атрибута: один, в котором находится раздел JSON, другой для изображения. Я хотел бы посмотреть, возможно ли его автоматическое преобразование в DTO.


Обновление 1

Полезные данные загрузки, отправленные клиентом, должны быть преобразованы в следующий DTO:

public class ExpensePostDto extends ExpenseBaseDto {

    private MultipartFile image;

    private String description;

    private List<Long> sharers;

}

Таким образом, вы можете сказать, что это смесь JSON и составной.


Решение

Решение проблемы - использовать FormData во внешнем интерфейсе и ModelAttribute во внутреннем:

@RequestMapping(path = "/{groupId}", method = RequestMethod.POST,
        consumes = {"multipart/form-data"})
public ExpenseSnippetGetDto create(@ModelAttribute ExpensePostDto expenseDto, @PathVariable long groupId, Principal principal) throws IOException {
   //...
}

а во внешнем интерфейсе избавьтесь от Content-Type, поскольку он должен определяться самим браузером, и используйте FormData (стандартный JavaScript). Это должно решить проблему.

Ваш тип содержимого неверен, FormData не создает application/json

Musa 15.04.2018 20:32

Я изменил его на multipart/form-data, но все равно получаю ту же ошибку.

Arian 16.04.2018 09:17

Это именно то, что вам нужно: stackoverflow.com/questions/25699727/…

Krzysztof Kaszkowiak 17.04.2018 21:59
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
43
3
88 260
8
Перейти к ответу Данный вопрос помечен как решенный

Ответы 8

Я создал свое последнее приложение для загрузки файлов на AngularJS и SpringBoot, которые достаточно похожи по синтаксису, чтобы помочь вам в этом.

Мой обработчик запросов на стороне клиента:

uploadFile=function(fileData){
    var formData=new FormData();
    formData.append('file',fileData);
    return $http({
        method: 'POST',
        url: '/api/uploadFile',
        data: formData,
        headers:{
            'Content-Type':undefined,
            'Accept':'application/json'
        }
    });
};

Следует отметить, что Angular автоматически устанавливает для меня многостраничный тип MIME и границу в значении заголовка Content-Type. Ваш не может, и в этом случае вам нужно установить его самостоятельно.

Мое приложение ожидает от сервера ответа JSON, поэтому заголовок «Принять».

Вы сами передаете объект FormData, поэтому вам нужно убедиться, что ваша форма устанавливает File для любого атрибута, который вы сопоставляете на своем контроллере. В моем случае он отображается на параметр file в объекте FormData.

Конечные точки моего контроллера выглядят так:

@POST
@RequestMapping("/upload")
public ResponseEntity<Object> upload(@RequestParam("file") MultipartFile file) 
{
    if (file.isEmpty()) {
        return new ResponseEntity<Object>(HttpStatus.BAD_REQUEST);
    } else {
        //...
    }
}

Вы можете добавить столько других @RequestParam, сколько захотите, включая ваш DTO, который представляет остальную часть формы, просто убедитесь, что он структурирован таким образом как дочерний элемент объекта FormData.

Ключевой вывод здесь заключается в том, что каждый @RequestParam является атрибутом полезной нагрузки тела объекта FormData в многостраничном запросе.

Если бы я изменил свой код для размещения ваших данных, он бы выглядел примерно так:

uploadFile=function(fileData, otherData){
    var formData=new FormData();
    formData.append('file',fileData);
    formData.append('expenseDto',otherData);
    return $http({
        method: 'POST',
        url: '/api/uploadFile',
        data: formData,
        headers:{
            'Content-Type':undefined,
            'Accept':'application/json'
        }
    });
};

Тогда конечная точка вашего контроллера будет выглядеть так:

@POST
@RequestMapping("/upload")
public ResponseEntity<Object> upload(@RequestParam("file") MultipartFile file, @RequestParam("expenseDto") ExpensePostDto expenseDto)
{
    if (file.isEmpty()) {
        return new ResponseEntity<Object>(HttpStatus.BAD_REQUEST);
    } else {
        //...
    }
}

Я все еще получаю ту же ошибку. Ваша конечная точка принимает только один параметр, который является file и имеет тип данных multipart form. Мой - это комбинация json и multipart. Я обновляю свой пост, добавляя DTO.

Arian 20.04.2018 08:38

Вы не можете этого сделать. Если вы посмотрите на чистый составной пакет, тело зарезервировано для данных файла. Вы можете указывать параметры пути, но не дополнительные полезные данные. Я должен был заметить это, когда ответил. Я исправлю свой ответ, когда буду отключен от мобильного телефона.

Stephan 20.04.2018 13:38

Что это за пример на Axios github тогда: github.com/axios/axios/blob/master/examples/upload/index.htm‌ l

Arian 22.04.2018 02:16

Они делают то же самое, что и я в приведенном выше примере, с formData.append (). Я думаю, вы не понимаете, как пакет создается за кулисами. Если у вас есть работающая копия их примера, я предлагаю вам посмотреть сетевой трафик в Chrome и посмотреть на структуру пакетов.

Stephan 23.04.2018 14:54

как заполнить otherData из formData.append('expenseDto',otherData);? Я пробовал var otherData = {'name':'a'}, но выдает ошибку Cannot convert value of type 'java.lang.String' to required type 'ExpenseDto'.

frank 10.10.2019 10:32

у вашего объекта ExpenseDTO есть один атрибут name и нет других обязательных атрибутов? Spring автоматически пытается десериализовать полезную нагрузку в POJO, предполагая, что POJO содержит и помечен как действительный из предоставленного JSON.

Stephan 21.10.2019 21:15

Добавьте тип потребителя в сопоставление запросов. Он должен работать нормально.

@POST
@RequestMapping("/upload")
public ResponseEntity<Object> upload(@RequestParam("file") MultipartFile file,consumes = "multipart/form-data") 
{
    if (file.isEmpty()) {
        return new ResponseEntity<Object>(HttpStatus.BAD_REQUEST);
    } else {
        //...
    }
}
Ответ принят как подходящий

Да, вы можете просто сделать это через класс-оболочку.

1) Создайте Class для хранения данных формы:

public class FormWrapper {
    private MultipartFile image;
    private String title;
    private String description;
}

2) Создайте HTML form для отправки данных:

<form method = "POST" enctype = "multipart/form-data" id = "fileUploadForm" action = "link">
    <input type = "text" name = "title"/><br/>
    <input type = "text" name = "description"/><br/><br/>
    <input type = "file" name = "image"/><br/><br/>
    <input type = "submit" value = "Submit" id = "btnSubmit"/>
</form>

3) Создайте метод для получения данных формы text и файла multipart:

@PostMapping("/api/upload/multi/model")
public ResponseEntity<?> multiUploadFileModel(@ModelAttribute FormWrapper model) {
    try {
        // Save as you want as per requiremens
        saveUploadedFile(model.getImage());
        formRepo.save(mode.getTitle(), model.getDescription());
    } catch (IOException e) {
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }

    return new ResponseEntity("Successfully uploaded!", HttpStatus.OK);
}

4) Способ сохранения file:

private void saveUploadedFile(MultipartFile file) throws IOException {
    if (!file.isEmpty()) {
        byte[] bytes = file.getBytes();
        Path path = Paths.get(UPLOADED_FOLDER + file.getOriginalFilename());
        Files.write(path, bytes);
    }
}

Не могли бы вы рассказать мне, как получить символы utf-8, если я передал сюда переменную "title". Потому что в настоящее время я получаю ???? за это. Английские символы работают нормально.

Vijay Shegokar 04.12.2018 11:47

@VijayShegokar, вы добавляли CharacterEncodingFilter в web.xml?

UsamaAmjad 04.12.2018 14:14

да. На самом деле приложений два. Первое приложение пересылает запрос другому приложению через Zuul Proxy. Я также получаю заголовок, описывающий эти значения как повторяющиеся (через запятую) во втором контроллере приложения. Но если я скопирую тот же код в первый контроллер приложения и получу доступ к нему, все будет работать нормально.

Vijay Shegokar 06.12.2018 04:12

Я думаю, вам следует создать для него отдельный вопрос, чтобы люди могли понять проблему. Или, если вы найдете похожие вопросы, отметьте меня.

UsamaAmjad 06.12.2018 06:03

Я создал похожую вещь, используя чистый JS и Spring Boot. Вот Репо. Я отправляю объект User как JSON и File как часть запроса multipart/form-data.

Соответствующие фрагменты приведены ниже

Код Controller

@RestController
public class FileUploadController {

    @RequestMapping(value = "/upload", method = RequestMethod.POST, consumes = { "multipart/form-data" })
    public void upload(@RequestPart("user") @Valid User user,
            @RequestPart("file") @Valid @NotNull @NotBlank MultipartFile file) {
            System.out.println(user);
            System.out.println("Uploaded File: ");
            System.out.println("Name : " + file.getName());
            System.out.println("Type : " + file.getContentType());
            System.out.println("Name : " + file.getOriginalFilename());
            System.out.println("Size : " + file.getSize());
    }

    static class User {
        @NotNull
        String firstName;
        @NotNull
        String lastName;

        public String getFirstName() {
            return firstName;
        }

        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }

        public String getLastName() {
            return lastName;
        }

        public void setLastName(String lastName) {
            this.lastName = lastName;
        }

        @Override
        public String toString() {
            return "User [firstName = " + firstName + ", lastName = " + lastName + "]";
        }

    }
}

Код HTML и JS

<html>    
<head>
    <script>
        function onSubmit() {

            var formData = new FormData();

            formData.append("file", document.forms["userForm"].file.files[0]);
            formData.append('user', new Blob([JSON.stringify({
                "firstName": document.getElementById("firstName").value,
                "lastName": document.getElementById("lastName").value
            })], {
                    type: "application/json"
                }));
            var boundary = Math.random().toString().substr(2);
            fetch('/upload', {
                method: 'post',
                body: formData
            }).then(function (response) {
                if (response.status !== 200) {
                    alert("There was an error!");
                } else {
                    alert("Request successful");
                }
            }).catch(function (err) {
                alert("There was an error!");
            });;
        }
    </script>
</head>

<body>
    <form name = "userForm">
        <label> File : </label>
        <br/>
        <input name = "file" type = "file">
        <br/>
        <label> First Name : </label>
        <br/>
        <input id = "firstName" name = "firstName" />
        <br/>
        <label> Last Name : </label>
        <br/>
        <input id = "lastName" name = "lastName" />
        <br/>
        <input type = "button" value = "Submit" id = "submit" onclick = "onSubmit(); return false;" />
    </form>
</body>    
</html>

У меня это сработало. Просто добавляя, нужно было добавить: processData: false, contentType: false, cache: false, чтобы все работало хорошо. Spring Boot 2.1.7. и не нужно было добавлять "потребляет".

Marco López 16.08.2019 16:55

@GSSwain у меня работает нормально. Как протестировать конечную точку из POSTMAN.

Gundamaiah 04.10.2020 14:30

идеально! Благодарность

Guilherme 07.06.2021 14:35
@RequestMapping(value = { "/test" }, method = { RequestMethod.POST })
@ResponseBody
public String create(@RequestParam("file") MultipartFile file, @RequestParam String description, @RequestParam ArrayList<Long> sharers) throws Exception {
    ExpensePostDto expensePostDto = new ExpensePostDto(file, description, sharers);
    // do your thing
    return "test";
}

Кажется, это самый простой выход, другие способы - добавить свой собственный messageConverter.

Вы должны сообщить Spring, что потребляете multipart/form-data, добавив consumes = "multipart/form-data" в аннотацию RequestMapping. Также удалите аннотацию RequestBody из параметра expenseDto.

@RequestMapping(path = "/{groupId}", consumes = "multipart/form-data", method = RequestMethod.POST)
public ExpenseSnippetGetDto create(ExpensePostDto expenseDto, 
   @PathVariable long groupId, Principal principal, BindingResult result) 
   throws IOException {
   //..
}

С опубликованным ExpensePostDtotitle в запросе игнорируется.

Редактировать

Вам также необходимо изменить тип содержимого на multipart/form-data. Судя по некоторым другим ответам, это значение по умолчанию для post. На всякий случай уточню:

'Content-Type': 'multipart/form-data'

Удалите это из интерфейса реакции:

 'Content-Type': 'application/json'

Измените контроллер на стороне Java:

   @PostMapping("/{groupId}")
   public Expense create(@RequestParam("image") MultipartFile image,  @RequestParam("amount") double amount, @RequestParam("description") String description, @RequestParam("title") String title) throws IOException {
         //storageService.store(file); ....
          //String imagePath = path.to.stored.image;
         return new Expense(amount, title, description, imagePath);
 }

Это можно было бы написать лучше, но постарался максимально приблизить его к исходному коду. Я надеюсь, что это помогает.

У меня был аналогичный вариант использования, когда у меня были некоторые данные JSON и загрузка изображений (подумайте об этом как о пользователе, пытающемся зарегистрироваться с личными данными и изображением профиля).

Ссылаясь на ответы @Stephan и @GSSwain, я придумал решение с Spring Boot и AngularJs.

Ниже приведен снимок моего кода. Надеюсь, это кому-то поможет.

    var url = "https://abcd.com/upload";
    var config = {
        headers : {
            'Content-Type': undefined
        }

    }
    var data = {
        name: $scope.name,
        email: $scope.email
    }
    $scope.fd.append("obj", new Blob([JSON.stringify(data)], {
                type: "application/json"
            }));

    $http.post(
        url, $scope.fd,config
    )
        .then(function (response) {
            console.info("success", response)
            // This function handles success

        }, function (response) {
            console.info("error", response)
            // this function handles error

        });

И контроллер SpringBoot:

@RequestMapping(value = "/upload", method = RequestMethod.POST, consumes = {   "multipart/form-data" })
@ResponseBody
public boolean uploadImage(@RequestPart("obj") YourDTO dto, @RequestPart("file") MultipartFile file) {
    // your logic
    return true;
}

Другие вопросы по теме