FreeMarker + Flying Saucer vs poi-tl + Collabora CODE:文档模板渲染的双引擎实践

开源地址:https://gitee.com/hhs_3/template-view

前言

做企业级系统的同学应该都有体会:文档模板渲染这个需求看似简单,实则暗坑满满。

比如一个房管局业务系统里,既有「不动产登记申请表」这样的简单表单------几行表格、填几个字段就能出 PDF;又有「商品房买卖合同」这种几十页的正式合同------页眉页脚、复杂表格、特殊排版,必须基于 Word 模板生成,一点格式都不能错。

如果你只选一套方案,大概率会踩坑:

  • 用 FreeMarker + HTML?简单表单很好使,但遇到复杂合同就抓瞎------HTML 排版能力有限,页眉页脚、分栏、水印根本搞不定
  • 用 Word + poi-tl?合同排版没问题了,但简单表单也得上 Word 模板 + Collabora CODE 外部服务,杀鸡用牛刀,部署和维护成本白白翻倍

所以我设计了双模板引擎 方案:简单模板用 HTML + FreeMarker + Flying Saucer,纯 Java 零依赖;复杂模板用 Word + poi-tl + Collabora CODE,高保真排版。两套引擎各司其职,业务侧按需选择,既不牺牲排版质量,也不浪费部署成本。

这个方案已经开源为 template-view ------ 基于 Spring Boot 3 + Vue 3 的模板管理与渲染平台。本文将详细介绍整体架构、两套引擎的技术选型思路和核心实现。


一、项目概览

功能特性

  • 双模板引擎 ------ FreeMarker(简单模板)+ poi-tl(复杂模板)
  • 多格式输出 ------ 支持 PDF 和 Word 文档输出
  • 版本管理 ------ 完整的版本发布、回滚、切换,乐观锁并发控制 + 操作审计日志
  • Collabora CODE 集成 ------ 高性能 Word → PDF 转换
  • Word 在线编辑 ------ 配合 online-view 项目实现基于 WOPI 协议的在线编辑
  • 中文字体支持 ------ 内置宋体、黑体、仿宋、楷体
  • 变量管理 ------ 可视化定义模板变量,自动生成默认数据模型

预览效果

模板列表页面:


简单模板渲染(HTML → PDF):


复杂模板渲染(Word → PDF):

二、整体架构

复制代码
┌─────────────────────────────────────────────────────┐
│                    Vue 3 前端                        │
│         Element Plus + TypeScript + Axios            │
└──────────────────────┬──────────────────────────────┘
                       │ HTTP / API
┌──────────────────────▼──────────────────────────────┐
│               Spring Boot 3 后端                     │
│  ┌───────────┐  ┌────────────┐  ┌────────────────┐ │
│  │ 模板管理   │  │ 版本管理    │  │  变量/分类管理  │ │
│  └───────────┘  └────────────┘  └────────────────┘ │
│  ┌──────────────────────────────────────────────┐   │
│  │              模板渲染引擎                      │   │
│  │  ┌─────────────────┐ ┌──────────────────────┐│   │
│  │  │ 简单模板引擎     │ │ 复杂模板引擎          ││   │
│  │  │ FreeMarker      │ │ poi-tl               ││   │
│  │  │ Flying Saucer   │ │ Collabora CODE       ││   │
│  │  │ OpenPDF         │ │                      ││   │
│  │  └─────────────────┘ └──────────────────────┘│   │
│  └──────────────────────────────────────────────┘   │
└──────────────────────┬──────────────────────────────┘
                       │
          ┌────────────┼────────────┐
          ▼            ▼            ▼
       MySQL 8      本地存储    Collabora CODE

技术栈总览

层级 技术 版本 说明
后端框架 Spring Boot 3.4.0 Java 17
ORM MyBatis-Plus 3.5.9 内置分页,代码量少
连接池 Druid 1.2.23 阿里高性能连接池
数据库 MySQL 8.x utf8mb4 字符集
简单模板引擎 FreeMarker 2.3.32 HTML 模板变量替换
HTML → PDF Flying Saucer + OpenPDF 9.1.22 纯 Java PDF 渲染
复杂模板引擎 poi-tl 1.12.2 Word 模板变量替换
Word → PDF Collabora CODE - 高保真文档转换
API 文档 Knife4j 4.5.0 Swagger 增强 UI
前端框架 Vue 3.4 Composition API
UI 组件库 Element Plus 2.6 企业级 UI
构建工具 Vite 5.2 极速构建
类型系统 TypeScript 5.4 严格模式

