web-dev-qa-db-ja.com

Spring Boot Controller-MultipartとJSONをDTOにアップロードする

フォーム内のファイルを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)を使用します。これで問題が解決するはずです。

20

はい、ラッパークラスを介して簡単に実行できます。

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);
        }
    }
20
UsamaAmjad

私は、純粋なJSとSpring Bootを使用して同様のものを作成しました。 リポジトリUserオブジェクトをJSONとして送信し、Filemultipart/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>
8
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";
}

これはここで最も簡単な方法のようですが、他の方法は独自のmessageConverterを追加することです。

2
Raghvendra Garg

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;
}
1
vis

リクエストマッピングにコンシューマタイプを追加します。これは正常に機能するはずです。

@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

ここでは、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 {
        //...
    }
}
1
Stephan

multipart/form-dataRequestMappingアノテーションに追加して、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'
0
ShaneCoder

反応フロントエンドからこれを削除します。

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

これはより良く書くことができますが、できる限り元のコードに近づけるようにしてみました。役に立てば幸いです。

0
Femi Nefa