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

    文章

    340

    浏览量

    14343
  • 传输数据
    +关注

    关注

    1

    文章

    122

    浏览量

    16103
  • 大数据
    +关注

    关注

    64

    文章

    8889

    浏览量

    137439

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

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

收藏 人收藏

    评论

    相关推荐

    校园点餐订餐外卖跑腿Java源码

    创建一个校园点餐订餐外卖跑腿系统是一个复杂的项目,涉及到前端、后端、数据库设计等多个方面。在这里,我可以提供一个简化的Java后端示例,使用Spring Boot框架来搭建一个基本的API服务。这个
    的头像 发表于 12-24 14:55 97次阅读
    校园点餐订餐外卖跑腿Java源码

    SSM开发环境的搭建教程 SSM与Spring Boot的区别

    SSM开发环境的搭建教程 SSM(Spring+SpringMVC+MyBatis)开发环境的搭建涉及多个步骤,以下是详细的教程: 创建Maven项目 : 使用Maven工具创建一个新的Maven
    的头像 发表于 12-16 18:13 449次阅读

    Spring 应用合并之路(二):峰回路转,柳暗花明

    提醒下,决定抛开 Spring Boot 内置的父子容器方案,完全自己实现父子容器。 如何加载 web 项目? 现在的难题只有一个:如何加载 web 项目?加载完成后,如何持续持有 web 项目?经过思考后,可以创建一个
    的头像 发表于 12-12 11:22 728次阅读

    Spring事务实现原理

    这些操作。 spring事务有编程式事务和声明式事务两种实现方式。编程式事务是通过编写代码来管理事务的提交、回滚、以及事务的边界。这意味着开发者需要在代码中显式地调用事务的开始、提交和回滚。声明式事务是通过配置来管理事务,您可以使用注解或XML配置来
    的头像 发表于 11-08 10:10 824次阅读
    <b class='flag-5'>Spring</b>事务<b class='flag-5'>实现</b>原理

    如何使用gzip压缩和解压缩技术

    Nginx是一款高性能的Web服务器,它也能够充当反向代理服务器和负载均衡器。在Web应用开发中,优化网站速度是一个非常重要的工作。使用gzip压缩和解压缩技术,可以有效减小传输文件的大小,提升网站的访问速度。
    的头像 发表于 09-29 15:57 402次阅读

    JSON协议是什么,物联网中的RTU中如何使用JSON协议和服务器交互

    一 概述 1.1 什么是 JSON JSON是JavaScript Object Notation的简称,中文含义为“JavaScript 对象表示法”,它是一种数据交换的文本格式,而不是一种编程
    的头像 发表于 09-25 16:14 764次阅读
    <b class='flag-5'>JSON</b>协议是什么,物联网中的RTU中如何使用<b class='flag-5'>JSON</b>协议和服务器交互

    Spring Cloud Gateway网关框架

    SpringCloud Gateway功能特征如下: (1) 基于Spring Framework 5, Project Reactor 和 Spring Boot 2.0 进行构建; (2) 动态路由:能够匹配任何请求属性;
    的头像 发表于 08-22 09:58 493次阅读
    <b class='flag-5'>Spring</b> Cloud Gateway网关框架

    单片机boot0和boot1怎么设置

    单片机Boot0和Boot1简介 Boot0和Boot1是单片机启动模式选择引脚,用于选择单片机的启动模式。 Boot0和
    的头像 发表于 08-22 09:50 2536次阅读

    vue+spring boot人员定位系统源码,实现实时定位、智慧调度、轨迹追踪

    、机具、物料上定位标签回传的位置信息数据,采用多维定位模式,精确定位人、机具、物料的实时位置,实现实时定位、物料标签配置、智慧调度、轨迹追踪、工时统计、区域物料统计、电子围栏等应用功能。 技术架构:java+ spring boot
    的头像 发表于 08-08 14:27 698次阅读
    vue+<b class='flag-5'>spring</b> <b class='flag-5'>boot</b>人员定位系统源码,<b class='flag-5'>实现</b>实时定位、智慧调度、轨迹追踪

    玩转Spring状态机

    说起Spring状态机,大家很容易联想到这个状态机和设计模式中状态模式的区别是啥呢?没错,Spring状态机就是状态模式的一种实现,在介绍Spring状态机之前,让我们来看看设计模式中
    的头像 发表于 06-25 14:21 953次阅读
    玩转<b class='flag-5'>Spring</b>状态机

    UWB室内外高精度一体化融合定位系统源码 UWB技术定位系统应用场景 Java+Spring boot+MYSQL 技术开发

    UWB室内外高精度一体化融合定位系统源码 UWB技术定位系统应用场景 Java+Spring boot+MYSQL 技术开发 系统聚焦基于UWB(超宽带)技术的底层定位网络和定位算法,通过对定位分站
    的头像 发表于 06-18 10:46 483次阅读
    UWB室内外高精度一体化融合定位系统源码 UWB技术定位系统应用场景 Java+<b class='flag-5'>Spring</b> <b class='flag-5'>boot</b>+MYSQL 技术开发

    PLC设备通过智能网关采用HTTP协议JSON文件对接MES、ERP等系统平台

    ,字符可自由修改,举例只是为了描述各种JSON文件的结构实现。MQTT协议的发布/订阅也是同样的对应方式。具体如下: 多个Key:Value的同级组合: 包含子对象: 增加时间戳,多种格式可以
    发表于 05-13 12:04

    Python压缩和解压缩实现代码分享

    压缩和解压缩是日常常用的操作,不管是windows上图形界面的操作,还是linux上用命令来进行压缩压缩,总的而言都还是比较方便的。
    的头像 发表于 04-11 11:28 1059次阅读

    Spring事务传播性的相关知识

    本文主要介绍了Spring事务传播性的相关知识。
    的头像 发表于 01-10 09:29 447次阅读
    <b class='flag-5'>Spring</b>事务传播性的相关知识

    使用Spring Boot 3.2虚拟线程搭建静态文件服务器

    Spring Boot 3.2 于 2023 年 11 月大张旗鼓地发布,标志着 Java 开发领域的一个关键时刻。这一突破性的版本引入了一系列革命性的功能。
    的头像 发表于 01-09 09:34 1126次阅读
    使用<b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b> 3.2虚拟线程搭建静态文件服务器