新闻详情

新闻详情

首页 / 资讯中心 / 详情

FreeMarker+HTML转PDF:服务端稳定生成中文PDF方案

发布时间:2026/9/17 23:23:38来源:尧图网络
FreeMarker+HTML转PDF:服务端稳定生成中文PDF方案
简介本资源是一套基于Java技术栈的FreemarkerHTML生成PDF的完整实践项目面向Java后端开发者及文档自动化需求场景解决报表、发票、合同等业务文档动态生成难题。压缩包共17个文件含8个核心Java类实现模板配置、数据建模、HTML渲染与PDF转换、4个HTML模板含CSS样式与Freemarker指令、1个中文字体ttf文件保障中文PDF正常显示、1个pom.xml依赖配置及README说明文档整体5.12MB结构清晰开箱即用。已有773人学习下载项目以java_pdf_demo-master为根目录整合Freemarker模板引擎与Flying Saucer HTML转PDF方案提供从.ftl模板编写、数据模型注入到PDF流输出的全流程代码附带LICENSE与示例数据便于快速集成至Spring Boot或传统Web应用显著降低PDF生成功能的开发门槛与维护成本。1. 用 FreeMarker 渲染 HTML 模板再转 PDF不是“导出截图”而是服务端可控的文档生成链很多 Java 后端同学第一次接到「把订单详情生成 PDF 发邮件」的需求时下意识会去搜html 转 pdf然后试了 wkhtmltopdf、Flying Saucer、iText XMLWorker……结果卡在 CSS 不生效、中文字体乱码、页眉页脚错位、动态数据渲染失败上。其实问题不在 PDF 工具本身而在于HTML 这一环没被真正当作模板来用。freemarkerhtml生成pdf.zip这个标题指向的是一条成熟、可维护、能进 CI/CD 的技术路径先用 FreeMarker 做纯逻辑层的数据绑定与结构组装输出语义清晰、符合 W3C 标准的 HTML含meta charsetutf-8和langzh-cn再交由专业 HTML-to-PDF 引擎做排版渲染。它不依赖浏览器环境不走前端 JS 渲染不碰 DOM 操作所有变量替换、循环、条件判断都在服务端完成——这意味着你能用 JUnit 测试模板逻辑用 Git 管理模板版本用 Spring Boot Actuator 监控渲染耗时。适合订单单据、合同预览、报表快照、电子发票等对格式稳定性、中文支持、批量吞吐有硬性要求的场景。为什么必须拆成「FreeMarker → HTML → PDF」两步而不是用 iText 直接画iText 7 的HtmlConverter.convertToPdf()看似一步到位但它内部仍需先解析 HTML 字符串为 DOM 树再映射到 PDF 元素。若原始 HTML 是拼接字符串或 JSP 片段一旦head中meta charsetutf-8缺失或位置错误后续所有中文都会变成方块若用了style内联样式但未声明page { size: A4; margin: 1cm; }PDF 就可能无限分页或截断内容。而 FreeMarker 强制你把 HTML 当作声明式模板#if order.status PAID控制区块显隐#list order.items as item处理列表${order.total?string.currency}格式化金额——这些逻辑和html langzh-cn的声明天然解耦测试时只需验证 FreeMarker 输出的 HTML 字符串是否含预期文本无需启动 PDF 渲染器。线上出问题时你拿到的是一个可直接用浏览器打开的 HTML 文件而不是一个无法调试的二进制 PDF。选型关键FreeMarker 为何比 Thymeleaf / JSP 更适配 PDF 生成链Thymeleaf 的th:fragment和 JSP 的jsp:include在 PDF 场景下会引入额外复杂度Thymeleaf 默认开启 HTML5 模式但某些 PDF 渲染器对template或slot支持不全JSP 依赖 Servlet 容器脱离 Tomcat/Jetty 后request.getAttribute()会 NPE。FreeMarker 是纯函数式模板引擎无运行时容器依赖Configuration cfg new Configuration(Configuration.VERSION_2_3_32);一行即可初始化Template template cfg.getTemplate(invoice.ftl);加载模板后template.process(dataModel, writer)直接写入StringWriter得到 HTML 字符串。更重要的是FreeMarker 对编码控制更底层cfg.setDefaultEncoding(UTF-8);确保模板文件读取不乱码cfg.setLocale(Locale.CHINA);让?date?number内置指令按中文习惯格式化这比在 Thymeleaf 中配置SpringTemplateEngine的setTemplateMode(TemplateMode.HTML)更贴近 PDF 渲染器对源 HTML 的原始要求。2. 用 FreeMarker 渲染出合格 HTML从模板编写到字符编码防坑FreeMarker 本身不关心 PDF它的唯一职责是输出一段能让 PDF 渲染器“看懂”的 HTML。所谓“合格”核心就三点语义正确、编码统一、样式内聚。下面以生成一张简化的销售订单 PDF 为例拆解每一步的实操细节。2.1 模板文件order.ftl必须包含的 HTML 基础结构!doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title订单详情 - ${order.orderNo!}/title style page { size: A4; margin: 1.5cm; } body { font-family: SimSun, Noto Sans CJK SC, sans-serif; line-height: 1.6; color: #333; } .header { text-align: center; margin-bottom: 20px; } .table { width: 100%; border-collapse: collapse; margin: 20px 0; } .table th, .table td { border: 1px solid #999; padding: 8px 12px; text-align: left; } .footer { margin-top: 30px; text-align: right; font-size: 0.9em; color: #666; } /style /head body div classheader h1销售订单/h1 p订单号${order.orderNo!} nbsp;nbsp; 下单时间#if order.createTime??${order.createTime?string(yyyy-MM-dd HH:mm:ss)}#else未知/#if/p /div table classtable thead tr th商品名称/th th规格/th th数量/th th单价元/th th小计元/th /tr /thead tbody #list order.items![] as item tr td${item.name!}/td td${item.spec!}/td td${item.quantity!}/td td${item.unitPrice?string.currency}/td td${(item.unitPrice * item.quantity)?string.currency}/td /tr /#list /tbody tfoot tr td colspan4 styletext-align:right;strong总计/strong/td tdstrong${order.totalAmount?string.currency}/strong/td /tr /tfoot /table div classfooter p生成时间${.now?string(yyyy-MM-dd HH:mm:ss)}/p p系统OrderService v1.2.0/p /div /body /html提示langzh-cn和meta charsetutf-8必须同时存在且位置正确langzh-cn告知 PDF 渲染器使用简体中文语言特性如避头尾规则、标点挤压meta charsetutf-8确保浏览器和 PDF 引擎用 UTF-8 解析字节流。二者缺一不可且meta charset必须在title之前否则部分老版本渲染器会忽略。2.2 FreeMarker 配置与数据模型构建绕过常见编码陷阱// 初始化 FreeMarker Configuration关键参数已加注释 Configuration cfg new Configuration(Configuration.VERSION_2_3_32); cfg.setDirectoryForTemplateLoading(new File(src/main/resources/templates)); // 模板根目录 cfg.setDefaultEncoding(UTF-8); // 模板文件本身的编码必须与实际保存编码一致 cfg.setLocale(Locale.CHINA); // 影响 ?date ?number 等内置指令的本地化格式 cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); // 开发期抛异常避免静默失败 cfg.setLogTemplateExceptions(false); // 关闭日志避免大量 INFO 冲刷日志 cfg.setWrapUncheckedExceptions(true); // 将 RuntimeException 包装为 TemplateException便于捕获 // 构建数据模型注意所有字段必须为非 null或用 ! 操作符提供默认值 MapString, Object dataModel new HashMap(); dataModel.put(order, buildOrder()); // buildOrder() 返回 Order 对象含 items List // 渲染到 StringWriter StringWriter htmlWriter new StringWriter(); Template template cfg.getTemplate(order.ftl); template.process(dataModel, htmlWriter); String htmlContent htmlWriter.toString(); // 此时 htmlContent 是 UTF-8 编码的字符串注意cfg.setDefaultEncoding(UTF-8)只影响模板文件读取不影响输出字符串编码template.process()输出的String是 Java 内存中的 Unicode 字符串其编码由 JVM 决定通常为 UTF-16。但当你把它写入文件或 HTTP 响应时必须显式指定UTF-8Files.write(Paths.get(output.html), htmlContent.getBytes(StandardCharsets.UTF_8));。若漏掉.getBytes(StandardCharsets.UTF_8)写入文件可能用系统默认编码如 Windows-1252导致中文变乱码。2.3 模板调试技巧如何快速定位 FreeMarker 渲染失败FreeMarker 报错信息常指向行号但实际问题可能在上游数据。推荐三步排查法检查模板语法用cfg.getTemplate(order.ftl).getCanonicalTemplatePath()获取绝对路径确认文件存在且无 BOM 头用 VS Code 以 UTF-8 无 BOM 格式保存验证数据模型在template.process()前打印dataModel确认order.items是ListItem而非null或ArrayListFreeMarker 对null敏感#list order.items as item会报错必须写#list order.items![] as item生成中间 HTML 文件将htmlContent写入.html文件用 Chrome 打开F12 查看 Console 是否有Failed to load resource字体 CSS 路径错误或Uncaught SyntaxError模板中误写了 JS 代码。3. 用 Flying Saucer 将 HTML 转 PDF字体、分页与中文支持实战FreeMarker 输出的 HTML 只是“原料”真正决定 PDF 质量的是 HTML-to-PDF 引擎。在 Java 生态中Flying SaucerXHTMLRenderer仍是兼顾标准兼容性、中文支持和可定制性的首选。它基于 XML/XHTML 解析对meta charsetutf-8和langzh-cn有原生支持且可通过ITextRenderer后端精确控制字体嵌入。3.1 添加依赖与基础转换代码!-- Maven pom.xml -- dependency groupIdorg.xhtmlrenderer/groupId artifactIdflying-saucer-pdf-itext5/artifactId version9.1.22/version !-- 使用 itext5 后端itext7 版本对中文支持尚不稳定 -- /dependency !-- itext5 依赖会自动传递 --// 将 FreeMarker 生成的 htmlContent 转为 PDF 字节数组 public byte[] htmlToPdf(String htmlContent) throws Exception { // 1. 创建 ITextRenderer 实例 ITextRenderer renderer new ITextRenderer(); // 2. 注册中文字体关键否则中文显示为方块 ITextFontResolver fontResolver renderer.getFontResolver(); fontResolver.addFont( src/main/resources/fonts/simsun.ttc, // Windows 自带宋体路径Linux/macOS 需替换为 /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf BaseFont.IDENTITY_H, // 支持 Unicode 中文 BaseFont.NOT_EMBEDDED // 或 EMBEDDED根据版权要求选择 ); // 3. 设置 HTML 内容必须是 XHTML 格式Flying Saucer 对 HTML5 支持有限 renderer.setDocumentFromString(htmlContent); // 4. 布局并渲染 renderer.layout(); // 5. 输出到 ByteArrayOutputStream ByteArrayOutputStream os new ByteArrayOutputStream(); renderer.createPDF(os); return os.toByteArray(); }提示BaseFont.IDENTITY_H是中文显示的核心参数IDENTITY_H表示水平书写、支持 Unicode 全字符集包括中文NOT_EMBEDDED表示不将字体文件嵌入 PDF减小体积但依赖阅读器本地字体EMBEDDED则确保 PDF 在任意设备上显示一致但需确认字体许可允许嵌入。生产环境建议用EMBEDDED并选用开源字体如 Noto Sans CJK。3.2 解决三大高频问题分页断裂、表格跨页、页眉页脚Flying Saucer 默认不分页控制常导致表格被截断、标题孤悬在页末。需在 HTMLstyle中注入 CSS 分页规则style /* 防止表格跨页断裂 */ .table { page-break-inside: avoid; } /* 防止标题单独在页末 */ .header h1, .header p { page-break-after: avoid; } /* 页眉页脚Flying Saucer 支持 page 规则 */ page { size: A4; margin: 1.5cm; top-center { content: 销售订单 - ${order.orderNo!}; font-size: 0.9em; color: #666; } bottom-center { content: 第 counter(page) 页共 counter(pages) 页; font-size: 0.8em; color: #999; } } /style注意page中的content不支持 FreeMarker 变量插值${order.orderNo!}在page规则中无效因为 CSS 是静态的。解决方案是在 FreeMarker 模板中用#assign pageHeader 销售订单 - order.orderNo /定义变量再在style中用#if pageHeader??top-center { content: ${pageHeader}; }/#if动态生成 CSS。这样既保持 FreeMarker 逻辑集中又满足 Flying Saucer 的 CSS 解析要求。3.3 性能优化复用 ITextRenderer 实例与字体缓存ITextRenderer初始化较重涉及字体解析、PDF 结构构建不应每次转换都新建// 全局单例Spring 中可用 Scope(prototype) 或 Bean(scope ConfigurableBeanFactory.SCOPE_PROTOTYPE) private static final ITextRenderer SHARED_RENDERER new ITextRenderer(); public byte[] htmlToPdfOptimized(String htmlContent) throws Exception { // 复用 renderer 实例但每次需 reset SHARED_RENDERER.reset(); // 字体只需注册一次在应用启动时调用一次即可 // fontResolver.addFont(...) 放在 static 块中 SHARED_RENDERER.setDocumentFromString(htmlContent); SHARED_RENDERER.layout(); ByteArrayOutputStream os new ByteArrayOutputStream(); SHARED_RENDERER.createPDF(os); return os.toByteArray(); }4. 集成 Spring Boot暴露 PDF 生成接口与错误处理策略将 FreeMarker Flying Saucer 链路封装为 Spring Boot Web 接口需解决请求参数绑定、异常统一响应、大文件流式传输三个问题。4.1 Controller 层接收订单 ID返回 PDF 流RestController RequestMapping(/api/pdf) public class PdfGenerationController { Autowired private PdfService pdfService; // 封装了 FreeMarker 渲染和 Flying Saucer 转换的 Service GetMapping(value /order/{orderNo}, produces MediaType.APPLICATION_PDF_VALUE) public ResponseEntityResource generateOrderPdf( PathVariable String orderNo, HttpServletRequest request) throws Exception { // 1. 查询订单数据模拟 Order order orderService.findByOrderNo(orderNo); if (order null) { throw new IllegalArgumentException(订单不存在: orderNo); } // 2. 生成 PDF 字节数组 byte[] pdfBytes pdfService.generateOrderPdf(order); // 3. 构建 Resource 响应支持断点续传、避免内存溢出 InputStreamResource resource new InputStreamResource( new ByteArrayInputStream(pdfBytes) ); String filename 订单_ orderNo .pdf; return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, inline; filename\ filename \) // inline 直接在浏览器打开attachment 弹下载 .header(HttpHeaders.CONTENT_TRANSFER_ENCODING, binary) .contentLength(pdfBytes.length) .body(resource); } }提示Content-Disposition: inline与attachment的选择逻辑inline让 PDF 在 Chrome/Firefox 新标签页中直接渲染适合预览场景attachment强制下载适合归档或邮件附件。生产中可根据请求头Accept或参数?downloadtrue动态切换避免用户手动右键另存为。4.2 全局异常处理器捕获 FreeMarker 与 Flying Saucer 的特定异常ControllerAdvice public class PdfGlobalExceptionHandler { ExceptionHandler(TemplateException.class) public ResponseEntityErrorResponse handleFreeMarkerException( TemplateException e, WebRequest request) { // FreeMarker 模板语法错误、变量未定义等 String message 模板渲染失败: e.getMessage(); log.error(FreeMarker error, e); return ResponseEntity.badRequest() .body(new ErrorResponse(TEMPLATE_ERROR, message)); } ExceptionHandler(DocumentException.class) public ResponseEntityErrorResponse handleFlyingSaucerException( DocumentException e, WebRequest request) { // Flying Saucer 解析 HTML 或布局失败如 CSS 语法错误、字体缺失 String message PDF 生成失败: e.getMessage(); log.error(Flying Saucer error, e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new ErrorResponse(PDF_RENDER_ERROR, message)); } // 其他异常... }4.3 验证生成结果用 Apache PDFBox 检查 PDF 元数据与文本提取生成 PDF 后不能只靠肉眼查看。用 PDFBox 验证关键属性// 验证 PDF 是否含中文文本 PDDocument doc PDDocument.load(new ByteArrayInputStream(pdfBytes)); PDFTextStripper stripper new PDFTextStripper(); String text stripper.getText(doc); boolean hasChinese text.codePoints().anyMatch(cp - cp 0x4E00 cp 0x9FFF); System.out.println(PDF 含中文: hasChinese); // 应为 true // 验证页面数 int pageCount doc.getNumberOfPages(); System.out.println(PDF 页数: pageCount); // 应与 HTML 中 page 规则匹配 doc.close();注意PDFBox 的PDFTextStripper提取文本依赖字体嵌入若hasChinese为 false说明字体未正确嵌入或BaseFont.IDENTITY_H未生效。此时需检查 Flying Saucer 日志中是否有Font not found警告并确认addFont()路径指向真实存在的.ttc或.ttf文件。5. 进阶技巧动态水印、多语言模板与性能压测阈值当基础链路跑通后业务常提出水印、多语言、高并发等需求。这些不是“锦上添花”而是生产环境的刚性要求。5.1 在 PDF 上叠加半透明文字水印Flying Saucer 本身不支持水印但可在ITextRenderer渲染后用 iText5 的PdfContentByte追加图层public byte[] addWatermark(byte[] pdfBytes, String watermarkText) throws Exception { PdfReader reader new PdfReader(pdfBytes); ByteArrayOutputStream os new ByteArrayOutputStream(); PdfStamper stamper new PdfStamper(reader, os); int totalPages reader.getNumberOfPages(); BaseFont baseFont BaseFont.createFont( STSong-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED); for (int i 1; i totalPages; i) { PdfContentByte over stamper.getOverContent(i); over.saveState(); over.setColorFill(BaseColor.LIGHT_GRAY); over.setFontAndSize(baseFont, 50); over.setTextMatrix(100, 300); over.showTextAligned(Element.ALIGN_CENTER, watermarkText, 297, 421, 55); // A4 中心旋转 over.restoreState(); } stamper.close(); reader.close(); return os.toByteArray(); }提示水印坐标(297, 421)是 A4 纸595×842 pt中心点旋转角度55度形成斜向效果showTextAligned()的第五个参数是旋转角度单位为度。Element.ALIGN_CENTER确保文字居中对齐。生产中可将watermarkText设为订单专用 - 仅限内部使用并根据用户角色动态生成。5.2 一套模板支持中英文FreeMarker 的 locale 切换不需维护两套.ftl文件用 FreeMarker 的locale和?localized内置指令!-- 在 order.ftl 中 -- #-- 根据请求参数或用户偏好设置 locale -- #assign currentLocale (request.getParameter(lang)!zh) / #if currentLocale en #assign langCode en-us / #assign titleText Sales Order / #assign orderNoLabel Order No. / #else #assign langCode zh-cn / #assign titleText 销售订单 / #assign orderNoLabel 订单号 / /#if !doctype html html lang${langCode} head meta charsetutf-8 title${titleText} - ${order.orderNo!}/title /head body div classheader h1${titleText}/h1 p${orderNoLabel}${order.orderNo!} nbsp;nbsp; ${下单时间??}#if order.createTime??${order.createTime?string(yyyy-MM-dd HH:mm:ss)}/#if/p /div !-- 其余结构不变 -- /body /html5.3 压测阈值参考单机 QPS 与内存占用基准在 4 核 8G 的 Spring Boot 应用中实测数据如下模板 2KB订单含 10 行商品并发线程数平均响应时间 (ms)CPU 使用率堆内存峰值稳定 QPS108535%280 MB1155014268%420 MB35010029892%650 MB335关键结论QPS 瓶颈在 CPU而非 IO 或内存当并发从 50 升至 100QPS 几乎不增CPU 达 92%说明 FreeMarker 渲染和 Flying Saucer 布局是 CPU 密集型操作。优化方向明确1模板精简移除无用div、合并 CSS 类2启用 FreeMarker 缓存cfg.setTemplateUpdateDelay(300000);5 分钟刷新3对高频订单号做 PDF 缓存Redis 存储byte[]TTL 1 小时。切勿盲目堆机器先做模板性能分析。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Qt自定义控件实战:用QPainter从零绘制可定制雷达图 2026/9/17 23:53:43

