Spring Boot + Gzip 压缩超大 JSON 对象,传输大小减少一半!
关注公众号,学习更多 Java 干货!
1. 业务背景
是这样的,业务背景是公司的内部系统有一个广告保存接口,需要 ADX 那边将投放的广告数据进行保存供后续使用。广告数据大概长这样:
{
"adName":"",
"adTag":""
}
adName
:广告名字adTag
:广告渲染的 HTML 代码,超级大数据库中都是用 text 类型来存放的,我看到最大的 adTag 足足有 60kb 大小…
因此,对与请求数据那么大的接口我们肯定是需要作一个优化的否则太大的数据传输有以下几个弊端:
对与需要占用而外的 CPU 计算资源来说,公司的内部系统属于 IO 密集型应用,因此用一些 CPU 资源来换取更快的网络传输其实是很划算的 使用过滤器在请求数据到达 Controller 之前对数据进行解压缩处理后重新写回到 Body 中,避免影响 Controller 的逻辑,代码零侵入 而对于改造接口的同时是否会影响到原来的接口这一点可以通过 HttpHeader 的 Content-Encoding=gzip 属性来区分是否需要对请求数据进行解压缩
2. 实现思路
前置知识:
Http 请求结构以及 Content-Encoding 属性 GZIP 压缩方式 Servlet Filter HttpServletRequestWrapper Spring Boot Java 输入输出流
实现流程图 :
推荐一个开源免费的 Spring Boot 最全教程:
https://github.com/javastacks/spring-boot-best-practice
核心代码:
创建一个 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);
}
}
}
Spring Boot 基础就不介绍了,推荐看这个免费教程:
https://github.com/javastacks/spring-boot-best-practice
3. 测试效果
注意一个大坑:千万不要直接将压缩后的 byte[]
当作字符串进行传输,否则你会发现压缩后的请求数据竟然比没压缩后的要大得多 🐶!一般有两种传输压缩后的 byte[]的方式:
将压缩后的 byet[]
进行 base64 编码再传输字符串,这种方式会损失掉一部分 GZIP 的压缩效果,适用于压缩结果要存储在 Redis 中的情况将压缩后的 byte[]
以二进制的形式写入到文件中,请求时直接在 body 中带上文件即可,用这种方式可以不损失压缩效果
Postman 测试 GZIP 压缩数据请求:
请求头指定数据压缩方式:
Body 带上压缩后的 byte[]
写入的二进制文件
执行请求,服务端正确处理了请求并且请求 size 缩小了将近一半,效果还是很不错的。
注:针对json 数据大小优化,也可以通过修改 nginx 配置项来解决,开启 contentType: gzip 内容传输编码支持,并对Tomcat进行配置即可。
版权声明:本文为CSDN博主「jinchange」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/weixin_43441509/article/details/123816603
往期推荐:
再见,Navicat!! 警报炸锅,FastJson 又立功了。。 高德导航红绿灯为啥能读秒? 为什么 List 原生排序比 stream() 流效率更高?
微信扫码关注该文公众号作者