SpringBoot配置Gzip压缩,解决小于server.compression.min-response-size会被压缩的问题,以及zuul如何正常输出压缩前的报文
为什么需要压缩报文以及如何压缩
使用场景:某个springboot微服务里面的某些请求,返回的报文比较大,在网络条件不好的时候,会导致网络传输耗时过多,可以考虑在该微服务配置压缩功能,一般考虑启用springboot自带的压缩功能,在配置文件配置
server:
#后端报文压缩相关
compression:
enabled: true #是否开启压缩
mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
#满足被压缩的最小报文大小
min-response-size: 1024KB
解决小于server.compression.min-response-size会被压缩的问题
网上的攻略一般写到这里就结束了,但是实际使用过程中会发现,如果controller返回的是字符串,低于1024k的不会被压缩,高于1024kb的会被压缩,这个可以通过F12观察chrome浏览器的Headers面板,如果Response Headers里面包含Content-Encoding: gzip则后端进行了压缩,反之则没进行压缩。
/**
* 字符串大报文
*
* @return 字符串
*/
@GetMapping("largeStr")
public String largeStrResponseBody() {
return generateBodyStr(6 * 1024 * 1024);
}
/**
* 随机生成避免浏览器缓存
*
* @param bytes 字符个数,一个字符就是1 byte
* @return 字符串
*/
private String generateBodyStr(int bytes) {
StringBuilder sb = new StringBuilder();
Random random = new Random();
for (int i = 0; i < bytes; i++) {
sb.append((char) (random.nextInt(26) + 'a'));
}
return sb.toString();
}
原因是什么呢?
根据 https://blog.csdn.net/qq_42483473/article/details/125334805 这篇文章说的,加上这个,controller返回对象的时候
server.compression.min-response-size 才会有效,否则都会被压缩
于是加上
class AddContentLengthFilter extends OncePerRequestFilter {
@SuppressWarnings("NullableProblems")
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
ContentCachingResponseWrapper cacheResponseWrapper;
if (!(response instanceof ContentCachingResponseWrapper)) {
cacheResponseWrapper = new ContentCachingResponseWrapper(response);
} else {
cacheResponseWrapper = (ContentCachingResponseWrapper) response;
}
filterChain.doFilter(request, cacheResponseWrapper);
cacheResponseWrapper.copyBodyToResponse();
}
}
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<AddContentLengthFilter> filterRegistrationBean() {
FilterRegistrationBean<AddContentLengthFilter> filterBean = new FilterRegistrationBean<>();
filterBean.setFilter(new AddContentLengthFilter());
filterBean.setUrlPatterns(Collections.singletonList("*"));
return filterBean;
}
}
zuul如何正常输出压缩前的报文
问题2:由于网关需要把请求返回报文打印到日志,如果是gzip压缩过的,使用的代码是
StreamUtils.copyToString(new GZIPInputStream(ctx.getResponseDataStream()),StandardCharsets. UTF_8)
由于流读取的不可逆性,也就是如果不采取特殊方法,从流中读取数据时,文件的读取位置会向前移动并且无法回退,这样返回出去的文件流就无法正常读取了,因为读取位置已经移动到了最后,无法再次读取了,这个时候看到一篇攻略:
https://www.jianshu.com/p/e9171909dcc9?utm_campaign=maleskine
增加一个类
import cn.hutool.core.util.StrUtil;
import com.feiynn.study.springcloud.zuul.filter.TimeCostPreFilter;
import com.google.common.base.Stopwatch;
import com.netflix.zuul.context.RequestContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StreamUtils;
import org.springframework.web.util.UrlPathHelper;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.nio.charset.Charset;
import java.util.Objects;
import java.util.zip.GZIPInputStream;
/**
* 由于流读取的不可逆性,也就是如果不采取特殊方法,从流中读取数据时,文件的读取位置会向前移动并且无法回退
* 因此在流使用完毕后的close()方法中输出报文内容更合理
* 参考:<a>https://www.jianshu.com/p/e9171909dcc9?utm_campaign=maleskine</a>
*
* @author Dean
* @see org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter 代码来自于SendResponseFilter类的内部类 RecordingInputStream
*/
@Slf4j
public class LoggerInputStreamWrapper extends InputStream {
private final RequestContext requestContext;
private final InputStream delegate;
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
public LoggerInputStreamWrapper(InputStream delegate, RequestContext requestContext) {
super();
this.delegate = Objects.requireNonNull(delegate);
this.requestContext = requestContext;
}
@Override
public int read() throws IOException {
int read = delegate.read();
if (read != -1) {
buffer.write(read);
}
return read;
}
@SuppressWarnings("NullableProblems")
@Override
public int read(byte[] b, int off, int len) throws IOException {
int read = delegate.read(b, off, len);
if (read != -1) {
buffer.write(b, off, read);
}
return read;
}
@Override
public void close() throws IOException {
String resp;
if (this.requestContext.getResponseGZipped()) {
Stopwatch stopwatch = Stopwatch.createStarted();
InputStream is = new GZIPInputStream(new ByteArrayInputStream(buffer.toByteArray()));
resp = StreamUtils.copyToString(is, Charset.defaultCharset());
log.debug("resp unzip cost time:[{}]", stopwatch.stop());
} else {
resp = buffer.toString();
}
//超过500个字符就用...省略
resp = StrUtil.maxLength(resp, 500);
UrlPathHelper urlPathHelper = new UrlPathHelper();
HttpServletRequest request = requestContext.getRequest();
Long requestStartTime = (Long) requestContext.get(TimeCostPreFilter.START_TIME_KEY);
//请求耗时
long duration = System.currentTimeMillis() - requestStartTime;
log.info("Request resp=[{}] uri=[{}] duration=[{}]", resp, urlPathHelper.getRequestUri(request), duration);
this.delegate.close();
}
}
这样既可以打印输出又不会影响请求响应。