DEV Community

Cover image for The Modular Layer JasperReports’ Powerful Engine Was Missing. Part 1: Where Jasper Came From and Why It Lacked Modularity
Dmitry Chaykin
Dmitry Chaykin

Posted on

The Modular Layer JasperReports’ Powerful Engine Was Missing. Part 1: Where Jasper Came From and Why It Lacked Modularity

Researching such big and complex products as JasperReports, and even more building your own practical tool for it, is clearly not a trend of 2026. This is the year when likes in media and stars on GitHub go to the next AI repository, written by another AI for a third AI.

So the text and the work itself may look out of place. It is my personal project. I don't claim it is something revolutionary, but it surely has every right to take a couple of lines in the dependencies of very different projects. The library is an attempt to generalize my experience deeply, and a personal challenge that is valuable for every developer: not just to write code, but to create something the community will actually use. And a line in the CV, of course.

Part of the thought from the first paragraph is relevant here too. By different estimates, a noticeable share of the traffic to developer sites and media already comes from AI agents, so this text is for them as well. These guys now also add those same lines to other people's projects.

Now I will talk about version 3.0.0 of the library, jasper-modular. Half a year of work since its first release removed various rough edges and fixed some weak spots that were hard to notice.

But first — a bit of background.

I worked with JasperReports on several projects, and the concept I am going to describe was born in pain, from many mistakes collected over the years. But at work there was no time to build a separate reporting tool, so I had to do it in my free time.

The text has two parts. This one is not a manual. It is about where Jasper came from, why it is designed the way it is, what happens with it now and which ideas my approach is built on. The second part is practice: how to add the library to a project and work with it, with the code and the templates.

Where Jasper came from

At the very beginning of the 2000s the Romanian developer Teodor Danciu was evaluating a big Java project where a lot of complex documents had to be generated and printed. He found that the ready-made solutions were too expensive for the project. He had experience with Windows report generators like Crystal Reports, and he decided that making a similar tool in Java would not be so hard. These are his own words from an interview in 2005.

At that time he also described the goal of the project. The engine can be embedded into any Java application, and its main purpose is page-oriented documents, ready to print. So from the very beginning it was a tool for documents, and how it would fit into the layers of an application was not part of the task.

In the world of Crystal Reports and its relatives, a report is a self-sufficient design file that owns everything: the query to the database, the bands, grouping, totals, formatting. The original tutorial from 2002 says directly that the whole structure of a report is a stack of sections from the title to the summary. And there it also says that the SQL query can be written right inside the report definition. A report was an application in miniature, a thing in itself, fully self-contained. By default it connected to the database by itself and ran its own query, no matter who made it, a developer or an analyst. In the client-server nineties this was normal, because applications went to the database directly anyway, and the business logic lived either in the client itself or in stored procedures.

A zoo of approaches

Self-sufficiency has a flip side. If a report lives on its own, it must be able to take data from anywhere, and over time many ways to do it appeared.

There were several of them from the very beginning. The same 2002 tutorial, next to the SQL query inside the template, describes passing a ready ResultSet from code. Later the number of ways only grew: queries over XML (since 2004) and JSON (since 2011), data adapters, when the report itself knows the database address and the password, passing the parent data source to a subreport, manual passing of parameters down the chain of subreports, and everything else Jasper collected in twenty-five years. None of them is the main one, and every project chooses its own. Even if you don't keep the query in the template, there are no fewer options: build SQL in Java and put it into the template query through a parameter, execute it yourself and pass the result, get the data via JPA and wrap it into beans. Reporting turns into a forest of unknown solutions. In every new project reports are built in their own way, and you have to figure everything out again.

The most painful place in this zoo is subreports. In Jasper they are the way to build a document from blocks. The grid of sections itself is flat, and you can insert a separately designed block into it only with a subreport. And here the hell begins: complicated passing of parameters, their manual binding to types and manual injecting too, design, parsing data inside, plus the complicated mechanism of "connecting" a subreport itself. On a real example it looks like this. Take an invoice with a block of items. To make one new field appear in this block, when data comes from Java you have to write it in three places: in the Java code where the parameter map is built, in the root template where it is passed to the subreport, and once again in the subreport itself. All this binding (parameter declarations in templates, passing them down the chain and the map of values in Java) I will call wiring from now on. And in all three places the field name is just a string that nobody checks. Make a mistake in one letter — and in the PDF you get "null" or an empty cell instead of the value, and not a single error in the logs. With SQL in the template it is not easier. You need to add the new column to the query, declare a field in the template with exactly the same name and not make a mistake with the type. You will find out about a typo only when somebody runs the report.

