补习系列-springboot mime类型处理

https://mp.weixin.qq.com/s/vWsqoV_5EIyLE0d0IrCbDA

目标

  1. 了解http常见的mime类型定义;

  2. 如何使用springboot 处理json请求及响应;

  3. 如何使用springboot 处理 xml请求及响应;

  4. http参数的获取及文件上传下载;

  5. 如何获得原始请求的字节流; 6.了解springboot 如何实现内容转换;

一、关于MIME

MIME的全称是Multipurpose Internet Mail Extensions,即多用途互联网邮件扩展,尽管读起来有些拗口,但大多数人可能都知道, 这是HTTP协议中用来定义文档性质及格式的标准。IETF RFC 6838,对HTTP传输内容类型进行了全面定义。 而 IANA(互联网号码分配机构)是负责管理所有标准MIME类型的官方机构。可以在这里)找到所有的标准MIME

服务器通过MIME告知响应内容类型,而浏览器则通过MIME类型来确定如何处理文档; 因此为传输内容(文档、图片等)设置正确的MIME非常重要。 通常Server会在HTTP响应中设置Content-Type,如下面的响应:

 
  1. HTTP/1.1 200 OK

  2. Server: Golfe2    

  3. Content-Length: 233

  4. Content-Type: application/html

  5. Date: Sun, 28 Dec 2018 02:55:19 GMT

这表示服务端将返回html格式的文档,而同样客户端也可以在HTTP请求中设置Content-Type以告知服务器当前所发送内容的格式。

如下面的请求体:

 
  1. POST / HTTP/1.1

  2. Host: localhost:8000

  3. User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0

  4. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

  5. Connection: keep-alive

  6. Content-Type: application/json

  7. Content-Length: 465

这表示客户端会发送application/json格式的数据到服务端,同时应该注意到Accept请求头,这个选项用于告知服务器应该返回什么样的数据格式(由客户端接收并完成解析)。

 

MIME的格式

 
  1. type/subtype

这是一个两级的分类,比较容易理解,第一级分类通常包含: 

类型描述
text普通文本
image某种图像
audio某种音频文件
video某种视频文件
application应用数据
multi-part复合内容

而二级类型则非常多,以下是一些常用的MIME: 

MIME描述
audio/wavwave音频流媒体文件
audio/webmwebm 音频文件格式
audio/oggogg多媒体文件格式的音频文件
audio/mpegmpeg多媒体文件格式的音频文件
image/gifgif图片
image/jpegjpeg图片
image/pngpng图片
image/svg+xmlsvg矢量图片
application/jsonjson格式
application/xmlxml格式
application/xhtml+xml扩展html格式
application/x-www-form-urlencoded表单url内容编码
application/octet-stream二进制格式
application/pdfpdf文档
application/atom+xmlatom订阅feed流
multipart/form-data多文档格式
text/plain普通文本
text/htmlhtml文档
text/csscss文件
text/javascriptjavascript文件
text/markdownmarkdown文档
video/mpegmpeg多媒体视频文件
video/quicktimemov多媒体视频文件

 

接下来,看看springboot如何实现几个常见类型格式的处理。

 

二、springboot-json处理

先看看这样一段代码:

 
  1.    @ResponseBody

  2.    @PostMapping(value = "/json", consumes= { MediaType.APPLICATION_JSON_UTF8_VALUE }, produces="application/json;charset=UTF-8")

  3.    public Map<String, Object> jsonIO(@RequestBody Map<String, Object> jsonData) {

  4.        Map<String, Object> resultData = new HashMap<>(jsonData);

  5.        resultData.put("resultCode", UUID.randomUUID().toString());

  6.        return resultData;

  7.    }

这是一个Controller层的方法定义,其中@PostMapping将该方法映射到/json路径的POST方法。

  1. consumes = { MediaType.APPLICATIONJSONUTF8_VALUE } 指定了该方法仅处理application/json的内容格式

  2. produces="application/json;charset=UTF-8" 则表示会在响应头中指定Content-Type=application/json;charset=UTF-8

  3. @RequestBody 指定了将请求的输入通过Json转换为DTO

  4. @ResponseBody 指定将响应对象转换为Json格式输出

 

通过观察请求响应,我们会得到以下的结果:

 
  1. ====> Request:

  2. Content-Type=application/json;

  3. {

  4.    "key": "value"

  5. }

  6. ====> Response:

  7. Content-Type=application/json;charset=UTF-8

  8. {

  9.    "resultCode": "1ec407e1-d753-4439-b31c-bb7e888aa6a2",

  10.    "key": "value"

  11. }

使用Postman工具进行调试,可以非常直观的获得想要的信息,点击这里可以下载

 

异常情况

