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.

Xiaolin Talks Programming
Xiaolin Talks Programming
Xiaolin Talks Programming
Spring Boot + iText 7: Dynamic PDFs & Electronic Signatures for Contracts/Reports

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

.ttc

font 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Spring BootPDF generationdigital signaturesAsync ProcessingAcroFormAGPL licensingiText 7PAdES
Xiaolin Talks Programming
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.