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
相关产品推荐
相关产品推荐