This is not criticism

Here it is important to clarify my position. I don't criticize Jasper. It is not a "badly designed library for developers" that I try to fix. It is a well-designed heir of a completely different tradition. A great engine and technology, to which I add what seems useful for the convenience of developers.

And in Jasper itself people saw the same problem and tried to answer it. Since version 6 it has report books, where instead of bands a report contains parts. Each part is a separate report with its own pages and even its own page size, parts can be nested into each other, and you can build a table of contents for the document. It is an attempt to build a document as a tree. Books still work today and could have become a good answer. But it is a tree of documents, not of blocks inside a page. You cannot build the header, a table and the footer of one page from parts. Books were designed from the start as assembling a document from whole pages, with a table of contents and different page formats. And there is no contract between code and template there either. Parts are connected by file paths, and data goes into each of them by hand, by string names. In the official sample, the database connection and the needed parameters are passed to the parts with data one by one, so it is the same wiring, only one level higher.

Roles exist, boundaries don't

Both the zoo and the manual wiring grow from the design of Jasper itself. In application development, such confusion, where getting data, passing it and displaying it are mixed together, has been solved for a long time by separating layers: the model separately, the view separately, and the controller between them. This is MVC, and the problem is not that nobody had thought of it back then. By 2001 the pattern itself was more than twenty years old, and in the same year as Jasper, version 1.0 of Struts came out, a framework for Java web applications built exactly on MVC. The MVC roles also exist in Jasper from the first day: the template draws, the data source carries the data, the engine fills. And, as we saw, it could take data from outside from the very beginning too.

Three things are missing: good mapping between Java objects and the template (the contract between the model and the template is string-based, manual and checked by nobody), limits on the choice of approaches (how to inject, how to pass data, how to parse data and so on) and boundaries between layers, so the view is allowed to go to the database directly. A parameter in Java and a parameter in XML match only because somebody typed them the same way by hand.

The pattern was sketched, but its separation never happened, because nobody wanted to separate the layers strictly back then. Report authors of that time saw the right of a template to go to the database by itself not as a hole in the boundary but as the main convenience. And there was nothing in the language itself to check such separation at compile time. Generics, annotations and annotation processing appeared in Java only in 2004–2006, and exactly on them the library is built now. Later the business logic moved into the server layers of applications, but reporting did not move with it and stayed attached to the database.

The engine in 2026

Maybe something changed over time? Twenty-five years have passed. What is happening with Jasper now — in short.

The engine is alive and supported. Version 7.0.8 came out in August 2026, and vulnerabilities get fixed. Since version 7, the modules for web and JPA moved from javax to jakarta: Servlet 6.0 instead of 4.0, Persistence 3.1 instead of 2.2, and for old applications the javax variants are kept. But the design of the core has basically not changed since then. Danciu compares template compatibility with Word documents. A JRXML created ten years ago must open in the new engine the same way as a DOC from 2003 opens in a new Office. So for them a template is a document format, not code with an API, and the main task is that templates with many years of history continue to work.

The layer of integration with applications belongs to nobody. Spring removed Jasper support already in version 5, in 2017. And when in 2026 users asked about Spring Boot 4 support, Danciu answered that he still had not heard what exactly the problem was, then showed with a sample that the engine works under Spring Boot 4, and closed the ticket.

For the engine team, embedding into applications is somebody else's layer, and by their logic this is right. But nobody else picked it up either. For the query "jasperreports spring boot" you find third-party blog articles, mostly from 2019–2023, and they teach you to build a report by hand: compile, fill, export step by step. And one of them still shows JasperReportsPdfView, the same class that Spring removed in 2017.

