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.
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
- Java 17+
- Spring Boot 3.3+ / 4.x
- JasperReports 6.x or 7.x
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>@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();}
}@Getter
@Setter
@JasperModularReport(templatePath = "/reports/invoice.jrxml")
public class InvoiceReport extends ModularReport {
private String customerName;
private String invoiceNumber;
private BigDecimal total;
private ItemsModule itemsModule;
}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);ByteArrayOutputStream out = new ByteArrayOutputStream();
JRPdfExporter exporter = new JRPdfExporter();
exporter.setExporterInput(new SimpleExporterInput(print));
exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(out));
exporter.exportReport();
byte[] pdf = out.toByteArray();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:
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.
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 alistortablecomponent for eachCollection<T>field- a repeating-subreport
listfor each collection of@JasperSubreportmodules - 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.
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.
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.
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/.
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/.
| 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
)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 |
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) |
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) |
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;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.
Place on any field to exclude it from JRXML generation and runtime filling.
@JasperIgnore
private transient String internalState;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
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).
- Parameter names come from the field. A subreport field
itemsModulenow producesitemsModuleReportanditemsModuleMapParameterinstead of<prefix>Reportand<prefix>MapParameter, where the prefix was theprefixattribute or the module's class name. Rename these parameters and their$P{...}references in your templates. prefixis removed from@JasperSubreport. Delete the attribute.- Bean collections default to
TABLE. A collection field without@JasperCollectionnow gets atablecomponent instead of alist. Components already in your templates are not affected; to keep generating lists, use@JasperCollection(type = CollectionComponentType.LIST). SubreportModule.getOrder()andisStartNewPage()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
@JasperModularReportclass does not extendModularReportor a@JasperSubreportclass does not extendSubreportModule. The error message says what to change.
Apache License 2.0 — see LICENSE.


