如何正确关闭作为对象成员的InputStream?
嘿,这个问题我太熟了——InputStream的单次读取限制绝对是这里的隐形大坑!先给你拆解清楚问题,再给你几个实用的解决方案:
不管是从Servlet Part获取的InputStream,还是用byte[]生成的ByteArrayInputStream,它们都有个共性:流是单向的,读完一次就到末尾了。如果你的uploadMethod或者后续业务流程需要多次读取这个流(比如先校验文件格式,再上传到存储服务),第二次读取就只会返回-1,直接导致逻辑失败。而且如果不小心提前关闭了流,后续连读都读不了。
根据你的两种Uploadable创建场景,这里有几个不同层级的解决思路:
方案1:转存为byte[](最通用稳妥)
直接把InputStream的内容提前读取成byte[]存在Uploadable里,这样就能随时生成新的ByteArrayInputStream来读取,彻底解决流只能读一次的问题。
修改后的Uploadable类:
class Uploadable { private byte[] content; // 替换原来的InputStream,用byte[]存储内容 // 从InputStream初始化 public Uploadable(InputStream is) throws IOException { try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = is.read(buffer)) != -1) { baos.write(buffer, 0, bytesRead); } this.content = baos.toByteArray(); } } // 直接从byte[]初始化(更高效) public Uploadable(byte[] content) { this.content = content; } // 每次调用都返回全新的InputStream public InputStream getInputStream() { return new ByteArrayInputStream(content); } }
这样两种创建场景都能完美适配:
- 从Servlet Part创建:
new Uploadable(part.getInputStream()) - 从byte[]创建:
new Uploadable(byteArray)
方案2:Servlet Part场景的特殊优化
如果你的业务只涉及Servlet上下文内的操作,其实Part对象本身已经把文件内容存在临时文件里了,每次调用part.getInputStream()都会返回一个新的流(因为它会重新打开临时文件)。这种情况下你可以考虑让Uploadable持有Part对象,而不是InputStream:
class Uploadable { private Part part; private byte[] content; // 可选,用于上下文外的场景 // 针对Part的构造方法 public Uploadable(Part part) { this.part = part; } public InputStream getInputStream() throws IOException { return part.getInputStream(); } }
但要注意:Servlet的临时文件会在请求结束后自动删除,如果需要在请求上下文之外使用Uploadable,这个方案就会失效,还是方案1更靠谱。
方案3:用可重置的流包装(适合大文件)
如果处理的是超大文件,不想把整个内容加载到内存,可以用BufferedInputStream包装原始流,通过mark/reset功能实现重复读取:
class Uploadable { private BufferedInputStream bufferedStream; public Uploadable(InputStream is) { this.bufferedStream = new BufferedInputStream(is); // 标记流的起始位置,缓冲区要足够容纳整个流(或者按需设置) bufferedStream.mark(Integer.MAX_VALUE); } public InputStream getInputStream() throws IOException { bufferedStream.reset(); // 重置到标记位置,实现重复读取 return bufferedStream; } // 记得用完后关闭流 public void close() throws IOException { bufferedStream.close(); } }
这个方案的限制:
- 原始InputStream必须支持mark/reset(ByteArrayInputStream、Part的InputStream都支持,但不是所有流都支持)
- 超大文件会占用大量内存缓冲区,可能引发OOM,要谨慎使用
如果你的文件大小都在合理范围内,方案1(转存为byte[])是最省心的选择,完全规避了流的各种坑,而且代码逻辑最简单。如果是处理GB级别的超大文件,再考虑方案3或者直接基于原始流做分段处理。
内容的提问来源于stack exchange,提问作者user1156544

