Spring Boot 控制器 - 将 Multipart 和 JSON 上传到 DTO

Posted

技术标签:

【中文标题】Spring Boot 控制器 - 将 Multipart 和 JSON 上传到 DTO【英文标题】:Spring Boot controller - Upload Multipart and JSON to DTO 【发布时间】:2018-09-25 11:41:58 【问题描述】:

我想将表单内的文件上传到 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

客户端发送的上传payload需要转换成如下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)。这应该可以解决问题。

【问题讨论】:

您的内容类型不正确,FormData 不会产生application/json 我把它改成了multipart/form-data,还是出现同样的错误。 这正是您所需要的:***.com/questions/25699727/… 【参考方案1】:

我使用纯 JS 和 Spring Boot 创建了一个类似的东西。 这是Repo. 我将User 对象作为JSONFile 作为multipart/form-data 请求的一部分发送。

相关的sn-ps如下

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>

【讨论】:

它对我有用。刚刚添加,需要添加:processData: false, contentType: false, cache: false,这样才能正常运行。春季启动 2.1.7。并且没有必要添加“消耗”。 @GSSwain 它对我来说工作正常。如何从 POSTMAN 测试端点。【参考方案2】:

是的,您可以通过包装类简单地做到这一点。

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

【讨论】:

如果我在“title”变量中传递,请告诉我如何获取 utf-8 字符。因为目前,我得到了????为了这。英文字符工作正常。 @VijayShegokar 你在web.xml 中添加了CharacterEncodingFilter 吗? 是的。实际上有两个应用程序。第一个应用程序通过 Zuul 代理将请求转发到另一个应用程序。我也得到了标题,在第二个应用程序控制器中将这些值描述为重复(逗号分隔)。但是如果我将相同的代码复制粘贴到第一个应用程序控制器中并访问它,那么一切正常。 我认为您应该为它创建一个单独的问题,以便人们能够理解问题。或者如果您能找到类似的问题,请标记我。 当我这样做时,“模型”中的所有字段都为空。 @ModelAttribute 无法将表单字段映射到 DTO 字段【参考方案3】:

我有一个类似的用例,我上传了一些 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;

【讨论】:

【参考方案4】:

您必须通过将consumes = "multipart/form-data" 添加到RequestMapping 注释来告诉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'

【讨论】:

【参考方案5】:

从 react 前端移除这个:

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

这可以写得更好,但尽量保持它尽可能接近您的原始代码。希望对你有帮助。

【讨论】:

【参考方案6】:
@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。

【讨论】:

【参考方案7】:

将消费者类型添加到您的请求映射中。它应该可以正常工作。

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

【讨论】:

【参考方案8】:

我在 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 响应,因此需要“Accept”标头。

您自己传递 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 
        //...
    

【讨论】:

我仍然遇到同样的错误。您的端点只需要一个参数,即file,类型为多部分表单数据。我的是 json 和 multipart 的组合。我更新了我的帖子以包含 DTO。 你不能那样做。如果您查看纯多部分数据包,则正文是为文件数据保留的。您可以放置​​路径参数,但不能放置额外的正文有效负载。当我回答时,我应该注意到这一点。当我离开手机时,我会更正我的答案。 那么Axios github上的这个例子是什么:github.com/axios/axios/blob/master/examples/upload/index.html 他们所做的正是我在上面的例子中使用 formData.append() 所做的。我认为您误解了数据包是如何在幕后构建的。如果您有他们示例的运行副本,我建议您在 chrome 中观察网络流量并查看数据包结构。 如何填充otherDataformData.append('expenseDto',otherData);?我试过var otherData = 'name':'a',但它抛出错误Cannot convert value of type 'java.lang.String' to required type 'ExpenseDto'

以上是关于Spring Boot 控制器 - 将 Multipart 和 JSON 上传到 DTO的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot Multi-Module maven 项目重新打包失败

Spring Boot:RESTful 控制器中的多层@Services 用于许多入站请求

如何从 Gradle Multi Build 项目中发布 Spring Boot 应用程序?

Spring Boot Multi Module Gradle Project 类路径问题:Package Not Found, Symbol not Found

spring boot sso

使用 Spring Boot 2 和 Spring Security 5 进行多因素身份验证