Модульная надстройка для JasperReports: подключение и что делает процессор при сборке. Часть 3

—

от автора

Это третья часть. В первой — откуда взялся 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:

  1. Находит существующий шаблон — в target/classes, куда Maven кладёт ресурсы до компиляции, или на своём classpath.

  2. Сравнивает его с классом и дописывает то, чего не хватает: параметры, датасеты, компоненты коллекций, банды субрепортов. Всё, что уже есть, находится по имени и не трогается.

  3. Сверяет шаблон с классом — об этом отдельная часть серии.

  4. Кладёт результат в 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/