三、双模板引擎 ------ 技术选型的深度思考

这是本项目最核心的设计决策。我将从实际需求出发,详细解释为什么简单模板和复杂模板选择了截然不同的技术栈。

3.1 简单模板:为什么选择 FreeMarker + Flying Saucer + OpenPDF?

简单模板的场景特征:

  • 模板内容是表单、申请表、简单合同、报告等结构化文档
  • 排版要求不苛刻,表格、文字、基础样式即可满足
  • 需要高频并发渲染(例如批量生成证明函)
  • 希望系统轻量部署,不依赖外部服务
技术选型分析

为什么模板引擎选 FreeMarker?

在 Java 生态的模板引擎中,主流选择有 FreeMarker、Thymeleaf、Velocity。选择 FreeMarker 的理由:

  1. 语法直观 ------ ${variable}<#if><#list> 对 HTML 模板非常友好,学习成本低
  2. 容错性好 ------ 内置 ${variable!默认值} 语法,变量缺失时不会报错而是输出默认值或空串,这在模板渲染场景中非常关键
  3. 轻量高效 ------ 编译型模板引擎,渲染性能优秀
  4. 与 Spring Boot 集成成熟 ------ spring-boot-starter-freemarker 开箱即用

相比之下,Thymeleaf 的自然模板语法(th:text)虽然更适合 Web 页面渲染,但在纯文本替换场景反而过于繁琐;Velocity 已停止维护。

为什么 HTML → PDF 选 Flying Saucer + OpenPDF?

HTML 转 PDF 的 Java 方案非常多:iText、Flying Saucer、OpenPDF、wkhtmltopdf、Puppeteer 等。选择 Flying Saucer + OpenPDF 的理由:

  1. 纯 Java 实现 ------ 不需要安装任何外部服务(wkhtmltopdf、Puppeteer 都需要),部署成本为零
  2. Flying Saucer 的 XHTML 渲染能力 ------ 能将标准 XHTML + CSS 渲染为 PDF,对表格、边框、字号等基础排版支持良好
  3. OpenPDF(iText 的 LGPL 分支) ------ Flying Saucer 底层依赖 OpenPDF 进行 PDF 绘制,完全开源且无 AGPL 限制(iText 5+ 是 AGPL 协议,商业使用受限)
  4. 中文字体支持 ------ 可以通过 ITextRenderer.getFontResolver().addFont() 注册系统字体或项目内置字体,完美支持中文渲染
  5. 并发友好 ------ 纯内存操作,无进程开销,天然支持高并发

为什么不用 iText 直接写 PDF?

iText 的编程式 PDF 生成(new Document()add Paragraph)需要大量代码来描述布局,维护成本极高。模板引擎 + 渲染的方式让非技术人员也能通过修改 HTML 来调整模板样式,显著降低了维护门槛。

渲染流程:

复制代码
HTML 模板文件
    │
    ▼ FreeMarker 引擎(变量替换)
XHTML(数据已填充)
    │
    ▼ Flying Saucer + OpenPDF
PDF 文件输出

核心代码片段:

java 复制代码
// 1. FreeMarker 变量替换
Template template = freemarkerConfig.getTemplate(templateName);
String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, dataModel);

// 2. 包装为完整的 XHTML 文档(补充 DOCTYPE、CSS 样式等)
String xhtml = wrapAsXhtml(html);

// 3. Flying Saucer 渲染为 PDF
ITextRenderer renderer = new ITextRenderer();
// 注册中文字体
renderer.getFontResolver().addFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
renderer.setDocumentFromString(xhtml);
renderer.layout();
renderer.createPDF(outputStream);

3.2 复杂模板:为什么选择 poi-tl + Collabora CODE?

复杂模板的场景特征:

  • 模板内容是正式合同、法律协议、招投标文件
  • 排版要求极其严格------页眉页脚、分页、复杂表格、特殊字体、水印
  • 业务人员习惯用 Word 编辑模板,不允许改变原有格式
  • 输出格式可能需要 PDF 和 Word 两种
技术选型分析

为什么模板引擎选 poi-tl?

