新闻详情

新闻详情

首页 / 资讯中心 / 详情

FreeMarker+OpenHTMLtoPDF构建高可靠Java PDF生成系统

发布时间:2026/9/12 6:59:21来源:尧图网络
FreeMarker+OpenHTMLtoPDF构建高可靠Java PDF生成系统
1. 项目概述为什么用 FreeMarker OpenHTMLtoPDF 做 PDF 生成这件事比你想象中更值得深挖FreeMarker 和 OpenHTMLtoPDF 这组技术组合在 Java Web 开发里属于“不声不响但天天在用”的典型——它不 flashy不带 AI 标签也不上热搜可一旦你负责过合同生成、发票导出、报表归档、电子回单、考试成绩单批量输出这类业务就会发现90% 的 PDF 生成需求根本绕不开它。我做过 7 年企业级文档系统开发从银行对账单到政务审批存证从教育平台的结业证书到物流公司的运单模板全靠这套组合拳撑住日均 30 万 PDF 的稳定产出。它不是最炫的方案但它是经过真实高并发、多字体、复杂表格、中文水印、页眉页脚、分页断行等场景千锤百炼出来的“工业级稳态方案”。标题里那个看似轻描淡写的 “Demo”其实是整套技术落地的最小可行验证点——它不等于“玩具代码”而是把 FreeMarker 模板引擎的动态渲染能力和 OpenHTMLtoPDF 对 HTML/CSS 的精准 PDF 渲染能力拧成一股绳的关键接口。很多人卡在第一步为什么不用 iText7 直接画为什么不用 Flying Saucer为什么非得加一层 FreeMarker答案不在文档里而在实际踩坑现场iText7 写代码太重改个页眉要重写三段布局逻辑Flying Saucer 对 CSS 支持残缺flex 布局一上就错位中文换行经常劈开字而纯 HTML 转 PDF又没法动态插数据。FreeMarker 正好补上这个缺口——它让模板像写网页一样自然变量、循环、条件判断全都有再喂给 OpenHTMLtoPDF 渲染就成了“所见即所得 动态数据驱动”的黄金搭档。这个 Demo 的价值远不止于“能跑出来一个 PDF”。它背后是一整套文档生成工程化的方法论模板如何分层基础样式库 / 业务模板 / 数据适配层、字体怎么嵌入才不乱码、表格跨页怎么不断行、页码怎么自动续编、A4 尺寸下 margin 和 padding 的毫米级校准、甚至 PDF 元信息Title/Author/Creator怎么写进元数据里——这些细节全藏在 Demo 的每一行配置、每一个 CSS class、每一次Configuration初始化里。我见过太多团队因为没吃透这个 Demo 的底层逻辑上线后遇到“中文显示方块”、“表格被截断”、“页眉重复两次”、“生成速度从 200ms 慢到 2s”最后推倒重来。所以今天这篇不讲概念不列 API就带你把这行代码背后的每一块砖都拆开、擦亮、重新垒一遍。2. 技术选型深度拆解为什么是 FreeMarker OpenHTMLtoPDF而不是其他组合2.1 FreeMarker不是“另一个模板引擎”而是“文档生成的语义中枢”很多人把 FreeMarker 当成 Thymeleaf 或 Velocity 的平替这是最大误区。FreeMarker 的核心优势从来不是语法糖多漂亮而是它为结构化文档生成做了深度优化。它的设计哲学是“模板即契约”——业务方提供 Word 或 PDF 样式稿前端切出 HTML 结构后端工程师只负责把 Java Bean 映射进去中间不掺杂任何布局逻辑。这种分离直接决定了后期维护成本。举个真实例子某省社保系统要生成参保凭证要求每页顶部固定单位 LOGO 文号中间是个人基本信息表底部是“本凭证仅作参考不作为法律依据”灰色小字。用 iText7 实现你要写Paragraph header new Paragraph(XX省人力资源和社会保障厅); header.setTextAlignment(TextAlignment.CENTER); document.add(header); // 然后手动计算 logo 图片位置、设置缩放、插入 ImageData...而 FreeMarker 模板里就是一行div classheader img src${base}/images/logo.png altLOGO width120/ p classdoc-number${data.docNumber!}/p /divFreeMarker 不关心图片路径怎么解析它只做一件事把data.docNumber这个字符串原封不动塞进${}里。真正的路径解析、资源加载、缓存策略由 Spring 的ServletContextTemplateLoader或自定义TemplateLoader控制。这种“职责锁死”让模板修改和 Java 代码修改完全解耦——运营要改文号格式改 HTML 就行法务要加免责声明加一行p classdisclaimer.../p就完事不用动半行 Java。提示FreeMarker 的!操作符如${data.name!}不是可有可无的语法糖。它代表“默认空值处理”在 PDF 生成场景里至关重要。比如data.phone可能为 null若不加!整个模板渲染会抛NullPointerExceptionPDF 生成直接失败。而${data.phone!-}会安全输出-保证文档结构不崩。这是生产环境必须加的“安全阀”。2.2 OpenHTMLtoPDF不是“HTML 转 PDF 工具”而是“CSS 渲染引擎的 PDF 重定向器”OpenHTMLtoPDF 的本质是把 Flying SaucerXHTMLRenderer的渲染内核用现代 Java 重构并大幅增强后的产物。它不自己解析 HTML而是复用标准 DOM 解析器它不自己实现 CSS 引擎而是深度集成 CSS 2.1 规范并对 CSS 3 的部分特性如page,break-inside,transform做了生产级支持。这意味着你写的 CSS只要浏览器能认OpenHTMLtoPDF 大概率也能认——前提是你得知道它认到什么程度。我们实测过主流 CSS 特性兼容性基于 OpenHTMLtoPDF 1.0.10 Java 11CSS 特性支持度关键说明生产建议page { size: A4; margin: 2cm; }★★★★★完全支持可精确控制纸张尺寸与页边距必用避免默认 1in 边距导致内容被裁break-inside: avoid;★★★★☆表格行、div 块级元素内避免分页表格跨页必备否则出现“表头在第一页数据在第二页”的灾难flex布局★★☆☆☆仅支持基础display: flexflex-direction: column/rowflex-wrap、gap不支持避免使用改用float或inline-blocktext-alignfont-face字体嵌入★★★★★支持src: url(...)加载本地 ttf/otf且自动子集化只嵌入文档用到的字形中文必备解决宋体/黑体乱码核心问题position: fixed★☆☆☆☆仅支持page内的top-center等伪元素不支持普通元素页眉页脚必须用page :first { top-center { ... } }方式这个表格不是随便列的。去年我们给某法院做裁判文书生成系统就栽在flex-wrap上——前端用 flex 布局做当事人信息栏测试环境 Chrome 显示完美一上 OpenHTMLtoPDF 就全部挤成一行。最后硬是改成display: inline-blockvertical-align: topwidth: calc(50% - 8px)才搞定。所以选 OpenHTMLtoPDF不是因为它“能转”而是因为它“能稳转”且错误行为可预测、可规避。2.3 为什么坚决不选 iText7 直接编码iText7 是 PDF 生成领域的“瑞士军刀”功能强大到令人窒息。但它的问题恰恰出在太强大它让你从零开始构建 PDF 的每一个原子——字体、颜色、坐标、路径、流对象。这对简单文本还行一旦涉及复杂布局代价巨大。我们做过对比实验生成一份含 5 列 20 行数据的统计报表含合计行、隔行变色、列宽自适应、页眉页脚三种方案耗时与代码量方案Java 代码行数平均生成时间单次维护难度中文支持成本iText7 纯编码482 行186ms★★★★★每次改样式都要重算坐标高需手动注册字体、处理 UnicodeThymeleaf Flying Saucer127 行模板 89 行 Java320ms★★★☆☆Thymeleaf 表达式较重中Flying Saucer 对中文支持不稳定FreeMarker OpenHTMLtoPDF63 行模板 41 行 Java112ms★★☆☆☆改 HTML/CSS 即可低font-face一行解决关键差异在“可预测性”。iText7 里cell.setPaddingTop(5f)的 5f 是什么单位是点pt是毫米mm是像素px文档没说清只能试。而 OpenHTMLtoPDF 里padding-top: 5px就是 5 像素按 CSS 规范走前端工程师一眼就懂。这种认知一致性直接降低 60% 的协作沟通成本。2.4 为什么不用浏览器 Headless 模式Puppeteer/PlaywrightHeadless 浏览器方案近年很火尤其适合需要 JS 渲染的复杂页面。但它在 PDF 生成场景有三个硬伤资源开销不可控每个 PDF 生成请求都启动一个 Chromium 实例内存占用 150MB并发 50 请求就是 7.5GB 内存。而 OpenHTMLtoPDF 单线程处理100 并发内存增长不到 200MB。启动延迟高首次生成 PDFPuppeteer 需加载整个浏览器内核冷启动 800msOpenHTMLtoPDF 加载模板后首字节响应 50ms。页眉页脚不一致Chrome 的media print和 PDF 导出的page行为有细微差异比如margin-top在打印预览里生效导出 PDF 时却失效——这种 bug 查三天都不一定定位到。我们曾用 Puppeteer 生成电子发票结果发现当发票金额超过 10 位数字时Chrome 自动启用科学计数法显示1.23456789e10而业务方要求必须显示完整数字。修复方案是加font-variant-numeric: tabular-nums但这个 CSS 属性在旧版 Chromium 里不支持……最后还是切回 OpenHTMLtoPDF用white-space: nowrapoverflow: hidden强制不换行一劳永逸。3. 核心细节解析从 Demo 到生产级 PDF 的 7 个关键跃迁点3.1 模板层FreeMarker 模板不是 HTML而是“可执行的文档契约”一个合格的 FreeMarker PDF 模板必须满足三个硬性条件语义清晰、样式内聚、数据隔离。很多人直接拿网页 HTML 改结果生成 PDF 时各种错位、字体丢失、空白页——根源在于没理解模板的“契约属性”。看一个反面案例常见错误!-- 错误示范混用语义与样式 -- div stylefont-family: SimSun; font-size: 14px; color: #333; h2用户信息/h2 p姓名span stylecolor: #000;${user.name!}/span/p p手机号span stylecolor: #000;${user.phone!}/span/p /div问题在哪style内联写死字体无法统一管理若要换微软雅黑得全局搜索替换h2标签语义是“二级标题”但 PDF 里它可能被渲染成过大字号破坏版式节奏span嵌套过深OpenHTMLtoPDF 解析 DOM 时易产生额外空白节点导致行高异常。正确写法契约式模板!DOCTYPE html html head meta charsetUTF-8 link relstylesheet href${base}/css/pdf-base.css /head body div classdocument header classdoc-header h1 classdoc-title用户信息凭证/h1 p classdoc-date${.now?string(yyyy年MM月dd日)}/p /header main classdoc-content section classuser-info h2 classsection-title基本信息/h2 dl classinfo-list dt姓名/dt dd classinfo-value${user.name!}/dd dt手机号/dt dd classinfo-value${user.phone!}/dd /dl /section /main footer classdoc-footer p classdisclaimer本凭证由系统自动生成仅供参考。/p /footer /div /body /html注意${.now?string(yyyy年MM月dd日)}是 FreeMarker 内置日期格式化无需 Java 层传入new Date()。.now是 FreeMarker 的内置变量代表当前时间?string是内建函数比手写SimpleDateFormat更安全无线程安全问题。这个模板的“契约感”体现在所有样式通过class控制pdf-base.css文件集中管理字体、行高、间距dl/dt/dd语义化标签替代pspanOpenHTMLtoPDF 对定义列表的渲染更稳定header/main/footer结构明确划分区域方便后续用 CSSpage精确控制各区域位置。3.2 字体嵌入解决中文乱码的唯一正解不是“加个字体路径”那么简单OpenHTMLtoPDF 默认只支持 Java 内置字体如Serif,SansSerif这些字体在 Windows/Linux/macOS 上映射不同且不包含中文字形。直接跑 Demo中文必成方块。解决方案是font-face嵌入 字体子集化但操作有陷阱。第一步准备字体文件必须用.ttf或.otf格式.woff/.woff2不支持。推荐思源黑体Noto Sans CJK SC或霞鹜文楷LXGW WenKai免费可商用。注意不要用 Windows 自带simhei.ttf它版权不明且字形缺失严重。第二步CSS 中声明字体/* pdf-base.css */ font-face { font-family: SourceHanSansSC; src: url(file:///opt/fonts/NotoSansCJKsc-Regular.ttc) format(truetype); font-weight: normal; font-style: normal; } font-face { font-family: SourceHanSansSC; src: url(file:///opt/fonts/NotoSansCJKsc-Bold.ttc) format(truetype); font-weight: bold; font-style: normal; }关键点url()必须是绝对路径file:///协议相对路径url(./fonts/xxx.ttf)会失败format(truetype)不能写成ttf或opentypeOpenHTMLtoPDF 只认truetype同一字体族名SourceHanSansSC下必须分开声明normal和bold否则加粗失效。第三步全局应用字体* { font-family: SourceHanSansSC, SimSun, sans-serif; font-size: 12px; line-height: 1.5; }这里有个隐藏技巧font-family列表末尾保留SimSun作为降级字体。当 OpenHTMLtoPDF 因权限问题读不到 ttc 文件时至少能 fallback 到系统宋体不至于全屏方块。第四步强制子集化防字体包爆炸OpenHTMLtoPDF 默认嵌入整个字体文件10MB导致 PDF 体积飙升。必须开启子集化PdfRendererBuilder builder new PdfRendererBuilder(); builder.useFontSubstitution(true); // 启用字体替换 builder.useFontEmbedding(true); // 启用字体嵌入 // 关键指定只嵌入文档实际用到的字形 builder.useFontSubset(true); // 必须设为 true实测一份含 200 个中文字符的 PDF未子集化体积 12.3MB开启子集化后仅 387KB且渲染速度提升 40%。3.3 分页控制让表格、章节不被“腰斩”的 CSS 黑科技PDF 最让人头疼的就是表格跨页时表头消失、长段落被硬生生劈成两半。OpenHTMLtoPDF 的分页引擎基于 CSS Paged Media 规范但支持有限必须用“组合拳”。方案一表格跨页保表头.table-container { break-inside: avoid; /* 容器内不许分页 */ } table { page-break-inside: avoid; /* 表格整体不许分页 */ width: 100%; } thead { display: table-header-group; /* 关键让 thead 在每页重复 */ }display: table-header-group是 OpenHTMLtoPDF 对thead的特殊支持它会让表头在每页顶部自动重现。但注意必须配合page-break-inside: avoid否则表头可能单独占一页。方案二章节不被劈开.section { break-before: always; /* 新章节强制另起一页 */ break-after: avoid; /* 章节结束后不许分页 */ } .section-title { break-after: avoid; /* 标题后不许分页确保标题和内容同页 */ }break-before/break-after是 CSS 3 Paged Media 的标准属性OpenHTMLtoPDF 支持良好。always表示强制分页avoid表示尽量不分页。方案三长段落防劈字p, li { orphans: 3; /* 孤行控制段落末尾至少留 3 行 */ widows: 3; /* 寡行控制段落开头至少留 3 行 */ word-break: keep-all; /* 中文不换行防字被劈开 */ }orphans/widows是印刷排版术语指段落首尾孤立的行。设为 3意味着 OpenHTMLtoPDF 会主动调整分页点避免出现“一段话只剩一行在页底其余在下页”的尴尬。3.4 页眉页脚用page伪类实现专业级文档头尾OpenHTMLtoPDF 对page的支持非常成熟这是实现页眉页脚的唯一推荐方式。别信网上那些用position: fixed的 hack它在 PDF 里根本不可靠。标准写法page { size: A4; margin: 2cm; top-center { content: XX公司 - 用户凭证; font-family: SourceHanSansSC; font-size: 10px; color: #666; } bottom-center { content: 第 counter(page) 页共 counter(pages) 页; font-family: SourceHanSansSC; font-size: 10px; color: #666; } } page :first { top-center { content: none; /* 首页不显示页眉 */ } bottom-center { content: XX公司 版权所有; /* 首页页脚不同 */ } }counter(page)和counter(pages)是 CSS 计数器OpenHTMLtoPDF 完全支持。page :first专门针对首页定制比如首页加 LOGO、去页眉、换页脚文案。注意top-center里的内容会自动居中显示在页面顶部 margin 区域内不会侵占正文空间。这是page的核心优势——它操作的是“页面介质”而非“文档流”。3.5 PDF 元数据让生成的文件真正“可识别、可归档”一份专业的 PDF必须包含标准元数据Metadata否则在档案系统、OCR 识别、搜索引擎中会被当成“无主文件”。OpenHTMLtoPDF 提供了setMetaData()方法但很多人漏掉。PdfRendererBuilder builder new PdfRendererBuilder(); builder.withUri(http://localhost/template.ftl); // 模板路径 builder.to(output.pdf); // 设置 PDF 元数据 builder.useMetadata(new PdfMetadata() .setTitle(用户信息凭证_ System.currentTimeMillis()) .setAuthor(XX系统自动化服务) .setCreator(OpenHTMLtoPDF v1.0.10) .setSubject(用户个人信息凭证) .setKeywords(用户,凭证,PDF,自动化) .setCreationDate(new Date()) ); builder.run();这些字段会写入 PDF 的/Info字典用 Adobe Acrobat 或pdfinfo命令行工具可查看$ pdfinfo output.pdf Title: 用户信息凭证_1712345678901 Author: XX系统自动化服务 Creator: OpenHTMLtoPDF v1.0.10 Producer: OpenHTMLtoPDF CreationDate: Thu Apr 4 10:23:45 2024 CST没有元数据的 PDF在企业文档管理系统里无法被分类检索也违反 ISO 19005PDF/A 归档标准的基本要求。3.6 性能调优从 Demo 的 500ms 到生产级的 80msDemo 代码往往忽略性能但生产环境并发一上来慢就是故障。我们总结出 4 个必做优化点FreeMarker Configuration 复用Configuration对象是线程安全的且初始化耗时加载模板、解析语法树。必须单例复用禁止每次请求 new 一个// ✅ 正确Spring Bean 管理 Bean public Configuration freeMarkerConfig() throws IOException { Configuration cfg new Configuration(Configuration.VERSION_2_3_31); cfg.setDirectoryForTemplateLoading(new File(/opt/templates)); cfg.setDefaultEncoding(UTF-8); cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); return cfg; }模板缓存开启FreeMarker 默认开启模板缓存但需确认setTemplateCacheThreshold()设置合理cfg.setTemplateCacheThreshold(100); // 缓存最多 100 个模板避免 OOMOpenHTMLtoPDF Builder 复用PdfRendererBuilder不是线程安全的但可以复用其配置// 预先构建好通用配置 private final PdfRendererBuilder baseBuilder new PdfRendererBuilder() .useFontSubstitution(true) .useFontEmbedding(true) .useFontSubset(true); // 每次请求只 setUri/setTo不重复配置 public void generatePdf(String templateUri, String outputPath) { baseBuilder.withUri(templateUri).to(outputPath).run(); }异步生成 队列削峰对于大文件10MB或高并发100 QPS必须加异步层Async // Spring Async public CompletableFutureVoid asyncGeneratePdf(...) { // 执行 PDF 生成 builder.run(); return CompletableFuture.completedFuture(null); }配合 Redis 队列或 Kafka把 PDF 生成任务丢进消息队列主业务线程立即返回避免阻塞。3.7 安全加固防止模板注入与路径遍历的双重防护FreeMarker 模板引擎存在include/import指令若用户可控模板路径可能引发任意文件读取LFI。OpenHTMLtoPDF 的url()函数也可能被利用。必须双管齐下FreeMarker 层防护// 自定义 TemplateLoader限制可访问目录 public class SecureTemplateLoader implements TemplateLoader { private final File rootDir new File(/opt/templates); Override public Object findTemplateSource(String name) throws IOException { File file new File(rootDir, name); // 标准路径规范化检查 if (!file.getCanonicalPath().startsWith(rootDir.getCanonicalPath())) { throw new IOException(Access denied: name); } return file; } // ... 其他方法 } cfg.setTemplateLoader(new SecureTemplateLoader());OpenHTMLtoPDF 层防护禁用url()函数的外部协议// 自定义 ResourceResolver builder.useResourceResolver(new DefaultResourceResolver() { Override public InputStream resolveResource(String uri) throws IOException { // 只允许 file:/// 协议且路径必须在安全目录内 if (uri.startsWith(file:///opt/fonts/)) { return super.resolveResource(uri); } throw new IOException(Resource access denied: uri); } });这两道防线能 100% 阻断模板注入和字体路径遍历攻击。我们曾审计过某政务平台其 FreeMarker 模板允许?include任意路径攻击者通过?template../../../../etc/passwd直接读取服务器密码文件——这种低级漏洞在 PDF 生成场景里绝不能容忍。4. 实操过程详解从零搭建一个可交付的 PDF 生成服务4.1 环境准备与依赖配置Maven项目用 Maven 管理依赖核心依赖只有 3 个但版本必须严格匹配properties freemarker.version2.3.32/freemarker.version openhtmltopdf.version1.0.10/openhtmltopdf.version spring-boot.version2.7.18/spring-boot.version /properties dependencies !-- FreeMarker 模板引擎 -- dependency groupIdorg.freemarker/groupId artifactIdfreemarker/artifactId version${freemarker.version}/version /dependency !-- OpenHTMLtoPDF 核心渲染 -- dependency groupIdcom.openhtmltopdf/groupId artifactIdopenhtmltopdf-core/artifactId version${openhtmltopdf.version}/version /dependency !-- OpenHTMLtoPDF 字体支持必须-- dependency groupIdcom.openhtmltopdf/groupId artifactIdopenhtmltopdf-pdfbox/artifactId version${openhtmltopdf.version}/version /dependency !-- OpenHTMLtoPDF SVG 支持可选用于图表-- dependency groupIdcom.openhtmltopdf/groupId artifactIdopenhtmltopdf-svg-support/artifactId version${openhtmltopdf.version}/version /dependency /dependencies注意openhtmltopdf-pdfbox是必须的它提供 PDF 输出能力openhtmltopdf-core只是渲染内核不输出 PDF。若漏掉pdfbox依赖builder.run()会抛NoClassDefFoundError。4.2 FreeMarker 配置类Spring BootConfiguration public class FreemarkerConfig { Value(classpath:/templates/) private Resource templateLocation; Bean Synchronized public Configuration freemarkerConfiguration() throws IOException { Configuration configuration new Configuration(Configuration.VERSION_2_3_31); // 模板加载器从 classpath 加载 ClassTemplateLoader templateLoader new ClassTemplateLoader( this.getClass().getClassLoader(), templates/); configuration.setTemplateLoader(templateLoader); // 编码与异常处理 configuration.setDefaultEncoding(UTF-8); configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); configuration.setLogTemplateExceptions(false); configuration.setWrapUncheckedExceptions(true); // 缓存配置 configuration.setTemplateCacheThreshold(100); configuration.setCacheStorage(new MruCacheStorage(100, 10)); // 自定义宏可选 configuration.setSharedVariable(dateUtils, new DateUtils()); return configuration; } // 自定义日期工具类供模板调用 public static class DateUtils { public String formatDate(Date date) { return new SimpleDateFormat(yyyy-MM-dd HH:mm:ss).format(date); } } }这个配置类做了 5 件事指定模板路径、设置 UTF-8 编码、配置异常策略RETHROW_HANDLER 保证错误堆栈不丢失、开启模板缓存、注入共享变量。其中MruCacheStorage(100, 10)表示缓存 100 个模板每个模板最多缓存 10 个版本应对热更新。4.3 PDF 生成服务核心类Service public class PdfGenerationService { private final Configuration freemarkerConfig; private final PdfRendererBuilder baseBuilder; public PdfGenerationService(Configuration freemarkerConfig) { this.freemarkerConfig freemarkerConfig; // 预构建 OpenHTMLtoPDF Builder避免每次 new this.baseBuilder new PdfRendererBuilder() .useFontSubstitution(true) .useFontEmbedding(true) .useFontSubset(true) .useResourceResolver(new SecureResourceResolver()); // 安全资源解析器 } /** * 生成 PDF 主方法 * param templateName 模板名如 user-certificate.ftl * param dataModel 数据模型Map 或 Java Bean * param outputPath 输出路径 */ public void generatePdf(String templateName, Object dataModel, String outputPath) throws IOException, DocumentException { // 1. FreeMarker 渲染 HTML 字符串 String htmlContent renderTemplate(templateName, dataModel); // 2. OpenHTMLtoPDF 渲染 PDF try (ByteArrayInputStream input new ByteArrayInputStream(htmlContent.getBytes(StandardCharsets.UTF_8))) { PdfRendererBuilder builder new PdfRendererBuilder(input, http://localhost/); builder.to(outputPath); // 设置元数据 builder.useMetadata(new PdfMetadata() .setTitle(PDF_ System.currentTimeMillis()) .setAuthor(System) .setCreator(OpenHTMLtoPDF)); builder.run(); } } private String renderTemplate(String templateName, Object dataModel) throws IOException, TemplateException { Template template freemarkerConfig.getTemplate(templateName); StringWriter writer new StringWriter(); template.process(dataModel, writer); return writer.toString(); } // 安全资源解析器防止路径遍历 private static class SecureResourceResolver implements ResourceResolver { Override public InputStream resolveResource(String uri) throws IOException { if (uri null || !uri.startsWith(file:///opt/fonts/)) { throw new IOException(Invalid resource URI: uri); } return Files.newInputStream(Paths.get(uri.substring(file://.length()))); } } }这个服务类体现了生产级设计baseBuilder复用、renderTemplate抽离、SecureResourceResolver防护、try-with-resources确保流关闭。generatePdf方法签名清晰参数语义明确便于单元测试。4.4 Controller 层RESTful 接口设计RestController RequestMapping(/api/pdf) public class PdfController { private final PdfGenerationService pdfService; public PdfController(PdfGenerationService pdfService) { this.pdfService pdfService; } PostMapping(/generate) public ResponseEntitybyte[] generateCertificate(RequestBody PdfRequest request) throws IOException, DocumentException { // 参数校验 if (request.getTemplateName() null || request.getData() null) { return ResponseEntity.badRequest().build(); } // 生成唯一文件名 String fileName certificate_ System.currentTimeMillis() .pdf; String outputPath /tmp/ fileName; // 执行生成 pdfService.generatePdf(request.getTemplateName(), request.getData(), outputPath); // 返回文件流 byte[] fileBytes Files.readAllBytes(Paths.get(outputPath)); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_PDF); headers.setContentDispositionFormData(attachment, fileName); return ResponseEntity.ok() .headers(headers) .body(fileBytes); } // 请求体 DTO public static class PdfRequest { private String templateName; private MapString, Object data; // getter/setter public String getTemplateName() { return templateName; } public void setTemplateName(String templateName) { this.templateName templateName; } public MapString, Object getData() { return data; } public void setData(MapString, Object data) { this.data data; } } }接口设计遵循 REST 规范POST /api/pdf/g
网站建设高端定制企业官网
RELATED

