Skip to content

Repository files navigation

jasper-modular-library

License Java Spring Boot JasperReports

A Spring Boot library for simplifying and unifying JasperReports report development.

jasper-modular brings modularity to JasperReports: you assemble a report from reusable subreport components, each declared as a field in a Java class. The processor generates the required parameters in the JRXML at compile time, and the runtime passes everything automatically — data is described as plain Java objects, and JRXML contains only design.

What makes this different

In standard JasperReports, subreport data is passed parameter-by-parameter: every field must be declared individually in the parent JRXML and wired by hand — one <subreportParameter> per field, one params.put() per field in Java. With many subreports this becomes dozens of manual entries across multiple files.

jasper-modular uses a different technique: a single embedded subreport receives exactly two parameters — the compiled report object (<field>Report) and a single Map<String, Object> (<field>MapParameter) containing all of the subreport's data. Inside the subreport, the map is automatically unpacked into individual parameters by JasperReports' built-in REPORT_PARAMETERS_MAP mechanism — a little-known capability that eliminates parameter-by-parameter drilling entirely. A List of subreport modules is rendered as a repeating subreport instead — one instance per element (see Lists of subreports).

Both parameters are generated automatically from your Java class fields at compile time: you never declare or wire them. By the time you open the template in Jaspersoft Studio, they are already there.


The problem this solves

Every project tames subreports differently, and almost every approach carries its own set of problems:

One giant JSON for everything — data is serialized into a single massive JSON object passed to all subreports via JsonDataSource, which extract what they need using JSON paths in JRXML; the data-selection logic ends up in XML templates and is painful to debug.

Direct SQL connection — the subreport receives REPORT_CONNECTION and runs its own SQL query, so business logic and SQL accumulate inside JRXML.

Passing REPORT_DATA_SOURCE directly — the root report's data source is forwarded to the subreport, but a data source is consumable and can only be used once, causing subtle, hard-to-trace bugs.

Manual parameter drilling through a cascade of subreports — each parameter is declared and mapped by hand, so adding one field means updating three places (Java class, root JRXML, subreport JRXML) and drift and typos are inevitable.

With jasper-modular:

  • A root report is just a Java class annotated with @JasperModularReport
  • A subreport is just a field in that class whose type is annotated with @JasperSubreport
  • The annotation processor generates all parameters and datasets in the JRXML at compile time
  • The runtime compiles, fills, and assembles the entire report — including all subreports and their data — no manual boilerplate
  • All data is passed through typed POJO-DTOs — JRXML contains only design
  • Build a component once and drop it into any report as a field

Requirements

  • Java 17+
  • Spring Boot 3.3+ / 4.x
  • JasperReports 6.x or 7.x

Installation

Add the starter — it pulls in everything except JasperReports itself, which you provide:

<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>${your.jasperreports.version}</version>
</dependency>

Add the annotation processor to the compiler plugin (required for JRXML generation). Pass your JasperReports version alongside it so the processor can use the correct API at compile time:

<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>${your.jasperreports.version}</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

For PDF export on JasperReports 7.x, add the PDF extension (intentionally excluded from the starter). On 6.x the PDF exporter is already in the core jasperreports jar, and this artifact does not exist:

<dependency>
    <groupId>net.sf.jasperreports</groupId>
    <artifactId>jasperreports-pdf</artifactId>
    <version>${your.jasperreports.version}</version>
</dependency>

Quick start

1. Create a subreport module

@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();}
}

2. Create the root report

@Getter
@Setter
@JasperModularReport(templatePath = "/reports/invoice.jrxml")
public class InvoiceReport extends ModularReport {

    private String customerName;
    private String invoiceNumber;
    private BigDecimal total;
    private ItemsModule itemsModule;
}

3. Build the report and render it

ItemsModule items = new ItemsModule(lineItems, subtotal);

InvoiceReport report = new InvoiceReport();
report.setCustomerName("Acme Corp");
report.setInvoiceNumber("INV-001");
report.setTotal(BigDecimal.valueOf(1500.00));
report.setItemsModule(items);

JasperPrint print = new JasperModularRenderer().render(report);

4. Export to PDF

ByteArrayOutputStream out = new ByteArrayOutputStream();
JRPdfExporter exporter = new JRPdfExporter();
exporter.setExporterInput(new SimpleExporterInput(print));
exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(out));
exporter.exportReport();

byte[] pdf = out.toByteArray();

Full example

The jasper-modular-sample project demonstrates a complete financial report built with the library. The report is assembled from nested reusable modules:

