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

    文章

    333

    浏览量

    14161
  • 传输数据
    +关注

    关注

    1

    文章

    94

    浏览量

    15989
  • 大数据
    +关注

    关注

    64

    文章

    8652

    浏览量

    136602

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

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

收藏 人收藏

    评论

    相关推荐

    Spring事务失效的十种常见场景

    Spring针对Java Transaction API (JTA)、JDBC、Hibernate和Java Persistence API(JPA)等事务 API,实现了一致的编程模型
    的头像 发表于 12-11 15:03 506次阅读

    Spring Boot 3.2支持虚拟线程和原生镜像

    Spring Boot 3.2 前几日发布,让我们用 Java 21、GraalVM 和虚拟线程来尝试一下。
    的头像 发表于 11-30 16:22 343次阅读

    深入浅出了解华为云 API 网关的 Gzip 功能

      Gzip 是什么 Gzip 是一种用于数据压缩的编码格式,经常被使用在基于 HTTP 协议的网络传输中。Gzip 功能允许服务器在传输数据是对其进行
    的头像 发表于 11-12 17:36 255次阅读
    深入浅出了解华为云 API 网关的 <b class='flag-5'>Gzip</b> 功能

    Spring Boot 的设计目标

    什么是Spring Boot Spring BootSpring 开源组织下的一个子项目,也是 S
    的头像 发表于 10-13 14:56 313次阅读
    <b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b> 的设计目标

    Spring Boot的启动原理

    可能很多初学者会比较困惑,Spring Boot 是如何做到将应用代码和所有的依赖打包成一个独立的 Jar 包,因为传统的 Java 项目打包成 Jar 包之后,需要通过 -classpath 属性
    的头像 发表于 10-13 11:44 373次阅读
    <b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b>的启动原理

    Spring Boot启动 Eureka流程

    在上篇中已经说过了 Eureka-Server 本质上是一个 web 应用的项目,今天就来看看 Spring Boot 是怎么启动 Eureka 的。 Spring Boot 启动 E
    的头像 发表于 10-10 11:40 422次阅读
    <b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b>启动 Eureka流程

    Spring Boot Actuator快速入门

    不知道大家在写 Spring Boot 项目的过程中,使用过 Spring Boot Actuator 吗?知道 Spring
    的头像 发表于 10-09 17:11 344次阅读

    Spring Boot时配置JSON序列化选项的几种方法

    Spring Boot Web 项目中,当使用JSON格式接收数据和返回数据的时候,Spring Boot 默认使用一个ObjectMap
    的头像 发表于 10-09 10:26 1253次阅读

    Spring Boot配置加载相关知识

    Spring BOOT 启动参数 在Java Web的开发完成后,以前我们都会打包成war文件,然后放大web容器,比如tomcat、jetty这样的容器。现在基于SpringBoot开发的项目
    的头像 发表于 10-07 15:47 254次阅读

    Spring Boot如何实现异步任务

    Spring Boot 提供了多种方式来实现异步任务,这里介绍三种主要实现方式。 1、基于注解 @Async @Async 注解是 Spring
    的头像 发表于 09-30 10:32 490次阅读

    Spring Boot Starter需要些什么

    ,写一个pulsar-spring-boot-starter是非常有必要的,在此之前,我们先看看一个starter需要些什么。 Spring Boot Starter spring-boot
    的头像 发表于 09-25 11:35 463次阅读
    <b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b> Starter需要些什么

    kafka client在 spring如何实现

    ,又认识了 spring-boot-starter ,今天不妨来看下如何写一个 pulsar-spring-boot-starter 模块。 目标 写一个完整的类似 kafka-spring-boot-st
    的头像 发表于 09-25 11:21 261次阅读
    kafka client在 <b class='flag-5'>spring</b>如何<b class='flag-5'>实现</b>

    Spring Boot虚拟线程和Webflux性能对比

    早上看到一篇关于Spring Boot虚拟线程和Webflux性能对比的文章,觉得还不错。内容较长,抓重点给大家介绍一下这篇文章的核心内容,方便大家快速阅读。
    发表于 09-24 14:54 294次阅读
    <b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b>虚拟线程和Webflux性能对比

    Spring Boot如何优雅实现数据加密存储、模糊匹配和脱敏

    近来我们都在围绕着使用Spring Boot开发业务系统时如何保证数据安全性这个主题展开总结,当下大部分的B/S架构的系统也都是基于Spring Boot + SpringMVC三层架
    的头像 发表于 06-19 14:42 1065次阅读
    <b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b>如何优雅<b class='flag-5'>实现</b>数据加密存储、模糊匹配和脱敏

    Spring Boot 3.1 中如何整合Spring Security和Keycloak

    虽然Keycloak 团队宣布了不再对Spring Security提供适配,但Spring Security长期以来一直为OAuth和OIDC提供强大的内置支持。所以,只要我们理解Spring Security是如何处理OAu
    的头像 发表于 06-08 14:54 724次阅读
    <b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b> 3.1 中如何整合<b class='flag-5'>Spring</b> Security和Keycloak