网关支持响应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
// ......
}
原因:
- 是在使用GZIP进行压缩时,创建GZIPInputStream对象时,会调用一个readHeader方法,此方法会从输入流中读取GZIP的头信息。GZIPInputStream先通过readUShort方法读取前两个字节然后与魔数进行比较,如果相同表示当前字节数组为GZIP格式的字节数组,如果不相同则表示当前字节数组不是GZIP格式的数组于是会抛出异常
java.util.zip.ZipException: Not in GZIP format
; - 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();
}
}
}
推荐指数:
3人已点赞

