web-dev-qa-db-fra.com

Contrôleur d'amorçage Spring - Téléchargez plusieurs parties et JSON vers DTO

Je souhaite télécharger un fichier dans un formulaire sur un noeud final Spring Boot API. 

L'interface utilisateur est écrite dans 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);
  }

C'est le code côté Java:

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

Mais je reçois cette exception du côté de Java:

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

Comment dois-je résoudre ce problème? Les points de terminaison API similaires et le code latéral javascript fonctionnent déjà.

Remarque J'ai vu une solution suggérant que le corps de la requête ait 2 attributs: un élément sous lequel la section json est placée, un autre pour l'image. J'aimerais voir s'il est possible de le convertir automatiquement en DTO. 

Mise à jour 1 La charge utile de téléchargement envoyée par le client doit être convertie vers le DTO suivant:

public class ExpensePostDto extends ExpenseBaseDto {

    private MultipartFile image;

    private String description;

    private List<Long> sharers;

}

Donc, vous pouvez dire que c'est un mélange de json et multipart.

Solution

La solution au problème consiste à utiliser FormData sur le front-end et ModelAttribute sur le backend: 

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

et sur le front-end, débarrassez-vous de Content-Type comme il devrait être déterminé par le navigateur lui-même et utilisez FormData (JS standard). Cela devrait résoudre le problème.

10
Arian Hosseinzadeh

Oui, vous pouvez simplement le faire via la classe wrapper.

1) Créer une Class pour contenir les données de formulaire

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

2) Créer Form pour les données

<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) Créer une méthode pour recevoir les données text du formulaire et le fichier multipart

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

4) Méthode de sauvegarde 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);
        }
    }
12
UsamaAmjad

J'avais créé une chose similaire en utilisant JS pur et Spring Boot . Voici le Repo. J'envoie un objet User sous la forme JSON et un File dans le cadre de la requête multipart/form-data.

Les extraits pertinents sont ci-dessous

Le code 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 + "]";
        }

    }
}

Les codes HTML et 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>
3
GSSwain
@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";
}

Cela semble être le moyen le plus simple, d’autres moyens pourraient être d’ajouter votre propre convertisseur de messages.

1
Raghvendra Garg

Ajoutez le type de consommateur à votre mappage de demandes. Cela devrait fonctionner correctement.

@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
this_is_om_vm

J'ai construit ma dernière application de téléchargement de fichiers sous AngularJS et SpringBoot, dont la syntaxe est suffisamment similaire pour vous aider ici.

Mon gestionnaire de demandes côté client:

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 définit automatiquement le type mime en plusieurs parties et la limite sur la valeur de l'en-tête 'Content-Type' pour moi. Le vôtre ne peut pas, dans ce cas, vous devez le régler vous-même. 

Mon application attend une réponse JSON du serveur, d'où l'en-tête "Accepter".

Vous transmettez vous-même l'objet FormData, vous devez donc vous assurer que votre formulaire définit le fichier sur l'attribut que vous mappez sur votre contrôleur. Dans mon cas, il est mappé sur le paramètre 'file' de l'objet FormData.

Mes points de terminaison de contrôleur ressemblent à ceci:

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

Vous pouvez ajouter autant d'autres @RequestParam que vous le souhaitez, y compris votre DTO qui représente le reste du formulaire, mais assurez-vous qu'il est structuré de cette manière en tant qu'enfant de l'objet FormData.

La clé à retenir ici est que chaque @RequestParam est un attribut de la charge utile du corps de l'objet FormData de la demande en plusieurs parties.

Si je modifiais mon code pour tenir compte de vos données, cela ressemblerait à ceci:

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'
        }
    });
};

Ensuite, le point de terminaison de votre contrôleur ressemblerait à ceci:

@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 {
        //...
    }
}
1
Stephan

Vous devez indiquer à Spring que vous utilisez multipart/form-data en ajoutant consumes = "multipart/form-data" à l'annotation RequestMapping. Supprimez également l'annotation RequestBody du paramètre 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 {
   //..
}

Avec ExpensePostDto posté, la title dans la demande est ignorée.

Modifier

Vous devrez également changer le type de contenu en multipart/form-data. On dirait que c'est la valeur par défaut pour post basée sur d'autres réponses. Juste pour être sûr, je le préciserais:

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

J'ai eu un cas d'utilisation similaire dans lequel j'avais des données JSON et une image téléchargée (pensez à un utilisateur qui tente de s'enregistrer avec des informations personnelles et une image de profil).

En se référant aux réponses @Stephan et @GSSwain, j'ai proposé une solution avec Spring Boot et AngularJs.

Ci-dessous, un aperçu de mon code. J'espère que ça aide quelqu'un.

    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.log("success", response)
            // This function handles success

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

        });

Et contrôleur 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;
}
0
vis

Enlevez ceci de la face avant de réaction:

 'Content-Type': 'application/json'

Modifiez le contrôleur latéral 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);
 }

Cela peut être mieux écrit, mais j'ai essayé de le garder le plus près possible de votre code d'origine. J'espère que ça aide.

0
Femi Nefa