Spring MVC返回InputStreamResource时CloseableHttpResponse提前关闭的解决办法
解决方案
你的问题核心在于finally块会在return语句执行前触发,导致CloseableHttpResponse被提前关闭,而InputStreamResource依赖的输入流也会随之失效。以下是两种可行的解决方案:
方案一:自定义追踪关闭的输入流
通过包装原始输入流,让CloseableHttpResponse在Spring MVC完成响应输出、关闭流时再同步关闭。
1. 实现自定义输入流类
public class CloseTrackingInputStream extends InputStream { private final InputStream delegate; private final CloseableHttpResponse response; public CloseTrackingInputStream(InputStream delegate, CloseableHttpResponse response) { this.delegate = delegate; this.response = response; } @Override public int read() throws IOException { return delegate.read(); } @Override public int read(byte[] b) throws IOException { return delegate.read(b); } @Override public int read(byte[] b, int off, int len) throws IOException { return delegate.read(b, off, len); } @Override public long skip(long n) throws IOException { return delegate.skip(n); } @Override public int available() throws IOException { return delegate.available(); } @Override public void close() throws IOException { try { delegate.close(); } finally { response.close(); } } @Override public synchronized void mark(int readlimit) { delegate.mark(readlimit); } @Override public synchronized void reset() throws IOException { delegate.reset(); } @Override public boolean markSupported() { return delegate.markSupported(); } }
2. 修改控制器方法
移除finally块,改用自定义流包装原始输入流,异常时手动关闭资源:
public ResponseEntity<InputStreamResource> getFileAsStream(@PathVariable("uuid") String uuid) throws IOException, InterruptedException { CloseableHttpResponse response = fileService.importFileByUuid(uuid); try { HttpEntity entity = response.getEntity(); if (entity == null) { throw new IllegalStateException(String.format( "Error during stream downloading, uuid = %s", uuid)); } HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(entity.getContentLength())); responseHeaders.add(HttpHeaders.CONTENT_TYPE, entity.getContentType().getValue()); responseHeaders.add(HttpHeaders.CONTENT_DISPOSITION, response.getFirstHeader(HttpHeaders.CONTENT_DISPOSITION).getValue()); InputStream inputStream = new CloseTrackingInputStream(entity.getContent(), response); InputStreamResource inputStreamResource = new InputStreamResource(inputStream); return ResponseEntity.ok() .headers(responseHeaders) .body(inputStreamResource); } catch (IOException | InterruptedException e) { response.close(); throw e; } }
方案二:使用StreamingResponseBody
Spring的StreamingResponseBody会在响应流完全输出后再执行后续清理逻辑,天然适配这种场景:
import org.apache.commons.io.IOUtils; public ResponseEntity<StreamingResponseBody> getFileAsStream(@PathVariable("uuid") String uuid) throws IOException, InterruptedException { CloseableHttpResponse response = fileService.importFileByUuid(uuid); try { HttpEntity entity = response.getEntity(); if (entity == null) { response.close(); throw new IllegalStateException(String.format( "Error during stream downloading, uuid = %s", uuid)); } HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(entity.getContentLength())); responseHeaders.add(HttpHeaders.CONTENT_TYPE, entity.getContentType().getValue()); responseHeaders.add(HttpHeaders.CONTENT_DISPOSITION, response.getFirstHeader(HttpHeaders.CONTENT_DISPOSITION).getValue()); StreamingResponseBody body = outputStream -> { try (InputStream inputStream = entity.getContent()) { IOUtils.copy(inputStream, outputStream); } finally { response.close(); } }; return ResponseEntity.ok() .headers(responseHeaders) .body(body); } catch (IOException | InterruptedException e) { response.close(); throw e; } }
注:IOUtils.copy来自Apache Commons IO工具包,也可以手动实现流的循环复制逻辑。
两种方案都能保证CloseableHttpResponse在流处理完成后再关闭,同时避免资源泄漏。
内容的提问来源于stack exchange,提问作者Evgenia Rubanova
相关产品推荐
相关产品推荐

