使用ResponseEntity并确保InputStream被关闭的正确流式传输方法

时间:2018-08-14 15:44:30

标签: spring spring-boot spring-web request-mapping

我们的一个应用程序泄漏了文件句柄,但我们尚未找到原因。

在代码中,我可以看到几个与此类似的函数:

public ResponseEntity<InputStreamResource> getFoo( ... ) {
    InputStream content = getContent(...)
    InputStreamResource isr = new InputStreamResource(content);
    return ResponseEntity.status(HttpServletResponse.SC_OK).body(isr);
}

(为简便起见,{if检查并删除了try / catch

我确信这部分会导致问题,因为当我使用JMeter对特定代码进行负载测试时,我可以看到getContent()在此阶段失败:

is = Files.newInputStream(f.toPath());

通常我会关闭InputStream,但是由于这段简短的代码,我无法在return或调用body之前关闭流。

当我运行lsof(代码在Linux上运行)时,我可以看到成千上万的文件以读取模式打开。因此,我确定此问题是由流未关闭引起的。

我应该使用最佳实践代码进行交易吗?

4 个答案:

答案 0 :(得分:5)

您可以尝试使用StreamingResponseBody

  

StreamingResponseBody

     

一种用于异步请求处理的控制器方法返回值类型,其中应用程序可以直接写入响应OutputStream,而无需占用Servlet容器线程。

由于您是在一个单独的线程上直接写响应,因此在解决close()之前要调用return的问题。

可能您可以从以下示例开始

public ResponseEntity<StreamingResponseBody> export(...) throws FileNotFoundException {
    //...

    InputStream inputStream = new FileInputStream(new File("/path/to/example/file"));


    StreamingResponseBody responseBody = outputStream -> {

        int numberOfBytesToWrite;
        byte[] data = new byte[1024];
        while ((numberOfBytesToWrite = inputStream.read(data, 0, data.length)) != -1) {
            System.out.println("Writing some bytes..");
            outputStream.write(data, 0, numberOfBytesToWrite);
        }

        inputStream.close();
    };

    return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=generic_file_name.bin")
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .body(responseBody);
}

此外,除了使用InputStream之外,您还可以尝试使用Files(自Java 7开始)

因此您不必管理InputStream

    File file = new File("/path/to/example/file");

    StreamingResponseBody responseBody = outputStream -> {
        Files.copy(file.toPath(), outputStream);
    };

答案 1 :(得分:3)

您可以重构读取本地文件并将其内容设置为HTTP响应正文的所有控制器方法:

您无需使用ResponseEntity方法,而是注入基础的HttpServletResponse并将从getContent(...)方法返回的输入流的字节复制到HttpServletResponse的输出流,例如通过使用Apache CommonsIO或Google Guava库的与IO相关的实用程序方法。无论如何,请确保关闭输入流!下面的代码通过使用“ try-with-resources”语句隐式地执行此操作,该语句在语句末尾关闭已声明的输入流。

@RequestMapping(value="/foo", method=RequestMethod.GET)
public void getFoo(HttpServletResponse response) {
    // use Java7+ try-with-resources
    try (InputStream content = getContent(...)) {

        // if needed set content type and attachment header
        response.addHeader("Content-disposition", "attachment;filename=foo.txt");
        response.setContentType("txt/plain");

        // copy content stream to the HttpServletResponse's output stream
        IOUtils.copy(myStream, response.getOutputStream());

        response.flushBuffer();
    }
}

参考:

https://docs.oracle.com/javase/7/docs/api/java/io/InputStream.html https://docs.oracle.com/javase/7/docs/api/java/lang/AutoCloseable.html https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html https://google.github.io/guava/releases/19.0/api/docs/com/google/common/io/ByteStreams.html https://commons.apache.org/proper/commons-io/javadocs/api-release/index.html

(尤其是研究类public static int copy(InputStream input, OutputStream output) throws IOException的方法public static int copyLarge(InputStream input, OutputStream output) throws IOExceptionorg.apache.commons.io.IOUtils

答案 2 :(得分:2)

假设您使用的是Spring,则您的方法可以返回Resource并让Spring处理其余部分(包括关闭基础流)。有few implementations of Resource are available within Spring API,否则您需要实现自己的。最后,您的方法将变得简单,并且需要下面的内容

public ResponseEntity<Resource> getFo0(...) {
    return new InputStreamResource(<Your input stream>);
}

答案 3 :(得分:0)

由于此InputStream基本上来自一个简单的文件,因此此代码可以很好地替代:

FileSystemResource fsr = new FileSystemResource(fileName);
return ResponseEntity.status(HttpServletResponse.SC_OK).body(fsr);

FileSystemResource可以使用java.util.Filejava.nio.file.Path甚至是String指向相关文件。