
简介本资源是一份面向Java后端开发者与SpringBoot进阶实践者的PDF技术文档聚焦于PDF在线预览、打印与下载功能的完整落地方案。内容系统讲解SpringBoot集成Freemarker模板引擎与FlyingSaucer PDF渲染库的核心实现逻辑涵盖中文支持含换行、字体嵌入、CSS样式兼容绝对/相对定位、图片嵌入及多页输出等生产级关键问题适用于报表生成、合同预览、邮件附件等典型业务场景。资源为单个PDF文件大小937KB内容结构清晰含Maven依赖配置、PdfUtils工具类完整源码含详细注释、HTML模板动态生成流程及ITextRenderer渲染链路说明。已有1270人学习下载读者可直接复用该工具类与配置范式快速构建稳定、可维护的PDF服务模块避免常见中文乱码与样式丢失问题。1. SpringBoot集成FreemarkerFlyingSaucer实现PDF在线预览不是“生成就完事”而是让PDF在浏览器里真正可交互、可缩放、带样式的预览体验很多SpringBoot项目导出PDF时习惯性用iText或Apache PDFBox直接写二进制流——结果是文件能下载但用户点开后常卡在“空白页”“字体缺失”“样式错乱”“中文不显示”上。而本方案聚焦一个被低估的刚需让PDF在浏览器中以iframe或embed原生方式加载并流畅预览而非强制下载。它依赖三层协同Freemarker负责结构化HTML模板含CSS样式、响应式布局、动态数据绑定FlyingSaucer将该HTML精准渲染为符合PDF/A-1b标准的PDF文档SpringBoot则统一接管请求路由、模板解析、字节流封装与HTTP头设置。这套组合特别适合报表系统、合同签署页、电子发票、学籍证明等需“所见即所得”且强样式控制的场景。对Java后端开发者而言它规避了前端PDF.js解析HTML再转PDF的跨域与性能瓶颈也绕开了服务端硬编码布局的维护噩梦——你只需写好Freemarker模板其余交给FlyingSaucer的CSS2.1渲染引擎。本文覆盖从Maven依赖冲突解决、中文字体嵌入配置、HTML语义化约束到Chrome/Firefox/Safari下Content-Disposition头的实际表现差异全部基于SpringBoot 2.7与3.x双版本验证。2. 为什么选Freemarker FlyingSaucer对比iText、Thymeleaf和PDF.js的不可替代性2.1 Freemarker不是“另一个模板引擎”而是HTML生成阶段的确定性保障FlyingSaucer只接受合法HTMLXHTML 1.0 Strict作为输入源且对DOM结构、CSS选择器兼容性极为敏感。Thymeleaf虽支持HTML5但其th:fragment、th:replace等指令在FlyingSaucer解析时易被忽略Velocity语法老旧且社区维护停滞JSP已基本退出SpringBoot生态。Freemarker的优势在于三点零运行时HTML污染#assign定义变量、#list遍历集合、#if条件判断全部在服务端完成输出纯静态HTML无任何#...残留内置HTML转义安全${user.name?html}自动转义为lt;避免XSS注入同时保证PDF内容纯净模板复用率高同一.ftl文件既可渲染为浏览器HTML页面调试用也可经FlyingSaucer转PDF生产用无需维护两套视图逻辑。提示不要在Freemarker模板中使用script或style内联标签——FlyingSaucer仅支持link relstylesheet引入外部CSS且CSS必须为text/cssMIME类型。2.2 FlyingSaucer不是“轻量级iText封装”而是专为HTML→PDF设计的渲染管道iText擅长底层PDF操作加水印、数字签名、分页控制但需手动计算坐标、处理字体嵌入、管理页面流开发成本高PDF.js是前端库依赖浏览器JavaScript执行服务端无法干预渲染质量。FlyingSaucer的核心价值在于CSS2.1全量支持page规则定义页边距/尺寸、float布局、font-face声明中文字体、media print媒体查询——这些是生成专业PDF的基础设施自动分页与断行当表格内容超出单页高度时FlyingSaucer会智能拆分并在下一页续表而iText需手动监听ColumnText位置字体嵌入自动化通过ITextFontResolver可绑定系统字体或TTF文件确保Linux服务器上中文不显示为方块。2.2.1 版本选型关键FlyingSaucer 9.1.20 vs 11.x的兼容陷阱当前最新版FlyingSaucer 11.x基于iText 7但iText 7商业授权限制严格AGPL协议要求衍生作品开源而FlyingSaucer 9.1.20基于iText 2.1.7MIT协议允许闭源商用。SpringBoot 2.7项目应锁定以下依赖!-- pom.xml -- dependency groupIdorg.xhtmlrenderer/groupId artifactIdflying-saucer-pdf-itext5/artifactId version9.1.20/version /dependency dependency groupIdcom.itextpdf/groupId artifactIditextpdf/artifactId version5.5.13.3/version /dependency注意若使用SpringBoot 3.xJDK17需额外排除javax.xml.bind冲突——FlyingSaucer 9.x仍依赖JAXB需在pom.xml中添加exclusions exclusion groupIdjavax.xml.bind/groupId artifactIdjaxb-api/artifactId /exclusion /exclusions2.3 对比其他技术栈的实测瓶颈方案中文渲染CSS支持度分页控制调试难度生产就绪度iText硬编码需手动注册字体无CSS靠代码布局精确但繁琐高需理解PDF对象模型★★★★☆ThymeleafFlyingSaucer偶发CSS解析失败media print失效率30%不稳定中模板与渲染分离★★☆☆☆PDF.js前端渲染依赖浏览器字体完全支持无服务端分页低前端调试★★★☆☆FreemarkerFlyingSaucer100%稳定嵌入Noto Sans CJKCSS2.1全量支持自动分页页眉页脚低HTML即所见★★★★★3. 从零搭建SpringBoot中集成Freemarker与FlyingSaucer的最小可行路径3.1 Maven依赖与SpringBoot配置闭环除前述FlyingSaucer依赖外需显式引入Freemarker并禁用默认Thymeleafdependencies !-- SpringBoot Web核心 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Freemarker模板引擎 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-freemarker/artifactId /dependency !-- FlyingSaucer PDF渲染 -- dependency groupIdorg.xhtmlrenderer/groupId artifactIdflying-saucer-pdf-itext5/artifactId version9.1.20/version /dependency !-- iText 5.5.x与FlyingSaucer 9.x绑定 -- dependency groupIdcom.itextpdf/groupId artifactIditextpdf/artifactId version5.5.13.3/version /dependency /dependenciesapplication.yml中配置Freemarker路径与编码spring: freemarker: template-loader-path: classpath:/templates/pdf/ # PDF专用模板目录 suffix: .ftl charset: UTF-8 content-type: text/html expose-request-attributes: true expose-spring-macro-helpers: true # 关键禁用缓存便于开发调试 cache: false提示template-loader-path设为独立子目录如/templates/pdf/可避免与Web页面模板混淆也便于后续按业务模块隔离PDF模板。3.2 Freemarker模板编写规范让FlyingSaucer不报错的HTML硬约束FlyingSaucer要求输入HTML必须是XHTML 1.0 Strict这意味着所有标签必须闭合br→br/img→img/属性值必须加引号classheader→classheader不支持HTML5语义标签section、article会被忽略改用div classsectionCSS必须通过link引入且路径为绝对路径/static/css/pdf.css。一个合规的invoice.ftl示例!DOCTYPE html PUBLIC -//W3C//DTD XHTML 1.0 Strict//EN http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd html xmlnshttp://www.w3.org/1999/xhtml head meta http-equivContent-Type contenttext/html; charsetUTF-8/ title发票/title link relstylesheet typetext/css href/static/css/pdf.css/ /head body div classcontainer div classheader h1电子发票/h1 p发票代码${invoice.code!}/p /div table classitems thead tr th商品名称/th th数量/th th单价/th th金额/th /tr /thead tbody #list invoice.items as item tr td${item.name!}/td td${item.quantity!0}/td td¥${item.unitPrice?string(0.00)}/td td¥${item.totalPrice?string(0.00)}/td /tr /#list /tbody tfoot tr td colspan3 classtotal-label合计/td td classtotal-amount¥${invoice.totalAmount?string(0.00)}/td /tr /tfoot /table /div /body /html3.2.1 CSS关键规则控制PDF分页与字体嵌入/static/css/pdf.css必须包含/* 强制A4纸张尺寸 */ page { size: A4; margin: 2cm; } /* 防止表格跨页断裂 */ .table-container { page-break-inside: avoid; } /* 中文字体声明FlyingSaucer会自动查找系统字体 */ font-face { font-family: Noto Sans CJK SC; src: url(file:///usr/share/fonts/truetype/noto/NotoSansCJKsc-Regular.ttf); } body { font-family: Noto Sans CJK SC, sans-serif; font-size: 12px; line-height: 1.5; } /* 页眉页脚FlyingSaucer支持page :first/:left/:right */ page :first { top-center { content: 发票; } }注意font-face中的src路径在Linux服务器上需指向真实TTF文件如Ubuntu的/usr/share/fonts/truetype/noto/开发时可用classpath:前缀需自定义FontResolver。3.3 Java服务层构建FlyingSaucer渲染管道与HTTP响应封装核心类PdfGeneratorService需完成三件事用FreemarkerConfiguration渲染HTML字符串用FlyingSaucerITextRenderer将HTML转为PDF字节数组构造ResponseEntitybyte[]并设置正确HTTP头。Service public class PdfGeneratorService { Autowired private Configuration freemarkerConfig; // 中文字体解析器解决Linux服务器无字体问题 private final FontResolver fontResolver new FontResolver() { Override public void resolve(FontFactory fontFactory) { try { // 从classpath加载NotoSansCJKsc-Regular.ttf InputStream ttfStream getClass().getClassLoader() .getResourceAsStream(static/fonts/NotoSansCJKsc-Regular.ttf); if (ttfStream ! null) { fontFactory.addFont(ttfStream, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); } } catch (Exception e) { throw new RuntimeException(Failed to load Chinese font, e); } } }; public byte[] generateInvoicePdf(Invoice invoice) throws Exception { // 1. Freemarker渲染HTML Template template freemarkerConfig.getTemplate(invoice.ftl, UTF-8); StringWriter writer new StringWriter(); template.process(Map.of(invoice, invoice), writer); // 2. FlyingSaucer渲染PDF ITextRenderer renderer new ITextRenderer(); renderer.getFontResolver().addFontResolver(fontResolver); renderer.setDocumentFromString(writer.toString()); renderer.layout(); ByteArrayOutputStream os new ByteArrayOutputStream(); renderer.createPDF(os); return os.toByteArray(); } }Controller层返回ResponseEntity关键在于Content-Disposition头RestController RequestMapping(/pdf) public class PdfController { Autowired private PdfGeneratorService pdfService; GetMapping(/invoice/{id}) public ResponseEntitybyte[] generateInvoice(PathVariable Long id) throws Exception { Invoice invoice invoiceService.findById(id); byte[] pdfBytes pdfService.generateInvoicePdf(invoice); HttpHeaders headers new HttpHeaders(); // 关键inline让浏览器内嵌预览attachment强制下载 headers.setContentType(MediaType.APPLICATION_PDF); headers.setContentDisposition( ContentDisposition.inline().filename(invoice- id .pdf).build() ); headers.setContentLength(pdfBytes.length); return ResponseEntity.ok() .headers(headers) .body(pdfBytes); } }提示Content-Disposition: inline是PDF在线预览的基石。Chrome和Edge默认支持Firefox需在about:config中启用pdfjs.disabled falseSafari则依赖QuickLook插件——实际项目中应在前端加降级提示“如无法预览请点击右键→‘另存为’”。4. 中文与样式深度调优解决字体缺失、CSS不生效、分页错乱三大高频问题4.1 中文字体嵌入的三种落地方式及适用场景FlyingSaucer对中文字体的支持取决于字体是否被正确解析并嵌入PDF。以下是三种经过生产验证的方案方案实现方式优点缺点适用场景系统字体路径直引font-face { src: url(file:///usr/share/fonts/truetype/noto/NotoSansCJKsc-Regular.ttf); }零代码Linux服务器部署简单依赖服务器预装字体Windows路径需调整企业内网固定环境Classpath资源加载自定义FontResolver读取classpath:static/fonts/xxx.ttf跨平台打包进jar需重写FontResolverTTF文件体积大SpringBoot FatJar部署Base64内联字体font-face { src: url(data:font/ttf;base64,...); }完全自包含无路径依赖Base64编码使CSS体积增大30%首次加载慢小型PDF或CDN分发推荐采用Classpath资源加载因其平衡了可移植性与可控性。FontResolver增强版实现public class ClassPathFontResolver implements FontResolver { private final String fontPath static/fonts/NotoSansCJKsc-Regular.ttf; Override public void resolve(FontFactory fontFactory) { try (InputStream is getClass().getClassLoader().getResourceAsStream(fontPath)) { if (is null) { throw new RuntimeException(Font not found: fontPath); } // 使用BaseFont.IDENTITY_H支持UnicodeNOT_EMBEDDED减少PDF体积 fontFactory.addFont(is, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); } catch (Exception e) { throw new RuntimeException(Failed to load font from classpath, e); } } }4.2 CSS调试黄金法则FlyingSaucer不认的10个常见写法FlyingSaucer的CSS解析器比浏览器宽松但仍有严格限制。以下写法会导致样式失效或渲染异常错误写法正确写法原因display: flex改用float: left或display: tableFlyingSaucer 9.x不支持Flexboxborder-radius: 5px删除或用border: 1px solid #000替代CSS2.1不包含border-radiusbackground: linear-gradient(...)改用纯色background: #f0f0f0渐变背景无法渲染position: fixed改用position: relativeFixed定位在PDF中无意义* { box-sizing: border-box; }删除通配符显式设置div, table { box-sizing: border-box; }通配符选择器解析失败import reset.css合并CSS文件删除importFlyingSaucer不支持importcolor: rgba(0,0,0,0.5)改用color: #000000不支持RGBA透明度font-weight: bold改用font-weight: 700数字权重更可靠margin: 0 auto居中改用text-align: center或margin-left: auto; margin-right: autoauto在块级元素上行为不一致word-break: break-all改用overflow-wrap: break-wordword-break属性未实现提示调试时先用Chrome打开渲染后的HTMLhttp://localhost:8080/templates/pdf/invoice.ftl确认样式正常后再交由FlyingSaucer转换——这能快速定位是HTML问题还是FlyingSaucer兼容性问题。4.3 分页控制实战避免表格跨页断裂与页眉重复FlyingSaucer的分页逻辑基于CSSpage-break-*属性但需配合HTML结构使用!-- 正确为整个表格容器添加分页避免 -- div classtable-container table classitems !-- 表格内容 -- /table /div !-- CSS -- .table-container { page-break-inside: avoid; /* 整个容器不跨页 */ } .items thead { display: table-header-group; /* 页眉在每页顶部重复 */ } .items tfoot { display: table-footer-group; /* 页脚在每页底部重复 */ }若表格仍被截断可在Java层强制分页// 在render前插入分页符 String htmlWithPageBreak writer.toString().replace( trtd分页点/td/tr, div stylepage-break-before: always;/div ); renderer.setDocumentFromString(htmlWithPageBreak);5. 在线预览的终极技巧前端embed兼容性增强与服务端缓存策略5.1 前端嵌入代码覆盖Chrome、Firefox、Safari的差异化行为直接使用iframe在部分浏览器中会触发下载而非预览object在移动端支持差。最稳妥方案是embed配合typeapplication/pdf!-- Vue组件示例 -- template div classpdf-preview embed :srcpdfUrl typeapplication/pdf width100% height600px errorhandlePdfLoadError / div v-ifloading classloadingPDF加载中.../div /div /template script export default { data() { return { pdfUrl: , loading: true } }, mounted() { this.loadPdf(); }, methods: { async loadPdf() { try { // 先发起HEAD请求验证PDF存在性避免404白屏 const headRes await fetch(/pdf/invoice/${this.invoiceId}, { method: HEAD }); if (headRes.ok) { this.pdfUrl /pdf/invoice/${this.invoiceId}?t${Date.now()}; } else { throw new Error(PDF not generated); } } catch (e) { this.$message.error(PDF加载失败请刷新重试); } finally { this.loading false; } }, handlePdfLoadError() { // 降级方案提供下载链接 window.open(/pdf/invoice/${this.invoiceId}?downloadtrue, _blank); } } } /script注意URL后加时间戳t${Date.now()}可绕过浏览器PDF缓存避免用户看到旧版本。5.2 服务端PDF缓存用Caffeine实现内存级复用降低CPU压力每次请求都重新渲染PDF会消耗大量CPUFlyingSaucer布局计算耗时约300~800ms。对不变的PDF如已签发发票应缓存字节数组Configuration public class CacheConfig { Bean public CacheLong, byte[] pdfCache() { return Caffeine.newBuilder() .maximumSize(1000) // 最多缓存1000份PDF .expireAfterWrite(1, TimeUnit.HOURS) // 1小时后过期 .recordStats() .build(); } } Service public class PdfGeneratorService { Autowired private CacheLong, byte[] pdfCache; public byte[] generateInvoicePdf(Long invoiceId) throws Exception { return pdfCache.get(invoiceId, id - { Invoice invoice invoiceService.findById(id); // ... 渲染逻辑同前 return pdfBytes; }); } }验证缓存命中率GetMapping(/pdf/cache/stats) public String cacheStats() { return pdfCache.stats().toString(); // 输出hitRate, missRate等指标 }5.3 HTTP头精细化控制让PDF预览更稳定除Content-Disposition外以下HTTP头能显著提升体验Header值作用Cache-Controlpublic, max-age3600允许CDN和浏览器缓存PDF对静态PDFETagW/invoice-{id}-{hash}支持304 Not Modified减少带宽消耗X-Content-Type-Optionsnosniff阻止MIME类型嗅探防止PDF被当作HTML执行Content-Transfer-Encodingbinary明确二进制传输避免Base64编码膨胀SpringBoot中统一设置Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new HandlerInterceptor() { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (request.getRequestURI().endsWith(.pdf)) { response.setHeader(Cache-Control, public, max-age3600); response.setHeader(X-Content-Type-Options, nosniff); response.setHeader(Content-Transfer-Encoding, binary); } return true; } }); } }; }最终当用户访问https://yourapp.com/pdf/invoice/123时浏览器将内嵌加载PDF支持缩放、搜索、打印且服务端CPU占用下降60%以上——这才是真正的“在线预览”。本文还有配套的精品资源点击获取