CompanyReport (@JasperModularReport)
├── title: TitleSubModule (@JasperSubreport)
│   └── companyDetails, period, currency, totals, List<String> highlights
├── financial: FinancialSubModule (@JasperSubreport)
│   ├── revenue: RevenueSubModule — totalRevenue, growthPercent, List<RevenueItem>
│   ├── expense: ExpenseSubModule — totalExpenses, growthPercent, List<ExpenseItem>
│   └── profit: ProfitSubModule — grossProfit, operatingProfit, netProfit, margin, List<ProfitBreakdown>
└── departments: List<DepartmentSubModule> — repeating subreport
    └── name, headcount, budget, List<EmployeeItem>

Each module is a standalone class with its own JRXML template. The root report declares them as fields; the processor wires the parameters and the renderer assembles the document.

Result:

Sample report — page 1 Sample report — page 2

Download full PDF


Data philosophy

The library intentionally uses POJO-DTOs as the only way to pass data into a report — no SQL in JRXML, no JSON. You retrieve data however you prefer (JPA, JDBC, external API), perform all calculations and mapping in plain Java code, and pass the ready objects to the report.

// Fetch data as usual
List<RevenueItem> items = revenueRepository.findByPeriod(period);
double total = items.stream().mapToDouble(RevenueItem::getAmount).sum();
double growth = calculateGrowth(items);

// Build the module — no SQL in the template
RevenueModule revenue = new RevenueModule(total, growth, items);

The payoff: the data structure lives in Java fields instead of SQL buried in XML, and all calculations and formatting run in plain code before rendering. The compiler catches typos in field names, and the report model stays a POJO you can unit-test without ever producing a PDF.


How it works

At compile time

The annotation processor (JrxmlGeneratorProcessor) runs during mvn compile, inspects every class annotated with @JasperModularReport and @JasperSubreport, and adds the missing elements to a copy of its JRXML template in target/generated-sources:

  • <parameter> for each field
  • <dataset> and a list or table component for each Collection<T> field
  • a repeating-subreport list for each collection of @JasperSubreport modules
  • Subreport bands in the <detail> section for each subreport field

Existing elements are detected by name and never overwritten — custom layout, styles, and expressions created in Jaspersoft Studio are always preserved. The processor also checks the template against the class: the build fails when the template declares a generated parameter or dataset that no field produces, or a parameter whose class differs from its field.

At runtime

When render(module) is called, the template is compiled from the JRXML resource (or taken from the in-memory cache), and all fields are traversed via reflection to build the Map<String, Object> parameters map. Subreport fields get their templates compiled and their parameter maps built recursively, producing <field>Report and <field>MapParameter. Collection fields are stored in the map as JRBeanCollectionDataSource values. These are parameters, not the root data source: in JRXML you reference them via $P{fieldName} in the <dataSourceExpression> of a list or table component. The fill uses JasperFillManager.fillReport() with JREmptyDataSource as the root data source (the library never uses band-iteration data sources) and returns a JasperPrint for export to any format.

Circular subreport dependencies (e.g. A -> B -> A) are detected automatically and throw a JasperModularException identifying the offending class, rather than a StackOverflowError.

Startup precompilation

On startup, JasperReportPrecompiler scans the configured base package and precompiles all templates into the shared JasperModularCompiler.CACHE, eliminating compilation latency on the first request. If any template fails to compile, the exception is rethrown — the application will not start with broken report templates.


Working with JRXML templates

New report — CREATE mode

With mode = GenerationMode.CREATE, mvn compile produces a ready-to-use JRXML in target/generated-sources containing everything — a <parameter> for every field, a <dataset> and list/table component for every collection, and subreport bands for every subreport field. Open it in Jaspersoft Studio, add your design (elements, fonts, colors, headers), and save the finished template to src/main/resources/reports/.

Existing report — INJECT mode (default)

When you add a field or subreport to an existing report class, the next mvn compile writes a file to target/generated-sources containing your original template plus only the missing elements — everything already in the template is left untouched. Open it in Jaspersoft Studio, place the new elements in the design, and copy the file back to src/main/resources/reports/.


Generation modes

Mode Behavior
INJECT (default) Injects missing elements into the existing JRXML without touching existing content
CREATE Creates a new JRXML from a blank design, ignoring any existing file
NONE No processing — manage the JRXML entirely by hand
@JasperModularReport(
        templatePath = "/reports/invoice.jrxml",
        mode = GenerationMode.CREATE
)

Configuration

jasper:
  modular:
    precompile-enabled: true           # default: true
    base-package: com.example.reports  # required for precompilation
Property Default Description
jasper.modular.precompile-enabled true Compile all templates at startup
jasper.modular.base-package "" Package to scan for report classes

Annotations reference

@JasperModularReport

Marks a class as a root report. The class must extend ModularReport.

