目標
- 了解http常見的mime類型定義;
- 如何使用springboot 處理json請求及響應;
- 如何使用springboot 處理 xml請求及響應;
- http參數的獲取及文件上傳下載;
- 如何獲得原始請求的字節流;
6.了解springboot 如何實現內容轉換;
一、關於MIME
MIME的全稱是Multipurpose Internet Mail Extensions,即多用途互聯網郵件擴展,盡管讀起來有些拗口,但大多數人可能都知道,
這是HTTP協議中用來定義文檔性質及格式的標准。IETF RFC 6838,對HTTP傳輸內容類型進行了全面定義。
而IANA(互聯網號碼分配機構)是負責管理所有標准MIME類型的官方機構。可以在這里)找到所有的標准MIME
服務器通過MIME告知響應內容類型,而瀏覽器則通過MIME類型來確定如何處理文檔;
因此為傳輸內容(文檔、圖片等)設置正確的MIME非常重要。
通常Server會在HTTP響應中設置Content-Type,如下面的響應:
HTTP/1.1 200 OK
Server: Golfe2
Content-Length: 233
Content-Type: application/html
Date: Sun, 28 Dec 2018 02:55:19 GMT
這表示服務端將返回html格式的文檔,而同樣客戶端也可以在HTTP請求中設置Content-Type以告知服務器當前所發送內容的格式。
如下面的請求體:
POST / HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Connection: keep-alive
Content-Type: application/json
Content-Length: 465
這表示客戶端會發送application/json格式的數據到服務端,同時應該注意到Accept請求頭,這個選項用於告知服務器應該返回什么樣的數據格式(由客戶端接收並完成解析)。
MIME的格式
type/subtype
這是一個兩級的分類,比較容易理解,第一級分類通常包含:
類型 | 描述 |
---|---|
text | 普通文本 |
image | 某種圖像 |
audio | 某種音頻文件 |
video | 某種視頻文件 |
application | 應用數據 |
multi-part | 復合內容 |
而二級類型則非常多,以下是一些常用的MIME:
MIME | 描述 |
---|---|
audio/wav | wave音頻流媒體文件 |
audio/webm | webm 音頻文件格式 |
audio/ogg | ogg多媒體文件格式的音頻文件 |
audio/mpeg | mpeg多媒體文件格式的音頻文件 |
image/gif | gif圖片 |
image/jpeg | jpeg圖片 |
image/png | png圖片 |
image/svg+xml | svg矢量圖片 |
application/json | json格式 |
application/xml | xml格式 |
application/xhtml+xml | 擴展html格式 |
application/x-www-form-urlencoded | 表單url內容編碼 |
application/octet-stream | 二進制格式 |
application/pdf | pdf文檔 |
application/atom+xml | atom訂閱feed流 |
multipart/form-data | 多文檔格式 |
text/plain | 普通文本 |
text/html | html文檔 |
text/css | css文件 |
text/javascript | javascript文件 |
text/markdown | markdown文檔 |
video/mpeg | mpeg多媒體視頻文件 |
video/quicktime | mov多媒體視頻文件 |
接下來,看看springboot如何實現幾個常見類型格式的處理。
二、springboot-json處理
先看看這樣一段代碼:
@ResponseBody
@PostMapping(value = "/json", consumes= { MediaType.APPLICATION_JSON_UTF8_VALUE }, produces="application/json;charset=UTF-8")
public Map<String, Object> jsonIO(@RequestBody Map<String, Object> jsonData) {
Map<String, Object> resultData = new HashMap<>(jsonData);
resultData.put("resultCode", UUID.randomUUID().toString());
return resultData;
}
這是一個Controller層的方法定義,其中@PostMapping將該方法映射到/json路徑的POST方法。
- consumes = { MediaType.APPLICATION_JSON_UTF8_VALUE } 指定了該方法僅處理application/json的內容格式
- produces="application/json;charset=UTF-8" 則表示會在響應頭中指定Content-Type=application/json;charset=UTF-8
- @RequestBody 指定了將請求的輸入通過Json轉換為DTO
- @ResponseBody 指定將響應對象轉換為Json格式輸出
通過觀察請求響應,我們會得到以下的結果:
====> Request:
Content-Type=application/json;
{
"key": "value"
}
====> Response:
Content-Type=application/json;charset=UTF-8
{
"resultCode": "1ec407e1-d753-4439-b31c-bb7e888aa6a2",
"key": "value"
}
使用Postman工具進行調試,可以非常直觀的獲得想要的信息,點擊這里可以下載
異常情況
如果,請求的內容格式不是json,而是其他的如application/x-www-form-urlencoded呢?
放心,框架會返回如下面的錯誤:
{
"timestamp": 1530626924715,
"status": 415,
"error": "Unsupported Media Type",
"exception": "org.springframework.web.HttpMediaTypeNotSupportedException",
"message": "Content type 'application/x-www-form-urlencoded' not supported",
"path": "/content/json"
}
三、springboot-xml處理
如上,通過springboot框架,我們快速實現了Json格式的輸入輸出。
那么,如何實現xml格式的處理呢?xml格式主要用於soap、rpc等領域,為了實現xml數據的序列化,我們需要添加jackson-xml依賴包
<!-- support for xml bean -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.8.6</version>
</dependency>
接下來,聲明一個Controller方法
@PostMapping(value = "/xml", consumes = {
MediaType.APPLICATION_XML_VALUE }, produces = MediaType.APPLICATION_XML_VALUE)
@ResponseBody
public ParamData xmlIO(@RequestBody ParamData data) {
data.setAge(data.getAge() + 1);
return data;
}
這次,我們指定了consumes、produces都是application/xml,通過@RequestBody、@ResponseBody注解之后,
springboot框架會自動根據需求的內容格式進行轉換。
這里的ParamData是一個簡單的Pojo類:
public static class ParamData {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
通過真實的請求-響應觀測,我們得到如下的結果:
====> Request:
Content-Type=application/xml;
<ParamData>
<name>Jim</name>
<age>1</age>
</ParamData>
====> Response:
Content-Type=application/xml;charset=UTF-8
<ParamData>
<name>Jim</name>
<age>2</age>
</ParamData>
BTW,springboot 完成自動類型轉換是通過內容協商實現的,相關的接口為ContentNegotiationManager。
默認情況下,對於聲明了consumes及produce屬性的方法,會按照聲明的值進行處理,否則格式的轉換會根據請求中的Content-Type、Accept頭部來進行判斷。
此外,實現請求/響應內容到DTO轉換功能的是HttpMessageConverter接口。
准確說,內容轉換是由springmvc框架提供,而springboot是一個整合模塊的腳手架
四、http參數處理
對於普通的表單請求參數處理,我們通常有兩種方式:
- 通過方法參數映射
@PostMapping(value = "/form", consumes = {
MediaType.APPLICATION_FORM_URLENCODED_VALUE }, produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody
public String form(@RequestParam("name") String name, @RequestParam("age") int age) {
return String.format("Welcome %s, you are %d years old", name, age);
}
- 通過參數綁定
@PostMapping(value = "/form1", consumes = {
MediaType.APPLICATION_FORM_URLENCODED_VALUE }, produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody
public String form1(ParamData data) {
return String.format("Welcome %s, you are %d years old. Bye", data.getName(), data.getAge());
}
form表單的請求內容格式為application/x-www-form-urlencoded,
一個請求的樣例如下:
====>Request:
Content-Length →40
Content-Type →text/plain;charset=UTF-8
Date →Mon, 16 Jul 2018 13:50:14 GMT
name=Lilei
age=11
====>Response:
Content-Length →40
Content-Type →text/plain;charset=UTF-8
Date →Mon, 16 Jul 2018 13:50:14 GMT
Welcome Lilei, you are 11 years old. Bye
五、文件上傳下載
對於文件上傳,我們需要將請求聲明為multipart/form-data格式,一個文件上傳的請求樣例如下:
POST / HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=---------------------------8721656041911415653955004498
Content-Length: 465
-----------------------------8721656041911415653955004498
Content-Disposition: form-data; name="name"
Test
-----------------------------8721656041911415653955004498
Content-Disposition: form-data; name="file"; filename="flower.jpg"
Content-Type: image/jpeg
....
-----------------------------8721656041911415653955004498--
參照以下的代碼可以實現簡單的文件上傳處理:
@PostMapping(value = "file", consumes = {
MediaType.MULTIPART_FORM_DATA_VALUE }, produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody
public String file(@RequestParam("name") String name, @RequestParam("file") MultipartFile file) {
logger.info("file receive {}", name);
if (file.isEmpty()) {
return "No File";
}
String fileName = file.getOriginalFilename();
File root = new File("D:/temp");
if (!root.isDirectory()) {
root.mkdirs();
}
try {
file.transferTo(new File(root, name));
return String.format("Upload to %s", fileName);
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return "Upload Failed";
}
這個例子非常簡單,通過聲明@RequestParam注解獲得MultipartFile 對象,在獲得上傳文件后存儲到服務器本地目錄。
當然,在真實的項目應用中你需要做的更多,比如文件的大小、類型校驗,將文件進行壓縮或將文件存放到大容量、高穩定性的分布式文件存儲系統等等。
這里不多啰嗦了,關於文件下載,可以通過以下的方法實現:
@GetMapping(path = "/download")
public ResponseEntity<Resource> download(@RequestParam("name") String name) throws IOException {
File file = new File("D:/temp", name);
Path path = Paths.get(file.getAbsolutePath());
ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(path));
return ResponseEntity.ok().header("Content-Disposition", "attachment;fileName=" + name)
.contentLength(file.length()).contentType(MediaType.APPLICATION_OCTET_STREAM).body(resource);
}
聰明的讀者一定會發現,除了將文件內容作為輸出之外,我們還為響應添加兩個header:
- Content-Type:application/octet-stream,這表示響應的文檔是未知的二進制數據,大多數情況下瀏覽器會直接下載;
- Content-Disposition →attachment;fileName=test.jpg,表示文檔應該作為附件保存,並名稱為test.jpg。
六、獲得原始字節流
在某些情況下,你可能需要獲得原始的請求字節流,比如實現內容的過濾,或者為了完成制作自己的RPC接口。
在springboot中獲得字節流非常簡單,從Servlet API的定義中可以發現,直接通過HttpServletRequest對象便可以獲取一個InputStream。
在我們定義的Controller方法中,還可以直接聲明流類型的參數以獲取數據。
@PostMapping(value = "/data", produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody
public String rawIO(InputStream dataStream) throws Exception {
return IOUtils.toString(dataStream, "UTF-8");
}
然而,如果這么做了,你可能會遇到一些麻煩:
當請求頭中Content-Type=application/x-www-form-urlencoded 時,你會獲得一個空的InputStream!
筆者曾經在制作代理服務器的時候遇到了這個問題,經過一番查閱,發現問題的原因在於:
按照Servlet規范,如果同時滿足下列條件,則請求體(Entity)中的表單數據,將被填充到request的parameter集合中(導致inputstream為空)。
1 這是一個HTTP/HTTPS請求
2 請求方法是POST
3 請求的類型Content-Type=application/x-www-form-urlencoded
4 Servlet調用了getParameter系列方法
springboot框架內置了HiddenHttpMethodFilter,用於支持瀏覽器form表單無法支持put/delete等請求方法的問題。
在Filter的實現中發現存在如下代碼:
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
requestToUse = new HttpMethodRequestWrapper(request, paramValue);
}
}
由於getParameter被提前調用,導致后續獲取InputStream為空。
該問題的解決方法是實現HttpServletRequest的代理,事先將InputStream保存起來供多次使用,通過高優先級的過濾器提前將Request對象置換可達到目的。
由於篇幅限制這里不做展開。感興趣的可以參考這里獲得更多信息。
參考文檔
mozilla開發手冊-MIME
springboot-requestmapping usage
JavaServlet3.1規范筆記
ServletRequest-InputStream多次獲取
小結
HTTP協議中定義了MIME標准,以實現傳輸內容格式的識別及轉換。
本文介紹了常見的MIME類型,並結合springboot框架的代碼樣例,講述如何完成Json/xml/字節流等常見類型的內容處理。
對於Http參數、文件的上傳下載提供了簡單代碼示例,讀者在充分了解用法之后可以進一步完善,並應用到實際的項目中去。
最后,歡迎繼續關注"美碼師的補習系列-springboot篇" ,期待更多精彩內容-