You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 23:50:02