使用ServletOutputStream在Java servlet中编写非常大的文件而不会出现内存问题

时间:2009-03-26 10:52:01

标签: java servlets stream websphere

我正在使用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,这没有帮助。

10 个答案:

答案 0 :(得分:41)

平均体面的servlet容器本身默认每隔2KB刷新一次流。在按顺序从同一个源传输数据时,您实际上不需要在flush()的{​​{1}}上显式调用OutputStream。在例如Tomcat(和Websphere!)中,这可以配置为HTTP连接器的HttpServletResponse属性。

如果内容长度事先未知(根据chunks!)并且客户端支持HTTP 1.1,那么普通的体面servlet容器也会在Servlet API specification中流式传输数据。

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

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

bufferSize

答案 1 :(得分:1)

flush是否适用于输出流。

我真的想评论你应该使用三参数形式的写入,因为缓冲区不一定完全读取(特别是在文件末尾(!))。除非您希望服务器意外死亡,否则try / finally将按顺序进行。

答案 2 :(得分:1)

我使用了一个包装输出流的类,使其可以在其他上下文中重用。它对我在浏览器中更快地获取数据效果很好,但我没有看过内存含义。 (请原谅我过时的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;
            }
        }
    }
}

答案 3 :(得分:1)

我也不确定flush() ServletOutputStream是否适用于此情况,但ServletResponse.flushBuffer()应将响应发送给客户端(至少每2.3 servlet规范)。

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

答案 4 :(得分:1)

那么,按照你的场景,你不应该在while循环(在每次迭代中)而不是在它之外的同时刷新(ing)吗?我会尝试使用更大的缓冲区。

答案 5 :(得分:1)

  1. Kevin的类应关闭m_out字段,如果它在close()运算符中不为null,我们不想泄漏,是吗?

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

答案 6 :(得分:1)

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();

答案 7 :(得分:0)

与你的记忆问题无关,while循环应该是:

while(bytesRead > 0);

答案 8 :(得分:0)

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

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

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

答案 9 :(得分:-1)

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();
            }