Springboot 之 Filter 實現超大響應 JSON 數據壓縮
- 2022 年 10 月 13 日
- 筆記
- spring, springboot, SpringCloud
簡介
項目中,請求時發送超大 json 數據外;響應時也有可能返回超大 json數據。上一篇實現了請求數據的 gzip 壓縮。本篇通過 filter 實現對響應 json 數據的壓縮。
先了解一下以下兩個概念:
- 請求頭:
Accept-Encoding : gzip
告訴服務器,該瀏覽器支持 gzip 壓縮 - 響應頭:
Content-Encoding : gzip
告訴瀏覽器,輸出信息使用了 gzip 進行壓縮
pom.xml 引入依賴
<project xmlns="//maven.apache.org/POM/4.0.0" xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.olive</groupId>
<artifactId>response-compression</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>response-compression</name>
<url>//maven.apache.org</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.14</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</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.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.14</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>
</project>
對Response進行包裝
GzipResponseWrapper 類重新定義了輸出流,攔截需要輸出的數據,直接緩存到 ByteArrayOutputStream 中。
package com.olive.filter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;
@Slf4j
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 (UnsupportedEncodingException e) {
log.error("GZipHttpServletResponse", e);
}
}
@Override
public ServletOutputStream 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(ByteArrayOutputStream 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.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.zip.GZIPOutputStream;
/**
* 壓縮過濾器
*
* 功能:對於返回給客戶端的數據進行gzip壓縮,提高響應速度
* 實現說明:
* 要對response對象的輸出數據進行gzip壓縮,首先得拿到後面servlet(controller)進行業務處理後往response對象里寫入的數據
* 可以通過重寫response對象,修改該對象內部的輸出流,使該流寫出數據時寫出到給定的位元組數組緩衝流當中,
* 並在重寫後的response對象內部提供一個獲取該位元組數組緩衝流的方法,這樣就可以截獲響應數據
* 然後就可以對截獲的響應數據通過Gzip輸出流進行壓縮輸出即可;
* 因為響應數據是gzip壓縮格式,不是普通的文本格式所以需要通過response對象(響應頭)告知瀏覽器響應的數據類型
*/
@Slf4j
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.getHeader(HttpHeaders.ACCEPT_ENCODING);
//searching for 'gzip' in ACCEPT_ENCODING header
if( acceptEncoding != null && acceptEncoding.indexOf(GZIP) >= 0){
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 GZIPOutputStream(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(1);
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", 200);
result.put("msg", "success");
byte[] bytes = null;
try {
bytes = FileUtils.readFileToByteArray(new File("C:\\Users\\2230\\Desktop\\凱平項目資料\\改裝車項目\\CXSSBOOT_DB_DDL-1.0.9.sql"));
}catch (Exception e){
}
String content = new String(bytes);
ArticleRequestVO vo = new ArticleRequestVO();
vo.setId(1L);
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 //127.0.0.1:8080/getArticle