フォーム内のファイルをSpring Boot APIエンドポイントにアップロードしたい。
UIは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つの属性が必要であることを示唆するソリューションを見ました。1つはjsonセクションの下にあり、もう1つはイメージ用です。自動的にDTOに変換できるかどうかを確認したいと思います。
pdate 1クライアントから送信されたアップロードペイロードは、次のDTOに変換する必要があります。
public class ExpensePostDto extends ExpenseBaseDto {
private MultipartFile image;
private String description;
private List<Long> sharers;
}
つまり、jsonとmultipartが混在していると言えます。
ソリューション
この問題の解決策は、フロントエンドで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
(標準JS)を使用します。これで問題が解決するはずです。
はい、ラッパークラスを介して簡単に実行できます。
1)フォームデータを保持するClass
を作成します
public class FormWrapper {
private MultipartFile image;
private String title;
private String description;
}
2)データ用の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 {
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)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);
}
}
私は、純粋な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>
@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を追加することです。
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.log("success", response)
// This function handles success
}, function (response) {
console.log("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;
}
リクエストマッピングにコンシューマタイプを追加します。これは正常に機能するはずです。
@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 {
//...
}
}
ここでは、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'
}
});
};
注意すべき点の1つは、Angularが、マルチパートMIMEタイプと境界を 'Content-Type'ヘッダー値に自動的に設定することです。あなたのものではないかもしれませんが、その場合は自分で設定する必要があります。
私のアプリケーションでは、サーバーからのJSON応答、つまり「Accept」ヘッダーが必要です。
自分でFormDataオブジェクトを渡すので、フォームがFileをControllerでマップする属性に設定していることを確認する必要があります。私の場合、FormDataオブジェクトの「file」パラメーターにマップされます。
コントローラーエンドポイントは次のようになります。
@POST
@RequestMapping("/upload")
public ResponseEntity<Object> upload(@RequestParam("file") MultipartFile file)
{
if (file.isEmpty()) {
return new ResponseEntity<Object>(HttpStatus.BAD_REQUEST);
} else {
//...
}
}
フォームの他の部分を表すDTOを含め、必要なだけ他の@RequestParamを追加できます。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 {
//...
}
}
multipart/form-data
をRequestMapping
アノテーションに追加して、consumes = "multipart/form-data"
を消費していることをspringに通知する必要があります。 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 {
//..
}
投稿されたExpensePostDto
では、リクエストのtitle
は無視されます。
編集
また、コンテンツタイプを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);
}
これはより良く書くことができますが、できる限り元のコードに近づけるようにしてみました。役に立てば幸いです。