Это третья часть. В первой — откуда взялся 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, billToMapParameterprivate 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.
ссылка на оригинал статьи https://habr.com/ru/articles/1086826/