A Spring Boot library for simplifying and unifying JasperReports report development.
Working with JasperReports in Java projects is hard. Passing data into templates is fragile and awkward, approaches vary wildly between projects, and any non-trivial report quickly devolves into a mess of SQL in XML, manual parameter mapping, and repetitive boilerplate.
The only realistic way to tame this complexity is modularity. When a report is assembled from reusable subreport components — each with its own data and design — the structure becomes clear and manageable. But the problem is that working with subreports directly in JasperReports is no easier. There is no built-in mechanism that makes it simple and logical: you have to manually declare parameters in the root report's JRXML, keep them in sync with Java code, pass each parameter by name — and update everything in multiple places on every change.
jasper-modular solves both problems at once — the data chaos and the subreport complexity. You simply declare a subreport as a field in a Java class and annotate it — 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
Every subreport in jasper-modular always produces exactly two parameters in the root JRXML — the compiled report object and the data map. Both are generated automatically from your Java class fields at compile time: you never declare them, you never wire them, you never think about them. By the time you open the template in Jaspersoft Studio, the parameters are already there. You just use your data.
Working with JasperReports is painful overall — handling data and passing it correctly into templates is difficult at every level. Modularity through subreports is the best way to bring order to this, but there is no standard mechanism for working with them conveniently. Every project solves it 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 and passed
to all subreports via JsonDataSource. Subreports extract the data they need using JSON paths
directly in JRXML. Data-selection logic ends up in XML templates, which makes debugging extremely
painful.
Direct SQL connection — the subreport receives REPORT_CONNECTION (the same JDBC connection as
the root report) and executes its own SQL query. Data is filtered through parameters passed down
from the root report. Business logic and SQL accumulate inside JRXML.
Passing REPORT_DATA_SOURCE directly — the root report's data source is forwarded to the
subreport. A data source is a consumable object — it can only be used once, after which it is
exhausted. This causes subtle, hard-to-trace bugs.
Manual parameter drilling through a cascade of subreports — each subreport parameter is declared
individually in the root report's JRXML and mapped by hand. With many subreports this means dozens
of <subreportParameter> entries per template. Adding a new field requires updating three places:
the Java class, the root JRXML, and the subreport JRXML — 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 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
- Building individual report components and reusing them across reports becomes simple and natural, making the overall experience of working with JasperReports significantly easier
- Java 17+
- Spring Boot 3.3+ / 4.x
- JasperReports 6.x or 7.x
The library provides separate starters for JasperReports 6.x and 7.x. Choose the one that matches your JasperReports version — each starter pulls in everything else automatically.
JasperReports 7.x:
<dependency>
<groupId>io.github.hhdevr</groupId>
<artifactId>jasper-modular-starter-jr7</artifactId>
<version>1.0.0</version>
</dependency>JasperReports 6.x and older (4.x, 5.x):
<dependency>
<groupId>io.github.hhdevr</groupId>
<artifactId>jasper-modular-starter-jr6</artifactId>
<version>1.0.0</version>
</dependency>Add the annotation processor to the compiler plugin (required for JRXML generation). Use the processor that matches your JasperReports version:
JasperReports 7.x:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.github.hhdevr</groupId>
<artifactId>jasper-modular-processor-jr7</artifactId>
<version>1.0.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>JasperReports 6.x and older:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.github.hhdevr</groupId>
<artifactId>jasper-modular-processor-jr6</artifactId>
<version>1.0.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>For PDF export, add the JasperReports PDF extension (intentionally excluded from the starter):
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports-pdf</artifactId>
<version>7.0.6</version> <!-- or 6.21.5 for JR6 -->
</dependency>@Getter
@Setter
@JasperSubreport(templatePath = "/reports/sub_items.jrxml", prefix = "Items")
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)
├── TitleSubModule (@JasperSubreport)
│ └── companyDetails, period, currency, totals
└── FinancialSubModule (@JasperSubreport)
├── RevenueSubModule (@JasperSubreport)
│ └── totalRevenue, growthPercent, List<RevenueItem>
├── ExpenseSubModule (@JasperSubreport)
│ └── totalExpenses, growthPercent, List<ExpenseItem>
└── ProfitSubModule (@JasperSubreport)
└── grossProfit, operatingProfit, netProfit, margin, List<ProfitBreakdown>
Each module is a standalone class with its own JRXML template. The root report simply declares them as fields — everything else is handled automatically.
Result:
The library intentionally uses POJO-DTOs as the only way to pass data into a report.
This means you do not write SQL queries inside JRXML and do not transform data into JSON. You retrieve data from the database using any approach you prefer (JPA, JDBC, external API), perform all necessary 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);Benefits of this approach:
- Readability - the data structure is described by Java fields, not SQL in XML
- Control - all calculations, formatting, and business logic happen in Java before rendering
- Type safety - the compiler and IDE prevent typos in field names
- Testability - the report model is a plain POJO, easily testable without rendering a PDF
The annotation processor (JrxmlGeneratorProcessor) runs during mvn compile and inspects all
classes annotated with @JasperModularReport and @JasperSubreport. For each class it:
- Reads all non-ignored fields via the Java Compiler API
- Identifies subreport fields, collection fields, and scalar fields
- Injects missing elements into the existing JRXML template:
<parameter>for each field<dataset>and alistortablecomponent for eachCollection<T>field- Subreport bands in the
<detail>section for each subreport field
- Writes the updated JRXML to
target/generated-sources
Existing elements are detected by name and never overwritten - custom layout, styles, and expressions created in Jaspersoft Studio are always preserved.
When render(module) is called:
- The template is compiled from the JRXML resource (or retrieved from the in-memory cache)
- All fields are traversed via reflection to build the
Map<String, Object>parameters map - Subreport fields are recursively compiled and filled, injecting
<prefix>Reportand<prefix>MapParameter - Collection fields are wrapped in
JRBeanCollectionDataSource JasperFillManager.fillReport()is called with the parameters map and an empty data source- The resulting
JasperPrintis returned for export to any format
Circular subreport dependencies (e.g. A -> B -> A) are detected automatically and throw a
JasperModularException with a clear message identifying the offending class, rather than
propagating a StackOverflowError.
On application startup, JasperReportPrecompiler scans the configured base package and precompiles
all report templates, storing them in the shared JasperModularCompiler.CACHE. This eliminates
compilation latency on the first report request in production.
If any template fails to compile, the error is logged and the exception is rethrown — the application will not start with broken report templates.
When you create a new report class with mode = GenerationMode.CREATE and run mvn compile, a
ready-to-use JRXML file appears in target/generated-sources. It already contains everything
needed:
<parameter>for every field in the class<dataset>with fields for every collection- A
listortablecomponent for displaying collection data - Subreport bands in the
<detail>section for every subreport field
Your workflow:
- Open the generated file from
target/generated-sourcesin Jaspersoft Studio - Add your design - place elements, configure fonts, colors, headers
- Save the finished template to
src/main/resources/reports/
All parameters, datasets, and subreports are already in place - you only need to add the design.
When you add a new field or a new subreport to an existing report class, the processor generates a
new file in target/generated-sources on the next compile. It contains your original template plus
only the missing elements - new parameters, datasets, subreports. Everything that was already in the
template is left untouched.
Your workflow:
- Add a field to the Java class
- Run
mvn compile - Open the updated file from
target/generated-sourcesin Jaspersoft Studio - the new parameters are there - Place the new elements in the design and copy the file back to
src/main/resources/reports/
mvn compile
|
+---------------+---------------+
| |
new class existing class
mode = CREATE mode = INJECT (default)
| |
v v
blank template your template + new
+ all parameters parameters/datasets/
+ datasets subreports
+ list/table components
+ subreport bands
| |
+---------------+---------------+
v
target/generated-sources/
your_report.jrxml
|
v
Jaspersoft Studio - add (for CREATE mode)
| update (for INJECT mode) your design
v
src/main/resources/reports/
your_report.jrxml <- final template
| Mode | Behavior |
|---|---|
INJECT (default) |
Injects missing elements into the existing JRXML without touching existing content |
CREATE |
Creates a new JRXML from the built-in blank template, overwriting 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.
| Attribute | Type | Required | Description |
|---|---|---|---|
templatePath |
String |
Yes | Classpath path to the JRXML file |
prefix |
String |
No | Parameter name prefix (default: simple class name) |
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) |
When @JasperCollection is absent, the processor defaults to a list component for backwards
compatibility.
@JasperCollection(type = CollectionComponentType.TABLE, columnWidth = 80)
private List<LineItem> items;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-jr7 - annotation processor for JasperReports 7.x
├── jasper-modular-processor-jr6 - annotation processor for JasperReports 6.x and older
├── jasper-modular-starter-jr7 - single dependency entry point for JasperReports 7.x
└── jasper-modular-starter-jr6 - single dependency entry point for JasperReports 6.x and older
The core, autoconfigure, and renderer modules are shared between both versions. Only the annotation
processor differs — JasperReports 6 requires explicit ComponentKey for list and table component
serialization, while JasperReports 7 infers the namespace automatically.
JasperModularRenderer.render() returns a format-neutral JasperPrint. Add the exporter for the
format you need:
XLSX:
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports-excel-poi</artifactId>
<version>7.0.6</version>
</dependency>Then use JRXlsxExporter or any other exporter from JasperReports. HTML export is available from
the core jasperreports jar without any additional dependency.
Apache License 2.0 - see LICENSE.


