Spring Boot + Gzip 压缩超大 JSON 对象,传输大小减少一半!

Posted Java技术栈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Boot + Gzip 压缩超大 JSON 对象,传输大小减少一半!相关的知识,希望对你有一定的参考价值。

点击关注公众号,Java干货及时送达

推荐阅读:

学习 Spring Cloud 微服务的最佳姿势!

Spring Cloud 2022 正式发布!


1. 业务背景

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


    "adName":"",
    "adTag":""
  • adName:广告名字

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

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

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

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

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

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

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

2. 实现思路

前置知识:
  • Http 请求结构以及 Content-Encoding 属性

  • GZIP 压缩方式

  • Servlet Filter

  • HttpServletRequestWrapper

  • Spring Boot

  • Java 输入输出流

另外,如果你近期准备面试跳槽,建议在Java面试库小程序在线刷题,涵盖 2000+ 道 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

End

Spring 旗下最牛逼的国产项目!

23 种设计模式实战(很全)

Spring Boot 3.0 正式发布,王炸!!

Spring Cloud Alibaba 最新重磅发布!

Nacos 2.2 正式发布,这次更新太炸了!

Spring Cloud 微服务最新课程!

以上是关于Spring Boot + Gzip 压缩超大 JSON 对象,传输大小减少一半!的主要内容,如果未能解决你的问题,请参考以下文章

Java如何在spring boot中启用gzip压缩并验证其生效

将 GZIP 压缩与 Spring Boot/MVC/JavaConfig 与 RESTful 结合使用

Spring Boot gzip 压缩响应的大小小于配置的 min-response-size

Spring gzip 与 Tomcat gzip

带有 http/2 和 ssl 的嵌入式 Tomcat 的 Spring Boot 2.1,gzip 不起作用

spring通过GZIP压缩提高网络传输效率(可以实现任何资源的gzip压缩包括AJAX)