0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Spring Boot+Filter实现Gzip压缩超大json对象

jf_ro2CN3Fa 来源:芋道源码 作者:芋道源码 2022-12-01 10:18 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群


1. 业务背景

是这样的,业务背景是公司的内部系统有一个广告保存接口,需要ADX那边将投放的广告数据进行保存供后续使用。 广告数据大概长这样:

  • adName是广告名字
  • adTag是广告渲染的HTML代码,超级大数据库中都是用text类型来存放的,我看到最大的adTag足足有60kb大小…
{
"adName":"",
"adTag":""
}

因此,对与请求数据那么大的接口我们肯定是需要作一个优化的否则太大的数据传输有以下几个弊端:

  • 占用网络带宽,而有些云产品就是按照带宽来计费的,间接浪费了钱
  • 传输数据大导致网络传输耗时

为了克服这几个问题团队中的老鸟产生一个想法:

请求广告保存接口时先将Json对象字符串进行GZIP压缩,那请求时传入的就是压缩后的数据,而GZIP的压缩效率是很高的,因此可以大大减小传输数据,而当数据到达广告保存接口前再将传来的数据进行解压缩,还原成JSON对象就完成了整个GZIP压缩数据的请求以及处理流程。

其实这样做也存在着弊端:

  • 请求变复杂了

    • 接口调用方那边需要对数据进行压缩
    • 接口执行方那边需要对拿到的数据进行解压
  • 需要额外占用更多的CPU计算资源

  • 可能会影响到原有的其他接口

对于以上几点基于我们公司当前的业务可以这样解决:

  • 对与需要占用而外的CPU计算资源来说,公司的内部系统属于IO密集型应用,因此用一些CPU资源来换取更快的网络传输其实是很划算的
  • 使用过滤器在请求数据到达Controller之前对数据进行解压缩处理后重新写回到Body中,避免影响Controller的逻辑,代码零侵入
  • 而对于改造接口的同时是否会影响到原来的接口这一点可以通过 HttpHeader 的Content-Encoding=gzip属性来区分是否需要对请求数据进行解压缩

那废话少说,下面给出实现方案

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

2. 实现思路

前置知识:
  • Http 请求结构以及Content-Encoding 属性
  • gzip压缩方式
  • Servlet Filter
  • HttpServletRequestWrapper
  • Spring Boot
  • Java 输入输出流

实现流程图:

e6600140-711c-11ed-8abf-dac502259ad0.png
核心代码:

创建一个SpringBoot项目,先编写一个接口,功能很简单就是传入一个Json对象并返回,以模拟将广告数据保存到数据库

/**
*@ClassName:ProjectController
*@Authorzhangjin
*@Date2022/3/2420:41
*@Description:
*/
@Slf4j
@RestController
publicclassAdvertisingController{

@PostMapping("/save")
publicAdvertisingsaveProject(@RequestBodyAdvertisingadvertising){
log.info("获取内容"+advertising);
returnadvertising;
}
}

/**
*@ClassName:Project
*@Authorzhangjin
*@Date2022/3/2420:42
*@Description:
*/
@Data
publicclassAdvertising{
privateStringadName;
privateStringadTag;
}

编写并注册一个拦截器

/**
*@ClassName:GZIPFilter
*@Authorzhangjin
*@Date2022/3/260:36
*@Description:
*/
@Slf4j
@Component
publicclassGZIPFilterimplementsFilter{

privatestaticfinalStringCONTENT_ENCODING="Content-Encoding";
privatestaticfinalStringCONTENT_ENCODING_TYPE="gzip";

@Override
publicvoidinit(FilterConfigfilterConfig)throwsServletException{
log.info("initGZIPFilter");
}

@Override
publicvoiddoFilter(ServletRequestservletRequest,ServletResponseservletResponse,FilterChainfilterChain)throwsIOException,ServletException{
longstart=System.currentTimeMillis();
HttpServletRequesthttpServletRequest=(HttpServletRequest)servletRequest;

StringencodeType=httpServletRequest.getHeader(CONTENT_ENCODING);
if(CONTENT_ENCODING_TYPE.equals(encodeType)){
log.info("请求:{}需要解压",httpServletRequest.getRequestURI());
UnZIPRequestWrapperunZIPRequestWrapper=newUnZIPRequestWrapper(httpServletRequest);
filterChain.doFilter(unZIPRequestWrapper,servletResponse);
}
else{
log.info("请求:{}无需解压",httpServletRequest.getRequestURI());
filterChain.doFilter(servletRequest,servletResponse);
}
log.info("耗时:{}ms",System.currentTimeMillis()-start);
}

@Override
publicvoiddestroy(){
log.info("destroyGZIPFilter");
}
}