Word 模板引擎的选择面比较窄,主要有 poi-tl 和 Apache POI 原生 API:

  1. 声明式语法 ------ 只需在 Word 中写 {``{variable}},poi-tl 自动完成替换,无需写代码
  2. 功能丰富 ------ 支持文本、图片、表格行循环(LoopRowTableRenderPolicy)、列表、嵌套等
  3. 基于 Apache POI ------ 底层使用 poi-ooxml,兼容 .docx 格式,稳定性有保障
  4. 轻量 ------ 核心 API 就一个 XWPFTemplate.compile().render(),上手极快

如果直接用 Apache POI 原生 API 操作 Word 文档,代码量会爆炸------替换一个表格需要遍历段落、运行、文本,处理合并单元格更是噩梦。poi-tl 将这一切封装成了优雅的声明式 API。

为什么 Word → PDF 选 Collabora CODE?

这是最关键的选择。Word 转 PDF 的方案有很多:

方案 保真度 性能 部署复杂度 并发能力
Apache POI + iText 低(丢失大量格式)
LibreOffice 命令行 低(每次启动进程)
Collabora CODE 高(基于 LibreOffice Technology) 高(长驻服务) 中(Docker 部署)
Aspose.Words 高(但商业授权昂贵)

选择 Collabora CODE 的理由:

  1. 高保真转换 ------ Collabora CODE 基于 LibreOffice Technology 引擎,对 Word 文档的兼容性是开源方案中最好的,页眉页脚、分栏、复杂表格、中文字体等都能完美保留
  2. 服务化架构,进程复用 ------ 虽然底层同样是 LibreOffice,但调用方式有本质区别:LibreOffice 命令行每次调用都要 fork 一个新的 soffice 进程,初始化运行环境耗时 2~5 秒,并发场景下极易资源耗尽;而 Collabora CODE 通过 LibreOfficeKit(LOKit) 将 LibreOffice 核心能力编译为动态链接库(.so/.dll),在自己的长驻进程内直接以函数调用的方式使用,无需每次启动新进程,相当于"Tomcat 处理请求"与"每次启动新 JVM"的区别
  3. 天然支持并发 ------ 内部维护 LibreOfficeKit 实例池,多线程安全地复用转换实例
  4. 一石二鸟 ------ 除了 PDF 转换,Collabora CODE 还支持 WOPI 协议的在线编辑功能,配合 online-view 项目可以直接在浏览器中编辑 Word 模板
  5. 开源免费 ------ 相比 Aspose.Words 动辄上万美元的商业授权,Collabora CODE 完全开源

LibreOffice CLI vs Collabora CODE 对比:

维度 LibreOffice 命令行 (soffice --convert-to) Collabora CODE
调用方式 每次请求 fork 新进程 长驻服务,REST API
LibreOffice 加载方式 进程级(每次重新加载) 库级(LibreOfficeKit,进程内复用)
单次转换启动耗时 2~5 秒 毫秒级(实例已预热)
并发处理 多进程并行,资源消耗大 实例池管理,资源可控
在线编辑 不支持 支持(WOPI 协议)

早期版本使用 LibreOffice 命令行做转换,不仅性能差,而且在并发场景下极不稳定。切换到 Collabora CODE 后,转换性能提升了一个数量级,同时还获得了在线编辑能力。

渲染流程:

复制代码
Word 模板文件(.docx)
    │
    ▼ poi-tl 引擎(变量替换)
渲染后的 Word 文档
    │
    ├── 输出格式为 Word ──→ 直接返回 .docx
    │
    └── 输出格式为 PDF
        │
        ▼ Collabora CODE REST API(/cool/convert-to)
    PDF 文件输出

核心代码片段:

java 复制代码
// 1. poi-tl 变量替换
Configure config = Configure.builder()
    .bind("table", new LoopRowTableRenderPolicy())  // 表格行循环策略
    .build();

try (InputStream is = new ByteArrayInputStream(templateBytes);
     ByteArrayOutputStream os = new ByteArrayOutputStream()) {

    XWPFTemplate template = XWPFTemplate.compile(is, config).render(dataModel);
    template.write(os);
    template.close();
    byte[] renderedWord = os.toByteArray();

    // 2. 如果需要 PDF,调用 Collabora CODE
    if (outputFormat == OutputFormat.PDF) {
        return collaboraPdfConverter.convertWordToPdf(renderedWord);
    }
    return renderedWord;
}

3.3 两种方案的对比总结

