ESPNBarnwell on five important Week 3 losses and their aftermathThe Jerusalem PostIranians stagger under soaring cost of living after seven months of war with the USESPN DeportesAutoridades de Manchester City rebaten veredicto de Premier LeagueBollywood HungamaGondhal team meets Maharashtra CM Devendra Fadnavis, appeals for financial support ahead of Oscar 2027 journeyDaily MaverickStudent protests and French public sector strike heap pressure on MacronRTP DesportoSalvador diz que Sporting de Braga "exige mais" do que o que foi apresentadoPunchMeet Nigerian dancer attempting 168-hour Guinness World RecordBillboardElton John Opens London’s Apple Music Hall With Intimate, Hit-Packed PerformanceVariety‘GMA’ Anchor and Christopher Reeve’s Son Will Reveals Testicular Cancer Diagnosis: ‘I Was Raised to Share One’s Struggles’Egypt IndependentBritain’s PM is on a ‘Burnham bounce.’ When will he fall to earth?Antara NewsJakarta rises to 140th worldwide in Global Cities Index 2026Il Fatto Quotidiano“Mostrava i genitali e si masturbava di fronte a una 18enne”: giudice sospeso dal Csm. Lui: “Solo frasi volgari, mi scuso”
The Daily Newsstand · Free, Always
Tuesday, September 29, 2026

Модульная надстройка для JasperReports: коллекции, рантайм и границы подхода. Часть 4

Translate

Оглавление серии

  1. Откуда взялся Jasper и почему он так устроен

  2. Один стандарт и три идеи

  3. Подключение и что делает процессор при сборке

  4. Коллекции, рантайм и границы подхода — эта статья

Это последняя часть серии. В третьей мы подключили библиотеку, написали первый модульный отчёт и посмотрели, что процессор дописывает в шаблон при сборке. Здесь — всё остальное: поля-коллекции и повторяющиеся субрепорты, что роняет сборку, как собирается карта параметров в рантайме, что происходит на старте приложения, как библиотека уживается с обеими версиями движка, пример целиком, переход с 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.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.