The core (compilation, filling, pixel-perfect layout, export) has worked reliably for a quarter of a century, and you can build on it without worry. But the fact remains that developers inherited a tool whose center of gravity is in another world. Nobody on that side is going to move it, so we have to do it ourselves.

What a modern developer expects

And they expect two things they got used to during the last ten years or so. The first is that layers are separated: data separately, view separately, MVC in one form or another. The second is that the interface is built as a tree of reusable blocks: the DOM tree in HTML, components in the frontend, their analogues in mobile development. You write a block once, and insert it wherever you need.

Jasper has neither of these in a convenient form, and it makes no sense to wait for it from its team, because they make an engine for documents, not a layer for applications.

This gap between what a developer is used to and what the engine gives is exactly what the library closes. First with built-in limits that reduce the zoo of options to one, and then with three ideas on top of them.

One standard and three ideas

So here is the idea. What stays as it was:

  • the whole JasperReports engine;
  • JRXML templates and Jaspersoft Studio for design.

What is added (the things the core does not have):

  • one standard instead of a zoo of approaches;
  • a tree instead of a grid: a report is built from nested modules;
  • layers: data, mapping and calculations in Java, layout in the template;
  • the object as a carrier: the structure, data and names of the report live in Java classes, and the report structure fully repeats the object tree.

The three ideas from the list (the tree, the layers and the object) stand on this standard, and none of them is new by itself. The point is that they work only together. A strict contract without composition gives the same monolithic template, only checked, and composition without a contract is the current subreport hell. And all three come from one move, turning a subreport into a field of a Java class.

One standard instead of a zoo

The first thing the library does is consciously ignore all the other ways Jasper lets you get and pass data, that whole zoo listed above. Only what fits the modular MVC approach stays. Data comes as objects, and wiring is generated. This does not touch layout, and everything Jasper can do in design stays yours. You can still do anything the old ways, past the library, but it will intentionally not help you with that. In exchange it gives one standard, and in new reports there is no reason to go around it anymore.

A tree instead of a grid

In classic Jasper, the structure of a report is defined by the engine. Remember the 2002 tutorial. The whole structure is built on sections: title, pageHeader, columnHeader, groupHeader, detail, groupFooter, columnFooter, pageFooter, summary. So the "structure" is a set of ready cells. They are fixed, they cannot be nested into each other, the engine defines their set, and the developer puts the content into the grid they were given.

My approach changes who owns the structure. The library goes around this predefined structure and simplifies it, to make an analogue of the DOM tree that is flexible to build and reuse. The root report is an empty container. De facto the library builds the report in one <detail> block, where all subreports live as nested components in identical bands, no matter how many there are. For Jasper, the report structure is now basically one type of tag instead of nine, and for the modular approach this is enough. The title block is a title module, totals are a totals module, so the root template doesn't need the <title> and <summary> bands for data. A module can contain modules, and those can contain their own, to any depth. A block written once is inserted into any report with one field. It is the same move that once happened in web layout, from tables to components. Instead of putting everything into the cells you were given, you describe your own components and nest them as you like.

This is how the report tree from the sample repository looks:

CompanyReport — root report
├── title: TitleSubModule — company details, period, currency, totals
├── financial: FinancialSubModule — financial block
│   ├── revenue: RevenueSubModule — revenue and a table of revenue items
│   ├── expense: ExpenseSubModule — expenses and a table of expense items
│   └── profit: ProfitSubModule — profit, margin and a breakdown table
└── departments: List<DepartmentSubModule> — one block per department
    └── name, headcount, budget and a table of employees
Enter fullscreen mode Exit fullscreen mode

Each module is a separate class with its own template.

The bands do not disappear — they move inside. Every leaf block still has its own detail, group totals and page-break rules, and the root report has the page frame: page numbers, headers and footers. Before, the set of sections was the structure of the whole document, now it is the inner design of a single block.

It is not a real DOM: there is no live tree, no events, no re-rendering here. It is a tree for assembling a document, and for reports it is enough.

Layers: data separately, layout separately

