是否存在类似GZipOutputStream的Java解压缩OutputStream过滤器?
实现GZIP格式的解压缩输出流过滤器
我完全懂你的需求——JDK里有GZipOutputStream用来写GZIP压缩数据,但偏偏缺了个对应的写入时自动解压缩GZIP数据的输出流(就像InflaterOutputStream对应DeflaterOutputStream那样)。而且你的编码流程是一串链式的OutputStreamFilter,总不能中途换成InputStream来包裹,还得避免全量缓冲或者开额外线程拖慢性能,确实有点头疼。
其实不用开线程,直接复用Inflater就行
GZIP格式本质就是在DEFLATE压缩数据外面套了层头部和尾部(包含校验和、标识这些),所以我们可以参考InflaterOutputStream的实现逻辑,自己写一个自定义输出流,同步处理解压缩,不用额外线程:
- 先解析掉GZIP的头部(跳过那些标识、版本、标志位之类的字段)
- 用
Inflater来处理核心的DEFLATE压缩数据 - 最后处理GZIP的尾部校验信息
给你一个简化版的实现示例,你可以基于这个完善:
import java.io.*; import java.util.zip.CRC32; import java.util.zip.Inflater; public class GZipInflaterOutputStream extends FilterOutputStream { private final Inflater inflater; private final CRC32 crc = new CRC32(); private byte[] buffer = new byte[4096]; // 可调的缓冲大小,平衡性能和内存 private boolean streamFinished = false; private boolean headerParsed = false; public GZipInflaterOutputStream(OutputStream out) { super(out); this.inflater = new Inflater(true); // 开启GZIP模式的Inflater } @Override public void write(int singleByte) throws IOException { write(new byte[]{(byte) singleByte}, 0, 1); } @Override public void write(byte[] inputBytes, int offset, int length) throws IOException { if (streamFinished) { throw new IOException("GZIP解压缩流已完成写入"); } if (!headerParsed) { parseGZipHeader(inputBytes, offset, length); // 计算头部解析后剩余的待处理字节 int remainingBytes = length - (currentHeaderOffset - offset); if (remainingBytes > 0) { processCompressedChunk(inputBytes, currentHeaderOffset, remainingBytes); } } else { processCompressedChunk(inputBytes, offset, length); } } private int currentHeaderOffset = 0; private void parseGZipHeader(byte[] inputBytes, int offset, int length) throws IOException { ByteArrayInputStream headerStream = new ByteArrayInputStream(inputBytes, offset, length); // 验证GZIP魔数 int magic1 = headerStream.read(); int magic2 = headerStream.read(); if (magic1 != 0x1F || magic2 != 0x8B) { throw new IOException("输入不是合法的GZIP流"); } // 验证压缩方法必须是DEFLATE int compMethod = headerStream.read(); if (compMethod != 8) { throw new IOException("不支持的GZIP压缩方法"); } int flags = headerStream.read(); // 跳过修改时间、压缩级别、操作系统标识(共6字节) headerStream.skip(6); // 如果有扩展字段,跳过 if ((flags & 0x04) != 0) { int extLength = headerStream.read() | (headerStream.read() << 8); headerStream.skip(extLength); } // 如果有注释,跳过直到遇到空字节 if ((flags & 0x08) != 0) { int commentByte; while ((commentByte = headerStream.read()) != 0) {} } // 如果有头部CRC校验,跳过 if ((flags & 0x10) != 0) { headerStream.skip(2); } // 记录解析完头部后,输入数组中剩余数据的起始位置 currentHeaderOffset = offset + (length - headerStream.available()); headerParsed = true; } private void processCompressedChunk(byte[] inputBytes, int offset, int length) throws IOException { inflater.setInput(inputBytes, offset, length); // 循环解压缩直到当前输入块处理完 while (!inflater.needsInput()) { int inflatedLength = inflater.inflate(buffer); if (inflatedLength == 0) { if (inflater.finished()) { finishStream(); break; } throw new IOException("GZIP压缩数据异常截断"); } // 更新CRC校验 crc.update(buffer, 0, inflatedLength); // 把解压缩后的数据写入目标输出流 out.write(buffer, 0, inflatedLength); } } private void finishStream() throws IOException { if (!streamFinished) { inflater.finish(); // 处理剩余的解压缩数据 while (!inflater.finished()) { int inflatedLength = inflater.inflate(buffer); if (inflatedLength == 0) { throw new IOException("GZIP流尾部数据异常"); } crc.update(buffer, 0, inflatedLength); out.write(buffer, 0, inflatedLength); } // 这里可以补充验证GZIP尾部的CRC和原始长度,示例中简化了这部分 streamFinished = true; } } @Override public void finish() throws IOException { finishStream(); } @Override public void close() throws IOException { try { finishStream(); } finally { inflater.end(); super.close(); } } }
几个关键提示
- 示例是简化版,生产环境要补全尾部的CRC32校验和原始长度验证,不然没法确保数据完整性
- 缓冲大小
buffer可以根据你的业务场景调整,比如8192字节在多数场景下性能不错 - 这个实现是同步处理的,调用
write时就会即时解压缩并写入目标流,不用开额外线程,也不会全量缓冲数据,完美适配你的链式输出流需求
这样你就可以把这个GZipInflaterOutputStream和其他OutputStreamFilter串起来用,完全符合你的编码顺序要求啦。
内容的提问来源于stack exchange,提问作者Renato
相关产品推荐
相关产品推荐