/**
*@ClassName:FilterRegistration
*@Authorzhangjin
*@Date2022/3/260:36
*@Description:
*/
@Configuration
publicclassFilterRegistration{

@Resource
privateGZIPFiltergzipFilter;

@Bean
publicFilterRegistrationBeangzipFilterRegistrationBean(){
FilterRegistrationBeanregistration=newFilterRegistrationBean<>();
//Filter可以new,也可以使用依赖注入Bean
registration.setFilter(gzipFilter);
//过滤器名称
registration.setName("gzipFilter");
//拦截路径
registration.addUrlPatterns("/*");
//设置顺序
registration.setOrder(1);
returnregistration;
}
}

实现RequestWrapper实现解压和写回Body的逻辑

/**
*@ClassName:UnZIPRequestWrapper
*@Authorzhangjin
*@Date2022/3/2611:02
*@Description:JsonString经过压缩后保存为二进制文件->解压缩后还原成JsonString转换成byte[]写回body中
*/
@Slf4j
publicclassUnZIPRequestWrapperextendsHttpServletRequestWrapper{

privatefinalbyte[]bytes;

publicUnZIPRequestWrapper(HttpServletRequestrequest)throwsIOException{
super(request);
try(BufferedInputStreambis=newBufferedInputStream(request.getInputStream());
ByteArrayOutputStreambaos=newByteArrayOutputStream()){
finalbyte[]body;
byte[]buffer=newbyte[1024];
intlen;
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(IOExceptionex){
log.info("解压缩步骤发生异常!");
ex.printStackTrace();
throwex;
}
}

@Override
publicServletInputStreamgetInputStream()throwsIOException{
finalByteArrayInputStreambyteArrayInputStream=newByteArrayInputStream(bytes);
returnnewServletInputStream(){

@Override
publicbooleanisFinished(){
returnfalse;
}

@Override
publicbooleanisReady(){
returnfalse;
}

@Override
publicvoidsetReadListener(ReadListenerreadListener){

}

publicintread()throwsIOException{
returnbyteArrayInputStream.read();
}
};
}

@Override
publicBufferedReadergetReader()throwsIOException{
returnnewBufferedReader(newInputStreamReader(this.getInputStream()));
}

}

附上压缩工具类

publicclassGZIPUtils{

publicstaticfinalStringGZIP_ENCODE_UTF_8="UTF-8";

/**
*字符串压缩为GZIP字节数组
*@paramstr
*@return
*/
publicstaticbyte[]compress(Stringstr){
returncompress(str,GZIP_ENCODE_UTF_8);
}

/**
*字符串压缩为GZIP字节数组
*@paramstr
*@paramencoding
*@return
*/
publicstaticbyte[]compress(Stringstr,Stringencoding){
if(str==null||str.length()==0){
returnnull;
}
ByteArrayOutputStreamout=newByteArrayOutputStream();
GZIPOutputStreamgzip;
try{
gzip=newGZIPOutputStream(out);
gzip.write(str.getBytes(encoding));
gzip.close();
}catch(IOExceptione){
e.printStackTrace();
}
returnout.toByteArray();
}

/**
*GZIP解压缩
*@parambytes
*@return
*/
publicstaticbyte[]uncompress(byte[]bytes){
if(bytes==null||bytes.length==0){
returnnull;
}
ByteArrayOutputStreamout=newByteArrayOutputStream();
ByteArrayInputStreamin=newByteArrayInputStream(bytes);
try{
GZIPInputStreamungzip=newGZIPInputStream(in);
byte[]buffer=newbyte[256];
intn;
while((n=ungzip.read(buffer))>=0){
out.write(buffer,0,n);
}
}catch(IOExceptione){
e.printStackTrace();
}
returnout.toByteArray();
}

/**
*解压并返回String
*@parambytes
*@return
*/
publicstaticStringuncompressToString(byte[]bytes)throwsIOException{
returnuncompressToString(bytes,GZIP_ENCODE_UTF_8);
}

/**
*
*@parambytes
*@return
*/
publicstaticbyte[]uncompressToByteArray(byte[]bytes)throwsIOException{
returnuncompressToByteArray(bytes,GZIP_ENCODE_UTF_8);
}

/**
*解压成字符串
*@parambytes压缩后的字节数组
*@paramencoding编码方式
*@return解压后的字符串
*/
publicstaticStringuncompressToString(byte[]bytes,Stringencoding)throwsIOException{
byte[]result=uncompressToByteArray(bytes,encoding);
returnnewString(result);
}

/**
*解压成字节数组
*@parambytes
*@paramencoding
*@return
*/
publicstaticbyte[]uncompressToByteArray(byte[]bytes,Stringencoding)throwsIOException{
if(bytes==null||bytes.length==0){
returnnull;
}
ByteArrayOutputStreamout=newByteArrayOutputStream();
ByteArrayInputStreamin=newByteArrayInputStream(bytes);
try{
GZIPInputStreamungzip=newGZIPInputStream(in);
byte[]buffer=newbyte[256];
intn;
while((n=ungzip.read(buffer))>=0){
out.write(buffer,0,n);
}
returnout.toByteArray();
}catch(IOExceptione){
e.printStackTrace();
thrownewIOException("解压缩失败!");
}
}

/**
*将字节流转换成文件
*@paramfilename
*@paramdata
*@throwsException
*/
publicstaticvoidsaveFile(Stringfilename,byte[]data)throwsException{
if(data!=null){
Stringfilepath="/"+filename;
Filefile=newFile(filepath);
if(file.exists()){
file.delete();
}
FileOutputStreamfos=newFileOutputStream(file);
fos.write(data,0,data.length);
fos.flush();
fos.close();
System.out.println(file);
}
}

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

3. 测试效果

注意一个大坑:千万不要直接将压缩后的byte[]当作字符串进行传输,否则你会发现压缩后的请求数据竟然比没压缩后的要大得多!一般有两种传输压缩后的byte[]的方式:

  • 将压缩后的byet[]进行base64编码再传输字符串,这种方式会损失掉一部分GZIP的压缩效果,适用于压缩结果要存储在Redis中的情况
  • 将压缩后的byte[]以二进制的形式写入到文件中,请求时直接在body中带上文件即可,用这种方式可以不损失压缩效果

Postman测试Gzip压缩数据请求:

  • 请求头指定数据压缩方式:
e66cba2a-711c-11ed-8abf-dac502259ad0.png
  • Body带上压缩后的byte[]写入的二进制文件
e67736f8-711c-11ed-8abf-dac502259ad0.png
  • 执行请求,服务端正确处理了请求并且请求size缩小了将近一半,效果还是很不错的,这样GZIP压缩数据的请求的处理就完成了,完整的项目代码在下方
e69f980a-711c-11ed-8abf-dac502259ad0.png

4. Demo地址

  • https://gitee.com/wx_1bceb446a4/gziptest


审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • spring
    +关注

    关注

    0

    文章

    341

    浏览量

    16045
  • 传输数据
    +关注

    关注

    1

    文章

    128

    浏览量

    16522
  • 大数据
    +关注

    关注

    64

    文章

    9092

    浏览量

    144042

原文标题:Spring Boot + Filter 实现 Gzip 压缩超大 json 对象,传输耗时大大减少

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    JSON:简洁代码高效搞定序列化与反序列化

    面对频繁的数据交互需求,用最简方式实现JSON序列化与反序列化已成为开发者必备技能,借助主流库,轻松实现零负担数据转换。JSON(JavaScriptObjectNotation)是一
    的头像 发表于 02-25 19:04 210次阅读
    <b class='flag-5'>JSON</b>:简洁代码高效搞定序列化与反序列化

    深入解析U-Boot image.c:RK平台镜像处理核心逻辑

    的SD/NAND/SPI等启动方式做了专属适配。本文将拆解image.c的核心逻辑,梳理RK平台镜像处理的关键流程,帮助开发者理解和调试启动相关问题。 一、文件定位与核心作用 image.c是U-Boot中镜像管理的核心模块,主要负责: •定义uImage镜像的属性枚举(架构、系统、类型、
    的头像 发表于 02-24 16:46 1764次阅读
    深入解析U-<b class='flag-5'>Boot</b> image.c:RK平台镜像处理核心逻辑

    极简代码,搞定JSON序列化与反序列化

    快速实现JSON数据的生成(序列化)与解析(反序列化)。 目前json库已全面支持LuatOS开发系列产品,开发者可根据项目实际需求,灵活选用并进行快速集成与开发。 一、 JSON基础
    的头像 发表于 02-23 21:46 428次阅读
    极简代码,搞定<b class='flag-5'>JSON</b>序列化与反序列化

    解析Rockchip平台U-Boot核心文件:boot_rkimg.c到底做了什么?

    在嵌入式开发中,U-Boot 作为引导程序的 “中流砥柱”,负责初始化硬件、加载内核并启动系统。对于 Rockchip 平台的设备(如常见的开发板、智能终端),boot_rkimg.c 是 U-Boot 中专门处理启动流程的核心
    的头像 发表于 02-03 15:29 929次阅读
    解析Rockchip平台U-<b class='flag-5'>Boot</b>核心文件:<b class='flag-5'>boot</b>_rkimg.c到底做了什么?

    Nginx Gzip压缩配置指南

    说起Gzip压缩,可能很多人觉得这是个老生常谈的话题。但我在这几年的运维工作中发现,真正把Gzip配置到位的网站其实不多。去年帮一个客户做性能优化,他们的网站日均带宽消耗在2TB左右,一看Nginx
    的头像 发表于 01-30 16:03 465次阅读

    从0到1搭建实时日志监控系统:基于WebSocket + Elasticsearch的实战方案

    问题。 WebSocket断连重试 :前端实现指数退避重连机制。 数据压缩 :对大文本日志启用Gzip压缩,减少带宽占用。 5. 最终效果 实时性 :日志从产生到展示延迟 < 1
    发表于 01-09 16:43

    详解DBC的Signal与JSON文本结合

    为了优化CAN数据发送与接收的操作流程,更改以前手动输入状态对应数据的模式,采用下拉列表选择内容,但这需要用到超出DBC原有承载能力的信息。因此,将JSON与其结合,采用JSON格式文本写入Signal的Comment属性,将Comment属性的字符串通过
    的头像 发表于 01-06 10:57 411次阅读
    详解DBC的Signal与<b class='flag-5'>JSON</b>文本结合

    linux的压缩和解压操作

    对于重要的文件我们不会考虑这样的方式。无损压缩不会影响文件,所以对于压缩我们最先考虑的是使用无损压缩的方式。 2、 单个文件压缩和解压用 gzip
    发表于 12-23 06:56

    借助CXL和压缩技术实现高效数据传输

    AI、科学计算、海量内存处理……这些硬核工作负载正在不断挑战系统极限。而 FPGA 异军突起,成为了实现高效数据传输的“关键推手”。想知道怎么在不改变整体架构的前提下,让带宽和能效实现“双飞跃”?答案就藏在压缩 IP 与基于 C
    的头像 发表于 12-19 09:43 507次阅读
    借助CXL和<b class='flag-5'>压缩</b>技术<b class='flag-5'>实现</b>高效数据传输

    电能质量在线监测装置的数据压缩存储功能支持的数据格式可以修改吗?

    操作性与法律效力 压缩算法 / 容器格式 ZLIB、GZIP、ZIP、LZ4 部分可配置 厂家会提供有限选择
    的头像 发表于 12-11 16:39 1213次阅读
    电能质量在线监测装置的数据<b class='flag-5'>压缩</b>存储功能支持的数据格式可以修改吗?

    一款基于Java+Spring Boot+Vue的智慧随访管理系统源码

    智慧随访管理系统源码,一款基于Java+Spring Boot+Vue的B/S架构医院随访管理系统源码,采用前后端分离技术(Ant-Design+MySQL5),具有自主版权和落地案例。 随访管理
    的头像 发表于 11-13 15:38 496次阅读
    一款基于Java+<b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b>+Vue的智慧随访管理系统源码

    优化boot4的乘法运算周期

    可以在不同的时钟周期内完成,从而并行化运算流程,提高乘法器的运算性能。 采用多级压缩:在Boot4乘法器中,使用了基于连乘算法的多级压缩技术。可以通过增加多级压缩,进一步降低管理乘法器
    发表于 10-21 13:17

    ulog_tag_lvl_filter_set()函数无法实现按照文档说明那样实现按模块过滤,怎么解决?

    ulog_tag_lvl_filter_set()函数无法实现按照文档说明那样实现按模块过滤,比如过滤掉所有drv.xxx的日志
    发表于 10-10 07:30

    基于FPGA的压缩算法加速实现

    本设计中,计划实现对文件的压缩及解压,同时优化压缩中所涉及的信号处理和计算密集型功能,实现对其的加速处理。本设计的最终目标是证明在充分并行化的硬件体系结构 FPGA 上
    的头像 发表于 07-10 11:09 2565次阅读
    基于FPGA的<b class='flag-5'>压缩</b>算法加速<b class='flag-5'>实现</b>

    不用联网不用编程,PLC通过智能网关快速实现HTTP协议JSON格式与MES等系统平台双向数据通讯

    进行解析后将数据写入到PLC,实现PLC与HTTP服务端双向通讯;作为服务端时根据客户端URL中的路径查找所配置的数据,打包成JSON文件后返回给客户端。
    的头像 发表于 05-13 14:40 1376次阅读
    不用联网不用编程,PLC通过智能网关快速<b class='flag-5'>实现</b>HTTP协议<b class='flag-5'>JSON</b>格式与MES等系统平台双向数据通讯