Использование ServletOutputStream для записи очень больших файлов в сервлете Java без проблем с памятью - PullRequest
37 голосов
/ 26 марта 2009

Я использую IBM Websphere Application Server v6 и Java 1.4 и пытаюсь записать большие файлы CSV в ServletOutputStream для загрузки пользователем. Размер файлов на данный момент составляет от 50 до 750 МБ.

Файлы меньшего размера не вызывают особых проблем, но с файлами большего размера создается впечатление, что они записываются в кучу, что приводит к ошибке OutOfMemory и отключению всего сервера.

Эти файлы могут быть предоставлены только аутентифицированным пользователям через HTTPS, поэтому я передаю их через сервлет, а не просто прикрепляю их в 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 ]

41 голосов
/ 16 июня 2010

Средний приличный сервлет-контейнер сам очищает поток по умолчанию каждые ~ 2 КБ. Вам действительно не нужно явно вызывать flush() для OutputStream из HttpServletResponse через определенные промежутки времени при последовательной потоковой передаче данных из одного и того же источника. Например, в Tomcat (и Websphere!) Это настраивается как атрибут bufferSize коннектора HTTP.

Средний достойный сервлет-контейнер также просто передает данные в чанках , если длина контента заранее неизвестна (согласно спецификации Servlet API !) И если клиент поддерживает HTTP 1.1.

Симптомы проблемы, по крайней мере, указывают на то, что сервлет-контейнер буферизирует весь поток в памяти перед сбросом. Это может означать, что заголовок длины содержимого не установлен, и / или контейнер сервлета не поддерживает кодирование по частям, и / или клиентская сторона не поддерживает кодирование по частям (то есть использует HTTP 1.0).

Чтобы исправить одно или другое, просто установите длину контента заранее:

response.setHeader("Content-Length", String.valueOf(new File(path).length()));
1 голос
/ 24 июня 2011

Пока условие не работает, вы должны проверить -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();
1 голос
/ 21 июня 2010
  1. Класс Кевина должен закрыть поле m_out, если оно не равно нулю в операторе close (), мы не хотим что-то утекать, не так ли?

  2. Как и оператор ServletOutputStream.flush(), операция HttpServletResponse.flushBuffer() также может очищать буферы. Однако, похоже, что это специфическая для реализации деталь относительно того, оказывают ли эти операции какое-либо влияние или мешает ли поддержка длины содержимого http. Помните, указание длины содержимого является опцией для HTTP 1.0, поэтому все должно просто вытекать, если вы сбрасываете данные. Но я не вижу этого

1 голос
/ 16 июня 2010

Итак, следуя вашему сценарию, разве вы не должны были находиться в потоке внутри цикла while (на каждой итерации), а не за его пределами? Я бы попробовал это, но с немного большим буфером.

1 голос
/ 30 марта 2009

Я также не уверен, что flush() на ServletOutputStream работает в этом случае, но ServletResponse.flushBuffer() должен отправить ответ клиенту (по крайней мере для спецификации 2.3 сервлета).

ServletResponse.setBufferSize() звучит многообещающе.

1 голос
/ 26 марта 2009

Я использовал класс, который оборачивает выходной поток, чтобы сделать его повторно используемым в других контекстах. Это помогло мне быстрее получить данные в браузере, но я не смотрел на последствия для памяти. (прошу прощения за мое устаревшее имя переменной 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;
            }
        }
    }
}
1 голос
/ 26 марта 2009

Работает ли flush в выходном потоке.

На самом деле я хотел бы прокомментировать, что вам следует использовать форму записи с тремя аргументами, поскольку буфер не обязательно полностью читается (особенно в конце файла (!)). Также будет попытка / finally, если вы не хотите, чтобы ваш сервер неожиданно умер.

0 голосов
/ 07 июля 2015

Ibm сервер приложений Websphere по умолчанию использует асинхронную передачу данных для сервлетов. Это означает, что он буферизует ответ. Если у вас есть проблемы с большими данными и исключениями OutOfMemory, попробуйте изменить настройки в WAS, чтобы использовать синхронный режим.

Перевод WebStainer WebSphere Application Server в синхронный режим

Вы также должны позаботиться о загрузке кусков и очистке их. Образец для загрузки из большого файла.

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();
            }
0 голосов
/ 25 апреля 2014

ваш код имеет бесконечный цикл.

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

смещение имеет одно и то же значение во всем цикле, поэтому, если изначально смещение = 0 , оно останется таковым на каждой итерации, что вызовет бесконечный цикл и приведет к OOM ошибка.

0 голосов
/ 26 марта 2009

не имеет отношения к вашей памяти, цикл while должен быть:

while(bytesRead > 0);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...