Spring Boot Poi-tl: Dynamic Word Generation with Templates, Table Loops & Watermarks
This tutorial demonstrates integrating Spring Boot with Poi-tl for dynamic Word document generation, covering template rendering, table row loops, image watermarks, dependency configuration, and production pitfalls like placeholder splitting and font issues.
Why Template Rendering Over POI Hard‑Coding
Writing Word documents directly with Apache POI means re‑implementing layout in Java code. Contracts often contain dozens of fields, multiple dynamic tables, and merged cells — POI’s API is ill‑suited for this. With Poi‑tl, business users design the .docx template in Word, inserting placeholders like {{contractNo}}. Developers only supply a data map; styles and layout stay in the template, so visual changes require no code modifications.
Compared to iText (PDF‑only) and EasyExcel (Excel‑focused), Poi‑tl is the most convenient for Word. If PDF preview is needed, a common approach is to convert the generated docx with LibreOffice headless.
Dependencies and Template Conventions
In Spring Boot 3.x, declare both Poi‑tl and an explicit POI version to avoid transitive conflicts:
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.12.1</version>
<exclusions>
<exclusion>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>Place templates under src/main/resources/templates/ and load them via getResourceAsStream. Poi‑tl only supports .docx (OOXML); legacy .doc files must be saved as .docx first.
Template placeholder conventions:
Plain text: {{contractNo}} Image: {{@logo}} Conditional block: {{?hasNote}}...{{/hasNote}} Loop block: {{#items}}...{{/items}} (table row loops use a special binding, see below)
Critical pitfall: Typing placeholders directly in Word can split them across multiple XML <w:r> runs due to IME, auto‑correct, or spell‑check. Always copy‑paste the complete placeholder as plain text. If already split, merge runs programmatically before compilation.
Basic Rendering Endpoint
A simple controller for a monthly contract report:
@RestController
@RequestMapping("/report")
public class ReportController {
private final MonthlyReportService reportService;
@GetMapping("/monthly")
public void monthly(@RequestParam("month") String month,
HttpServletResponse response) throws IOException {
Map<String, Object> data = new HashMap<>();
data.put("reportName", "月度合同统计报告");
data.put("month", month);
data.put("count", reportService.countContracts(month));
data.put("totalAmount", "¥ " + reportService.totalAmount(month));
data.put("createTime", LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
response.setHeader("Content-Disposition", "attachment; filename=report.docx");
try (InputStream is = getClass().getResourceAsStream("/templates/monthly.docx");
XWPFTemplate template = XWPFTemplate.compile(is).render(data)) {
template.write(response.getOutputStream());
}
}
}Map keys match template tags. Pre‑process null values to empty strings to avoid special‑case rendering logic.
Images, Hyperlinks, and Date Formatting
Image placeholders start with @. Use PictureRenderData for explicit width/height control:
data.put("companyLogo", new PictureRenderData(240, 80, PictureType.PNG, logoBytes));Raw byte[] works but yields unpredictable sizes; PictureRenderData is strongly recommended.
Hyperlinks require a render policy bound in Configure:
Configure config = Configure.builder()
.bind("detailLink", new HyperLinkRenderPolicy())
.build();
Map<String, Object> data = new HashMap<>();
data.put("detailLink", new HyperLinkTextRenderData("查看详情", "https://example.com"));The template uses {{detailLink}}. Note that dynamic links in contracts are rare; static links in the template are simpler.
Poi‑tl has no built‑in date formatting. Format dates in Java (e.g., add a dateStr property to loop items) rather than invoking methods in the template.
Dynamic Tables: Row Loop
Design the template with a header row and a single loop row:
+------+----------+--------+--------+
| 序号 | 商品名称 | 数量 | 备注 |
+------+----------+--------+--------+
| {{items}} | {{name}} | {{num}} | {{remark}} |
+------+----------+--------+--------+Bind LoopRowTableRenderPolicy to the loop variable:
Configure config = Configure.builder()
.bind("items", new LoopRowTableRenderPolicy())
.build();
List<Goods> goodsList = buildGoodsList();
Map<String, Object> data = new HashMap<>();
data.put("items", goodsList);
data.put("totalPrice", "¥ 2,369.00");
XWPFTemplate.compile(new ByteArrayInputStream(tplBytes), config)
.render(data)
.write(out);Each Goods object’s name, num, remark become the rendering context for that row; no prefix needed in the template. For a sequence number, add an index field in code and use {{index}} in the first cell. The loop row’s original formatting (font, borders) is copied to new rows.
Conditional blocks ( {{?special}}...{{/special}}) remove the entire segment when the flag is false.
Merging cells dynamically (e.g., based on data) is not supported by Poi‑tl directives. After rendering, obtain the XWPFDocument and manipulate XWPFTable cells’ vMerge properties. Be cautious: pre‑existing merged cells in the template may behave unexpectedly during row replication; prototype first.
Headers, Footers, and Watermarks
Poi‑tl renders only the document body. Header/footer placeholders are version‑dependent and risky; the author avoids dynamic content there. Instead, after rendering, use POI to replace header placeholders:
XWPFDocument document = template.getXWPFDocument();
for (XWPFHeader header : document.getHeaderList()) {
for (XWPFParagraph p : header.getParagraphs()) {
for (XWPFRun run : p.getRuns()) {
String text = run.getText(0);
if (text != null && text.contains("{{contractNo}}")) {
run.setText(text.replace("{{contractNo}}", contractNo), 0);
}
}
}
}For “Page X of Y” in footers, insert a Word PAGE field directly in the template footer — POI field creation is verbose and unnecessary.
Watermarks: generate a semi‑transparent PNG (A4 size, 45° tiled logo, low opacity) in memory with Java2D, then insert it into the header so it appears on every page. Key implementation details:
Set rotation center to canvas center before rotating ( g.rotate(...) alone rotates around top‑left, leaving blank corners).
Use AlphaComposite.SrcOver.derive(0.2f) for transparency; SrcAtop discards transparent pixels and loses the watermark.
Insert the PNG into the first paragraph of XWPFHeader. For official “behind text” watermarks, set behindDoc attributes, but header background is usually sufficient.
Server‑Side Export Considerations
Content‑Type: Must be
application/vnd.openxmlformats-officedocument.wordprocessingml.document.
Chinese filenames: Encode with URLEncoder and use RFC 5987 filename* parameter to avoid truncation/garbling.
Template compilation caching: Compilation is expensive. Do not store a compiled XWPFTemplate in a @Component and reuse it — the underlying XWPFDocument is not thread‑safe. Instead, at startup read the template file into a byte[] cache; each export creates a fresh ByteArrayInputStream and compiles anew. This avoids disk I/O and concurrency issues.
Large documents: Poi‑tl builds a full DOM in memory. For hundreds of pages (e.g., tenders), either increase heap, split into multiple Word files (merging styles is hard), or switch to a PDF pipeline. Practical limit: tens to a few hundred rows; beyond 500 rows reconsider the approach.
PDF preview: Call LibreOffice headless via ProcessBuilder:
soffice --headless --convert-to pdf --outdir /output /docs/report.docxSet a timeout. documents4j wraps the same external dependency. If PDF is not required, serve docx directly.
Memorable Production Pitfalls
Placeholder split across runs: XML shows {{name}} as {{ + na + me}}. Poi‑tl misses it. Fix: copy‑paste placeholders, or merge runs before compile.
Using .doc templates: Poi‑tl only reads .docx. Convert and verify layout after conversion.
Residual revision marks / content controls: Collaboratively edited templates may contain tracked changes or content controls that break parsing. Accept all revisions and remove extra controls before finalizing.
Missing Chinese fonts on Linux: PDF conversion yields tofu blocks. Install fonts-noto-cjk: apt-get install -y fonts-noto-cjk For special fonts, copy font files into the container. When setting fonts via POI, specify eastAsia for Chinese; ascii alone falls back to default.
Summary
Poi‑tl handles ~90% of Word template rendering needs. Adopt a “template‑first” workflow: let document owners design layout, decouple from data, and have developers bind tags. Built‑in APIs cover text, images, table row loops, and conditional blocks. For complex headers, dynamic cell merging, and watermarks, drop down to POI after rendering — keep business code away from XML manipulation. If your system frequently batch‑generates contracts or reports, consider a dedicated template rendering service exposing byte[] or download URLs to reduce duplication.
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.
