Springboot 之 Filter 实现超大响应 JSON 数据压缩

Java
253
0
0
2023-09-04
标签   SpringBoot

简介

项目中,请求时发送超大 json 数据外;响应时也有可能返回超大 json 数据。《Springboot 之 Filter 实现 Gzip 压缩超大 json 对象》实现了请求数据的 gzip 压缩。本篇通过 filter 实现对响应 json 数据的压缩。先了解一下以下两个概念:

  • 请求头: Accept-Encoding : gzip 告诉服务器,该浏览器支持 gzip 压缩
  • 响应头: Content-Encoding : gzip 告诉浏览器,输出信息使用了 gzip 进行压缩

pom .xml 引入依赖

 <project xmlns="#; xmlns:xsi="#34;
 xsi:schemaLocation=" #;>
 <modelVersion>.0.0</modelVersion>

 <groupId>com.olive</groupId>
 <artifactId>response-compression</artifactId>
 <version>.0.1-SNAPSHOT</version>
 <packaging>jar</packaging>

 <name>response-compression</name>
 <url>

 <parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>.5.14</version>
  <relativePath /> <!-- lookup parent from repository -->
 </parent>

 <properties>
  <project.build.sourceEncoding>UTF-</project.build.sourceEncoding>
  < maven .compiler.source>8</maven.compiler.source>
  <maven.compiler.target></maven.compiler.target>
 </properties>

 <dependencies>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
  </dependency>
  <dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
  </dependency>
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
   <groupId>com.alibaba.fastjson</groupId>
   <artifactId> fastjson 2</artifactId>
   <version>.0.14</version>
  </dependency>
  <dependency>
   <groupId>commons-io</groupId>
   <artifactId>commons-io</artifactId>
   <version>.9.0</version>
  </dependency>
 </dependencies>
</project>

对Response进行包装

GzipResponseWrapper 类重新定义了输出流,拦截需要输出的数据,直接缓存到 byte ArrayOutputStream 中。

 package com.olive.filter;

import lombok.extern.slfj.Slf4j;

import  javax .servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.Http servlet Response;
import javax.servlet.http.HttpServletResponseWrapper;
import  java .io.*;

@Slfj
public class GzipResponseWrapper  extends  HttpServletResponseWrapper {

    /**
     * 字节数组缓冲流,用来保存截获到的输出数据
     */
     private  ByteArrayOutputStream buffer;

    /**
     * 重新定义servlet输出流,改变输出目的地将响应内容输出到给定的字节数组缓冲流中
     */
    private GzipResponseWrapper.CustomServletOutputStream servletOutputStream;

    /**
     * 同上
     */
    private PrintWriter writer;

    public GzipResponseWrapper(HttpServletResponse response) {
        super(response);
        //original HttpServletResponse object
        buffer = new ByteArrayOutputStream();
        servletOutputStream = new GzipResponseWrapper.CustomServletOutputStream(buffer);
        try {
            writer = new PrintWriter(new OutputStreamWriter(buffer, response.getCharacterEncoding()), true);
        } catch (UnsupportedEncoding Exception  e) {
            log.error("GZipHttpServletResponse", e);
        }
    }

    @Override
    public  Servlet OutputStream getOutputStream() throws IOException {
        return servletOutputStream;
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        return writer;
    }

    @Override
    public  void  flushBuffer() throws IOException {
        if (servletOutputStream != null) {
            servletOutputStream.flush();
        }
        if (writer != null) {
            writer.flush();
        }
    }

    /**
     * 向外部提供一个获取截获数据的方法
     * @return 从response输出流中截获的响应数据
     */
    public byte[] getOutputData() throws IOException {
        flushBuffer();
        return buffer.toByteArray();
    }

    private  static  class CustomServletOutputStream extends ServletOutputStream {

        /**
         * 字节数组缓冲流,用来保存截获到的输出数据
         */
        private ByteArrayOutputStream buffer;

        public CustomServletOutputStream( Byte ArrayOutputStream buffer) {
            this.buffer = buffer;
        }

        @Override
        public  boolean  isReady() {
            return true;
        }

        @Override
        public void setWriteListener(WriteListener listener) {
        }

        /**
         * 重写输出流相关的方法
         * 将输出数据写出到给定的ByteArrayOutputStream缓冲流中保存起来
         * @param b 输出的数据
         * @throws IOException
         */
        @Override
        public void write(int b) throws IOException {
            buffer.write(b);
        }
    }
}

定义GzipFilter对输出进行拦截

GzipFilter 拦截器获取缓存的需要输出的数据,进行压缩,在输出数据之前先设置响应头 Content-Encoding : gzip

 package com.olive.filter;

