Оглавление серии
Коллекции, рантайм и границы подхода — эта статья
Это последняя часть серии. В третьей мы подключили библиотеку, написали первый модульный отчёт и посмотрели, что процессор дописывает в шаблон при сборке. Здесь — всё остальное: поля-коллекции и повторяющиеся субрепорты, что роняет сборку, как собирается карта параметров в рантайме, что происходит на старте приложения, как библиотека уживается с обеими версиями движка, пример целиком, переход с 2.0.x и то, за что за всё это приходится платить.
Коллекции
Поле-коллекция бинов — private List<LineItem> items; — превращается в три вещи:
параметр
itemsклассаJRBeanCollectionDataSource;датасет
itemsс полями по геттерамLineItem: имя — по правилам JavaBeans, класс — по возвращаемому типу геттера;компонент, который прогоняет датасет: по умолчанию таблица (
table) с заголовками колонок по именам свойств, список (list) — через@JasperCollection(type = CollectionComponentType.LIST). Ширину колонки задаётcolumnWidth, по умолчанию 100.
Геттеры у элемента обязательны: JRBeanCollectionDataSource читает значения через них. Поэтому record в роли элемента коллекции отклоняется при сборке, а для элемента-интерфейса поля датасета берутся по его геттерам.
Коллекция простых значений — строк, чисел, дат, enum — устроена иначе: у элемента нет свойств, которые стоило бы читать. Для private List<String> highlights; процессор генерирует датасет с единственным полем:
<subDataset name="highlights"> <field name="_THIS" class="java.lang.String"/> </subDataset>
_THIS — зарезервированное имя JasperReports (JRAbstractBeanDataSource.CURRENT_BEAN_MAPPING): в такое поле кладётся сам элемент коллекции, и в ячейке пишется $F{_THIS}. Заголовок колонки берётся из имени параметра.
Пустая или null-коллекция в карту не попадает, а сгенерированные компоненты помечены removeLineWhenBlank — пустой блок схлопывается. @JasperIgnore на поле исключает его из генерации и заполнения; на поле класса-элемента — из сгенерированного датасета.
Повторяющиеся субрепорты
Если элемент коллекции сам является модулем — private List<DepartmentModule> departments; с @JasperSubreport на DepartmentModule, — это уже не таблица, а повторяющийся субрепорт: по экземпляру на каждый элемент. Процессор генерирует параметр departmentsDataSource, датасет departmentsDataset с двумя полями — params и report — и компонент list, который для каждой строки рисует субрепорт из $F{report} с параметрами из $F{params}.
В рантайме каждый элемент приносит свой скомпилированный шаблон, поэтому в одном списке могут жить наследники модуля с разными шаблонами. Элементы, у которых isEmpty() вернул true, и null пропускаются. @JasperCollection здесь не действует.
Что проверяется при сборке
В третьей версии процессор не только дописывает шаблон, но и сверяет его с классом. Сборка падает, если:
шаблон объявляет параметр или датасет, который порождается полями, но ни одно поле класса его больше не порождает — поле переименовали или удалили;
класс параметра в шаблоне не совпадает с типом поля;
два поля дают одно имя параметра;
класс с
@JasperModularReportне наследуетModularReport, класс с@JasperSubreportне наследуетSubreportModuleили на классе стоят обе аннотации;два класса указывают на один
templatePath;элемент коллекции — record или коллекция модулей объявлена с типом без
@JasperSubreport.
Сообщение говорит, что делать. Например, после переименования поля items в positions:
Template declares 'itemsReport' but no field of InvoiceReport produces it. If the field was renamed, rename the parameter and its $P{itemsReport} references in the template; if it was removed, delete the parameter and the element that uses it.
Предупреждением, без остановки сборки, процессор отмечает коллекции, для которых нечего генерировать: сырой List, List<?> или тип элемента без читаемых свойств.
Граница проверки — контракт, который порождает процессор: коллекции JRBeanCollectionDataSource и параметры вида …Report, …MapParameter, …DataSource с соответствующими классами. Остальные параметры и выражения вроде $P{companyDetails}.getName() остаются вашими — их проверит только компиляция шаблона в самом JasperReports.
Рантайм
render() делает три шага: берёт скомпилированный шаблон (компиляция — один раз на путь, дальше из кэша), собирает карту параметров из полей и заполняет отчёт от JREmptyDataSource. Данные всегда едут параметрами, поэтому соединение с базой шаблону не передаётся вовсе.
Карта собирается обходом полей вверх по иерархии класса:
null-поля и поля с@JasperIgnoreпропускаются;скаляры кладутся как есть;
коллекции бинов — свежим
JRBeanCollectionDataSourceна каждый рендер. Классический баг «передал датасорс в субрепорт — получил пустой блок» здесь нельзя написать: одноразовому курсору взяться неоткуда;модуль — своим шаблоном и своей изолированной картой. Два модуля могут оба завести поле
titleи не столкнутся;цикл модулей (
A → B → A) даёт исключение с цепочкой имён вместоStackOverflowError.
Карту можно проверить обычным юнит-тестом, не генерируя PDF: report.fillMapParameters() публичен. Скаляры и коллекции проверяются вообще без шаблонов; для полей-субрепортов шаблоны должны лежать на тестовом classpath — модуль компилирует свой шаблон, когда попадает в карту.
Прекомпиляция на старте
Стартер регистрирует JasperReportPrecompiler: на старте приложения он находит в jasper.modular.base-package классы с аннотациями — включая наследников, унаследовавших аннотацию, — и компилирует каждый шаблон по одному разу в общий кэш. Если хоть один шаблон не компилируется, исключение пробрасывается и приложение не стартует. Обычный контекстный тест @SpringBootTest в CI краснеет на битом шаблоне — задолго до того, как отчёт кто-то запросит. Самая живая джасперовская боль последних лет — «работает в Studio, умирает на сервере» — ловится здесь, при деплое, а не от бухгалтерии в конце месяца.
Кэш очищается при каждом старте, так что перезапуск через devtools подхватывает изменённые шаблоны. Отключается прекомпиляция через jasper.modular.precompile-enabled: false; без base-package она пропускается с предупреждением.
JasperReports 6 и 7
Сама библиотека от версии движка не зависит: в ядре и процессоре JasperReports объявлен в provided-скоупе, версию приносите вы (в 3.0.0 стартер дополнительно тянет 7.0.6 транзитивно — поэтому движок и объявляют явно). Скомпилированные .jasper на диск не пишутся, так что намеренный слом их совместимости в седьмой версии нас не касается. Всё проверено на 6.21.5 и 7.0.6.
А вот шаблоны у каждой версии свои. Классический JRXML седьмой движок не читает, новый формат не читает шестой, и одного файла на обе версии не бывает. Процессор пишет шаблоны в диалекте той версии JasperReports, что лежит у него на пути. Пример к библиотеке поэтому состоит из двух модулей — sample-jr6 и sample-jr7: Java-код в них одинаковый, шаблоны разные.
Пример целиком
jasper-modular-sample — финансовый отчёт компании, собранный из модулей:
CompanyReport (@JasperModularReport) ├── title: TitleSubModule │ └── companyDetails, period, currency, итоги, List<String> highlights ├── financial: FinancialSubModule │ ├── revenue: RevenueSubModule — List<RevenueItem> │ ├── expense: ExpenseSubModule — List<ExpenseItem> │ └── profit: ProfitSubModule — List<ProfitBreakdown> └── departments: List<DepartmentSubModule> — повторяющийся субрепорт └── name, headcount, budget, List<EmployeeItem>
Корневой шаблон почти пуст: заголовок, колонтитул и по банде на каждое поле. Отделы начинаются с новой страницы — разрыв перед списком добавлен руками, как и положено дизайну.
Переход с 2.0.x
Третья версия ломает совместимость в пяти местах:
имена параметров субрепортов теперь от поля: вместо
<prefix>Reportи<prefix>MapParameter, где префиксом был атрибутprefixили имя класса модуля, —<field>Reportи<field>MapParameter. Старые параметры и их$P{...}в шаблонах нужно переименовать — сборка сама перечислит, какие;атрибут
prefixиз@JasperSubreportудалён;коллекция бинов без
@JasperCollectionтеперь генерируется таблицей, а не списком; уже созданные компоненты не меняются;SubreportModule.getOrder()иisStartNewPage()удалены — на рендер они никогда не влияли;шаблон и базовые классы проверяются при сборке.
Подробности — в разделе «Migrating from 2.0.x» в README.
Шероховатости и границы
Перенос сгенерированного шаблона из
target/generated-sourcesв ресурсы — ручной шаг и самое неудобное место цикла. Процессор переписывает файл целиком в своём форматировании, а новые элементы кладёт отдельными бандами в конец detail с раскладкой по умолчанию: банда коллекции высотой 90, строка таблицы 30, колонка 100 пикселей, заголовки — имена полей. Дизайн — дальше в Studio.Компоненты коллекций и субрепорты процессор ищет в detail. Если перенести сгенерированный элемент в другую банду, при следующей сборке он появится в detail снова — это следствие модели, где структуру задают модули, а не баг.
Описан и проверен Maven. В Gradle ресурсы и скомпилированные классы лежат в разных каталогах, процессор не находит существующий шаблон и выдаёт заготовку с предупреждением.
Коллекция внутри элемента коллекции генерируется только полем датасета; вложенный
listсnew JRBeanCollectionDataSource($F{...})пишете сами.Объектная модель материализует данные в память — для стотысячестрочных выгрузок честнее курсор с виртуализатором.
Вёрстку библиотека не рисует и экспорт не делает: дизайн — в Jaspersoft Studio, экспорт — стандартными средствами JasperReports.
Цена проверки при компиляции
Есть ещё один размен, и это не баг, который когда-нибудь починят, а само устройство подхода. Шаблоны лежат в ресурсах приложения, а структура отчёта — в Java-классах. Значит, любое изменение проходит через сборку и деплой: и новое поле, и правка, которая касается одной только вёрстки.
Во многих компаниях отчёты меняют каждую неделю и намеренно мимо релизного цикла: аналитик правит шаблон на сервере отчётов, и это работает в тот же день. С модульным подходом так не выйдет: каждое изменение идёт обычным путём релиза, зато со всеми проверками из предыдущих частей. Если отчёты у вас меняются чаще, чем выкатывается приложение, подход вас затормозит, и лучше знать об этом заранее.
Библиотека открытая, Apache 2.0: github.com/hhdevr/jasper-modular-library, на Maven Central — io.github.hhdevr:jasper-modular-starter, рядом есть репозиторий с примером. Вопросы и критика — в issues.

