You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring项目忽略性能时,如何实现JSON响应超2KB自动压缩?

解决Spring JSON响应超过2KB才压缩的问题

Spring自带的server.compression.min-response-size=2048配置仅对String类型响应生效,因为JSON序列化多采用流式输出,无法提前获取Content-Length来判断是否触发压缩。如果不考虑性能损耗,我们可以通过缓冲整个JSON响应体,计算实际大小后再决定是否压缩,以下是两种可行方案:

方案一:使用ResponseBodyAdvice拦截JSON响应

通过Spring MVC的ResponseBodyAdvice,在响应写入前先序列化并缓冲JSON内容,判断大小后再处理压缩逻辑:

import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.GZIPOutputStream;

@ControllerAdvice
public class JsonGzipAdvice implements ResponseBodyAdvice<Object> {

    private static final int MIN_COMPRESS_THRESHOLD = 2048; // 2KB

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 仅处理JSON转换器
        return MappingJackson2HttpMessageConverter.class.isAssignableFrom(converterType);
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        if (body == null) {
            return body;
        }

        ServletServerHttpResponse servletResponse = (ServletServerHttpResponse) response;
        HttpServletResponse httpResp = servletResponse.getServletResponse();

        try {
            // 缓冲序列化后的JSON内容
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
            converter.write(body, selectedContentType, new ServerHttpResponse() {
                @Override
                public OutputStream getBody() throws IOException {
                    return baos;
                }

                @Override
                public org.springframework.http.HttpHeaders getHeaders() {
                    return new org.springframework.http.HttpHeaders();
                }
            });

            byte[] jsonBytes = baos.toByteArray();
            int contentSize = jsonBytes.length;

            if (contentSize >= MIN_COMPRESS_THRESHOLD) {
                // 开启GZIP压缩
                httpResp.setHeader("Content-Encoding", "gzip");
                httpResp.setContentLength(-1); // 压缩后长度不确定,不设置固定值

                // 写入压缩后的内容
                try (GZIPOutputStream gzipOut = new GZIPOutputStream(httpResp.getOutputStream())) {
                    gzipOut.write(jsonBytes);
                }
                return null; // 已手动写入响应,返回null跳过默认处理
            } else {
                // 无需压缩,设置原内容长度并返回
                httpResp.setContentLength(contentSize);
                return body;
            }
        } catch (Exception e) {
            // 异常时回退到原响应逻辑
            e.printStackTrace();
            return body;
        }
    }
}

注意事项

  • 需确保Spring Boot已开启基础压缩配置:
    server.compression.enabled=true
    server.compression.mime-types=application/json
    
  • 该方案仅针对JSON响应生效,不会影响其他类型的响应输出。

方案二:使用全局Filter包装响应流

通过自定义Servlet Filter,替换响应输出流为缓冲流,待整个响应写入完成后判断大小并压缩:

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.GZIPOutputStream;

public class GzipCompressionFilter implements Filter {

    private static final int MIN_COMPRESS_THRESHOLD = 2048;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpReq = (HttpServletRequest) request;
        HttpServletResponse httpResp = (HttpServletResponse) response;

        // 仅对支持GZIP且为JSON的请求处理
        String acceptEncoding = httpReq.getHeader("Accept-Encoding");
        if (acceptEncoding != null && acceptEncoding.contains("gzip") 
            && httpResp.getContentType() != null && httpResp.getContentType().contains("application/json")) {
            
            BufferedResponseWrapper bufferedResp = new BufferedResponseWrapper(httpResp);
            chain.doFilter(request, bufferedResp);

            byte[] responseBytes = bufferedResp.getBuffer();
            if (responseBytes.length >= MIN_COMPRESS_THRESHOLD) {
                httpResp.setHeader("Content-Encoding", "gzip");
                httpResp.setContentLength(-1);
                try (GZIPOutputStream gzipOut = new GZIPOutputStream(httpResp.getOutputStream())) {
                    gzipOut.write(responseBytes);
                }
            } else {
                httpResp.getOutputStream().write(responseBytes);
            }
        } else {
            chain.doFilter(request, response);
        }
    }

    // 包装响应流实现缓冲
    private static class BufferedResponseWrapper extends HttpServletResponseWrapper {
        private final ByteArrayOutputStream buffer;
        private final ServletOutputStream outputStream;

        public BufferedResponseWrapper(HttpServletResponse response) {
            super(response);
            this.buffer = new ByteArrayOutputStream();
            this.outputStream = new BufferedServletOutputStream(buffer);
        }

        @Override
        public ServletOutputStream getOutputStream() throws IOException {
            return outputStream;
        }

        public byte[] getBuffer() {
            return buffer.toByteArray();
        }

        private static class BufferedServletOutputStream extends ServletOutputStream {
            private final ByteArrayOutputStream buffer;

            public BufferedServletOutputStream(ByteArrayOutputStream buffer) {
                this.buffer = buffer;
            }

            @Override
            public void write(int b) throws IOException {
                buffer.write(b);
            }

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

            @Override
            public void setWriteListener(WriteListener listener) {
                // 空实现,按需扩展
            }
        }
    }
}

配置Filter生效

在Spring Boot中添加配置类启用Servlet组件扫描:

import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ServletComponentScan
public class FilterConfig {
}

同时给GzipCompressionFilter添加@WebFilter(urlPatterns = "/*")注解,指定拦截路径。

方案对比

  • ResponseBodyAdvice:更贴合Spring MVC的处理流程,精准针对JSON响应,代码逻辑与Spring生态结合更紧密。
  • Filter:全局拦截所有请求,适用范围更广,若后续需要扩展其他响应类型的压缩逻辑更方便。

内容的提问来源于stack exchange,提问作者Sunil Kumar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 21:35:19