import lombok.extern.slfj.Slf4j;
import org.springframework.http.Http header s;

import javax.servlet.*;
import javax.servlet.http.HttpServlet request ;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.zip.GZIPOutputStream;

/**
 * 压缩过滤器
 *
 * 功能:对于返回给客户端的数据进行gzip压缩,提高响应速度
 * 实现说明:
 *     要对response对象的输出数据进行gzip压缩,首先得拿到后面servlet(controller)进行业务处理后往response对象里写入的数据
 *     可以通过重写response对象,修改该对象内部的输出流,使该流写出数据时写出到给定的字节数组缓冲流当中,
 *     并在重写后的response对象内部提供一个获取该字节数组缓冲流的方法,这样就可以截获响应数据
 *     然后就可以对截获的响应数据通过Gzip输出流进行压缩输出即可;
 *     因为响应数据是gzip压缩格式,不是普通的文本格式所以需要通过response对象(响应头)告知浏览器响应的数据类型
 */
@Slfj
public class GzipFilter implements Filter {

    private final String GZIP = "gzip";

    public void destroy() {
        log.info("GzipFilter destroy");
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        log.info("GzipFilter start");
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;
        String acceptEncoding = request.get Header (HttpHeaders.ACCEPT_ENCODING);
        //searching for 'gzip' in ACCEPT_ENCODING header
        if( acceptEncoding != null && acceptEncoding.indexOf(GZIP) >=){
            GzipResponseWrapper gzipResponseWrapper = new GzipResponseWrapper(response);
            //pass the customized response object to controller to capture the output data
            chain.doFilter(request, gzipResponseWrapper);
            //get captured data
            byte[] data = gzipResponseWrapper.getOutputData();
            log.info("截获到数据:" + data.length + " bytes");
            //get gzip data
            ByteArrayOutputStream gzipBuffer = new ByteArrayOutputStream();
            GZIPOutputStream gzipOut = new GZIP Output Stream(gzipBuffer);
            gzipOut.write(data);
            gzipOut.flush();
            gzipOut.close();
            byte[] gzipData = gzipBuffer.toByteArray();
            log.info("压缩后数据:" + gzipData.length + " bytes");
            //set response header and output
            response.setHeader(HttpHeaders.CONTENT_ENCODING, GZIP);
            response.getOutputStream().write(gzipData);
            response.getOutputStream().flush();
        }else{
            chain.doFilter(req, resp);
        }
    }

    public void init(FilterConfig config) throws ServletException {
        log.info("GzipFilter init");
    }

}

注册 GzipFilter 拦截器

 package com.olive.config;

import com.olive.filter.GzipFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context. annotation .Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 注册filter
 */
@Configuration
public class FilterRegistration {

    @Bean
    public FilterRegistrationBean<GzipFilter> gzipFilterRegistrationBean() {
        FilterRegistrationBean<GzipFilter> registration = new FilterRegistrationBean<>();
        //Filter可以new,也可以使用依赖注入 Bean 
        registration.setFilter(new GzipFilter());
        //过滤器名称
        registration.setName("gzipFilter");
        //拦截路径
        registration.addUrlPatterns("/*");
        //设置顺序
        registration.setOrder();
        return registration;
    }
}

定义 Controller

该 Controller 非常简单,主要读取一个大文本文件,作为输出的内容。

 package com.olive.controller;

import java.io. File ;
import java.util. HashMap ;
import java.util.Map;

import com.olive.vo.ArticleRequestVO;
import org.apache.commons.io.FileUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

 @RequestMapping("/getArticle")
 public Map<String, Object> getArticle(){
   Map <String, Object> result = new HashMap<>();
  result.put("code",);
  result.put("msg", "success");
  byte[] bytes = null;
  try {
   bytes = FileUtils.readFileToByteArray(new File("C:UsersDesktop凯平项目资料改装车项目CXSSBOOT_DB_DDL-1.0.9.sql"));
  }catch (Exception e){

  }
  String content = new String(bytes);
  ArticleRequestVO vo = new ArticleRequestVO();
  vo.setId(L);
  vo.setTitle("BUG弄潮儿");
  vo.setContent(content);
  result.put("body", vo);
  return result;
 }

}

Controller 返回数据的 VO

 package com.olive.vo;

import lombok.Data;

import java.io.Serializable;

@Data
public class ArticleRequestVO implements Serializable {

    private Long id;

    private String title;

    private String content;

}

定义 Springboot 引导类

 package com.olive;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }

}

测试

测试的curl

 curl -X POST  

Springboot 之 Filter 实现超大响应 JSON 数据压缩