一般我们会使用 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--