客戶端解析伺服器響應的multipart/form-data資料

hahadelphi發表於2021-09-09

multipart/form-data,多部件請求體。這個請求體比較特殊,它可以拆分為多個部件,每個部件都有自己的headerbody,最常用的地方就是:客戶端檔案上傳,因為有多個部件,在上傳檔案的時候,還可以在body中新增其他的資料。jsonform。。。

一般來說,都是客戶端發起multipart/form-data請求 ,伺服器進行解析。而且這種東西的編碼解碼工作一般都是由底層的容器/框架完成。開發根本不必關心。但是我最近遇到了一個需求:

伺服器響應multipart/form-data(包含了一個二進位制檔案和其他的文字資料),客戶端來解析

意味著,需要自己完成2個東西

  1. 在服務端完成multipart/form-data的資料編碼,並且響應給客戶端
  2. 在客戶端獲取到響應後,進行資料的解碼

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 庫進行編碼

<!--  -->
<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": "", "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 庫進行解碼

<!--  -->
<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("", 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 
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": "", "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 資料。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2471/viewspace-2826119/,如需轉載,請註明出處,否則將追究法律責任。

相關文章