维度 简单模板 复杂模板
模板格式 HTML (.html/.ftl) Word (.docx)
变量语法 ${variable} {``{variable}}
模板引擎 FreeMarker poi-tl
PDF 生成 Flying Saucer + OpenPDF Collabora CODE
外部依赖 Collabora CODE Docker 容器
部署复杂度 极低 中等
排版能力 表格、文字、基础 CSS Word 全部功能
并发性能 优秀(纯内存) 优秀(服务池化)
适用场景 表单、申请表、报告 正式合同、法律文件
维护门槛 低(改 HTML) 低(改 Word)

四、版本管理设计

模板的版本管理是本平台的核心功能之一,参考了语义化版本规范(Semantic Versioning):

版本状态机

复制代码
         创建
          │
          ▼
      ┌────────┐  发布   ┌───────────┐
      │  DRAFT  │───────→│ PUBLISHED  │◄──── 切换(SetActive)
      │  草稿   │        │  已发布     │
      └────────┘        └─────┬─────┘
          │                   │
          │ 删除              │ 归档 / 新版本发布时旧版本自动归档
          ▼                   ▼
        删除            ┌──────────┐
                        │ ARCHIVED │
                        │  已归档   │──── 回滚 → 创建新 PUBLISHED
                        └──────────┘

关键设计点

  1. 乐观锁 ------ lock_version 字段防止并发修改冲突
  2. 非破坏性回滚 ------ 回滚不是覆盖,而是从归档版本创建一个新的已发布版本,保留完整历史
  3. 审计日志 ------ 每个版本操作(CREATE、PUBLISH、ARCHIVE、ROLLBACK、DELETE、SWITCH)都记录操作人、时间、状态变更
  4. 草稿上限 ------ 可配置每个模板的最大草稿数(默认 5),避免版本堆积

五、核心代码解析

5.1 统一渲染入口

TemplateRenderService 作为统一入口,根据模板类型分发到不同的渲染引擎:

java 复制代码
public byte[] render(byte[] templateBytes,
                     TemplateType templateType,
                     OutputFormat outputFormat,
                     Map<String, Object> dataModel) throws IOException {

    if (templateType == TemplateType.COMPLEX) {
        // 复杂模板:poi-tl + Collabora CODE
        return complexTemplateService.render(templateBytes, outputFormat, dataModel);
    } else if (templateType == TemplateType.SIMPLE) {
        // 简单模板:FreeMarker + Flying Saucer
        String templateContent = new String(templateBytes, StandardCharsets.UTF_8);
        String html = htmlTemplateProcessor.process(templateContent, dataModel);
        String xhtml = htmlTemplateProcessor.wrapAsXhtml(html);
        return pdfRenderer.renderToPdf(xhtml);
    }

    throw new IllegalArgumentException("不支持的模板类型: " + templateType);
}

5.2 中文 PDF 渲染

PdfRenderer 在启动时加载项目内置的中文字体,渲染时注册到 Flying Saucer:

java 复制代码
// 启动时加载字体
@PostConstruct
public void loadFonts() {
    // 从 resources/fonts/ 加载宋体、黑体、仿宋、楷体
    // 也支持从系统字体目录加载
}

// 渲染时注册字体
ITextRenderer renderer = new ITextRenderer();
for (FontInfo font : loadedFonts) {
    renderer.getFontResolver().addFont(
        font.getPath(), BaseFont.IDENTITY_H, BaseFont.EMBEDDED
    );
}

5.3 Collabora CODE 集成

CollaboraPdfConverter 通过 REST API 与 Collabora CODE 通信:

java 复制代码
// 构建 multipart 请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);

MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("data", new ByteArrayResource(wordBytes) {
    @Override
    public String getFilename() {
        return "document.docx";
    }
});
body.add("format", "pdf");

HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);

// POST /cool/convert-to
ResponseEntity<byte[]> response = restTemplate.exchange(
    serverUrl + "/cool/convert-to", HttpMethod.POST, requestEntity, byte[].class
);

六、前端架构

前端基于 Vue 3 + TypeScript + Element Plus 构建,采用 Composition API 风格:

复制代码
frontend/src/
├── api/               # API 接口层
│   ├── template.ts    # 模板、版本、渲染相关 API
│   ├── category.ts    # 分类 API
│   └── variable.ts    # 变量 API
├── views/
│   ├── template/      # 模板管理(列表、变量定义)
│   ├── category/      # 分类管理
│   └── render/        # 模板渲染(数据填充、预览、下载)
├── layouts/           # 布局组件(侧边栏 + 顶栏)
├── router/            # 路由配置
├── utils/
│   └── request.ts     # Axios 封装(Blob 错误解析、统一错误处理)
└── types/index.ts     # TypeScript 类型定义

