1. Введение: почему снова «Excel»?
Одним из наиболее распространённых и одновременно сложных требований при разработке корпоративных решений, несомненно, является **«обработка Excel»**. Пользователям в реальной бизнес-среде недостаточно просто просматривать эффектные информационные панели на веб-экранах: им необходимо выполнять запросы к сложным табличным данным с несколькими уровнями заголовков, скачивать шаблоны, оформленные в виде древовидной структуры на основе групп, работать в автономном режиме, а затем массово загружать результаты обратно в систему.
Долгое время Apache POI фактически был стандартом обработки Excel в экосистеме Java backend. Благодаря большому количеству примеров в экосистеме открытого исходного кода и неоспоримому преимуществу в виде бесплатного использования большинство проектов начинали с POI. Однако при сочетании следующих сложных бизнес-требований команды разработчиков сталкиваются с огромным техническим барьером.
-
Создание шаблонов с групповыми полями и **сложными иерархическими многоуровневыми заголовками (Multi-Header)**, а также древовидными структурами
-
При внедрении высокопроизводительного табличного компонента, такого как SpreadJSв веб-интерфейсах между фронтендом и бэкендом могут возникать расхождения в форматировании и синхронизации данных
-
Помимо простого ввода и вывода значений, SUMPRODUCT, DSUM и других расширенных механизмов статистических вычислений и вычисления формулсерверная оценка в реальном времени
-
При обработке от десятков до сотен тысяч строк JVM OutOfMemoryError(OOM) и скачки GC
В этой статье рассматриваются реальные ограничения, с которыми мы столкнулись в среде Apache POI, а также описывается наш опыт модернизации архитектуры за счёт внедрения **DsExcel (Document Solutions for Excel, ранее GcExcel)**. В частности, рассматривается синергия нативной интеграции с JSON и SpreadJS на фронтенде, а также стратегии импорта шаблонов с древовидной структурой, экспорта таблиц с несколькими уровнями заголовков и обработки сложных формулкоторые мы ранее тщательно рассматривали на этапах моделирования предметной области и проектирования данных, подробно — на уровне кода и архитектуры.
2. Подробное сравнение серверных движков Excel: Apache POI и DsExcel
Ключевыми факторами, которые следует учитывать при выборе библиотеки Excel для бэкенда, являются архитектура памяти, механизм вычисления формул, эффективность работы с шаблонами и лицензионные затраты.
2.1 Архитектура и модели управления памятью
Двойственность Apache POI (XSSFWorkbook и SXSSFWorkbook)
-
XSSFWorkbook (DOM-модель): Загружает все листы, строки, ячейки и объекты стилей файла Excel (XLSX) в кучу JVM в виде полного графа объектов. После превышения объёма данных в несколько десятков тысяч записей для каждой ячейки создаётся множество объектов-обёрток Java, из-за чего потребление памяти растёт экспоненциально, быстро приводя к OOM.
-
SXSSFWorkbook (потоковая оконная модель): Чтобы избежать OOM, хранит в памяти только скользящее окно (например, последние 100 строк), а остальные данные выгружает во временные файлы на диске. Однако это связано с серьёзными ограничениями.
-
Невозможно изменять уже обработанные строки или обращаться к ним в обратном порядке.
-
Динамический пересчёт объединённых ячеек (Merged Region) или иерархических заголовков крайне затруднён.
-
Вычисление формул со ссылками на другие листы и поддержка сводных таблиц практически невозможны.
-
При создании отчётов на основе шаблонов сложно одновременно считывать форматирование существующего шаблона и выполнять потоковую запись (Read-Write Template).
Высокопроизводительная архитектура движка DsExcel
-
DsExcel идеально имитирует механизм вычислений и внутренние структуры данных Microsoft Excel, используя при этом высокооптимизированную структуру с отображением в память, подходящую для сред Java/.NET.
-
Оптимизация памяти на основе указателей и примитивных массивов: Вместо бесконечного создания тяжёлых экземпляров для каждой ячейки, как в POI, данные и стили кэшируются на основе внутренних индексов, что позволяет ограничить использование кучи примерно от 1/5 до 1/10 уровня POI.
-
Он способен полностью в памяти с высокой скоростью создавать и анализировать книги со ста тысячами строк и более без временного ввода-вывода на диск.
2.2 Различия в механизмах вычисления формул
На практике поддержка часто запрашиваемых формул, таких как SUMPRODUCT, DSUM, INDEX/MATCH, XLOOKUP и динамические массивы (Spill), является решающим показателем зрелости движка.
-
Apache POI: Хотя FormulaEvaluator встроен, список поддерживаемых функций ограничен. В частности, при вычислении SUMPRODUCT, выполняющей операции с массивами по нескольким условиям, или функций базы данных (DSUM, DCOUNT) реализации часто отсутствуют, либо при разборе сложных вложенных скобок регулярно возникает NotImplementedException.
-
DsExcel: Он включает независимый высокопроизводительный механизм вычисления формул, на 100% совместимый со спецификациями Microsoft Excel, и способен безупречно вычислять более 450 стандартных функций Excel в автономном режиме (на стороне сервера). Это гарантирует полную согласованность формул без установки Excel Desktop или Office Interop (Interop).
2.3 Эффективность привязки шаблонов
-
Подход POI: После загрузки разработанного формата Excel разработчики должны вручную подсчитывать индексы строк и столбцов (rowNum++, cellNum++) и задавать через код Java стили шрифтов, границ и цвета фона. Даже если специалист по планированию или дизайнер изменит всего одну строку форматирования, вся логика вычисления индексов на бэкенде нарушится.
-
Подход DsExcel: Мощная Декларативный механизм шаблоновпредоставляется по умолчанию. Если непосредственно в файле Excel записать такой синтаксис, как {{ds.fieldName}} и {{ds.group(R=T)}}, серверная часть сможет автоматически выполнять развертывание повторяющихся строк, вставку промежуточных и общих итогов, а также динамическое объединение ячеек — достаточно передать Java POJO или набор данных JSON.
2.4. Сводная сравнительная таблица
|
Критерий сравнения |
Apache POI (XSSF / SXSSF) |
DsExcel (Document Solutions for Excel) |
|---|---|---|
|
Лицензия |
Открытый исходный код (Apache License 2.0, бесплатно) |
Коммерческая лицензия (MESCIUS, лицензия разработчика/основная лицензия) |
|
Объем используемой памяти |
XSSF: очень высокий / SXSSF: низкий |
Очень низкий и стабильный благодаря оптимизации в памяти |
|
Ограничения потоковой обработки |
Ограничения на чтение и изменение, отсутствие вычисления формул |
Полная поддержка интерфейса (доступен произвольный доступ) |
|
Интеграция со SpreadJS |
Нестандартная (требуется разбор двоичных данных XLSX) |
Встроенная поддержка (полная совместимость с fromJson и toJson) |
|
Поддержка вычисления формул |
В основном поддерживаются базовые функции, при этом имеются ограничения для сложных формул |
Поддерживает более 450 функций Excel и динамические массивы |
|
Механизм шаблонов |
Отсутствует (требуется ручное программирование) |
Встроенный декларативный синтаксис шаблонов |
|
Возможности преобразования документов |
Требуется интеграция с отдельными инструментами |
Прямая поддержка преобразования в PDF, HTML и изображения |
3. Синергия с внешним интерфейсом (SpreadJS): полноценный конвейер Excel для всего стека
Для систем, которые используют SpreadJSв качестве компонента сетки во внешнем интерфейсе, выбор DsExcel в качестве серверного механизма является не просто альтернативой, а важным архитектурным решением.
3.1. Обмен исходным JSON без потерь (нулевая потеря точности)
Как правило, экспорт или импорт содержимого веб-экрана (сетки) в серверный файл Excel или из него требует прохождения следующих сложных узких мест.
-
Существующий подход (SpreadJS + Apache POI):
a. Сгенерировать двоичный blob XLSX на клиенте (SpreadJS)
b. Передать по сети несколько МБ двоичных данных
c. На серверной стороне POI распаковывает ZIP-архив и анализирует тысячи XML-файлов
d. После изменения снова сжать данные в ZIP-архив и вернуть его клиенту
—Проблемы: Перегрузка ЦП, напрасный расход пропускной способности сети и потеря расширенных стилей CSS/тем/условного форматирования, которые POI не может интерпретировать.
-
Современная архитектура (SpreadJS + DsExcel):
—SpreadJS и DsExcel используют **одинаковую схему данных и спецификацию JSON (формат JSON SpreadJS)**.
—Клиент передает только сериализованную строку JSON сетки (spread.toJSON()).
—Серверная часть (DsExcel) считывает ее одной строкой.
Workbook workbook = new Workbook();
workbook.fromJson(spreadJsonString);
—После выполнения бизнес-логики, крупномасштабного объединения данных и маскирования на сервере результат можно либо вернуть с помощью workbook.toJson(), либо немедленно преобразовать в файл .xlsx или .pdf для конечного распространения.
3.2. Сохранение стилей, множественных объединений и условного форматирования без потерь
Многоуровневые объединения заголовков (Span), иерархии шрифтов, гистограммы данных (Data Bar), цветовые шкалы и правила проверки данных с раскрывающимися списками, настроенные пользователями в SpreadJS, отражаются в серверной части DsExcel со 100%-ной точностью. Структурные проблемы, возникающие при использовании POI — необходимость индивидуального сопоставления CellStyle и беспокойство по поводу переполнения индексов, — полностью устраняются.
4. Практический пример: решение сложных задач в корпоративной предметной области
4.1. Пример 1: импорт и экспорт Excel без потерь для иерархической веб-сетки с многоуровневыми заголовками (Multi-Header)
-
Формулировка проблемы:Веб-таблица на экране имеет структуру сгруппированных заголовков с тремя и более уровнями. Чтобы обработать её с помощью POI, нам пришлось рекурсивно вычислять координаты горизонтальных и вертикальных объединений с использованием CellRangeAddress, при этом координаты искажались всякий раз, когда столбцы изменялись динамически.
-
Решение на основе DsExcel + SpreadJS:Передавая структуру заголовков столбцов SpreadJS непосредственно в DsExcel с помощью fromJson, можно идеально сохранить внутренние координаты объединений. Это устраняет несоответствия между схемами заголовков на сервере и во внешнем интерфейсе.
4.2 Случай 2: шаблон импорта данных с иерархической структурой групповых полей
-
Постановка проблемы:Модель сущностей содержала подкатегории внутри основных категорий, а сгруппированные под ними поля динамических атрибутов образовывали иерархическую структуру.
-
Применение шаблонизатора DsExcel:При задании в шаблоне XLSX декларативных маркеров, таких как {{ds.groupName(M=T)}}, и простой передаче набора данных автоматически выполнялись динамическое расширение строк и объединение ячеек. Логика ручного вычисления указателей была устранена, что максимально повысило удобство сопровождения.
4.3 Случай 3: обработка сложных формул (SUMPRODUCT, DSUM) и вычисления на стороне сервера
-
Постановка проблемы:Нам требовалось немедленно проверять на сервере результаты сложных условных расчётов с весовыми коэффициентами в загружаемых файлах.
-
Стратегия решения:При вызове DsExcel's workbook.calculate() встроенный механизм, совместимый с Excel, определяет все зависимые ячейки и вычисляет точные результаты, позволяя избежать дублирования математической логики в коде Java.
4.4 Случай 4: архитектура доменной модели для управления вложениями и данными
-
Проектирование архитектуры:Чтобы предотвратить исчерпание HTTP-потоков при обработке больших объёмов данных, мы внедрили асинхронный конвейер с очередью (Spring Task / RabbitMQ) и использовали try-with-resources для немедленного освобождения ресурсов книги DsExcel, благодаря чему удалось поддерживать стабильное потребление памяти JVM.
5. Практическое руководство по коду: POI и DsExcel
5.1 Пример реализации на Apache POI (ручное сопоставление координат)
Workbook poiWorkbook = new XSSFWorkbook();
Sheet sheet = poiWorkbook.createSheet("계층형데이터");
Row headerRow1 = sheet.createRow(0);
headerRow1.createCell(0).setCellValue("조직 인프라 정보");
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 1));
int rowIndex = 2;
for (DepartmentDto dto : dataList) {
Row row = sheet.createRow(rowIndex++);
row.createCell(0).setCellValue(dto.getDivision());
// ... 인덱스 수동 관리 및 스타일 설정 생략
}
5.2 Пример реализации конвейера DsExcel + SpreadJS
public byte[] generateExcelWithDsExcel(String spreadJsJson, List<DepartmentDto> dataList) {
try (Workbook workbook = new Workbook()) {
workbook.fromJson(spreadJsJson); // 프론트 디자인 로드
IWorksheet sheet = workbook.getActiveSheet();
sheet.addDataSource("dept", dataList); // 데이터 바인딩
workbook.calculate(); // 수식 재계산
ByteArrayOutputStream out = new ByteArrayOutputStream();
workbook.save(out, SaveFileFormat.Xlsx);
return out.toByteArray();
}
}
6. Заключение и руководство по принятию решения о переходе
6.1 Когда можно продолжать использовать POI
-
Экспорт необработанных данных в простом формате одномерной таблицы
-
Создание небольших файлов Excel с фиксированным форматом (не более нескольких тысяч записей)
-
Проекты с открытым исходным кодом, которые не могут выделить средства на дополнительные лицензионные расходы
6.2 Когда настоятельно рекомендуется перейти на DsExcel
-
SpreadJSПроекты, которым требуется идеальная среда WYSIWYG за счёт внедрения
-
Управление шаблонами, содержащими сложные многоуровневые заголовки, иерархические данные и большое количество объединённых ячеек
-
Когда требуется проверка на стороне сервера корректности сложных формул, таких как SUMPRODUCT и XLOOKUP
-
Когда стабильность системы при обработке больших объёмов данных оказывается под угрозой из-за сбоев POI, вызванных нехваткой памяти (OOM)
Заключительные замечания
В современных корпоративных архитектурах Excel — это не просто файл, а набор ключевых бизнес-правил. Сочетание DsExcel и SpreadJSпозволит разработчикам отказаться от утомительных вычислений координат и сосредоточиться на бизнес-логике.
BigJumbo