网关支持响应GZIP时GZIPInputStream压缩流问题

社区作者 | 2035 | 2020-01-07

一 背景

在构建网关应用时,为了快速响应客户端,一般我们会将业务中台接口响应内容压缩后返回给客户端调用者。

二 方案

2.1 判断客户端是否支持压缩流

根据客户端请求头的accept-encoding是否包含gzip来判断客户端是否支持压缩流的读取。

public Boolean isGzipRequested() { Collection<String> values = headers.get("accept-encoding"); if (ObjectUtils.isEmpty(values)) { return Boolean.FALSE; } for (String value : values) { if (value.contains("gzip")) { return Boolean.TRUE; } } return Boolean.FALSE; }

2.2 将响应内容压缩

如果支持gzip,就响应内容压缩。

public InputStream getInputStream() { if (!isGzipRequested()) { // 返回普通流 return getByteArrayInputStream(); } try { // 返回压缩流 return getGZIPInputStream(); } catch (Exception e) { // 压缩出异常,返回普通流 return getByteArrayInputStream(); } } /** * 返回普通流 */ private InputStream getByteArrayInputStream() { return new ByteArrayInputStream(getResponseBody().getBytes(StandardCharsets.UTF_8)); } /** * 返回压缩流 */ private InputStream getGZIPInputStream() throws IOException { return new GZIPInputStream(getByteArrayInputStream()); }

2.3 写回客户端

Object run0() { RequestContext context = RequestContext.getCurrentContext() HttpServletResponse servletResponse = context.getResponse() // 添加响应内容编码 servletResponse.setCharacterEncoding(StandardCharsets.UTF_8.toString()) OutputStream outStream = servletResponse.getOutputStream() InputStream is = null AbstractCompatibleResponse response = context.get(ZuulRequestContextConstants.RESPONSE) try { // 获取写入流 is = response.getInputStream() // 添加响应头信息 addResponseHeaders(servletResponse, response.getResponseHeaders()) // 写回响应内容 writeResponse(is, outStream) } finally { if (is != null) { try { is.close() } catch (Exception e) {} } try { outStream.flush() outStream.close() } catch (IOException e) {} } return null } /** * 写回响应内容 */ private static void writeResponse(InputStream zin, OutputStream out) { byte[] bytes = new byte[BUFFER_SIZE] int bytesRead = -1 while ((bytesRead = zin.read(bytes)) != -1) { try { out.write(bytes, 0, bytesRead) out.flush() } catch (IOException e) { logger.warn("write response error", e) } if (bytesRead == bytes.length) { bytes = new byte[bytes.length * 2] } } }

三 问题

上面的2.2步骤getGZIPInputStream方法执行时会报java.util.zip.ZipException: Not in GZIP format异常。

看一下具体是哪里抛出来的:

/** * GZIP header magic number. */ public final static int GZIP_MAGIC = 0x8b1f; public GZIPInputStream(InputStream in, int size) throws IOException { super(in, new Inflater(true), size); usesDefaultInflater = true; readHeader(in); } /** * 读取GZIP成员标头,并返回此成员标头的总字节数。 */ private int readHeader(InputStream this_in) throws IOException { CheckedInputStream in = new CheckedInputStream(this_in, crc); crc.reset(); // Check header magic if (readUShort(in) != GZIP_MAGIC) { throw new ZipException("Not in GZIP format"); } // Check compression method if (readUByte(in) != 8) { throw new ZipException("Unsupported compression method"); } // Read flags // ...... }

原因:

  1. 是在使用GZIP进行压缩时,创建GZIPInputStream对象时,会调用一个readHeader方法,此方法会从输入流中读取GZIP的头信息。GZIPInputStream先通过readUShort方法读取前两个字节然后与魔数进行比较,如果相同表示当前字节数组为GZIP格式的字节数组,如果不相同则表示当前字节数组不是GZIP格式的数组于是会抛出异常java.util.zip.ZipException: Not in GZIP format
  2. String的getBytes方法获取到的字节数组不是一个压缩的字节流,所以在读取前两个字节与压缩流的魔数比较时自然就报错了;

四 解决

修改getGZIPInputStream方法如下:

/** * 返回压缩流 */ private InputStream getGZIPInputStream() throws IOException { ByteArrayOutputStream os = new ByteArrayOutputStream(); GZIPOutputStream gos; ByteArrayInputStream is = null; try { gos = new GZIPOutputStream(os); gos.write(getResponseBody().getBytes(StandardCharsets.UTF_8.toString())); gos.close(); is = new ByteArrayInputStream(os.toByteArray()); return new GZIPInputStream(is); } finally { os.close(); if (null != is) { is.close(); } } }
文章标签: JavaTomcat
推荐指数:

3人已点赞

网关支持响应GZIP时GZIPInputStream压缩流问题

关于作者 📝

社区作者

开发者客栈官方认证作者

等级 LV5

粉丝 30

获赞 198

经验 1145