The second idea is the boundaries Jasper was missing from its birth. That one standard is what draws them. Since data comes only as ready objects, the template has no way to go to the database. No SQL in the template, no JSON paths. You get data the way you are used to: from repositories via JPA or JDBC, from an external API, and in a separate reporting microservice even straight from a request to a controller. Then it is like in the rest of the application. You calculate in Java, map into a typed tree of objects and give it to the report.

The template returns to its proper role. The library took away the work that was never its job: getting data, knowing the model structure, manual wiring. What stays is what a view should have: layout, typography, page-break rules.

This is maybe the most arguable point of the approach, so I will talk about it separately. A self-sufficient report that runs its own queries is a legitimate model for business intelligence (BI) systems and report servers. There the report is the application, and the server takes care of permissions, connections and scheduling. But inside your application such a template becomes a shadow data access layer. It goes past your transactions, tests and monitoring, and partly past permissions too. The application will check who can run the report, but not what exactly the query in the template selects. One could make the data source configurable, but the data would still go past the Java repository.

When Java gets the data, everything listed above also covers the report, and the engine stays one rendering step that is easy to measure.

Layout is convenient to move with the mouse in Jaspersoft Studio, and logic is convenient to test and refactor in code, so each part should live where it is easier to work with it.

The object as a carrier: structure, data and names in one place

The third idea is the one the first two stand on. In classic Jasper the same meaning lives in several places at once: the structure in XML templates, the data in Java code, and the names are duplicated as strings in both. In the library all of this moves into one structure of Java classes:

@JasperModularReport(templatePath = "/reports/invoice.jrxml")
public class InvoiceReport extends ModularReport {
    private String invoiceNumber;
    private AddressModule billingAddress;
    private AddressModule shippingAddress;
    // one subreport per shipment
    private List<ShipmentModule> shipments;
}
Enter fullscreen mode Exit fullscreen mode

The address and shipment modules and the invoice item look like this:

@JasperSubreport(templatePath = "/reports/address.jrxml")
public class AddressModule extends SubreportModule {

    private String city;
    private String street;
    private String zip;

    @Override
    public boolean isEmpty() {
        return city == null && street == null && zip == null;
    }
}

@JasperSubreport(templatePath = "/reports/shipment.jrxml")
public class ShipmentModule extends SubreportModule {

    private String trackingNumber;
    private LocalDate shippedAt;
    private List<Item> items; // its own table inside the shipment block

    @Override
    public boolean isEmpty() {
        return items == null || items.isEmpty();
    }
}

public class Item {
    private String name;
    private int quantity;
    private BigDecimal price;
}
Enter fullscreen mode Exit fullscreen mode

Every module has its own template, the subreport base class and one required method isEmpty. An empty block simply does not get into the report. Item is a normal class with fields, without any annotations. From a list of such objects the library itself makes a simple Jasper table in the template (or a list, if you set it in the field annotation) and fills it with data. And if the list elements are modules, like ShipmentModule, you get a repeating subreport. Every shipment has its own block with its own template, and inside it, its own table of items.

InvoiceReport is the report. Its fields carry three things at once. Structure: module fields are the branches of the tree, their nesting is the nesting of blocks in the document. Data: field values are what gets into the report. And names: the field name becomes the parameter name in the template, and its type becomes the parameter type. From this class the template gets the parameter invoiceNumber, separate parameters for each of the two addresses and the shipments blocks, and the table of items goes into the template of each shipment. Nobody types these names by hand anymore, neither in Java nor in XML.

One consequence I understood not at once. The same address module stands in the report twice: as the billing address and as the shipping address. These are two different places with different data. That's why in version 3 the parameter names come from the field name (billingAddress, shippingAddress) and not from the AddressModule class, as it was before. The place in the tree is defined by the field, and the type only says what kind of block it is.

And this is how the three ideas connect: the tree is the class fields, the contract between data and layout is the names and types of the fields, and the data is their values. Saving lines of code is a nice bonus here. More important is that the report structure now lives in a Java model, where you can see it in the code, version it, refactor it and reuse it with normal language tools. And the template follows it. A new field appears in it automatically, and a renamed module or a changed type of a simple field will not pass the build. More about this below.

What is hidden under the hood

