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.
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);
}
}
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>
@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.
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 {
//...
}
}
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 {
//...
}
}
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'
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;
}
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.