客户端解析服务器响应的multipart/form-data数据
2020-09-01 本文已影响0人
JonathanYee
multipart/form-data
,多部件请求体。这个请求体比较特殊,它可以拆分为多个部件,每个部件都有自己的header
和body
,最常用的地方就是:客户端文件上传,因为有多个部件,在上传文件的时候,还可以在body
中添加其他的数据。json
,form
。。。
一般来说,都是客户端发起multipart/form-data
请求 ,服务器进行解析。而且这种东西的编码解码工作一般都是由底层的容器/框架完成。开发根本不必关心。但是我最近遇到了一个需求:
服务器响应
multipart/form-data
(包含了一个二进制文件和其他的文本数据),客户端来解析
意味着,需要自己完成2个东西
- 在服务端完成
multipart/form-data
的数据编码,并且响应给客户端 - 在客户端获取到响应后,进行数据的解码
multipart/form-data
的请求体,看起来像这样(省略了部分 header)
POST /foo HTTP/1.1
Content-Length: 68137
Content-Type: multipart/form-data; boundary=---------------------------974767299852498929531610575
---------------------------974767299852498929531610575
Content-Disposition: form-data; name="description"
some text
---------------------------974767299852498929531610575
Content-Disposition: form-data; name="myFile"; filename="foo.txt"
Content-Type: text/plain
(content of the uploaded file foo.txt)
---------------------------974767299852498929531610575
服务端的编码
使用 org.apache.httpcomponents
库进行编码
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpmime -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.12</version>
</dependency>
Controller
通过 MultipartEntityBuilder
, 添加多个部件,每个部件有自己的名字,类型。构建出一个 HttpEntity
对象。可以从这个对象中获取到编码后的IO流以及ContentType
,直接响应给 客户端就完事儿,比较简单。
import java.io.File;
import java.nio.charset.StandardCharsets;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.HttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.StringBody;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriUtils;
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping
public void test (HttpServletResponse response) throws Exception {
HttpEntity httpEntity = MultipartEntityBuilder.create()
// 表单 => (部件名称,数据,类型),要注意uri编码
.addPart("name", new StringBody(UriUtils.encode("SpringBoot中文社区", StandardCharsets.UTF_8), ContentType.APPLICATION_FORM_URLENCODED))
// JSON => (部件名称,JSON,类型)
.addPart("info", new StringBody("{\"site\": \"https://springboot.io\", \"year\": 2019}", ContentType.APPLICATION_JSON))
// 文件 => ( 部件名称,文件,类型,文件名称)
.addBinaryBody("logo", new File("D:\\logo.png"), ContentType.IMAGE_PNG, "logo.png")
.build();
// 设置ContentType
response.setContentType(httpEntity.getContentType().getValue());
// 响应客户端
httpEntity.writeTo(response.getOutputStream());
}
}
客户端的解码
使用commons-fileupload
库进行解码
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
MultipartTest
看这个代码,会觉得似曾相识。不错,在Servlet3.0
以前,HttpServletRequest
还没有getPart
方法的时候 ,大家都是通过 commons-fileupload
来从multipart/form-data
请求中解析出数据的。
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileItemHeaders;
import org.apache.commons.fileupload.FileUploadBase;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.RequestContext;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.portlet.PortletFileUpload;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
/**
* 自己定义一个RequestContext的实现
*/
class SimpleRequestContext implements RequestContext {
private final Charset charset; // 编码
private final MediaType contentType; // contentType
private final InputStream content; // 数据
public SimpleRequestContext(Charset charset, MediaType contentType, InputStream content) {
this.charset = charset;
this.contentType = contentType;
this.content = content;
}
@Override
public String getCharacterEncoding() {
return this.charset.displayName();
}
@Override
public String getContentType() {
return this.contentType.toString();
}
@Override
public int getContentLength() {
try {
return this.content.available();
} catch (IOException e) {
}
return 0;
}
@Override
public InputStream getInputStream() throws IOException {
return this.content;
}
}
public class MultipartTest {
public static void main(String[] args) throws IOException, FileUploadException {
// 获取服务器响应的IO流
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<Resource> responseEntity = restTemplate.getForEntity("http://localhost:8081/test", Resource.class);
// 创建RequestContext对象
RequestContext requestContext = new SimpleRequestContext(StandardCharsets.UTF_8, responseEntity.getHeaders().getContentType(),
responseEntity.getBody().getInputStream());
// 解析器创建
FileUploadBase fileUploadBase = new PortletFileUpload();
FileItemFactory fileItemFactory = new DiskFileItemFactory();
fileUploadBase.setFileItemFactory(fileItemFactory);
fileUploadBase.setHeaderEncoding(StandardCharsets.UTF_8.displayName());
// 解析出所有的部件
List<FileItem> fileItems = fileUploadBase.parseRequest(requestContext);
for (FileItem fileItem : fileItems) {
// 请求头
System.out.println("headers:==========================");
FileItemHeaders fileItemHeaders = fileItem.getHeaders();
Iterator<String> headerNamesIterator = fileItemHeaders.getHeaderNames();
while (headerNamesIterator.hasNext()) { // 迭代name
String headerName = headerNamesIterator.next();
Iterator<String> headerValueIterator = fileItemHeaders.getHeaders(headerName);
while (headerValueIterator.hasNext()) { // 迭代value
String headerValue = headerValueIterator.next();
System.out.println(headerName + ":" + headerValue);
}
}
// 请求体
System.out.println("body:==========================");
if(fileItem.isFormField()) { // 是普通表单项
byte[] data = fileItem.get();
System.out.println(new String(data, StandardCharsets.UTF_8));
} else { // 是文件表单项
String fileName = fileItem.getName(); // 文件的原始名称
InputStream inputStream = fileItem.getInputStream(); // 文件的IO流
System.out.println("fileName=" + fileName + ", size=" + inputStream.available());
}
System.out.println();
}
}
}
完整的日志输出
17:18:55.384 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET http://localhost:8081/test
17:18:55.449 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[application/json, application/*+json, */*]
17:18:56.426 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
17:18:56.461 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [org.springframework.core.io.Resource] as "multipart/form-data;boundary=0W40KHiHJTyo5H_n1EIL68aM4tNRhPa-7Vp"
headers:==========================
content-disposition:form-data; name="name"
content-type:application/x-www-form-urlencoded; charset=ISO-8859-1
content-transfer-encoding:8bit
body:==========================
SpringBoot%E4%B8%AD%E6%96%87%E7%A4%BE%E5%8C%BA
headers:==========================
content-disposition:form-data; name="info"
content-type:application/json; charset=UTF-8
content-transfer-encoding:8bit
body:==========================
{"site": "https://springboot.io", "year": 2019}
headers:==========================
content-disposition:form-data; name="logo"; filename="logo.png"
content-type:image/png
content-transfer-encoding:binary
body:==========================
fileName=logo.png, size=2423
客户端准确的解析出了服务器响应的 multipart/form-data
数据。