Подтвердить что ты не робот

Использование ServletOutputStream для записи очень больших файлов в сервлете Java без проблем с памятью

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

Меньшие файлы не вызывают слишком много проблем, но с более крупными файлами кажется, что он записывается в кучу, которая затем вызывает ошибку 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 один раз за итерацию цикла и после цикла, что не помогло.

4b9b3361

Ответ 1

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

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

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

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

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

Ответ 2

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

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

Ответ 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

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

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

Ответ 5

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

Ответ 6

  • Класс Kevin должен закрыть поле m_out, если он не равен null в операторе close(), мы не хотим утечки вещей, не так ли?

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

Ответ 7

Условие 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();

Ответ 8

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

while(bytesRead > 0);

Ответ 9

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

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

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

Ответ 10

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

Установка WebCphere Application Server WebContainer в синхронный режим

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

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