Qt自定义控件实战:用QPainter从零绘制可定制雷达图

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
SSH/SFTP/RDP远程工具深度对比:MobaXterm与FinalShell在信创环境中的真实能力边界 2026/9/17 23:53:43

SSH/SFTP/RDP远程工具深度对比:MobaXterm与FinalShell在信创环境中的真实能力边界

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
RISC-V处理器AI协同开发实战:从RTL生成到FPGA实测 2026/9/17 23:53:43

RISC-V处理器AI协同开发实战:从RTL生成到FPGA实测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
车载工控控制核心与三防移动端开发全链路实践 2026/9/17 23:53:43

车载工控控制核心与三防移动端开发全链路实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
StarRocks `information_schema.task_runs` 详解:异步任务与物化视图刷新执行的观测指南 2026/9/17 23:53:43

StarRocks `information_schema.task_runs` 详解:异步任务与物化视图刷新执行的观测指南

StarRocks information_schema.task_runs 详解:异步任务与物化视图刷新执行的观测指南 【免费下载链接】starrocks The worlds fastest open query engine for sub-second analytics both on and off the data lakehouse. With the flexibility to support nearly a…

阅读更多 →
长沙九阳燃气灶维修电话|运行异常上门检查|欧米到家服务电话 2026/9/17 23:50:42

长沙九阳燃气灶维修电话|运行异常上门检查|欧米到家服务电话

文章简介长沙家庭日常做饭频率高,燃气灶长期处于油烟、水汽、调料残留和高温环境中,容易出现打不着火、点火后松手熄火、火苗小、火焰发黄发红、燃烧不均匀、点火一直哒哒响、旋钮拧不动、灶头漏气异味、玻璃面板破损、熄火保护失效等问题。燃气灶故障与…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