Spring Boot + Gzip 压缩超大 JSON 对象,传输大小减少一半!
近期文章精选 :
Java面试指南网站:javaguide.cn
来源:https://blog.csdn.net/weixin_43441509/article/details/123816603
1. 业务背景
是这样的,业务背景是公司的内部系统有一个广告保存接口,需要 ADX 那边将投放的广告数据进行保存供后续使用。广告数据大概长这样:
{
"adName":"",
"adTag":""
}
-
adName
:广告名字 -
adTag
:广告渲染的 HTML 代码,超级大数据库中都是用 text 类型来存放的,我看到最大的 adTag 足足有 60kb 大小…
因此,对与请求数据那么大的接口我们肯定是需要作一个优化的否则太大的数据传输有以下几个弊端:
- 占用网络带宽,而有些云产品就是按照带宽来计费的,间接浪费了钱
- 传输数据大导致网络传输耗时
为了克服这几个问题团队中的老鸟产生一个想法:
请求广告保存接口时先将 JSON 对象字符串进行 GZIP 压缩,那请求时传入的就是压缩后的数据,而 GZIP 的压缩效率是很高的,因此可以大大减小传输数据,而当数据到达广告保存接口前再将传来的数据进行解压缩,还原成 JSON 对象就完成了整个 GZIP 压缩数据的请求以及处理流程。
其实这样做也存在着弊端:
-
需要额外占用更多的 CPU 计算资源
-
可能会影响到原有的其他接口
-
请求变复杂了: 接口调用方那边需要对数据进行压缩、 接口执行方那边需要对拿到的数据进行解压
对于以上几点基于我们公司当前的业务可以这样解决:
- 对与需要占用而外的 CPU 计算资源来说,公司的内部系统属于 IO 密集型应用,因此用一些 CPU 资源来换取更快的网络传输其实是很划算的
- 使用过滤器在请求数据到达 Controller 之前对数据进行解压缩处理后重新写回到 Body 中,避免影响 Controller 的逻辑,代码零侵入
- 而对于改造接口的同时是否会影响到原来的接口这一点可以通过 HttpHeader 的 Content-Encoding=gzip 属性来区分是否需要对请求数据进行解压缩
那废话少说,下面给出实现方案
2. 实现思路
前置知识:
- Http 请求结构以及 Content-Encoding 属性
- GZIP 压缩方式
- Servlet Filter
- HttpServletRequestWrapper
- Spring Boot
- Java 输入输出流
实现流程图 :
图片核心代码:
创建一个 SpringBoot 项目,先编写一个接口,功能很简单就是传入一个 JSON 对象并返回,以模拟将广告数据保存到数据库
/**
* @ClassName: ProjectController
* @Author zhangjin
* @Date 2022/3/24 20:41
* @Description:
*/
@Slf4j
@RestController
public class AdvertisingController {
@PostMapping("/save")
public Advertising saveProject(@RequestBody Advertising advertising) {
log.info("获取内容"+ advertising);
return advertising;
}
}
/**
* @ClassName: Project
* @Author zhangjin
* @Date 2022/3/24 20:42
* @Description:
*/
@Data
public class Advertising {
private String adName;
private String adTag;
}
编写并注册一个拦截器
/**
* @ClassName: GZIPFilter
* @Author zhangjin
* @Date 2022/3/26 0:36
* @Description:
*/
@Slf4j
@Component
public class GZIPFilter implements Filter {
private static final String CONTENT_ENCODING = "Content-Encoding";
private static final String CONTENT_ENCODING_TYPE = "gzip";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("init GZIPFilter");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
long start = System.currentTimeMillis();
HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
String encodeType = httpServletRequest.getHeader(CONTENT_ENCODING);
if (CONTENT_ENCODING_TYPE.equals(encodeType)) {
log.info("请求:{} 需要解压", httpServletRequest.getRequestURI());
UnZIPRequestWrapper unZIPRequestWrapper = new UnZIPRequestWrapper(httpServletRequest);
filterChain.doFilter(unZIPRequestWrapper,servletResponse);
}
else {
log.info("请求:{} 无需解压", httpServletRequest.getRequestURI());
filterChain.doFilter(servletRequest,servletResponse);
}
log.info("耗时:{}ms", System.currentTimeMillis() - start);
}
@Override
public void destroy() {
log.info("destroy GZIPFilter");
}
}
/**
* @ClassName: FilterRegistration
* @Author zhangjin
* @Date 2022/3/26 0:36
* @Description:
*/
@Configuration
public class FilterRegistration {
@Resource
private GZIPFilter gzipFilter;
@Bean
public FilterRegistrationBean<GZIPFilter> gzipFilterRegistrationBean() {
FilterRegistrationBean<GZIPFilter> registration = new FilterRegistrationBean<>();
//Filter可以new,也可以使用依赖注入Bean
registration.setFilter(gzipFilter);
//过滤器名称
registration.setName("gzipFilter");
//拦截路径
registration.addUrlPatterns("/*");
//设置顺序
registration.setOrder(1);
return registration;
}
}
实现 RequestWrapper
实现解压和写回 Body 的逻辑
/**
* @ClassName: UnZIPRequestWrapper
* @Author zhangjin
* @Date 2022/3/26 11:02
* @Description: JsonString经过压缩后保存为二进制文件 -> 解压缩后还原成JsonString转换成byte[] 写回body中
*/
@Slf4j
public class UnZIPRequestWrapper extends HttpServletRequestWrapper {
private final byte[] bytes;
public UnZIPRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
try (BufferedInputStream bis = new BufferedInputStream(request.getInputStream());
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
final byte[] body;
byte[] buffer = new byte[1024];
int len;
while ((len = bis.read(buffer)) > 0) {
baos.write(buffer, 0, len);
}
body = baos.toByteArray();
if (body.length == 0) {
log.info("Body无内容,无需解压");
bytes = body;
return;
}
this.bytes = GZIPUtils.uncompressToByteArray(body);
} catch (IOException ex) {
log.info("解压缩步骤发生异常!");
ex.printStackTrace();
throw ex;
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
附上压缩工具类
public class GZIPUtils {
public static final String GZIP_ENCODE_UTF_8 = "UTF-8";
/**
* 字符串压缩为GZIP字节数组
* @param str
* @return
*/
public static byte[] compress(String str) {
return compress(str, GZIP_ENCODE_UTF_8);
}
/**
* 字符串压缩为GZIP字节数组
* @param str
* @param encoding
* @return
*/
public static byte[] compress(String str, String encoding) {
if (str == null || str.length() == 0) {
return null;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPOutputStream gzip;
try {
gzip = new GZIPOutputStream(out);
gzip.write(str.getBytes(encoding));
gzip.close();
} catch (IOException e) {
e.printStackTrace();
}
return out.toByteArray();
}
/**
* GZIP解压缩
* @param bytes
* @return
*/
public static byte[] uncompress(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return null;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
try {
GZIPInputStream ungzip = new GZIPInputStream(in);
byte[] buffer = new byte[256];
int n;
while ((n = ungzip.read(buffer)) >= 0) {
out.write(buffer, 0, n);
}
} catch (IOException e) {
e.printStackTrace();
}
return out.toByteArray();
}
/**
* 解压并返回String
* @param bytes
* @return
*/
public static String uncompressToString(byte[] bytes) throws IOException {
return uncompressToString(bytes, GZIP_ENCODE_UTF_8);
}
/**
*
* @param bytes
* @return
*/
public static byte[] uncompressToByteArray(byte[] bytes) throws IOException {
return uncompressToByteArray(bytes, GZIP_ENCODE_UTF_8);
}
/**
* 解压成字符串
* @param bytes 压缩后的字节数组
* @param encoding 编码方式
* @return 解压后的字符串
*/
public static String uncompressToString(byte[] bytes, String encoding) throws IOException {
byte[] result = uncompressToByteArray(bytes, encoding);
return new String(result);
}
/**
* 解压成字节数组
* @param bytes
* @param encoding
* @return
*/
public static byte[] uncompressToByteArray(byte[] bytes, String encoding) throws IOException {
if (bytes == null || bytes.length == 0) {
return null;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
try {
GZIPInputStream ungzip = new GZIPInputStream(in);
byte[] buffer = new byte[256];
int n;
while ((n = ungzip.read(buffer)) >= 0) {
out.write(buffer, 0, n);
}
return out.toByteArray();
} catch (IOException e) {
e.printStackTrace();
throw new IOException("解压缩失败!");
}
}
/**
* 将字节流转换成文件
* @param filename
* @param data
* @throws Exception
*/
public static void saveFile(String filename,byte [] data)throws Exception{
if(data != null){
String filepath ="/" + filename;
File file = new File(filepath);
if(file.exists()){
file.delete();
}
FileOutputStream fos = new FileOutputStream(file);
fos.write(data,0,data.length);
fos.flush();
fos.close();
System.out.println(file);
}
}
3. 测试效果
注意一个大坑:千万不要直接将压缩后的 byte[]
当作字符串进行传输,否则你会发现压缩后的请求数据竟然比没压缩后的要大得多 🐶!一般有两种传输压缩后的 byte[]的方式:
-
将压缩后的
byet[]
进行 base64 编码再传输字符串,这种方式会损失掉一部分 GZIP 的压缩效果,适用于压缩结果要存储在 Redis 中的情况 -
将压缩后的
byte[]
以二进制的形式写入到文件中,请求时直接在 body 中带上文件即可,用这种方式可以不损失压缩效果
Postman 测试 GZIP 压缩数据请求:
- 请求头指定数据压缩方式:
-
Body 带上压缩后的
byte[]
写入的二进制文件
- 执行请求,服务端正确处理了请求并且请求 size 缩小了将近一半,效果还是很不错的。
4. Demo 地址
这样 GZIP 压缩数据的请求的处理就完成了,完整的项目代码在下方:
https://gitee.com/wx_1bceb446a4/gziptest
········· END ··············
👉 欢迎准备 Java 面试以及学习 Java 的同学加入我的 知识星球 ,干货很多!收费虽然是白菜价,但星球里的内容或许比你参加上万的培训班质量还要高。
👉 《Java 面试指北》 持续更新完善中!这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ......)、优质面经等内容。
推荐阅读 :
👉 如果本文对你有帮助的话,欢迎 点赞&在看&分享 ,这对我继续分享&创作优质文章非常重要。非常感谢!