关键技术选择:

  • Element Plus ------ 企业级 UI 组件库,表格、表单、对话框等开箱即用
  • Vite ------ 开发时热更新极速,构建速度快
  • TypeScript 严格模式 ------ 前后端类型统一,减少运行时错误
  • Axios Blob 处理 ------ 模板渲染接口返回二进制流,前端需特殊处理错误(尝试将 Blob 解析为 JSON 错误信息)

七、数据库设计

共 7 张表,核心表关系如下:

复制代码
template_category (分类)
    │
    │ 1:N
    ▼
template_info (模板信息)
    │
    ├── 1:N ──→ template_version (模板版本,含 lock_version 乐观锁)
    │                │
    │                └── 1:N ──→ template_version_audit (版本审计日志)
    │
    ├── 1:N ──→ template_variable (模板变量定义,支持层级嵌套)
    │
    └── N:N ──→ template_font (字体管理)

template_render_log (渲染日志,独立记录)

SQL 初始化脚本位于 sql/schema.sql,包含完整的建表语句和种子数据。


八、快速部署

1. 准备环境

  • JDK 17+
  • Maven 3.6.3+
  • MySQL 8.0+
  • Node.js 16+
  • Docker(用于 Collabora CODE)

2. 初始化数据库

sql 复制代码
CREATE DATABASE template_platform CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
bash 复制代码
mysql -u root -p template_platform < sql/schema.sql

3. 启动 Collabora CODE

部署文档:https://gitee.com/hhs_3/online-view#/hhs_3/online-view/blob/master/./doc/Collabora CODE部署文档.md

bash 复制代码
docker-compose up -d

4. 配置环境变量

bash 复制代码
DB_HOST=localhost
DB_PORT=3306
DB_NAME=template_platform
DB_USERNAME=root
DB_PASSWORD=your_password
COLLABORA_SERVER=http://localhost:9980

5. 启动后端

bash 复制代码
mvn spring-boot:run

6. 启动前端

bash 复制代码
cd frontend
npm install
npm run dev

7. 访问应用


九、API 使用示例

渲染模板

http 复制代码
POST /api/template/render
Content-Type: application/json

{
  "templateCode": "contract_001",
  "outputFormat": "PDF",
  "dataModel": {
    "title": "房屋买卖合同",
    "partyA": "张三",
    "partyB": "李四",
    "amount": "1000000",
    "address": "北京市朝阳区XX路XX号"
  }
}

返回值为 PDF 或 Word 二进制流,前端可直接用于预览或下载。


十、写在最后

设计理念

这个项目的核心设计理念是务实

  • 不追求万能方案 ------ 没有用一套引擎覆盖所有场景,而是根据场景复杂度选择最合适的技术栈
  • 渐进式复杂度 ------ 简单模板零外部依赖即可运行,复杂模板按需引入 Collabora CODE
  • 业务人员友好 ------ 模板编辑用 HTML 或 Word,不需要开发人员介入日常模板维护
  • 生产级考量 ------ 版本管理、乐观锁、审计日志、中文字体,这些都是从实际生产环境中沉淀的需求

开源地址

欢迎 Star、Fork、PR!


相关推荐
A Everyman6 天前
Java 高效生成 Word 文档:poi-tl 的使用
java·pdf·word·poi-tl
_修铁路的3 个月前
【Poi-tl】 Word模板填充导出
java·word·poi-tl
晴天sir3 个月前
关于使用poi-tl读取本地图片,转为base64编码批量插入word的解决方法
java·exception·poi-tl
嗯、.4 个月前
使用Itext9生成PDF水印,兼容不同生成引擎的坐标系(如: Skia、OpenPDF)
java·pdf·itextpdf·openpdf·坐标变换矩阵
随便叫个啥呢5 个月前
java使用poi-tl模版+vform自定义表单生成word
java·word·poi-tl
mzlogin7 个月前
Java|FreeMarker 复用 layout
java·后端·freemarker
nbsaas-boot8 个月前
用 FreeMarker 动态构造 SQL 实现数据透视分析
数据库·windows·sql·freemarker·数据报表
咿呀咿呀咿10 个月前
Itex+freemarker 导出PDF文件时✓无法正常显示
freemarker·itext