一文学会 SpringBoot gzip 压缩传输(请求/响应)
前言
经常我们都会与服务端进行大数据量的文本传输,例如 JSON 就是常见的一种格式。通过 REST API 接口进行 GET 和 POST 请求,可能会有大量的文本格式数据提交、返回。然后对于文本,它有很高的压缩率,如果在 GET/POST 请求时候对文本进行压缩会节省大量的网络带宽,减少网络时延。
HTTP 协议在相应部分支持 Content-Encoding: gzip
,浏览器请求时带上 Accept-Encoding: gzip
即可,服务端对返回的 response body 进行压缩,并在 response 头带上 Content-Encoding: gzip
,浏览器会自动解析。
然而 HTTP 没有压缩 request body 的设计,因为在客户端发起请求时并不知道服务器是否支持压缩。因此没法通过 HTTP 协议来解决,只能在服务端做一些过滤器进行判断,人为约束。压缩和解压在提升网络带宽的同时,会带来 CPU 资源的损耗。
本文将手把手带你实现 SpringBoot 项目中,请求时 和响应时 对 body 文本进行 gzip 压缩,减小网络时延,提升传输效率。
一、请求压缩(request compress)
1. SpringBoot 整合 gzip
考虑到通用性,仿效 response 的 header Content-Encoding: gzip
方式。
客户端把压缩过的 json 作为 post-body 传输,然后增加一个 request header: Content-Encoding: gzip
来告诉服务器端是压缩的格式。
服务端增加一个 Filter,对 request 头进行检查,如果有 Content-Encoding 则解压缩后继续。这样不影响现有程序。
(1)Springboot 添加请求过滤器
增加 2 个类:
ContentEncodingFilter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package cn.frankfeekr.sample.controller;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Service;import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@Service public class ContentEncodingFilter extends OncePerRequestFilter { Logger logger = LoggerFactory.getLogger(ContentEncodingFilter.class); @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String conentEncoding = request.getHeader("Content-Encoding" ); if (conentEncoding != null && ("gzip" .equalsIgnoreCase(conentEncoding) || "deflate" .equalsIgnoreCase(conentEncoding))) { logger.trace("Content-Encoding: {}" , conentEncoding); chain.doFilter(new GZIPRequestWrapper(request), response); return ; } chain.doFilter(request, response); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 package cn.frankfeekr.sample.controller;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import javax.servlet.ReadListener;import javax.servlet.ServletInputStream;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletRequestWrapper;import java.io.IOException;import java.io.InputStream;import java.io.UnsupportedEncodingException;import java.util.zip.DeflaterInputStream;import java.util.zip.GZIPInputStream;public class GZIPRequestWrapper extends HttpServletRequestWrapper { private final static Logger logger = LoggerFactory.getLogger(GZIPRequestWrapper.class); protected HttpServletRequest request; public GZIPRequestWrapper (HttpServletRequest request) { super (request); this .request = request; } @Override public ServletInputStream getInputStream () throws IOException { ServletInputStream sis = request.getInputStream(); InputStream is = null ; String conentEncoding = request.getHeader("Content-Encoding" ); if ("gzip" .equalsIgnoreCase(conentEncoding)) { is = new GZIPInputStream(sis); } else if ("deflate" .equalsIgnoreCase(conentEncoding)) { is = new DeflaterInputStream(sis); } else { throw new UnsupportedEncodingException(conentEncoding + " is not supported." ); } final InputStream compressInputStream = is; return new ServletInputStream() { ReadListener readListener; @Override public int read () throws IOException { int b = compressInputStream.read(); if (b == -1 && readListener != null ) { readListener.onAllDataRead(); } return b; } @Override public boolean isFinished () { try { return compressInputStream.available() == 0 ; } catch (IOException e) { logger.error("error" , e); if (readListener != null ) { readListener.onError(e); } return false ; } } @Override public boolean isReady () { try { return compressInputStream.available() > 0 ; } catch (IOException e) { logger.error("error" , e); if (readListener != null ) { readListener.onError(e); } return false ; } } @Override public void setReadListener (final ReadListener readListener) { this .readListener = readListener; sis.setReadListener(new ReadListener() { @Override public void onDataAvailable () throws IOException { logger.trace("onDataAvailable" ); if (readListener != null ) { readListener.onDataAvailable(); } } @Override public void onAllDataRead () throws IOException { logger.trace("onAllDataRead" ); } @Override public void onError (Throwable throwable) { logger.error("onError" , throwable); if (readListener != null ) { readListener.onError(throwable); } } }); } }; } }
(2)SpringBoot Controller Demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package cn.frankfeekr.sample.controller;import com.alibaba.fastjson.JSON;import org.springframework.web.bind.annotation.*;import java.util.*;@ResponseBody @RestController @RequestMapping (produces = "application/json;charset=UTF-8" )public class HelloController { @RequestMapping (value = "/gzip" , method = RequestMethod.POST, consumes = "application/json" ) public String post (@RequestBody Map<String, String> request) { return request.toString(); } }
2. 客户端测试
(1)gzip body 方式请求
只要在 Headers 头域带上 gzip,即可告知服务器为通过 gzip 方式来提交
1 2 3 4 5 6 7 8 # 生成一个 gzip 压缩的包 echo '{"type": "json", "length": 2020, "name": "Frank"}' | gzip > body.gz # curl 命令模拟 POST gzip 压缩请求 curl --location --request POST 'http://127.0.0.1:9090/gzip' \ --header 'Content-Type: application/json' \ --header 'Content-Encoding: gzip' \ --data-binary '@body.gz'
断点调试结果如下:
(2)json body 方式请求
如果不需要进行压缩,则不带上 Content-Encoding: gzip
头域配置项即可。
1 2 3 4 5 # curl 命令模拟 gzip curl --location --request POST 'http://127.0.0.1:9090/gzip' \ --header 'Content-Type: application/json' \ --header 'Content-Encoding: gzip' \ --data-raw '{"type": "json", "length": 2020, "name": "Frank"}'
上述可以发现如果通过 data-raw 方式请求,则必须要去掉 --header 'Content-Encoding: gzip'
,否则会出现 400 Bad Request 错误。现正确请求如下:
1 2 3 4 # curl 命令模拟 gzip curl --location --request POST 'http://127.0.0.1:9090/gzip' \ --header 'Content-Type: application/json' \ --data-raw '{"type": "json", "length": 2020, "name": "Frank"}'
二、响应压缩(response compress)
1. SpringBoot 配置 response 策略
SpringBoot 默认是不开启 gzip 压缩的,需要我们手动开启,在配置文件中添加两行
1 2 3 4 server: compression: enabled: true mime-types: application/json,application/xml,text/html,text/plain,text/css,application/x-javascript
注意下上面配置中的 mime-types
,在 SpringBoot 2.0+ 的版本中,默认值如下,所以一般我们不需要特意添加这个配置
1 2 3 4 private String[] mimeTypes = new String[]{"text/html" , "text/xml" , "text/plain" , "text/css" , "text/javascript" , "application/javascript" , "application/json" , "application/xml" };
2. 测试
写一个测试的demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package cn.frankfeekr.sample.controller;import com.alibaba.fastjson.JSON;import org.springframework.web.bind.annotation.*;import java.util.*;@ResponseBody @RestController @RequestMapping (produces = "application/json;charset=UTF-8" )public class HelloController { @GetMapping ("bigReq" ) public String bigReqList () { List<String> result = new ArrayList<>(2048 ); for (int i = 0 ; i < 2048 ; i++) { result.add(UUID.randomUUID().toString()); } return JSON.toJSONString(result); } }
测试效果,可以明显发现请求 body 被压缩,时延也变小。
参考资料