一般我们会使用 multipart/form-data
请求来上传文件。multipart/form-data
请求可以有多个子请求体,每个子请求体都可以有自己的 header 和 body。
本文将带你了解如何在 Spring Boot 应用中使用 multipart/form-data
请求同时上传文件、JSON、表单数据。
服务端 Controller {#服务端-controller}
定义文件上传 controller。
package cn.springdoc.demo.controller;
import java.io.IOException;
import java.io.InputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import cn.springdoc.demo.model.Meta;
@RestController
@RequestMapping("/upload")
public class UploadController {
private static final Logger log = LoggerFactory.getLogger(UploadController.class);
/**
* 文件上传
* @param file
* @param response
* @return
* @throws IOException
*/
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String upload ( @RequestPart("logo") MultipartFile file,// 文件
@RequestPart("meta") Meta meta, // 元信息
@RequestPart("title") String title // 标题
) throws IOException {
log.info("Logo 文件名称:{}", file.getOriginalFilename());
log.info("Logo 表单名称:{}", file.getName());
log.info("Logo 文件大小:{}", file.getSize());
log.info("Logo 文件类型:{}", file.getContentType());
log.info("Meta:{}", meta);
log.info("Title:{}", title);
// 丢弃上传的文件数据
try(InputStream in = file.getInputStream()){
int ret = StreamUtils.drain(in);
log.info("丢弃字节:{}", ret);
}
return "success";
}
}
在 controller 中定义了一个文件上传方法。有3个参数,分别是上传的文件、JSON 参数、表单参数。
注意,我们使用的是
@RequestPart
注解,而不是@RequestParam
。spring 会根据 multipart 请求中每个 part 的 content-type 来选择HttpMessageConverter
对参数进行封装。也就说,使用@RequestPart
会自动把 multipart 请求中的 JSON 参数封装为 Java 对象。
其中 JSON 对象(Meta
)的定义如下。
package cn.springdoc.demo.model;
public class Meta {
private String host; // 主机地址
private String keywords; // 关键字
private String description; // 介绍
// 省略get/set/toString方法
}
使用 RestTemplate {#使用-resttemplate}
使用 RestTemplate
同时上传文件、JSON 以及表单参数。
package cn.springdoc.test;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
public class MultipartTest {
public static void main(String[] args) {
RestTemplate restTemplate = new RestTemplate();
// 消息头
HttpHeaders headers = new HttpHeaders();
// Content-Type 为 multipart/form-data
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
// body builder
MultipartBodyBuilder builder = new MultipartBodyBuilder();
// 文件参数
builder.part("logo", new FileSystemResource("C:\\Users\\KevinBlandy\\Desktop\\512.png"), MediaType.IMAGE_PNG);
// json 参数
// 这里的 content-type 很重要,spring 会根据此使用对应的 HttpMessageConverter 封装参数为对象
builder.part("meta", "{\"host\": \"springdoc.cn\", \"keywords\": \"spring,spring boot\", \"description\": \"Everything about spring\"}", MediaType.APPLICATION_JSON);
// 普通表单参数
builder.part("title", "Spring 中文网");
// 构建完整的 http 消息
HttpEntity<MultiValueMap<String, HttpEntity<?>>> httpEntity = new HttpEntity<>(builder.build(), headers);
// 发起请求,获取响应
ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:8080/upload", httpEntity, String.class);
System.out.println(responseEntity.getBody());
}
}
客户端的代码很简单,通过 MultipartBodyBuilder
来设置文件、JSON、表单参数,注意要正确地设置每个参数对应的 content-type。
测试 {#测试}
启动服务端后,执行客户的测试方法,服务端输出日志如下:
c.s.demo.controller.UploadController : Logo 文件名称:512.png
c.s.demo.controller.UploadController : Logo 表单名称:logo
c.s.demo.controller.UploadController : Logo 文件大小:19825
c.s.demo.controller.UploadController : Logo 文件类型:image/png
c.s.demo.controller.UploadController : Meta:Meta [host=springdoc.cn, keywords=spring,spring boot, description=Everything about spring]
c.s.demo.controller.UploadController : Title:Spring 中文网
c.s.demo.controller.UploadController : 丢弃字节:19825
如你所见,不论是文件,JSON 还是表单参数都准确地获取到了。
使用 Javascript {#使用-javascript}
在 HTML 客户端中,使用 Javascript 也可以实现同时上传文件、JSON 和表单数据。
在服务端的 src/main/resources
目录下新建 public
目录。在 public
目录中新建一个 index.html
作为客户端,其内容如下:
public
目录是公共资源目录,该目录中的文件可以被浏览器直接访问,目录中的 index.html
是默认主页。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>文件、JSON、表单数据上传</title>
</head>
<body>
<input type="file"/>
<script type="text/javascript">
// 监听文件选择事件
document.querySelector('input').addEventListener('change', e => {
let files = e.target.files;
if (!files){
return ;
}
// 选择的文件
let file = files[0];
// 构建 multipart 请求体
let formData = new FormData();
// 文件参数
formData.set('logo', file);
// json 参数
// 注意,最后指定了 content type 为 json
formData.set('meta', new Blob([JSON.stringify({'host': 'springdoc.cn', 'keywords': 'spring,spring boot', 'description': 'Everything about spring'})], {type: "application/json"}));
// 普通表单参数
formData.set('title', 'Spring 中文网')
// 发起请求
fetch('/upload', {
method: 'POST',
body: formData // 请求体
}).then(resp => {
if(resp.ok){
resp.text().then(msg => {
console.log(msg);
});
} else {
// TODO 异常响应
}
}).catch(err => {
console.err(err);
});
});
</script>
</body>
</html>
- 监听
<input type="file"/>
节点的change
事件,在用户选择要上传的文件后,通过 javascript 获取到用户选择的文件。 - 通过
FormData
对象构建 multipart 请求体,注意要设置 json 请求体的 content-type。 - 使用
fetch
发起请求,监听响应。
测试 {#测试-1}
启动服务器端,打开浏览器访问 http://localhost:8080/
。
选择文件后就会立即执行上传,查看后端日志:
c.s.demo.controller.UploadController : Logo 文件名称:512.png
c.s.demo.controller.UploadController : Logo 表单名称:logo
c.s.demo.controller.UploadController : Logo 文件大小:19825
c.s.demo.controller.UploadController : Logo 文件类型:image/png
c.s.demo.controller.UploadController : Meta:Meta [host=springdoc.cn, keywords=spring,spring boot, description=Everything about spring]
c.s.demo.controller.UploadController : Title:Spring 中文网(来自 Javascript)
c.s.demo.controller.UploadController : 丢弃字节:19825
一切OK。
最后,你可以在 谷歌浏览器 中通过 控制台 的 网络 面板查看上传请求的 payload
来更直观地了解 multipart 请求:
------WebKitFormBoundaryIASBRR0cSKzNYBdy
Content-Disposition: form-data; name="logo"; filename="512.png"
Content-Type: image/png
------WebKitFormBoundaryIASBRR0cSKzNYBdy
Content-Disposition: form-data; name="meta"; filename="blob"
Content-Type: application/json
{"host":"springdoc.cn","keywords":"spring,spring boot","description":"Everything about spring"}
------WebKitFormBoundaryIASBRR0cSKzNYBdy
Content-Disposition: form-data; name="title"
Spring 中文网(来自 Javascript)
------WebKitFormBoundaryIASBRR0cSKzNYBdy--