1. 業(yè)務(wù)背景
是這樣的,業(yè)務(wù)背景是公司的內(nèi)部系統(tǒng)有一個廣告保存接口,需要ADX那邊將投放的廣告數(shù)據(jù)進(jìn)行保存供后續(xù)使用。 廣告數(shù)據(jù)大概長這樣:
- adName是廣告名字
- adTag是廣告渲染的HTML代碼,超級大數(shù)據(jù)庫中都是用text類型來存放的,我看到最大的adTag足足有60kb大小…
{
"adName":"",
"adTag":""
}
因此,對與請求數(shù)據(jù)那么大的接口我們肯定是需要作一個優(yōu)化的否則太大的數(shù)據(jù)傳輸有以下幾個弊端:
- 占用網(wǎng)絡(luò)帶寬,而有些云產(chǎn)品就是按照帶寬來計費(fèi)的,間接浪費(fèi)了錢
- 傳輸數(shù)據(jù)大導(dǎo)致網(wǎng)絡(luò)傳輸耗時
為了克服這幾個問題團(tuán)隊中的老鳥產(chǎn)生一個想法:
請求廣告保存接口時先將Json對象字符串進(jìn)行GZIP壓縮,那請求時傳入的就是壓縮后的數(shù)據(jù),而GZIP的壓縮效率是很高的,因此可以大大減小傳輸數(shù)據(jù),而當(dāng)數(shù)據(jù)到達(dá)廣告保存接口前再將傳來的數(shù)據(jù)進(jìn)行解壓縮,還原成JSON對象就完成了整個GZIP壓縮數(shù)據(jù)的請求以及處理流程。
其實(shí)這樣做也存在著弊端:
-
請求變復(fù)雜了
- 接口調(diào)用方那邊需要對數(shù)據(jù)進(jìn)行壓縮
- 接口執(zhí)行方那邊需要對拿到的數(shù)據(jù)進(jìn)行解壓
-
需要額外占用更多的CPU計算資源
-
可能會影響到原有的其他接口
對于以上幾點(diǎn)基于我們公司當(dāng)前的業(yè)務(wù)可以這樣解決:
- 對與需要占用而外的CPU計算資源來說,公司的內(nèi)部系統(tǒng)屬于IO密集型應(yīng)用,因此用一些CPU資源來換取更快的網(wǎng)絡(luò)傳輸其實(shí)是很劃算的
- 使用過濾器在請求數(shù)據(jù)到達(dá)Controller之前對數(shù)據(jù)進(jìn)行解壓縮處理后重新寫回到Body中,避免影響Controller的邏輯,代碼零侵入
- 而對于改造接口的同時是否會影響到原來的接口這一點(diǎn)可以通過 HttpHeader 的Content-Encoding=gzip屬性來區(qū)分是否需要對請求數(shù)據(jù)進(jìn)行解壓縮
那廢話少說,下面給出實(shí)現(xiàn)方案
基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
- 項目地址:https://github.com/YunaiV/ruoyi-vue-pro
- 視頻教程:https://doc.iocoder.cn/video/
2. 實(shí)現(xiàn)思路
前置知識:
- Http 請求結(jié)構(gòu)以及Content-Encoding 屬性
- gzip壓縮方式
- Servlet Filter
- HttpServletRequestWrapper
- Spring Boot
- Java 輸入輸出流
實(shí)現(xiàn)流程圖:
核心代碼:
創(chuàng)建一個SpringBoot項目,先編寫一個接口,功能很簡單就是傳入一個Json對象并返回,以模擬將廣告數(shù)據(jù)保存到數(shù)據(jù)庫
/**
*@ClassName:ProjectController
*@Authorzhangjin
*@Date2022/3/2420:41
*@Description:
*/
@Slf4j
@RestController
publicclassAdvertisingController{
@PostMapping("/save")
publicAdvertisingsaveProject(@RequestBodyAdvertisingadvertising){
log.info("獲取內(nèi)容"+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("/*");
//設(shè)置順序
registration.setOrder(1);
returnregistration;
}
}
實(shí)現(xiàn)RequestWrapper實(shí)現(xiàn)解壓和寫回Body的邏輯
/**
*@ClassName:UnZIPRequestWrapper
*@Authorzhangjin
*@Date2022/3/2611:02
*@Description:JsonString經(jīng)過壓縮后保存為二進(jìn)制文件->解壓縮后還原成JsonString轉(zhuǎn)換成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無內(nèi)容,無需解壓");
bytes=body;
return;
}
this.bytes=GZIPUtils.uncompressToByteArray(body);
}catch(IOExceptionex){
log.info("解壓縮步驟發(fā)生異常!");
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字節(jié)數(shù)組
*@paramstr
*@return
*/
publicstaticbyte[]compress(Stringstr){
returncompress(str,GZIP_ENCODE_UTF_8);
}
/**
*字符串壓縮為GZIP字節(jié)數(shù)組
*@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壓縮后的字節(jié)數(shù)組
*@paramencoding編碼方式
*@return解壓后的字符串
*/
publicstaticStringuncompressToString(byte[]bytes,Stringencoding)throwsIOException{
byte[]result=uncompressToByteArray(bytes,encoding);
returnnewString(result);
}
/**
*解壓成字節(jié)數(shù)組
*@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("解壓縮失敗!");
}
}
/**
*將字節(jié)流轉(zhuǎn)換成文件
*@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 實(shí)現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
3. 測試效果
注意一個大坑:千萬不要直接將壓縮后的byte[]當(dāng)作字符串進(jìn)行傳輸,否則你會發(fā)現(xiàn)壓縮后的請求數(shù)據(jù)竟然比沒壓縮后的要大得多!一般有兩種傳輸壓縮后的byte[]的方式:
- 將壓縮后的byet[]進(jìn)行base64編碼再傳輸字符串,這種方式會損失掉一部分GZIP的壓縮效果,適用于壓縮結(jié)果要存儲在Redis中的情況
- 將壓縮后的byte[]以二進(jìn)制的形式寫入到文件中,請求時直接在body中帶上文件即可,用這種方式可以不損失壓縮效果
Postman測試Gzip壓縮數(shù)據(jù)請求:
- 請求頭指定數(shù)據(jù)壓縮方式:
- Body帶上壓縮后的byte[]寫入的二進(jìn)制文件
- 執(zhí)行請求,服務(wù)端正確處理了請求并且請求size縮小了將近一半,效果還是很不錯的,這樣GZIP壓縮數(shù)據(jù)的請求的處理就完成了,完整的項目代碼在下方
4. Demo地址
- https://gitee.com/wx_1bceb446a4/gziptest
審核編輯 :李倩
-
spring
+關(guān)注
關(guān)注
0文章
340瀏覽量
14338 -
傳輸數(shù)據(jù)
+關(guān)注
關(guān)注
1文章
122瀏覽量
16101 -
大數(shù)據(jù)
+關(guān)注
關(guān)注
64文章
8882瀏覽量
137403
原文標(biāo)題:Spring Boot + Filter 實(shí)現(xiàn) Gzip 壓縮超大 json 對象,傳輸耗時大大減少
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論