Spring Boot 与 Gzip 压缩

响应压缩是 Web 应用一种常见的优化手段,通过压缩算法减小传输数据的体积,提高传输效率、节约带宽。客户端接收到数据后,使用相同的算法对数据进行解压从而获取到原始数据。

客户端和服务器需要通过 Header 来协商双方支持的压缩算法。

  • Accept-Encoding:请求头,告诉服务器客户端支持的压缩算法(多个使用逗号分割)。例如:Accept-Encoding: gzip, deflate
  • Content-Encoding:响应头,告诉客户端当前 Payload 使用的编码方式(压缩算法)。例如:Content-Encoding: gzip

常用的压缩算法如下:

  • gzip
  • deflate
  • br

JDK 提供了对 GZIP 压缩算法的实现:GZIPOutputStreamGZIPInputStream,我们可以用它们来实现 Gzip 压缩和解压缩。

使用 Gzip 压缩响应

在 Spring Boot 应用中创建一个 Controller,使用 GZIPOutputStream 把一张图片文件(20 KB)压缩后响应给客户端。

package cn.springdoc.demo.web.controller;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.zip.GZIPOutputStream;

import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;


@RestController
@RequestMapping("/demo")
public class DemoController {

    @GetMapping
    public void file (HttpServletRequest request, HttpServletResponse response) throws IOException {

        // 20.0 KB 大小的图片文件
        Path file = Paths.get("C:\\Users\\KevinBlandy\\Desktop\\springdoc-logo.png");
        
        // 设置文件类型
        response.setContentType(Optional.ofNullable(Files.probeContentType(file)).orElse(MediaType.APPLICATION_OCTET_STREAM_VALUE));
        
        // 设置压缩方式为 gzip 【关键点 1:设置正确的 CONTENT_ENCODING 头】
        response.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
        
        // 包装 response 流为 gzip 流 【关键点 2:使用 GZIPOutputStream 封装 response 流,并写出数据】
        try(GZIPOutputStream gzipOutputStream = new GZIPOutputStream(response.getOutputStream())){
            // 响应给客户端
            Files.copy(file, gzipOutputStream);
        }
    }
}

如上。关键点在于设置 CONTENT_ENCODING Header 为 gzip,告诉浏览器使用了 gzip 压缩算法,浏览器会自动使用相同算法进行解压缩。

最后,使用 GZIPOutputStream 封装 response 流,往 gzipOutputStream 中写入的数据就会被 gzip 压缩。

启动应用,使用浏览器访问:http://localhost:8080/demo

Spring Boot Gzip 压缩响应

通过控制台的网络面板,你可以看到:

  1. 浏览器通过 Accept-Encoding 告诉服务器,它支持 gzip 压缩算法。
  2. 服务器正确地指定了 Payload 的编码类型为 gzip
  3. 由于使用了 Gzip 压缩,数据的传输体积小于文件体积。

图片在浏览器中预览成功,也说明服务器和客户端都进行了正确的编解码。

Spring Boot 配置响应压缩

对于这种如此常用的功能,Spring Boot 早已提供了开箱即用的支持。

可以在 application.yaml / application.properties 文件中配置如下属性,开启全局 Gzip 响应压缩:

属性 说明 默认值
server.compression.enabled 是否开启全局响应压缩 false
server.compression.excluded-user-agents 以逗号分隔的 User Agent 列表,对这些 User Agent 的响应不会被压缩。
server.compression.mime-types 逗号分割的文件 MIME Type(媒体类型),这些类型的文件才会被压缩。 [text/html, text/xml, text/plain, text/css, text/javascript, application/javascript, application/json, application/xml]
server.compression.min-response-size 进行压缩的最低 Content-Length 值。 2KB

application.yaml 中添加如下配置:

server:
  compression:
    # 开启响应压缩
    enabled: true
    mime-types: 
      - image/png # 压缩 png 图片
    # 进行压缩的最小体积
    min-response-size: 1KB

其实只需要设置 server.compression.enabled=true 即可,这里故意设置 server.compression.min-response-size=1KB 完全是为了进行演示,因为示例图片不足 2KB

server.compression.min-response-size 值不应该过小,否则压缩后的数据体积可能比原始数据还大。

还需要覆盖 server.compression.mime-types 配置,因为默认配置的压缩的文件类型列表中不包含图片。

修改 Controller,如下:

@GetMapping
public ResponseEntity<Resource> file (HttpServletRequest request, HttpServletResponse response) throws IOException {

    // 20.0 KB 大小的图片文件
    Path file = Paths.get("C:\\Users\\KevinBlandy\\Desktop\\springdoc-logo.png");
    
    return ResponseEntity.ok()
            .contentType(MediaType.IMAGE_PNG) // 正确设置图片的 Content Type,浏览器才会预览图片
            .body(new InputStreamResource(Files.newInputStream(file)));
}

这次不自己使用 GZIPOutputStream 进行压缩响应,而是直接返回 ResponseEntity<Resource> 对象。这也是关键点,如果你想基于配置的全局 Gzip 响应压缩生效,则不能自己使用 HttpServletResponse 进行数据响应,必须要通过返回对象,由 DispatcherServlet 处理,全局响应压缩才会生效。

重启应用,用浏览器再次请求 http://localhost:8080/demo,你会发现结果跟上节中的测试结果一样。全局 Gzip 压缩配置生效。