簡體   English   中英

Spring 引導 controller - 上傳多部分和 JSON 到 DTO

[英]Spring Boot controller - Upload Multipart and JSON to 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 個屬性:一個是 JSON 部分,另一個是圖像。 我想看看是否可以將它自動轉換為 DTO。


更新 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 (標准 JavaScript)。 那應該可以解決問題。

是的,您可以簡單地通過包裝類來完成。

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

我使用純 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 + "]";
        }

    }
}

HTMLJS代碼

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

我有一個類似的用例,我有一些 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;
}
@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。

我在 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 會自動為我在“Content-Type”標頭值上設置多部分 MIME 類型和邊界。 你的可能沒有,在這種情況下你需要自己設置。

我的應用程序需要來自服務器的 JSON 響應,因此是“接受”標頭。

您自己傳入 FormData 對象,因此您需要確保您的表單將 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 {
        //...
    }
}

將消費者類型添加到您的請求映射中。它應該可以正常工作。

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

您必須通過在RequestMapping注釋中添加consumes = "multipart/form-data"來告訴 spring 您正在使用multipart/form-data 還要從expenseDto參數中刪除RequestBody注釋。

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

這可以寫得更好,但盡量讓它盡可能接近你的原始代碼。 我希望它有幫助。

在我的例子中,問題是 DTO class 具有公共字段而不是 getter/setter 方法

public class MyDTO {
    public MultipartFile[] files;
    public String user;
    public String text;
}

DTO class 必須有 getters/setters,否則它不會工作。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM