補習系列(2)-springboot mime類型處理

目標

  1. 瞭解http常見的mime類型定義;
  2. 如何使用springboot 處理json請求及響應;
  3. 如何使用springboot 處理 xml請求及響應;
  4. http參數的獲取及文件上傳下載;
  5. 如何得到原始請求的字節流; 6.瞭解springboot 如何實現內容轉換;

1、關於MIME

MIME的全稱是Multipurpose Internet Mail Extensions,即多用途互聯網郵件擴展,儘管讀起來有些拗口,但大多數人可能都知道, 這是HTTP協議中用來定義文檔性質及格式的標準。IETF RFC 6838,對HTTP傳輸內容類型進行了全面定義。 而IANA(互聯網號碼分配機構)是負責管理全部標準MIME類型的官方機構。能夠在這裏)找到全部的標準MIMEjavascript

服務器經過MIME告知響應內容類型,而瀏覽器則經過MIME類型來肯定如何處理文檔; 所以爲傳輸內容(文檔、圖片等)設置正確的MIME很是重要。css

一般Server會在HTTP響應中設置Content-Type,以下面的響應:html

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以告知服務器當前所發送內容的格式。 以下面的請求體:java

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請求頭,這個選項用於告知服務器應該返回什麼樣的數據格式(由客戶端接收並完成解析)。git

MIME的格式github

type/subtype

這是一個兩級的分類,比較容易理解,第一級分類一般包含:web

類型 描述
text 普通文本
image 某種圖像
audio 某種音頻文件
video 某種視頻文件
application 應用數據
multi-part 複合內容

而二級類型則很是多,如下是一些經常使用的MIME:spring

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如何實現幾個常見類型格式的處理。json

2、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方法。

  1. consumes = { MediaType.APPLICATION_JSON_UTF8_VALUE } 指定了該方法僅處理application/json的內容格式
  2. produces="application/json;charset=UTF-8" 則表示會在響應頭中指定Content-Type=application/json;charset=UTF-8
  3. @RequestBody 指定了將請求的輸入經過Json轉換爲DTO
  4. @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"
}

3、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是一個整合模塊的腳手架

4、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

5、文件上傳下載

對於文件上傳,咱們須要將請求聲明爲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:

  1. Content-Type:application/octet-stream,這表示響應的文檔是未知的二進制數據,大多數狀況下瀏覽器會直接下載;
  2. Content-Disposition →attachment;fileName=test.jpg,表示文檔應該做爲附件保存,併名稱爲test.jpg。

6、得到原始字節流

在某些狀況下,你可能須要得到原始的請求字節流,好比實現內容的過濾,或者爲了完成製做本身的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篇" ,期待更多精彩內容^-^

相關文章
相關標籤/搜索