Spring Boot + iText 7: Dynamic PDFs & Electronic Signatures for Contracts/Reports
This guide covers integrating iText 7 with Spring Boot to generate dynamic PDFs via AcroForm templates or code, add PAdES-compliant digital signatures with visible seals, and implement async processing with RabbitMQ, MinIO storage, and expiring download links, plus font handling, memory optimization, and AGPL licensing advice.
Business Background
The article outlines three common PDF generation scenarios: online contracts (user-filled terms, pricing, duration), electronic invoices (tax ID, amount, QR code), and financial reports (charts, department filtering). The core requirements are automatic PDF rendering from data and adding electronic seals for legal validity.
Technology Selection: Why iText 7
The author compares three Java PDF libraries:
iText 7 : Comprehensive feature set including AcroForm filling, digital signatures, PAdES, PDF/A. Open-source version is AGPL; commercial license required for SaaS distribution.
OpenPDF : Lightweight fork of iText 4, suitable for simple reports but lacks PAdES and complete AcroForm support.
Apache FOP : Uses XSL-FO, good for fixed-layout publications but weak on form interaction and dynamic signatures.
iText 7 is chosen for its mature PdfAcroForm module (supports flattening), official PAdES support, and acceptable high-concurrency performance with font caching.
Spring Boot Integration: Chinese Font Handling
Dependencies
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext7-core</artifactId>
<version>7.2.5</version>
<type>pom</type>
</dependency>For digital signatures, add itext-sign 7.2.5 and BouncyCastle ( bcprov-jdk15on and bcpkix-jdk15on ~1.70) to avoid version conflicts.
Font Registration Pitfalls
.ttcfont collections (e.g., simsun.ttc) require specifying the font index during registration. setBold() does not work for Chinese fonts; a separate bold font file (e.g., SimHei.ttf) must be loaded.
The solution caches fonts at startup via a @Component:
@Component
public class ChineseFontProvider {
private static final String FONT_PATH = "classpath:/fonts/simsun.ttc";
private static PdfFont songFont;
@PostConstruct
public void init() {
try {
FontProgram fontProgram = FontProgramFactory.createFont(FONT_PATH, 0, false);
songFont = PdfFontFactory.createFont(fontProgram, PdfEncodings.IDENTITY_H);
} catch (IOException e) {
throw new RuntimeException("字体加载失败", e);
}
}
public static PdfFont getSongFont() {
return songFont;
}
}Usage:
document.add(new Paragraph("中华人民共和国").setFont(ChineseFontProvider.getSongFont()));Dynamic PDF Generation: Template Filling + Code Drawing
Approach 1: AcroForm Template Filling
Create a PDF template with form fields (e.g., contractNo, customerName) using Adobe Acrobat. Fill programmatically:
public byte[] fillTemplate(Map<String, String> data) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfDocument pdfDoc = new PdfDocument(new PdfReader("template_contract.pdf"), new PdfWriter(baos));
PdfAcroForm form = PdfAcroForm.getAcroForm(pdfDoc, true);
data.forEach(form::setField);
form.flattenFields(); // makes fields immutable
pdfDoc.close();
return baos.toByteArray();
}Key pitfalls:
Form field names must match code keys exactly.
If template contains Chinese, embed fonts during template creation or set font per field via form.getField(name).setValue(value, font).
Always call flattenFields() to prevent post-generation modification.
Approach 2: Code-Based Tables and Charts
For dynamic columns, use iText's Table API:
Table table = new Table(4);
table.setWidthUnit(UnitValue.createPercentValue(100));
table.addHeaderCell(new Cell().add(new Paragraph("项目").setFont(songFont)));
// ... add header cells for 数量, 单价, 金额
for (SalesItem item : items) {
table.addCell(new Cell().add(new Paragraph(item.getName()).setFont(songFont)));
// ... add quantity, price, amount
}
// Merge cells for total
Cell merged = new Cell(1, 4).add(new Paragraph("合计:" + total).setFont(songFont));
table.addCell(merged);
document.add(table);Charts are not built-in; the author uses JFreeChart to generate a BufferedImage, then embeds via ImageDataFactory.create().
Electronic Signatures: PAdES Digital Signatures with Visible Seals
Certificate Preparation
Production uses CA-issued certificates; testing uses keytool to generate a PKCS12 keystore:
keytool -genkeypair -alias testcert -keyalg RSA -keysize 2048 -storetype PKCS12 \
-keystore keystore.p12 -storepass changeit -dname "CN=Test, OU=Dev, O=Company, L=City, C=CN"Signing Implementation
public byte[] signPdf(byte[] pdfBytes, byte[] imageBytes, String certPath, String password) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// Load certificate
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(new FileInputStream(certPath), password.toCharArray());
String alias = ks.aliases().nextElement();
PrivateKey privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray());
Certificate[] chain = ks.getCertificateChain(alias);
// Append mode protects original content
PdfReader reader = new PdfReader(new ByteArrayInputStream(pdfBytes));
PdfSigner signer = new PdfSigner(reader, baos, new StampingProperties().useAppendMode());
// Visible seal appearance
PdfSignatureAppearance appearance = signer.getSignatureAppearance();
appearance.setPageRect(new Rectangle(450, 700, 150, 60));
appearance.setPageNumber(1);
ImageData image = ImageDataFactory.create(imageBytes);
appearance.setSignatureGraphic(image);
appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC_AND_DESCRIPTION);
appearance.setDescription("合同签署");
// RSA with SHA-256, PAdES-EPES
IExternalSignature externalSignature = new PrivateKeySignature(privateKey, "SHA-256");
signer.signDetached(new BouncyCastleDigest(), externalSignature, chain, null, null, null, 0, PdfSigner.CryptoStandard.CADES);
return baos.toByteArray();
}Signing must be the final PDF modification step; any subsequent change invalidates the signature.
Signature Verification
public boolean verifyPdfSignature(byte[] pdfBytes) throws Exception {
PdfDocument pdfDoc = new PdfDocument(new PdfReader(new ByteArrayInputStream(pdfBytes)));
SignatureUtil signatureUtil = new SignatureUtil(pdfDoc);
List<String> names = signatureUtil.getSignatureNames();
for (String name : names) {
PdfPKCS7 pkcs7 = signatureUtil.verifySignature(name);
if (pkcs7.verifySignatureIntegrity()) {
System.out.println("签名有效,签署者:" + pkcs7.getSignName());
return true;
} else {
System.out.println("签名无效");
return false;
}
}
return false;
} verifySignatureIntegrity()returns false if the PDF was altered after signing.
Service Encapsulation: Async, Queue, and Download Links
Service Interface
public interface PdfGenerateService {
byte[] generate(PdfGenerateRequest request); // sync
String generateAsync(PdfGenerateRequest request); // async, returns taskId
TaskStatus getTaskStatus(String taskId);
String getDownloadUrl(String taskId, Duration expiry); // temporary link
}Async Implementation
Initial attempt with @Async + CompletableFuture.supplyAsync caused thread-pool confusion. Final pattern uses a dedicated executor and returns a task ID immediately:
@Async("pdfTaskExecutor")
@Override
public CompletableFuture<String> generateAsync(PdfGenerateRequest request) {
String taskId = UUID.randomUUID().toString();
try {
byte[] pdfBytes = templateFiller.fill(request);
if (request.isNeedSign()) {
pdfBytes = signerService.sign(pdfBytes, request.getSignImage());
}
storageService.storePdf(taskId, pdfBytes); // e.g., MinIO, DB
taskRepository.updateStatus(taskId, TaskStatus.SUCCESS);
} catch (Exception e) {
taskRepository.updateStatus(taskId, TaskStatus.FAILED);
}
return CompletableFuture.completedFuture(taskId);
}Task status must be persisted externally (Redis/DB), not in an in-memory Map, to avoid single-point loss.
Message Queue for Peak Shaving
Batch report generation (thousands of PDFs) uses RabbitMQ:
Producer sends PdfGenerateRequest to pdf.generate.queue.
Consumer generates PDF, stores to MinIO/OSS, updates DB status.
@Component
public class PdfGenerateConsumer {
@RabbitListener(queues = "${pdf.queue.name}")
public void handle(PdfGenerateRequest request) {
try {
byte[] pdfBytes = pdfGenerateService.generate(request);
String objectKey = storageService.upload(pdfBytes, request.getFileName());
// update DB: task completed, save objectKey
} catch (Exception e) {
// record failure, route to retry or dead-letter queue
}
}
}Kafka works similarly with consumer-group and offset management.
Expiring Download Links
Direct MinIO exposure is risky. A Redis token maps to the object key with TTL:
public String createDownloadUrl(String objectKey, Duration duration) {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(PREFIX + token, objectKey, duration);
return "/pdf/download/" + token;
}Controller validates token and streams the PDF:
@GetMapping("/pdf/download/{token}")
public void download(@PathVariable String token, HttpServletResponse response) {
String objectKey = downloadUrlService.getObjectKey(token);
byte[] data = storageService.download(objectKey);
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "attachment; filename=report.pdf");
response.getOutputStream().write(data);
}Expired tokens cause getObjectKey to throw, denying access.
Performance and Security Lessons
Memory Optimization
Large batches: write to temporary files or stream directly via response.getOutputStream() instead of ByteArrayOutputStream.
Cache font objects globally (the ChineseFontProvider singleton does this).
Sensitive Data Masking
Mask at source (e.g., store phone as 138****1234). Visual redaction with PdfCanvas black rectangles is not secure—text remains extractable.
PdfPage page = pdfDoc.getFirstPage();
PdfCanvas canvas = new PdfCanvas(page);
canvas.setFillColor(ColorConstants.BLACK);
canvas.rectangle(x, y, width, height);
canvas.fill();Template Injection Prevention
Escape or whitelist user input when filling form fields to avoid breaking template structure (e.g., <script> tags).
Signature and Encryption Order
Sign last; any encryption or modification after signing breaks the signature. If encryption is required, apply before signing but beware compatibility issues. The project avoids post-signing encryption, relying on external access control.
AGPL Licensing
iText 7 is AGPL. SaaS products must purchase a commercial license; internal-only systems may use AGPL. Legal review is essential.
Conclusion
iText 7 is feature-complete but complex. The described solution powers electronic contract and invoice workflows, handling tens of thousands of PDFs daily. The article shares practical patterns for template filling, digital signatures, async processing, and operational concerns.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Xiaolin Talks Programming
Focuses on sharing original technical insights. Senior architect at a top tech company with years of experience in technical architecture and management, and extensive interview experience. Offers one-on-one technical coaching, guiding you from beginner to architecture design to technical management. Follow for free learning resources. Free one-on-one interview coaching to help you land offers quickly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
