Третья часть серии — практическая. Как подключить библиотеку к Maven-проекту, написать первый модульный отчёт и что после этого происходит с шаблоном при сборке.Субрепорт — это класс, отчёт — тоже класс, а поля класса и есть содержимое отчёта. Имена параметров шаблона берутся из имён полей, поэтому один и тот же модуль можно поставить в отчёт дважды и получить два независимых субрепорта со своими данными. Аннотационный процессор при компиляции находит существующий JRXML, дописывает в него недостающие параметры, датасеты и банды субрепортов и кладёт результат в target/generated-sources. Шаблон в src он не трогает никогда.Отдельно — про то, кто владеет шаблоном, когда в один файл пишут и кодогенератор, и человек в Jaspersoft Studio, и где у этой схемы слабое место.Первая часть — откуда взялся Jasper и почему он так устроен, вторая — один стандарт и три идеи. Читать далее
Уровень сложностиСредний
Время на прочтение6 мин
Охват и читатели8.4K
Обзор
Оглавление серии
Подключение и что делает процессор при сборке — эта статья
Коллекции, проверки и рантайм — скоро
Прекомпиляция, версии движка и ограничения — скоро
Это третья часть. В первой — откуда взялся JasperReports и почему ему не хватало модульности, во второй — один стандарт и три идеи, на которых держится библиотека. Дальше идёт практика, по одной теме на часть. Здесь — как подключить библиотеку, написать первый модульный отчёт и что после этого происходит с шаблоном при сборке.
Всё ниже относится к версии 3.0.0. По сравнению с 2.0.x в ней появились повторяющиеся субрепорты из списков модулей, пропуск пустых модулей, датасеты для коллекций простых значений и сверка шаблона с классом при сборке, а имена параметров теперь берутся из полей. Про переход с 2.0.x — в последней части серии.
Требования: Java 17+ и JasperReports 6.x или 7.x — версию движка задаёте вы. Автоконфигурация и прекомпиляция приезжают со стартером, собранным под Spring Boot 3.3; ядро библиотеки от Spring не зависит вовсе и работает и без него.
ПодключениеСтартер приносит ядро, процессор и автоконфигурацию, а заодно и сам JasperReports: в версии 3.0.0 движок приезжает транзитивно, версии 7.0.6. Поэтому объявляйте его явно и той версии, которая нужна вам — прямая зависимость перебивает транзитивную. Это не формальность: диалекты JRXML у шестой и седьмой версий несовместимы, и версия движка определяет, в каком из них процессор напишет ваш шаблон.
<dependency>
<groupId>io.github.hhdevr</groupId>
<artifactId>jasper-modular-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports</artifactId>
<version>${jasperreports.version}</version>
</dependency>
Аннотационный процессор подключается в компилятор вместе с той же версией JasperReports — он читает и пишет шаблоны её API:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.github.hhdevr</groupId>
<artifactId>jasper-modular-processor</artifactId>
<version>3.0.0</version>
</path>
<path>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports</artifactId>
<version>${jasperreports.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
В седьмой версии движка экспорт в PDF вынесен в отдельный артефакт jasperreports-pdf — в стартер он намеренно не включён, добавьте его сами. И укажите пакет с отчётами для прекомпиляции на старте:
jasper:
modular:
base-package: com.example.reports
Модуль и отчётСубрепорт — класс, унаследованный от SubreportModule и помеченный @JasperSubreport:
@Getter
@Setter
@AllArgsConstructor
@JasperSubreport(templatePath = "/reports/sub_items.jrxml")
public class ItemsModule extends SubreportModule {
private List<LineItem> items;
private BigDecimal subtotal;
@Override
public boolean isEmpty() {
return items == null || items.isEmpty();
}
}
isEmpty() говорит, что рендерить нечего: такой модуль пропускается целиком — в отчёт не уходят ни его шаблон, ни его данные.
Корневой отчёт — класс от ModularReport с @JasperModularReport. Его поля и есть содержимое отчёта:
@Getter
@Setter
@JasperModularReport(templatePath = "/reports/invoice.jrxml")
public class InvoiceReport extends ModularReport {
private String customerName;
private String invoiceNumber;
private BigDecimal total;
private ItemsModule items;
}
Геттеры и сеттеры здесь только для удобства: поля отчётов и модулей рантайм читает рефлексией напрямую. Геттеры обязательны в другом месте — у классов-элементов коллекций, об этом в следующей части.
Имена параметров — от имени поляСкаляры становятся параметрами шаблона с теми же именами: customerName, invoiceNumber, total. Для поля items типа ItemsModule в шаблоне отчёта появятся два параметра: itemsReport — скомпилированный шаблон модуля и itemsMapParameter — карта его данных.
Имя берётся из поля, а не из класса модуля. Это важно, когда один модуль стоит в отчёте дважды:
private AddressModule billTo; // billToReport, billToMapParameter
private AddressModule shipTo; // shipToReport, shipToMapParameter
Два поля одного типа — два независимых субрепорта со своими данными. Если двум полям всё-таки достанется одно имя — например, поле с тем же именем объявлено и в родительском классе, — сборка упадёт и попросит переименовать одно из них.
РендерInvoiceReport report = new InvoiceReport();
report.setCustomerName("Acme Corp");
report.setInvoiceNumber("INV-001");
report.setTotal(new BigDecimal("1500.00"));
report.setItems(new ItemsModule(lineItems, subtotal));
JasperPrint print = new JasperModularRenderer().render(report);
byte[] pdf = JasperExportManager.exportReportToPdf(print);
render() возвращает стандартный JasperPrint, дальше — любой экспортёр JasperReports: PDF, XLSX, HTML.
По умолчанию процессор работает в режиме INJECT:
Находит существующий шаблон — в target/classes, куда Maven кладёт ресурсы до компиляции, или на своём classpath.
Сравнивает его с классом и дописывает то, чего не хватает: параметры, датасеты, компоненты коллекций, банды субрепортов. Всё, что уже есть, находится по имени и не трогается.
Сверяет шаблон с классом — об этом отдельная часть серии.
Кладёт результат в target/generated-sources/annotations по тому же пути. Шаблон в src процессор не меняет никогда.
Дальше вы открываете сгенерированный файл в Jaspersoft Studio, расставляете новые элементы и копируете его обратно в src/main/resources. Если шаблона ещё нет, процессор предупредит и сгенерирует заготовку с нуля.
Для поля items в шаблон отчёта добавится вот что (сокращённо, без uuid):
<parameter name="itemsReport" class="net.sf.jasperreports.engine.JasperReport"/>
<parameter name="itemsMapParameter" class="java.util.Map"/>
<band height="100" splitType="Stretch">
<subreport>
<reportElement positionType="Float" x="0" y="0" width="555" height="100" isRemoveLineWhenBlank="true"/>
<parametersMapExpression><![CDATA[$P{itemsMapParameter}]]></parametersMapExpression>
<dataSourceExpression><![CDATA[new net.sf.jasperreports.engine.JREmptyDataSource()]]></dataSourceExpression>
<subreportExpression><![CDATA[$P{itemsReport}]]></subreportExpression>
</subreport>
</band>
Здесь и работает механизм, ради которого всё затевалось. Субрепорт получает ровно два значения — свой скомпилированный шаблон и одну карту, а JasperReports сам раскладывает карту в параметры субрепорта: это встроенный механизм REPORT_PARAMETERS_MAP, о котором мало кто знает. В sub_items.jrxml при этом объявлены обычные параметры items и subtotal — их процессор тоже сгенерировал, из полей ItemsModule. Никаких <subreportParameter> по одному на каждое поле.
Режим задаётся в аннотации: INJECT — по умолчанию, как описано выше; CREATE — новая заготовка с чистого листа, существующий шаблон игнорируется, ориентация страницы берётся из orientation; NONE — процессор класс не трогает и не проверяет.
Если процессор пишет в шаблон, а человек правит тот же шаблон в Studio, чей это файл? Это классическая проблема кодогенерации в артефакт, который редактируют руками, так что стоит сказать, как библиотека делит работу.
Шаблон в src/main/resources принадлежит людям: разработчику и тому, кто верстает отчёт. Процессор туда не пишет никогда. При сборке он берёт шаблон, дописывает только недостающий вайринг и кладёт результат в target/generated-sources. Всё, что в шаблоне уже есть, он находит по имени и оставляет как было, так что вёрстка, стили и расставленные руками элементы не трогаются. Класс владеет именами и вайрингом, человек в Studio — вёрсткой.
Слабое место — обратный путь. Скопировать сгенерированный шаблон в ресурсы нужно руками, и сборка этот шаг не проверяет: забыли — новое поле просто не появится в отчёте. Плюс процессор переписывает файл целиком в своём форматировании, поэтому диф после копирования больше, чем само изменение. Следующее, что я планирую в библиотеке, — проверка, которая роняет сборку, если в шаблоне из src не хватает вайринга для поля.
В следующей части — коллекции, повторяющиеся субрепорты, что роняет сборку и как устроен рантайм.
Код — github.com/hhdevr/jasper-modular-library, пример — github.com/hhdevr/jasper-modular-sample.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Особенности агентской разработки ПО, обеспечивающей соответствие замыслу и требованиям | 0 | 7.39 | 26-09-2026 |
| 2 | Ты не найдёшь эту ошибку. Потому что её нет в твоём коде. Как Self-describing API спасает от чужих рефакторингов | 5 | 8 | 07-07-2026 |
| 3 | SQL Server Reporting Services — дюжина советов | 5 | 7 | 18-04-2014 |
| 4 | Свободные функции вместо методов, но с полиморфизмом. Что это даёт на самом деле? | 0 | 7.56 | 24-07-2026 |
| 5 | Формула «идеального enterprise» для open-source | 0 | 18.47 | 12-08-2026 |
| 6 | Почему LLM нельзя просто подключить к базе данных и получить GenBI? | 0 | 7.7 | 24-07-2026 |
| 7 | [Перевод] Раньше ПО работало шустро, потому что иначе было никак | 0 | 7 | 28-06-2026 |
| 8 | proxy_pass и fastcgi_pass — одна машина | 0 | 8.53 | 06-08-2026 |
| 9 | Harness engineering: как за год собрать фабрику из десятка конвейеров | 0 | 5.82 | 24-07-2026 |