如果,请求的内容格式不是json,而是其他的如application/x-www-form-urlencoded呢? 放心,框架会返回如下面的错误:

 
  1. {

  2.    "timestamp": 1530626924715,

  3.    "status": 415,

  4.    "error": "Unsupported Media Type",

  5.    "exception": "org.springframework.web.HttpMediaTypeNotSupportedException",

  6.    "message": "Content type 'application/x-www-form-urlencoded' not supported",

  7.    "path": "/content/json"

  8. }

 

三、springboot-xml处理

如上,通过springboot框架,我们快速实现了Json格式的输入输出。 那么,如何实现xml格式的处理呢?xml格式主要用于soap、rpc等领域,为了实现xml数据的序列化,我们需要添加jackson-xml依赖包

 
  1. <!-- support for xml bean -->

  2.    <dependency>

  3.        <groupId>com.fasterxml.jackson.dataformat</groupId>

  4.        <artifactId>jackson-dataformat-xml</artifactId>

  5.        <version>2.8.6</version>

  6.    </dependency>

接下来,声明一个Controller方法

 
  1.    @PostMapping(value = "/xml", consumes = {

  2.            MediaType.APPLICATION_XML_VALUE }, produces = MediaType.APPLICATION_XML_VALUE)

  3.    @ResponseBody

  4.    public ParamData xmlIO(@RequestBody ParamData data) {

  5.        data.setAge(data.getAge() + 1);

  6.        return data;

  7.    }

这次,我们指定了consumes、produces都是application/xml,通过@RequestBody、@ResponseBody注解之后, springboot框架会自动根据需求的内容格式进行转换。

这里的ParamData是一个简单的Pojo类:

 
  1.    public static class ParamData {

  2.  

  3.        private String name;

  4.        private int age;

  5.  

  6.        public String getName() {

  7.            return name;

  8.        }

  9.  

  10.        public void setName(String name) {

  11.            this.name = name;

  12.        }

  13.  

  14.        public int getAge() {

  15.            return age;

  16.        }

  17.  

  18.        public void setAge(int age) {

  19.            this.age = age;

  20.        }

  21.  

  22.    }

 

通过真实的请求-响应观测,我们得到如下的结果:

 
  1. ====> Request:

  2. Content-Type=application/xml;

  3. <ParamData>

  4.  <name>Jim</name>

  5.  <age>1</age>

  6. </ParamData>

  7. ====> Response:

  8. Content-Type=application/xml;charset=UTF-8

  9. <ParamData>

  10.    <name>Jim</name>

  11.    <age>2</age>

  12. </ParamData>

 

BTW,springboot 完成自动类型转换是通过内容协商实现的,相关的接口为ContentNegotiationManager。 默认情况下,对于声明了consumes及produce属性的方法,会按照声明的值进行处理,否则格式的转换会根据请求中的Content-Type、Accept头部来进行判断。

此外,实现请求/响应内容到DTO转换功能的是HttpMessageConverter接口。

准确说,内容转换是由springmvc框架提供,而springboot是一个整合模块的脚手架

 

四、http参数处理

对于普通的表单请求参数处理,我们通常有两种方式:

  • 通过方法参数映射

 
  1.   @PostMapping(value = "/form", consumes = {

  2.            MediaType.APPLICATION_FORM_URLENCODED_VALUE }, produces = MediaType.TEXT_PLAIN_VALUE)

  3.    @ResponseBody

  4.    public String form(@RequestParam("name") String name, @RequestParam("age") int age) {

  5.        return String.format("Welcome %s, you are %d years old", name, age);

  6.    }

  • 通过参数绑定

 
  1. @PostMapping(value = "/form1", consumes = {

  2.            MediaType.APPLICATION_FORM_URLENCODED_VALUE }, produces = MediaType.TEXT_PLAIN_VALUE)

  3.    @ResponseBody

  4.    public String form1(ParamData data) {

  5.        return String.format("Welcome %s, you are %d years old. Bye", data.getName(), data.getAge());

  6.    }

form表单的请求内容格式为application/x-www-form-urlencoded

一个请求的样例如下:

 
  1. ====>Request:

  2. Content-Length →40

  3. Content-Type →text/plain;charset=UTF-8

  4. Date →Mon, 16 Jul 2018 13:50:14 GMT

  5.  

  6. name=Lilei

  7. age=11

  8.  

  9. ====>Response:

  10. Content-Length →40

  11. Content-Type →text/plain;charset=UTF-8

  12. Date →Mon, 16 Jul 2018 13:50:14 GMT

  13.  

  14. Welcome Lilei, you are 11 years old. Bye

 

五、文件上传下载

