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

IT技术 javascript java spring-boot
2021-01-31 22:02:04

我想将表单内的文件上传到 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)。那应该可以解决问题。

6个回答

是的,您可以简单地通过包装类来完成。

1)创建一个Class来保存表单数据:

public class FormWrapper {
    private MultipartFile image;
    private String title;
    private String description;
}

2)创建form用于提交数据的HTML

<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);
    }
}
是的。实际上有两个应用程序。第一个应用程序通过 Zuul 代理将请求转发到另一个应用程序。我也得到了标题,将这些值描述为第二个应用程序控制器中的重复(逗号分隔)。但是如果我在 First application Controller 中复制粘贴相同的代码并访问它,那么一切正常。
2021-03-20 22:02:04
当我这样做时,“模型”中的所有字段都为空。@ModelAttribute 以某种方式无法将表单字段映射到 DTO 字段
2021-03-24 22:02:04
如果我在“title”变量中传递,你能告诉我如何获得 utf-8 字符吗?因为目前,我得到 ???? 为了这。英文字符工作正常。
2021-03-28 22:02:04
@VijayShegokar 你加入CharacterEncodingFilterweb.xml吗?
2021-04-11 22:02:04
我认为您应该为其创建一个单独的问题,以便人们可以理解该问题。或者,如果您能找到类似的问题,请标记我。
2021-04-12 22:02:04

我使用纯 JS 和 Spring Boot 创建了一个类似的东西。这是回购。 我发送User对象JSONFile作为部分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>
它对我有用。只是添加,需要添加:processData:false,contentType:false,cache:false,这样才能正常运行。春季启动 2.1.7。并且没有必要添加“消耗”。
2021-03-14 22:02:04
@GSSwain 它对我来说很好用。如何从 POSTMAN 测试端点。
2021-04-05 22:02:04

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