Как избежать использования toString (). getBytes ("UTF-8"), чтобы избежать ошибки OOM? Есть ли лучший подход для преобразования в byte [] из StringWriter? - PullRequest
0 голосов
/ 11 января 2019

У меня есть веб-приложение для составления отчетов. Приложение получает данные из базы данных и сохраняет данные в объекте StringWriter. Я должен получить эти данные в формате байтового массива, чтобы создать CSV-файл и отправить его в браузер.

Ниже приведен фрагмент кода

 return new FileTransfer(fileName, reportType.getMimeType(),
                    new ByteArrayInputStream(generateCSV(reportType, grid, new DataList(), params).toString().getBytes("UTF-8")));

, где generateCSV возвращает объект StringWriter, затем для преобразования его в байтовый массив, который я вызываю toString, а затем метод getBytes(). Ниже показано, как выглядит метод generateCSV

StringWriter generateCSV(ReportType reportType, GridConfig grid, DataList dataList, String params) {......}

Проблема в том, что, когда в моем отчете есть огромные записи (более 1 миллиона), метод getBytes() завершается с ошибкой

java.lang.OutOfMemoryError: Размер запрашиваемого массива превышает ограничение виртуальной машины

Все данные отчета при преобразовании в объект String имеют огромное количество символов (миллиарды). Метод .getBytes("UTF-8") преобразует его в массив, каждый элемент массива в виде одного символа. А для 1 миллиона записей размер символа превышает ограничение размера MAX JVM ARRAY (https://plumbr.io/outofmemoryerror/requested-array-size-exceeds-vm-limit).

Теперь, как я могу избежать использования toString().getBytes("UTF-8"), чтобы избежать ошибки OOM? Есть ли лучший подход для преобразования в байтовый массив из StringWriter?

Ответы [ 2 ]

0 голосов
/ 14 января 2019

Странно получить результат generateCSV как StringWriter; предпочтительным решением было бы позволить методу записать цель при генерации, чтобы у вас не было всего содержимого в памяти.

В любом случае вам следует прибегнуть к конструктору FileTransfer(String, String mimeType, OutputStreamLoader), чтобы получить цель OutputStream, когда пришло время записывать фактические данные.

Если вы не можете избежать промежуточного StringWriter, вы должны, по крайней мере, избегать вызова toString для него, так как построение String подразумевает создание копии всего буфера.

Таким образом, решение может выглядеть следующим образом:

return new FileTransfer(fileName, reportType.getMimeType(), new OutputStreamLoader() {
    public void close() {}
    public void load(OutputStream out) throws IOException {
        // the best would be to let generateCSV write to out directly
        // otherwise use:
        StringBuffer sb = generateCSV(reportType, grid, new DataList(), params).getBuffer();
        Writer w = new OutputStreamWriter(out, "UTF-8")
        final int bufSize = 8192;
        for(int s = 0, e; s < sb.length(); s = e) {
            e = Math.min(sb.length(), s + bufSize);
            w.write(sb.substring(s, e));
        }
        w.flush(); // let the caller close the OutputStream
    }
});

Альтернативой StringWriter будет CharArrayWriter, которая имеет writeTo​(Writer out), что устраняет необходимость в реализации цикла копирования вручную и может быть еще более эффективным , Но, как уже было сказано, рефакторинг generateCSV для прямой записи в цель был бы еще лучше.

0 голосов
/ 11 января 2019

StringWriter хранит свое содержимое в памяти. Так что не стоит использовать его с большими файлами.

Вы должны попытаться разделить Файл напрямую на InputStream без StringWriter в середине. Как насчет вашей собственной реализации InputStream, которая читает и конвертирует файл в csv на лету.

Можете ли вы показать нам метод generateCSV?

...