对于文件上传,我们需要将请求声明为multipart/form-data格式,一个文件上传的请求样例如下:

 
  1. POST / HTTP/1.1

  2. Host: localhost:8000

  3. 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

  4. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

  5. Accept-Language: en-US,en;q=0.5

  6. Accept-Encoding: gzip, deflate

  7. Connection: keep-alive

  8. Upgrade-Insecure-Requests: 1

  9. Content-Type: multipart/form-data; boundary=---------------------------8721656041911415653955004498

  10. Content-Length: 465

  11.  

  12. -----------------------------8721656041911415653955004498

  13. Content-Disposition: form-data; name="name"

  14.  

  15. Test

  16. -----------------------------8721656041911415653955004498

  17. Content-Disposition: form-data; name="file"; filename="flower.jpg"

  18. Content-Type: image/jpeg

  19.  

  20. ....

  21. -----------------------------8721656041911415653955004498--

参照以下的代码可以实现简单的文件上传处理:

 
  1. @PostMapping(value = "file", consumes = {

  2.            MediaType.MULTIPART_FORM_DATA_VALUE }, produces = MediaType.TEXT_PLAIN_VALUE)

  3.    @ResponseBody

  4.    public String file(@RequestParam("name") String name, @RequestParam("file") MultipartFile file) {

  5.  

  6.        logger.info("file receive {}", name);

  7.  

  8.        if (file.isEmpty()) {

  9.            return "No File";

  10.        }

  11.        String fileName = file.getOriginalFilename();

  12.  

  13.        File root = new File("D:/temp");

  14.        if (!root.isDirectory()) {

  15.            root.mkdirs();

  16.        }

  17.        try {

  18.            file.transferTo(new File(root, name));

  19.            return String.format("Upload to %s", fileName);

  20.        } catch (IllegalStateException e) {

  21.            e.printStackTrace();

  22.        } catch (IOException e) {

  23.            e.printStackTrace();

  24.        }

  25.        return "Upload Failed";

  26.    }

 

这个例子非常简单,通过声明@RequestParam注解获得MultipartFile 对象,在获得上传文件后存储到服务器本地目录。 当然,在真实的项目应用中你需要做的更多,比如文件的大小、类型校验,将文件进行压缩或将文件存放到大容量、高稳定性的分布式文件存储系统等等。

 

这里不多啰嗦了,关于文件下载,可以通过以下的方法实现:

 
  1.    @GetMapping(path = "/download")

  2.    public ResponseEntity<Resource> download(@RequestParam("name") String name) throws IOException {

  3.  

  4.        File file = new File("D:/temp", name);

  5.        Path path = Paths.get(file.getAbsolutePath());

  6.        ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(path));

  7.        return ResponseEntity.ok().header("Content-Disposition", "attachment;fileName=" + name)

  8.                .contentLength(file.length()).contentType(MediaType.APPLICATION_OCTET_STREAM).body(resource);

  9.    }

聪明的读者一定会发现,除了将文件内容作为输出之外,我们还为响应添加两个header:

  1. Content-Type:application/octet-stream,这表示响应的文档是未知的二进制数据,大多数情况下浏览器会直接下载;

  2. Content-Disposition →attachment;fileName=test.jpg,表示文档应该作为附件保存,并名称为test.jpg。

 

六、获得原始字节流

在某些情况下,你可能需要获得原始的请求字节流,比如实现内容的过滤,或者为了完成制作自己的RPC接口。 在springboot中获得字节流非常简单,从Servlet API的定义中可以发现,直接通过HttpServletRequest对象便可以获取一个InputStream。

 

在我们定义的Controller方法中,还可以直接声明流类型的参数以获取数据。

 
  1.    @PostMapping(value = "/data", produces = MediaType.TEXT_PLAIN_VALUE)

  2.    @ResponseBody

  3.    public String rawIO(InputStream dataStream) throws Exception {

  4.        return IOUtils.toString(dataStream, "UTF-8");

  5.    }

然而,如果这么做了,你可能会遇到一些麻烦: 当请求头中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的实现中发现存在如下代码:

 
  1.    if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {

  2.            String paramValue = request.getParameter(this.methodParam);

  3.            if (StringUtils.hasLength(paramValue)) {

  4.                requestToUse = new HttpMethodRequestWrapper(request, paramValue);

  5.            }

  6.        }

 

由于getParameter被提前调用,导致后续获取InputStream为空。 该问题的解决方法是实现HttpServletRequest的代理,事先将InputStream保存起来供多次使用,通过高优先级的过滤器提前将Request对象置换可达到目的。 由于篇幅限制这里不做展开。感兴趣的可以参考这里获得更多信息。

 

小结

HTTP协议中定义了MIME标准,以实现传输内容格式的识别及转换。 本文介绍了常见的MIME类型,并结合springboot框架的代码样例,讲述如何完成Json/xml/字节流等常见类型的内容处理。

对于Http参数、文件的上传下载提供了简单代码示例,读者在充分了解用法之后可以进一步完善,并应用到实际的项目中去。

最后,欢迎继续关注"美码师的补习系列-springboot篇" ,期待更多精彩内容^-^

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值