Getting Started with Spring Boot
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page builds the skeleton of the Bookshelf Billing service used by the rest of the section: a Spring Boot 4 application
on Java 21 with iText and JasperReports on the classpath, one "Hello PDF" in each library, and the HTTP plumbing that
returns the file to a browser or an API client. Versions are stated as of 2026-10-09: iText Core 9.8.0, JasperReports
7.0.8 and Spring Boot 4.1.1. The Hello PDF examples below were compiled and run against exactly those versions on a Java 21
language level (--release 21 through the Spring Boot parent), and both endpoints returned a valid one-page PDF.
The Spring Boot basics (project layout, starters, configuration) are not repeated here, see Spring Boot getting started; for controllers and response handling in general see REST APIs with Spring Boot.
The request-to-PDF flow
Whatever library builds the document, a PDF endpoint has the same shape: the controller validates the request, a service loads the data and produces the bytes, and the controller sends them with the right headers. The only decision that changes the code is whether the bytes are first collected in memory or written straight to the response.
The sections below follow this diagram from left to right: dependencies first, then the two libraries, then the controller.
Maven setup for iText 9.8
Since iText 7 the library is distributed as modules, and since iText 8.0 the aggregator is called itext-core (it was
itext7-core before). It is a POM aggregator: depending on it with
<type>pom</type> pulls in the kernel, layout, forms, signatures, barcodes, SVG and the other core modules at once.
Signing and encryption additionally need a Bouncy Castle provider, and iText wants exactly one of two adapters on the
classpath: bouncy-castle-adapter or bouncy-castle-fips-adapter. Use the plain adapter unless a FIPS 140 validated
provider is a requirement. Both come from Maven Central under com.itextpdf.
iText is licensed under the AGPLv3 or under a commercial licence. With AGPL, a service that lets users interact with it over a network must offer its complete source code to those users, so for a closed-source product a commercial licence is needed. The licensing consequences are discussed in the Introduction & Approaches page of this section.
<properties>
<java.version>21</java.version>
<itext.version>9.8.0</itext.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<!-- iText 9: the POM aggregator plus exactly one Bouncy Castle adapter -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-core</artifactId>
<version>${itext.version}</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>bouncy-castle-adapter</artifactId>
<version>${itext.version}</version>
</dependency>
</dependencies>
Notes on the setup:
-
spring-boot-starter-webmvcis the Spring Boot 4 name of the servlet web starter. The parent POM isspring-boot-starter-parent4.1.1 as of 2026-10-09. -
Commercial add-ons such as pdfCalligraph or pdfOptimizer are not on Maven Central; they are served from
https://repo.itextsupport.com/and need a licence. pdfHTML (html2pdf) is on Central under AGPL or commercial terms. -
If dependency convergence or a BOM is preferred,
itext-corecan be imported once independencyManagement, but a single property for the version, as above, is enough for a service.
Official documentation: Installing iText for Java and the
itext-core page on Maven Central.
Hello PDF with iText
A PDF is written by a PdfWriter into a PdfDocument, and the high-level Document adds layout elements on top of it.
All three are AutoCloseable, so a try-with-resources block closes them in the right order, which also writes the
cross-reference table and trailer. The service below can write to any OutputStream, which is what both the in-memory and
the streaming endpoint need. setCloseStream(false) keeps iText from closing a stream it does not own.
package com.example.billing;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import org.springframework.stereotype.Service;
@Service
public class ItextHelloService {
/** Writes a one-page PDF to any stream; the caller owns the stream. */
public void write(OutputStream out, String customer) {
PdfWriter writer = new PdfWriter(out);
writer.setCloseStream(false);
try (PdfDocument pdf = new PdfDocument(writer);
Document document = new Document(pdf)) {
document.add(new Paragraph("Hello PDF from Bookshelf Billing"));
document.add(new Paragraph("Customer: " + customer));
}
}
public byte[] render(String customer) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
write(buffer, customer);
return buffer.toByteArray();
}
}
Closing the Document closes the underlying PdfDocument too, so the explicit second resource is harmless and keeps the
lifecycle visible. The default font is Helvetica, a standard font that is not embedded; embedding fonts and Unicode text
are covered in the iText pages later in this section.
Official documentation: iText jump-start tutorial, chapter 1,
which still describes the current PdfWriter, PdfDocument and Document classes.
Maven setup for JasperReports 7.0.8
JasperReports 7 is split into modules, and the core artifact no longer contains the PDF exporter, the fonts or a
Java compiler. A service that fills reports and exports PDF needs four artifacts, all in the net.sf.jasperreports group and
all at the same version:
-
jasperreportsis the engine: parsing JRXML, compiling, filling and the exporter API. -
jasperreports-pdfis the PDF exporter. Its PDF writer is OpenPDF (com.github.librepdf:openpdfin a Jaspersoft build,1.3.43.jaspersoft.2for 7.0.8). The PDF producer is pluggable through thenet.sf.jasperreports.export.pdf.producer.factoryproperty, but the default is what these pages use. -
jasperreports-fontsbundles the DejaVu Sans, Serif and Mono families, which are the default report fonts. -
jasperreports-jdtis the Eclipse JDT compiler used to compile report expressions at run time, so the service does not need a full JDK orjavacat run time.
JasperReports is licensed under the LGPL-3.0, so using it from a closed-source service is allowed. The core targets Java 8, but the Jakarta, Spring and Spring Boot modules need Java 17 or later, which Spring Boot 4 satisfies.
<properties>
<jasperreports.version>7.0.8</jasperreports.version>
</properties>
<dependencies>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports</artifactId>
<version>${jasperreports.version}</version>
</dependency>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports-pdf</artifactId>
<version>${jasperreports.version}</version>
</dependency>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports-fonts</artifactId>
<version>${jasperreports.version}</version>
</dependency>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports-jdt</artifactId>
<version>${jasperreports.version}</version>
</dependency>
</dependencies>
Charts, barcodes, Excel export and the other optional features are separate artifacts (for example jasperreports-charts)
and are added by the pages that use them. In the Community Edition of 7.0.8, tagged PDF (PDF/UA and the "a" levels of PDF/A)
is not available: it needs the commercial JasperReports Library Professional. In the Community Edition the default
tagger raises an error when tagging is requested, and a silent tagger can be configured to skip the tags.
Official documentation: the JasperReports Maven sample and the Spring Boot sample.
Hello PDF with JasperReports
A report is a JRXML template. JasperReports 7 uses the new Jackson-based JRXML format, where every element is an
<element kind="..."> rather than the <staticText> and <textField> tags of version 6 and earlier; a v6 file has to be converted with
Jaspersoft Studio 7. The template below has one parameter and a title band. Save it as
src/main/resources/reports/hello.jrxml.
<?xml version="1.0" encoding="UTF-8"?>
<jasperReport name="hello" language="java" pageWidth="595" pageHeight="842"
columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20">
<parameter name="customer" class="java.lang.String"/>
<title height="60">
<element kind="staticText" x="0" y="0" width="555" height="30" fontSize="18.0">
<text><![CDATA[Hello PDF from Bookshelf Billing]]></text>
</element>
<element kind="textField" x="0" y="30" width="555" height="20">
<expression><![CDATA["Customer: " + $P{customer}]]></expression>
</element>
</title>
</jasperReport>
The service compiles the template once, when the bean is created, and fills it for every request. Compiling is the
expensive step, while the compiled JasperReport object is thread-safe and reusable. Filling produces a JasperPrint, a
format-independent document, and JasperExportManager turns it into PDF bytes.
package com.example.billing;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import net.sf.jasperreports.engine.JREmptyDataSource;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
@Service
public class JasperHelloService {
private final JasperReport report;
public JasperHelloService() throws IOException, JRException {
// Compile once at start-up; the compiled report is thread-safe and reusable.
try (InputStream template = new ClassPathResource("reports/hello.jrxml").getInputStream()) {
this.report = JasperCompileManager.compileReport(template);
}
}
public void write(OutputStream out, String customer) throws JRException {
// JasperReports adds its own entries to this map, so it must be mutable (not Map.of).
Map<String, Object> parameters = new HashMap<>();
parameters.put("customer", customer);
// The template has no detail rows: an empty data source with one record still produces the page.
JasperPrint print = JasperFillManager.fillReport(report, parameters, new JREmptyDataSource());
JasperExportManager.exportReportToPdfStream(print, out);
}
}
The parameters map must be mutable. Map.of(...) is tempting in Java 21 code, but fillReport stores its own parameters, such as the
format factory, in the same map, and an immutable map makes the fill fail with an UnsupportedOperationException. Use a HashMap, or copy
an immutable one into a HashMap first.
The third argument matters too. A report filled without a data source or a connection has no records, and with the
default whenNoDataType="NoPages" the result is a document with no pages. new JREmptyDataSource() supplies one empty
record, so the title band is printed. Setting whenNoDataType="AllSectionsNoDetail" on the <jasperReport> element is
the template-side alternative.
For production services, compile .jrxml at build time into .jasper files (the JasperReports Maven plugin or an Ant task) and
load those, which removes the compiler from the start-up path. Run-time compilation, as above, is the simplest start and is why the JDT
module is on the classpath.
Official documentation: the JasperReports Spring Boot sample and Maven sample.
Returning the PDF from a controller
This replaces the servlet and JSP era approach of writing to HttpServletResponse by hand, setting the content type and
working around old Internet Explorer behaviour with extra headers. Those workarounds are obsolete: in Spring Boot 4 the
controller returns a ResponseEntity<byte[]> for a document built in memory, or a ResponseEntity<StreamingResponseBody> for
one that is written directly to the response. Spring sets the status, the content type and the length, and the same code works
with any modern browser. See the
Spring ResponseEntity reference
and the asynchronous request and streaming reference.
The two return types make different trade-offs:
ResponseEntity<byte[]>-
The whole PDF is built in memory first. The
Content-Lengthheader is known, a failure while building the document becomes a clean HTTP 500 before any byte is sent, and the code is easy to test. Use it for invoices, receipts and anything up to a few megabytes. StreamingResponseBody-
The callback writes directly to the response
OutputStreamon a separate task executor thread. Memory use stays low for large reports, and the first bytes reach the client early. The cost is that the response is already committed when the callback fails, so the client sees a truncated file instead of an error status, and the response is chunked without aContent-Length. Configure the MVC async task executor and timeout for long reports.
package com.example.billing;
import java.io.IOException;
import net.sf.jasperreports.engine.JRException;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
@RestController
public class PdfController {
private final ItextHelloService itext;
private final JasperHelloService jasper;
public PdfController(ItextHelloService itext, JasperHelloService jasper) {
this.itext = itext;
this.jasper = jasper;
}
/** Small document: build it in memory, so the length is known and errors become a clean 500. */
@GetMapping(value = "/hello/itext", produces = MediaType.APPLICATION_PDF_VALUE)
public ResponseEntity<byte[]> itextInline(@RequestParam(defaultValue = "Ada") String customer) {
byte[] pdf = itext.render(customer);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
ContentDisposition.inline().filename("hello.pdf").build().toString())
.contentType(MediaType.APPLICATION_PDF)
.contentLength(pdf.length)
.body(pdf);
}
/** Larger document: write straight to the response; attachment makes browsers download it. */
@GetMapping(value = "/hello/jasper/{customer}", produces = MediaType.APPLICATION_PDF_VALUE)
public ResponseEntity<StreamingResponseBody> jasperDownload(@PathVariable String customer) {
StreamingResponseBody body = out -> {
try {
jasper.write(out, customer);
} catch (JRException e) {
throw new IOException("Report generation failed", e);
}
};
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
ContentDisposition.attachment().filename("hello-report.pdf").build().toString())
.contentType(MediaType.APPLICATION_PDF)
.body(body);
}
}
Run the application with mvn spring-boot:run and request curl -OJ http://localhost:8080/hello/jasper/Ada to save the second
file under the name given by the server.
Content-Disposition: inline versus attachment
The Content-Disposition header tells the browser what to do with the body. inline asks it to display the PDF in the viewer
and attachment asks it to download the file; both can carry a filename that is used as the suggested name. Build the header
with ContentDisposition instead of concatenating strings: the builder quotes the name and, when the name contains non-ASCII
characters, adds the RFC 5987 filename* form with UTF-8 encoding. Never put a user-supplied value in the file name without
sanitising it, because a header value is a header injection and a path traversal vector. A download link for a document that the user
has to save should use attachment, while a preview in a new tab should use inline. The PDF media type is application/pdf,
available as MediaType.APPLICATION_PDF.
Official documentation: the MDN Content-Disposition reference
and RFC 6266, and the Javadoc for
StreamingResponseBody
and ResponseEntity.
Headless AWT and fonts in containers
JasperReports measures text with the Java 2D font machinery (AWT), and iText can use it for some image and font operations. A server has no
display, so Java must run in headless mode, otherwise some code paths fail on a machine without X11. Headless is the default on Linux servers
without a display, but set it explicitly so a developer workstation behaves like the container. Either set the system property before the
first AWT class is used, as the example application does in main, or pass -Djava.awt.headless=true to the JVM.
public static void main(String[] args) {
System.setProperty("java.awt.headless", "true");
SpringApplication.run(BillingApplication.class, args);
}
The second container issue is fonts. A slim base image such as a JRE-only image often has no fontconfig library and no system fonts, and
Java then fails when it looks up a font, with errors that mention fontconfig or libfreetype, or
reports silently fall back to different glyphs. There are two complementary fixes:
-
In JasperReports, keep
jasperreports-fontson the classpath: it registers the bundled DejaVu fonts as report fonts, which are embedded in the PDF, so the output is identical on a laptop and in a container. -
In the image, install
fontconfigand a font package, so any other Java 2D use (and any report that names a system font) works. On a Debian based JRE image that isfontconfigandfonts-dejavu-core.
FROM eclipse-temurin:21-jre
RUN apt-get update \
&& apt-get install -y --no-install-recommends fontconfig fonts-dejavu-core \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY target/bookshelf-billing-0.0.1-SNAPSHOT.jar app.jar
ENV JAVA_TOOL_OPTIONS="-Djava.awt.headless=true"
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
For iText, fonts that are not embedded are not a container problem as long as the standard 14 fonts are used, because they are not read from the operating system; fonts for non-Latin text have to be supplied with the application and embedded. That is covered in the iText fonts page later in this section. This Dockerfile was not built as part of this page, so check it against the base image your organisation uses.
Official documentation: the JasperReports
fonts sample and the Java SE
GraphicsEnvironment Javadoc, which describes headless mode.
What the older books say
The servlet chapter of the iText book consulted for this section (B1, written for iText 1.4 and JDK 1.4) sends a PDF from a servlet or a JSP,
with workarounds for the content-type handling of Microsoft Internet Explorer of that time. Those workarounds no longer apply.
The current approach is the controller shown above: a ResponseEntity<byte[]> or a StreamingResponseBody with a
Content-Disposition header built by ContentDisposition. The code in that book is also written for Java 1.3 to 5, without generics.
Every example on this page uses Java 21 idioms instead: try-with-resources for iText documents and streams, constructor injection,
typed collections and, in the pages that follow, records for the data passed to templates and text blocks for embedded markup.