Apache POI гораздо быстрее использует HSSF, чем XSSF - что дальше?
У меня возникли проблемы с разбором файлов .xlsx
с Apache POI - я получаю java.lang.OutOfMemoryError: Java heap space
в моем развернутом приложении. Я обрабатываю файлы только под 5 Мбайт и около 70 000 строк, поэтому мое подозрение от чтения других вопросов заключается в том, что что-то не так.
Как было предложено в этом комментарии, я решил запустить SSPerformanceTest.java
с предлагаемыми переменными, чтобы посмотреть, нет ли чего-то неправильного в моем коде или настройке. Результаты показывают значительное различие между HSSF (.xls
) и XSSF (.xlsx
):
1) HSSF 50000 50 1: Истек 1 секунда
2) SXSSF 50000 50 1: Истекшее 5 секунд
3) XSSF 50000 50 1: Истекшее 15 секунд
FAQ специально говорит:
Если вы не можете запустить это с 50 000 строк и 50 столбцов во всех HSSF, XSSF и SXSSF менее чем за 3 секунды (в идеале намного меньше!), проблема связана с вашей средой.
Далее, он говорит, чтобы выполнить XLS2CSV.java
, который я сделал. Подача в файл XSSF, сгенерированный выше (с 50000 строк и 50 столбцов), занимает около 15 секунд - столько же, сколько требуется для записи файла.
Что-то не так с моей средой, и если да, то как я буду исследовать дальше?
Статистика из VisualVM показывает, что куча использовала съемку до 1,2 ГБ во время обработки. Наверняка, это слишком высоко, учитывая, что дополнительный концерт на вершине кучи по сравнению с началом обработки начался?
![Здесь пустое место здесь?]()
Примечание. Указанное выше исключение кучного пространства происходит только в процессе производства (в Google App Engine) и только для файлов .xlsx
, однако тесты, упомянутые в этом вопросе, были запущены на моей машине разработки с помощью -Xmx2g
. Я надеюсь, что если я смогу исправить проблему в моей настройке разработки, она будет использовать меньше памяти при развертывании.
Трассировка стека из движка приложения:
Вызвано: java.lang.OutOfMemoryError: пространство кучи Java at org.apache.xmlbeans.impl.store.Cur.createElementXobj(Cur.java:260) at org.apache.xmlbeans.impl.store.Cur $CurLoadContext.startElement(Cur.java:2997) at org.apache.xmlbeans.impl.store.Locale $SaxHandler.startElement(Locale.java:3211) at org.apache.xmlbeans.impl.piccolo.xml.Piccolo.reportStartTag(Piccolo.java:1082) на org.apache.xmlbeans.impl.piccolo.xml.PiccoloLexer.parseAttributesNS(PiccoloLexer.java:1802) на org.apache.xmlbeans.impl.piccolo.xml.PiccoloLexer.parseOpenTagNS(PiccoloLexer.java:1521)
Ответы
Ответ 1
Я столкнулся с такой же проблемой, чтобы прочитать громоздкий файл .xlsx с помощью Apache POI, и я наткнулся на
excel-streaming-reader-github
Эта библиотека служит оболочкой для этого потокового API, сохраняя при этом синтаксис стандартного API POI
Эта библиотека может помочь вам прочитать большие файлы.
Ответ 2
Средний лист XLSX, который я работаю, составляет около 18-22 листов из 750 000 рядов с 13-20 колонками. Это вращение в веб-приложении Spring с множеством других функций. Я дал всему приложению не столько большую память: -Xms1024m -Xmx4096m
- и он отлично работает!
Прежде всего, код сбрасывания: неправильно загружать каждую строку данных в память и запускать ее. В моем случае (отчетность из базы данных PostgreSQL) я переработал процедуру дампа данных, чтобы использовать RowCallbackHandler
для записи в мой XLSX, во время этого, когда я достигаю "моего предела" из 750000 строк, я создаю новый лист. И рабочая книга создается с окном видимости в 50 строк. Таким образом, я могу сбрасывать огромные объемы: размер файла XLSX составляет около 1230 МБ.
Некоторый код для написания листов:
jdbcTemplate.query(
new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
PreparedStatement statement = connection.prepareStatement(finalQuery, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
statement.setFetchSize(100);
statement.setFetchDirection(ResultSet.FETCH_FORWARD);
return statement;
}
}, new RowCallbackHandler() {
Sheet sheet = null;
int i = 750000;
int tableId = 0;
@Override
public void processRow(ResultSet resultSet) throws SQLException {
if (i == 750000) {
tableId++;
i = 0;
sheet = wb.createSheet(sheetName.concat(String.format("%02d%n", tableId)));
Row r = sheet.createRow(0);
Cell c = r.createCell(0);
c.setCellValue("id");
c = r.createCell(1);
c.setCellValue("Дата");
c = r.createCell(2);
c.setCellValue("Комментарий");
c = r.createCell(3);
c.setCellValue("Сумма операции");
c = r.createCell(4);
c.setCellValue("Дебет");
c = r.createCell(5);
c.setCellValue("Страхователь");
c = r.createCell(6);
c.setCellValue("Серия договора");
c = r.createCell(7);
c.setCellValue("Номер договора");
c = r.createCell(8);
c.setCellValue("Основной агент");
c = r.createCell(9);
c.setCellValue("Кредит");
c = r.createCell(10);
c.setCellValue("Программа");
c = r.createCell(11);
c.setCellValue("Дата начала покрытия");
c = r.createCell(12);
c.setCellValue("Дата планового окончания покрытия");
c = r.createCell(13);
c.setCellValue("Периодичность уплаты взносов");
}
i++;
PremiumEntity e = PremiumEntity.builder()
.Id(resultSet.getString("id"))
.OperationDate(resultSet.getDate("operation_date"))
.Comments(resultSet.getString("comments"))
.SumOperation(resultSet.getBigDecimal("sum_operation").doubleValue())
.DebetAccount(resultSet.getString("debet_account"))
.Strahovatelname(resultSet.getString("strahovatelname"))
.Seria(resultSet.getString("seria"))
.NomPolica(resultSet.getLong("nom_polica"))
.Agentname(resultSet.getString("agentname"))
.CreditAccount(resultSet.getString("credit_account"))
.Program(resultSet.getString("program"))
.PoliciStartDate(resultSet.getDate("polici_start_date"))
.PoliciPlanEndDate(resultSet.getDate("polici_plan_end_date"))
.Periodichn(resultSet.getString("id_periodichn"))
.build();
Row r = sheet.createRow(i);
Cell c = r.createCell(0);
c.setCellValue(e.getId());
if (e.getOperationDate() != null) {
c = r.createCell(1);
c.setCellStyle(dateStyle);
c.setCellValue(e.getOperationDate());
}
c = r.createCell(2);
c.setCellValue(e.getComments());
c = r.createCell(3);
c.setCellValue(e.getSumOperation());
c = r.createCell(4);
c.setCellValue(e.getDebetAccount());
c = r.createCell(5);
c.setCellValue(e.getStrahovatelname());
c = r.createCell(6);
c.setCellValue(e.getSeria());
c = r.createCell(7);
c.setCellValue(e.getNomPolica());
c = r.createCell(8);
c.setCellValue(e.getAgentname());
c = r.createCell(9);
c.setCellValue(e.getCreditAccount());
c = r.createCell(10);
c.setCellValue(e.getProgram());
if (e.getPoliciStartDate() != null) {
c = r.createCell(11);
c.setCellStyle(dateStyle);
c.setCellValue(e.getPoliciStartDate());
}
;
if (e.getPoliciPlanEndDate() != null) {
c = r.createCell(12);
c.setCellStyle(dateStyle);
c.setCellValue(e.getPoliciPlanEndDate());
}
c = r.createCell(13);
c.setCellValue(e.getPeriodichn());
}
});
После того, как я переработал свой код при отправке данных в XLSX, я столкнулся с проблемой: для их открытия требуется Office в 64 бита. Поэтому мне нужно разделить мою книгу с большим количеством листов на отдельные файлы XLSX с отдельными листами, чтобы сделать их доступными для чтения на среднем компьютере. И снова я использовал небольшие окна видимости и поточную обработку и поддерживал работоспособность всего приложения без каких-либо взглядов OutOfMemory.
Некоторый код для чтения и разделения листов:
OPCPackage opcPackage = OPCPackage.open(originalFile, PackageAccess.READ);
ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(opcPackage);
XSSFReader xssfReader = new XSSFReader(opcPackage);
StylesTable styles = xssfReader.getStylesTable();
XSSFReader.SheetIterator iter = (XSSFReader.SheetIterator) xssfReader.getSheetsData();
int index = 0;
while (iter.hasNext()) {
InputStream stream = iter.next();
String sheetName = iter.getSheetName();
DataFormatter formatter = new DataFormatter();
InputSource sheetSource = new InputSource(stream);
SheetToWorkbookSaver saver = new SheetToWorkbookSaver(sheetName);
try {
XMLReader sheetParser = SAXHelper.newXMLReader();
ContentHandler handler = new XSSFSheetXMLHandler(
styles, null, strings, saver, formatter, false);
sheetParser.setContentHandler(handler);
sheetParser.parse(sheetSource);
} catch(ParserConfigurationException e) {
throw new RuntimeException("SAX parser appears to be broken - " + e.getMessage());
}
stream.close();
// this creates new File descriptors inside storage
FileDto partFile = new FileDto("report_".concat(StringUtils.trimToEmpty(sheetName)).concat(".xlsx"));
File cloneFile = fileStorage.read(partFile);
FileOutputStream cloneFos = new FileOutputStream(cloneFile);
saver.getWb().write(cloneFos);
cloneFos.close();
}
и
public class SheetToWorkbookSaver implements XSSFSheetXMLHandler.SheetContentsHandler {
private SXSSFWorkbook wb;
private Sheet sheet;
private CellStyle dateStyle ;
private Row currentRow;
public SheetToWorkbookSaver(String workbookName) {
this.wb = new SXSSFWorkbook(50);
this.dateStyle = this.wb.createCellStyle();
this.dateStyle.setDataFormat(this.wb.getCreationHelper().createDataFormat().getFormat("dd.mm.yyyy"));
this.sheet = this.wb.createSheet(workbookName);
}
@Override
public void startRow(int rowNum) {
this.currentRow = this.sheet.createRow(rowNum);
}
@Override
public void endRow(int rowNum) {
}
@Override
public void cell(String cellReference, String formattedValue, XSSFComment comment) {
int thisCol = (new CellReference(cellReference)).getCol();
Cell c = this.currentRow.createCell(thisCol);
c.setCellValue(formattedValue);
c.setCellComment(comment);
}
@Override
public void headerFooter(String text, boolean isHeader, String tagName) {
}
public SXSSFWorkbook getWb() {
return wb;
}
}
Таким образом, он считывает и записывает данные. Думаю, в вашем случае вы должны переделать свой код на те же шаблоны: сохранить в памяти только малую площадь данных. Поэтому я бы предложил для чтения создать пользовательский SheetContentsReader
, который будет толкать данные в какую-либо базу данных, где ее можно легко обрабатывать, агрегировать и т.д.