Attribute Type Required Description
templatePath String Yes Classpath path to the JRXML file
mode GenerationMode No Generation strategy (default: INJECT)
orientation PageOrientation No Page orientation (default: PORTRAIT)

@JasperSubreport

Marks a class as a subreport module. The class must extend SubreportModule.

Parameter names in the parent template come from the field that holds the module: a field items becomes itemsReport and itemsMapParameter, a list of such modules in a field items becomes itemsDataSource.

Attribute Type Required Description
templatePath String Yes Classpath path to the JRXML file
mode GenerationMode No Generation strategy (default: INJECT)
orientation PageOrientation No Page orientation (default: PORTRAIT)

@JasperCollection

Controls the JRXML component type for a collection field.

Attribute Type Required Description
type CollectionComponentType No LIST or TABLE (default: TABLE)
columnWidth int No Pixel width of each column (default: 100)

The default component type is TABLE — whether the annotation is present (without an explicit type) or absent entirely. Use type = CollectionComponentType.LIST for a list component.

Dataset fields come from the element class's JavaBean getters, so element classes need getters; records are rejected at compile time. A collection of simple values — String, numbers, dates, enums — gets a single _THIS field, which JasperReports fills with the element itself ($F{_THIS} in the cell).

@JasperCollection(type = CollectionComponentType.TABLE, columnWidth = 80)
private List<LineItem> items;

Lists of subreports

How a List field is rendered depends entirely on its element type — the two cases are kept strictly separate:

Element type Rendered as Field annotation
Plain data class (bean) An inline list / table component in the same template @JasperCollection (opt.)
A @JasperSubreport module A repeating subreport — one subreport instance per element none

When the element type is a @JasperSubreport module, the list is treated as real subreports: the processor injects a repeating subreport into the parent template, and at runtime each element is filled recursively into its own parameter map and rendered once. No manual JRXML wiring is needed, and @JasperCollection does not apply.

// Inline — element is a plain bean → list/table of the report
@JasperCollection(type = CollectionComponentType.TABLE)
private List<LineItem> items;

// Repeating subreport — element is a @JasperSubreport module → rendered once per element
private List<DepartmentModule> departments;

Choosing between them:

  • Inline list / table — for plain tabular rows. Lightweight: one template, no extra compilation.
  • Repeating subreport — when each element is a self-contained, reusable section with its own layout (or its own nesting / page break).

A self-referential module (a @JasperSubreport class holding a List of itself) is rejected by the circular-dependency guard.

@JasperIgnore

Place on any field to exclude it from JRXML generation and runtime filling.

@JasperIgnore
private transient String internalState;

Module structure

jasper-modular-parent
├── jasper-modular-core              — annotations, contracts, base classes, renderer
├── jasper-modular-autoconfigure     — Spring Boot autoconfiguration and precompiler
├── jasper-modular-processor         — annotation processor (JasperReports 6.x and 7.x)
└── jasper-modular-starter           — single dependency entry point

Exporting to other formats

JasperModularRenderer.render() returns a format-neutral JasperPrint — export it with any JasperReports exporter. The XLSX (JRXlsxExporter) and HTML (HtmlExporter) exporters ship with the core jasperreports jar on both 6.x and 7.x; PDF on 7.x needs jasperreports-pdf (see Installation). Only the legacy .xls exporter needs extra libraries — on 7.x:

<dependency>
    <groupId>net.sf.jasperreports</groupId>
    <artifactId>jasperreports-excel-poi</artifactId>
    <version>${your.jasperreports.version}</version>
</dependency>

and on 6.x — Apache POI (org.apache.poi:poi).


Migrating from 2.0.x

  • Parameter names come from the field. A subreport field itemsModule now produces itemsModuleReport and itemsModuleMapParameter instead of <prefix>Report and <prefix>MapParameter, where the prefix was the prefix attribute or the module's class name. Rename these parameters and their $P{...} references in your templates.
  • prefix is removed from @JasperSubreport. Delete the attribute.
  • Bean collections default to TABLE. A collection field without @JasperCollection now gets a table component instead of a list. Components already in your templates are not affected; to keep generating lists, use @JasperCollection(type = CollectionComponentType.LIST).
  • SubreportModule.getOrder() and isStartNewPage() are removed. They never affected rendering; delete the overrides and set order and page breaks in the template.
  • Templates and base classes are checked at compile time. The build fails when a template declares a generated parameter or dataset that no field produces, or a parameter whose class differs from its field, and when a @JasperModularReport class does not extend ModularReport or a @JasperSubreport class does not extend SubreportModule. The error message says what to change.

License

Apache License 2.0 — see LICENSE.


Analytics

About

Modular JasperReports for Spring Boot: a report is a Java class, subreports are its fields, and the annotation processor keeps JRXML templates in sync at build time.

Topics

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages