【问题标题】:Using ServletOutputStream to write very large files in a Java servlet without memory issues使用 ServletOutputStream 在 Java servlet 中写入非常大的文件而不会出现内存问题
【发布时间】:2010-10-15 16:14:50
【问题描述】:

我正在使用 IBM Websphere Application Server v6 和 Java 1.4 并尝试将大型 CSV 文件写入ServletOutputStream 以供用户下载。目前文件大小在 50-750MB 之间。

较小的文件不会造成太大的问题,但对于较大的文件,它似乎正在被写入堆中,这会导致 OutOfMemory 错误并关闭整个服务器。

这些文件只能通过 HTTPS 提供给经过身份验证的用户,这就是为什么我通过 Servlet 为它们提供服务,而不是仅仅将它们粘贴在 Apache 中。

我正在使用的代码是(在此周围删除了一些绒毛):

    resp.setHeader("Content-length", "" + fileLength);
    resp.setContentType("application/vnd.ms-excel");
    resp.setHeader("Content-Disposition","attachment; filename=\"export.csv\"");

    FileInputStream inputStream = null;

    try
    {
        inputStream = new FileInputStream(path);
        byte[] buffer = new byte[1024];
        int bytesRead = 0;

        do
        {
            bytesRead = inputStream.read(buffer, offset, buffer.length);
            resp.getOutputStream().write(buffer, 0, bytesRead);
        }
        while (bytesRead == buffer.length);

        resp.getOutputStream().flush();
    }
    finally
    {
        if(inputStream != null)
            inputStream.close();
    }

FileInputStream 似乎没有引起问题,就像我写入另一个文件或只是完全删除写入内存使用似乎不是问题一样。

我在想的是resp.getOutputStream().write 被存储在内存中,直到数据可以发送到客户端。所以整个文件可能会被读取并存储在resp.getOutputStream() 中导致我的内存问题和崩溃!

我已经尝试缓冲这些流,还尝试使用来自java.nio 的通道,但这些似乎都不会对我的内存问题产生任何影响。我还在循环的每次迭代和循环之后刷新了一次 OutputStream,但这没有帮助。

【问题讨论】:

标签: java servlets stream websphere


【解决方案1】:

flush 是否对输出流起作用。

我真的想评论一下,您应该使用三参数形式的写入,因为缓冲区不一定是完全读取的(尤其是在文件末尾(!))。除非您希望您的服务器意外死机,否则尝试/最终将是有序的。

【讨论】:

  • Flush 确实对输出流起作用。是的,它周围确实有一个 try/finally 块,输入流在其中关闭。我已经尝试过读取和写入的 1 和 3-arg 版本,但似乎没有什么不同,所以为了便于阅读,我在帖子中使用了 1 arg 版本。
【解决方案2】:

与你的内存问题无关,while循环应该是:

while(bytesRead > 0);

【讨论】:

  • 嗯,如果我将 while 循环设置为它永远不会向输出流写入任何内容。除非我将初始读取移到循环之外。也许我应该改用它,而 ((bytesRead = inputStream.read(buffer, offset, buffer.length)) != -1 ) 会更安全。无论哪种方式都不相关:(
  • 警告:返回 0 字节是完全可能的,不应该终止循环。
【解决方案3】:

我使用了一个包装输出流的类,使其可在其他上下文中重用。它在更快地将数据传输到浏览器方面对我来说效果很好,但我还没有研究内存影响。 (请原谅我过时的 m_ 变量命名)

import java.io.IOException;
import java.io.OutputStream;

public class AutoFlushOutputStream extends OutputStream {

    protected long m_count = 0;
    protected long m_limit = 4096; 
    protected OutputStream m_out;

    public AutoFlushOutputStream(OutputStream out) {
        m_out = out;
    }

    public AutoFlushOutputStream(OutputStream out, long limit) {
        m_out = out;
        m_limit = limit;
    }

    public void write(int b) throws IOException {

        if (m_out != null) {
            m_out.write(b);
            m_count++;
            if (m_limit > 0 && m_count >= m_limit) {
                m_out.flush();
                m_count = 0;
            }
        }
    }
}

【讨论】:

    【解决方案4】:

    我也不确定ServletOutputStream 上的flush() 在这种情况下是否有效,但ServletResponse.flushBuffer() 应该将响应发送给客户端(至少按照 2.3 servlet 规范)。

    ServletResponse.setBufferSize() 听起来也很有希望。

    【讨论】:

      【解决方案5】:

      那么,按照你的场景,你不应该在 while 循环内(每次迭代)而不是在它之外刷新(ing)吗?我会尝试这样做,不过缓冲区要大一些。

      【讨论】:

        【解决方案6】:

        默认情况下,一般的 servletcontainer 本身每 ~2KB 刷新一次流。当从同一个源顺序流式传输数据时,您真的不需要在HttpServletResponseOutputStream 上显式调用flush()。例如,在 Tomcat(和 Websphere!)中,这可配置为 HTTP 连接器的 bufferSize 属性。

        如果事先不知道内容长度(根据 Servlet API specification!)并且客户端支持 HTTP 1.1,那么一般的 servletcontainer 也只是在 chunks 中流式传输数据。

        问题症状至少表明 servletcontainer 在刷新之前正在缓冲内存中的整个流。这可能意味着未设置内容长度标头和/或 servletcontainer 不支持分块编码和/或客户端不支持分块编码(即它使用的是 HTTP 1.0)。

        要修复一个或另一个,只需预先设置内容长度:

        response.setContentLengthLong(new File(path).length());
        

        或者当您还没有使用 Servlet 3.1 时:

        response.setHeader("Content-Length", String.valueOf(new File(path).length()));
        

        【讨论】:

          【解决方案7】:
          1. Kevin 的班级应该关闭 m_out 字段,如果它在 close() 运算符中不为空,我们不想泄露内容,是吗?

          2. 除了ServletOutputStream.flush() 操作符,HttpServletResponse.flushBuffer() 操作也可以刷新缓冲区。但是,关于这些操作是否有任何影响,或者 http 内容长度支持是否受到干扰,这似乎是一个特定于实现的细节。请记住,指定内容长度是 HTTP 1.0 上的一个选项,因此如果您刷新内容,则内容应该只是流出。但我没看到

          【讨论】:

          • 1) 这值得商榷。该类没有创建流,因此您可以说它没有所有权并且不应该执行关闭操作。
          【解决方案8】:

          while条件不起作用,使用前需要检查-1。请为输出流使用一个临时变量,它更易于阅读,并且可以安全地重复调用 getOutputStream()。

          OutputStream outStream = resp.getOutputStream();
          while(true) {
              int bytesRead = inputStream.read(buffer);
              if (bytesRead < 0)
                break;
              outStream.write(buffer, 0, bytesRead);
          }
          inputStream.close();
          out.close();
          

          【讨论】:

            【解决方案9】:

            您的代码有一个无限循环。

            do
            {
                bytesRead = inputStream.read(buffer, offset, buffer.length);
                resp.getOutputStream().write(buffer, 0, bytesRead);
            }
            while (bytesRead == buffer.length);
            

            offset 在整个循环中具有相同的值,因此如果最初 offset = 0,它将在每次迭代中保持不变,这将导致无限循环并导致到OOM错误。

            【讨论】:

              【解决方案10】:

              默认情况下,IBM websphere 应用服务器对 servlet 使用异步数据传输。这意味着它缓冲响应。如果您遇到大数据和 OutOfMemory 异常的问题,请尝试更改 WAS 上的设置以使用同步模式。

              Setting the WebSphere Application Server WebContainer to synchronous mode

              您还必须注意加载块并刷新它们。 从大文件加载的示例。

              ServletOutputStream os = response.getOutputStream();
              FileInputStream fis = new FileInputStream(file);
                          try {
                              int buffSize = 1024;
                              byte[] buffer = new byte[buffSize];
                              int len;
                              while ((len = fis.read(buffer)) != -1) {
                                  os.write(buffer, 0, len);
                                  os.flush();
                                  response.flushBuffer();
                              }
                          } finally {
                              os.close();
                          }
              

              【讨论】:

                猜你喜欢
                • 2011-11-04
                • 1970-01-01
                • 1970-01-01
                • 2012-04-15
                • 2012-03-26
                • 1970-01-01
                • 2016-04-15
                • 1970-01-01
                相关资源
                最近更新 更多