Everything above is what the developer writes: classes, fields, one isEmpty per module and a couple of annotations that say this class is a report, this one is a subreport, and the template lies here. The library takes over everything else, the mechanical work that used to be done by hand.

At compile time an annotation processor reads these classes and adds all the wiring to the templates: parameters, datasets, tables or lists for collections, bands with subreports. It adds only what is missing and never touches the layout, the styles or elements placed by hand. It also checks the template against the class. If a module or a collection was renamed or removed but its old wiring stayed in the template, or the type of a simple field changed, the build fails and says what exactly to fix.

On application start, the templates of all reports and modules from the package you set are compiled in advance. This is done by the Spring Boot autoconfiguration, and you need only one setting. A broken template breaks the start, and so also a normal context test in CI. You learn about the problem during build and deploy, not when a user clicked "Generate".

At runtime everything comes down to one call that renders the object. Under it, the base classes your reports and modules extend do all the mechanics: they compile and cache templates, walk the object tree, build a separate parameter map for every block, pass subreports down to any depth and wrap collections into data sources. The root fill always starts from an empty data source, and the template does not get a database connection at all, so data comes only from objects.

In vanilla Jasper everything stands on agreements. Names will match if you type them the same way, and a template will not go to the database if nobody points it there. On its own path the library does not rely on a word of honor. There is no place to make a typo in the wiring, a template with old wiring or wrong types will not build, a broken one will not deploy, and the library itself will not point a template to the database. Not everything is checked, though — only the link between code and template. Whether you put the right data into the object and how it is laid out is still on you. But the main class of errors does not reach the user anymore: "added a new field to the data and forgot to connect it in the template", "renamed a module and forgot to fix the XML", "added data to the root report but did not pass it to the sub- or sub-sub-report".

Nobody stops you from going around it. Old reports can still be filled by hand, with your own parameters, your own connection and SQL in the template. The library will forgive this, it just will not check it. That's why you can move to it step by step, one report at a time, while the others keep working as before in the same application.

One thing stays manual: copying the generated template from target back to resources. It is the same file that later goes to the analyst. The build does not check this step, so if you forget it, the new field simply will not appear in the report. More about it in the second part.

Who does what

And one more consequence is that the work of the developer and the analyst can finally be really separated.

The developer describes the structure and data in Java, builds the project, and gets templates that already contain everything: parameters, collections, subreports, connected to each other. These templates can be given to anyone: an analyst, a designer, the manager who ordered the report, even a client. They open them in Jaspersoft Studio and design whatever they want. All variables and all blocks are already inside, and nothing has to be connected by hand anymore. They don't need to know what happens in the code, where the data comes from and how SQL runs there. And the developer doesn't need to spend a long time on how to hook everything up in Jasper by hand, wire the parameters and check them. If the analyst needs new data, the developer adds a field to the report or a module, and after the next build it appears in the template by itself.

The developer can do all of this themselves too, and it also works. I went the way of maximum simplification while keeping the value, which is a strict contract without giving up either JRXML or Jaspersoft Studio. Even when you have one simple report, building it with the library and designing the look in Jaspersoft Studio is much faster than doing it the classic way. At least you get rid of injecting fields and manually binding the passed data to them. The processor will generate the fields in the template, and the library will bind the data to them.

Instead of a conclusion

There is also some irony here. I started with AI agents — and I will finish with them. I suspect that this text will be useful not only for those who read it to the end. In half a year the indexing will get there, and to the question "how to work with JasperReports" an AI agent will start to recommend this library to someone. If one day you get such advice, here it is explained what it is and why.

The second part has all the practical things: how to add the library, what exactly the processor adds to templates, how collections and repeating subreports work, how to return the generated template to resources, the full list of build checks and where the approach has rough edges. I hope to publish the second part in the coming days.

The library is open source, Apache 2.0: github.com/hhdevr/jasper-modular-library. On Maven Central it is io.github.hhdevr:jasper-modular-starter, and there is also a sample repository. It works with both version 6 and version 7 of JasperReports. It takes the one you have in your project, and the processor writes templates in its format. If you try it on your reporting, I will be glad to get any feedback in issues, including criticism.

Top comments (0)