SpringBoot配置Gzip压缩,解决小于server.compression.min-response-size会被压缩的问题,以及zuul如何正常输出压缩前的报文

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();
    }
}

这样既可以打印输出又不会影响请求响应。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值