SpringBoot项目实战:用Flying Saucer + iText 5搞定HTML转PDF(附完整依赖与中文乱码解决方案)
SpringBoot实战构建高可靠HTML转PDF服务的技术精要在当今企业级应用开发中将HTML内容转换为PDF文档已成为报表生成、电子合同、账单推送等场景的刚需。不同于简单的格式转换生产环境下的PDF服务需要应对中文排版、动态数据注入、样式兼容性等复杂挑战。本文将基于SpringBoot 2.7 Flying Saucer iText5技术栈手把手构建一个工业级PDF生成服务。1. 环境搭建与依赖管理1.1 精准控制依赖版本构建稳定的PDF生成服务首要条件是确保依赖库版本严格匹配。以下是经生产验证的依赖组合!-- 核心PDF生成库 -- dependency groupIdorg.xhtmlrenderer/groupId artifactIdflying-saucer-pdf-itext5/artifactId version9.0.6/version /dependency !-- iText核心库必须匹配此版本 -- dependency groupIdcom.itextpdf/groupId artifactIditextpdf/artifactId version5.5.10/version /dependency !-- 亚洲字体支持 -- dependency groupIdcom.itextpdf/groupId artifactIditext-asian/artifactId version5.2.0/version /dependency !-- 模板引擎 -- dependency groupIdorg.freemarker/groupId artifactIdfreemarker/artifactId version2.3.31/version /dependency关键提示flying-saucer-pdf-itext5 9.0.6与itextpdf 5.5.10存在强版本耦合任意版本变更都可能导致渲染异常1.2 字体资源准备在resources目录下创建fonts文件夹放入以下字体文件simsun.ttc宋体simhei.ttf黑体msyh.ttf微软雅黑目录结构示例src/main/resources ├── fonts │ ├── simsun.ttc │ ├── simhei.ttf │ └── msyh.ttf └── templates └── report.ftl2. 核心架构设计2.1 三层式服务架构// 控制器层示例 RestController RequestMapping(/api/pdf) public class PdfController { Autowired private PdfGenerationService pdfService; PostMapping(/generate) public ResponseEntitybyte[] generatePdf(RequestBody PdfRequest request) { byte[] pdfBytes pdfService.generateFromTemplate( request.getTemplateName(), request.getDataModel() ); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_PDF); headers.setContentDisposition( ContentDisposition.attachment() .filename(document.pdf, StandardCharsets.UTF_8) .build() ); return new ResponseEntity(pdfBytes, headers, HttpStatus.OK); } }2.2 模板服务实现Service public class PdfGenerationServiceImpl implements PdfGenerationService { Value(${pdf.template.dir:/templates}) private String templateDirectory; private Configuration freemarkerConfig; PostConstruct public void init() throws IOException { freemarkerConfig new Configuration(Configuration.VERSION_2_3_31); freemarkerConfig.setClassLoaderForTemplateLoading( getClass().getClassLoader(), templateDirectory ); freemarkerConfig.setDefaultEncoding(UTF-8); } Override public byte[] generateFromTemplate(String templateName, MapString, Object data) { try { String htmlContent renderTemplate(templateName, data); return PdfRenderEngine.htmlToPdf(htmlContent); } catch (Exception e) { throw new PdfGenerationException(PDF生成失败, e); } } private String renderTemplate(String templateName, MapString, Object data) throws IOException, TemplateException { Template template freemarkerConfig.getTemplate(templateName); StringWriter writer new StringWriter(); template.process(data, writer); return writer.toString(); } }3. 关键技术实现3.1 PDF渲染引擎public class PdfRenderEngine { private static final String FONT_DIR /fonts/; public static byte[] htmlToPdf(String html) throws DocumentException, IOException { ITextRenderer renderer new ITextRenderer(); // 配置中文字体 ITextFontResolver fontResolver renderer.getFontResolver(); fontResolver.addFont(FONT_DIR simsun.ttc, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); fontResolver.addFont(FONT_DIR simhei.ttf, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); // 设置DPI提高打印质量 renderer.setDPI(300); renderer.setDocumentFromString(html); renderer.layout(); try (ByteArrayOutputStream output new ByteArrayOutputStream()) { renderer.createPDF(output); return output.toByteArray(); } } }3.2 高级样式控制在HTML模板中使用CSS控制PDF样式时需特别注意style /* 全局页面设置 */ page { size: A4; margin: 1cm; top-center { content: 企业报表; font-family: SimSun; } } /* 中文字体回退策略 */ body { font-family: SimSun, Microsoft YaHei, sans-serif; line-height: 1.6; } /* 表格样式增强 */ .data-table { border-collapse: collapse; width: 100%; } .data-table th { background-color: #f5f5f5; font-weight: bold; } .data-table td, .data-table th { border: 1px solid #ddd; padding: 8px; } /style4. 生产环境优化方案4.1 性能调优参数参数项推荐值作用说明XHTML-Parser非验证模式关闭DTD验证提升解析速度ImageCache启用减少网络图片重复下载PDF Version1.4兼容性与文件大小的平衡Compression启用压缩文本和图像数据配置示例renderer.getSharedContext().setReplacedElementFactory( new ReplacedElementFactoryImpl() { Override public ReplacedElement createReplacedElement( LayoutContext c, BlockBox box, UserAgentCallback uac, int cssWidth, int cssHeight) { // 启用图片缓存 if (box.getElement().getNodeName().equals(img)) { String src box.getElement().getAttribute(src); if (src.startsWith(http)) { return new ImageReplacedElement( loadCachedImage(src), cssWidth, cssHeight); } } return super.createReplacedElement(c, box, uac, cssWidth, cssHeight); } } );4.2 常见问题解决方案中文乱码终极方案HTML模板必须声明UTF-8编码CSS中明确指定中文字体族PDF渲染器加载物理字体文件确保FreeMarker输出使用UTF-8图片加载异常处理网络图片使用绝对URL本地图片使用classpath路径备用图片机制img srcprimary.png onerrorthis.srcfallback.png /CSS3特性兼容表特性支持情况替代方案Flexbox部分使用表格布局CSS Grid不支持传统divfloat布局Transforms不支持使用静态定位Animations不支持移除动态效果4.3 安全加固措施// HTML净化处理 public String sanitizeHtml(String rawHtml) { PolicyFactory policy new HtmlPolicyBuilder() .allowElements(p, table, img, div, span) .allowUrlProtocols(http, https) .allowAttributes(src).onElements(img) .allowAttributes(class).globally() .build(); return policy.sanitize(rawHtml); }在项目实践中发现当需要生成包含复杂表格的PDF时使用传统的HTML表格标签配合colspan/rowspan往往比尝试CSS布局更可靠。特别是在处理财务报告等需要精确对齐的场景下以下表格结构经过验证具有最佳兼容性table classfinancial-table colgroup col stylewidth: 15% col stylewidth: 25% col stylewidth: 60% /colgroup thead tr th rowspan2科目/th th colspan2金额万元/th /tr tr th本期/th th上期/th /tr /thead tbody tr td流动资产/td td${current.asset}/td td${previous.asset}/td /tr /tbody /table