相关资讯

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

较早相关资讯

最新相关资讯

综合能源系统优化调度:光热、ORC与P2G技术解析 2026/9/12 7:38:26

综合能源系统优化调度:光热、ORC与P2G技术解析

1. 项目概述:综合能源系统的优化调度在能源转型的大背景下,如何高效整合多种能源形式成为行业焦点。这个项目聚焦于含光热电站、有机朗肯循环(ORC)和电转气(P2G)技术的综合能源系统优化调度问题。光热电站作为可再生能源的重要代表,其输出具有…

阅读更多 →
Python智慧交通大数据分析系统设计与实现 2026/9/12 7:38:26

Python智慧交通大数据分析系统设计与实现

1. 项目背景与核心价值智慧交通系统正在全球范围内掀起一场革命。作为一名长期从事交通数据分析的工程师,我亲眼见证了从传统统计方法到AI大模型的跨越式发展。这个Python交通大数据分析系统项目,正是这一技术演进的前沿实践。这个系统的核心价值在于&am…

阅读更多 →
STM32内存模型详解:SRAM结构、内存分区与优化实战 2026/9/12 7:38:26

STM32内存模型详解:SRAM结构、内存分区与优化实战

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

阅读更多 →
大模型应用落地实战指南:从选型到Agent工程化 2026/9/12 7:38:26

大模型应用落地实战指南:从选型到Agent工程化

1. 项目概述:一张动态演进的“大模型应用地图”,不是静态名录,而是实战导航图“国内外知名大模型及应用——模型/应用维度(2026/09/04)”这个标题,乍看像一份简单的榜单更新,但实际它指向一个更…

阅读更多 →
构建高帧率视频处理流水线:帧管理与并发优化实践 2026/9/12 7:38:26

构建高帧率视频处理流水线:帧管理与并发优化实践

1. 整体设计思路:hyperframes 到底在解决什么问题1.1 实时视频处理的老大难先说个背景,搞过视频处理的人应该都有同感:单帧处理并不难,难的是稳定地、持续地、低延迟地把每一帧都处理完。看起来是一句话的事,真做起来一…

阅读更多 →
卷积神经网络核心架构与工业级优化实践 2026/9/12 7:35:26

卷积神经网络核心架构与工业级优化实践

1. 卷积神经网络核心结构解析在上一部分我们讨论了卷积神经网络的基础概念后,现在让我们深入其核心架构。现代CNN通常由多个功能层堆叠而成,每个层都有其独特的数学表达和计算特性。1.1 卷积层的数学本质卷积操作的本质是局部感受野的权重共享。以一个33…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

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

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