前段时间因为相关业务需求需要后台生成pdf文件,对于一直crud的程序员来说,这无疑是需要一定时间来做技术预研的。下面根据我的实践经验总结一下我是如何使用java生成pdf文件的。
根据spring mvc的设计模式,理论上来说,我们可以把pdf文件视作一个View视图,那么整个mvc模型如下图:
如果按照上图所示,那么我们要编写一个pdf视图解析器,这无疑是一个有难度的事情。但是把思路转换一下,我们可以先把model转换成html,再通过html转换成pdf是不是会更容易一点?
1.如何把model转换成html?
这个问题spring mvc已经替我们解决了,thymeleaf的实现无非就是一个活生生的model转换成html的例子。
2.html如何转换成pdf?
基于IText | 基于FlyingSaucer | 基于WKHtmlToPdf | 基于pd4ml | |
跨平台性 | 跨平台 | 跨平台 | 跨平台 | 跨平台 |
是否安装软件 | 否 | 否 | 需安装WKHtmlToPdf | 否 |
是否收费 | 免费 | 免费 | 免费 | 收费 |
转换Html效率 | 速度快 | 未测 | 速度慢。相比URL来说,效率较慢。能忽略一些html语法或资源是否存在问题。 | 速度快。部分CSS样式不支持。 |
效果 | 存在样式失真问题。对html语法有一定要求 | 存在样式失真问题。对html语法有较高要求。 | 失真情况较小,大部分网页能按Chome浏览器显示的页面转换 | 部分CSS样式有问题。 |
转换URL效率 | 未测 | 未测 | 效率不是特别高 | 未测 |
效果 | 未测 | 未测 | 部分网页由于其限制,或将出现html网页不完整。 | 未测 |
优点 | 不需安装软件、转换速度快 | 不需安装软件、转换速度快 | 生成PDF质量高 | 不需要安装软件、转换速度快 |
缺点 | 对html标签严格,少一个结束标签就会报错;服务器需要安装字体 | 对html标签严格,少一个结束标签就会报错;服务器需要安装字体 | 需要安装软件、时间效率不高 | 对部分CSS样式不支持。 |
分页 | 图片 | 表格 | 链接 | 中文 | 特殊字符 | 整体样式 | 速度 | |
IText | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 失真问题 | 快 |
FlyingSaucer | 未知 | 未知 | 未知 | 未知 | 未知 | 未知 | 未知 | 快 |
WKHtmlToPdf | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 很好 | 慢 |
pd4ml | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 失真问题 | 快 |
对比以上各类实现:
1.WKHtmlToPdf因为转换速度慢、需要安装软件的缺点被暂时排除在外;pd4ml因为是收费的,并且同样存在一些常见的样式失真问题,直接排除;
2.剩下的就是在IText和FlyingSaucer的实现方案中做选择,对比之下,选择IText作为我们的最终实现方案
【相关依赖】
<dependency> <groupId>com.itextpdf</groupId> <artifactId>itextpdf</artifactId> <version>5.5.13.2</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itext-asian</artifactId> <version>5.2.0</version> </dependency> <dependency> <groupId>com.itextpdf.tool</groupId> <artifactId>xmlworker</artifactId> <version>5.5.13.2</version> </dependency> <dependency> <groupId>org.xhtmlrenderer</groupId> <artifactId>flying-saucer-pdf-itext5</artifactId> <version>9.1.22</version> </dependency> 复制代码
【代码实现】
import com.itextpdf.text.pdf.BaseFont; import com.zx.silverfox.common.exception.GlobalException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.xhtmlrenderer.pdf.ITextFontResolver; import org.xhtmlrenderer.pdf.ITextRenderer; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; @Slf4j public final class HtmlUtil { private HtmlUtil() { } // 字体路径,放在资源目录下 private static final String FONT_PATH = "classpath:simsun.ttc"; public static void file2Pdf(File htmlFile, String pdfFile) throws GlobalException { try (OutputStream os = new FileOutputStream(pdfFile)) { String url = htmlFile.toURI().toURL().toString(); ITextRenderer renderer = new ITextRenderer(); renderer.setDocument(url); // 解决中文支持 ITextFontResolver fontResolver = renderer.getFontResolver(); // 获取字体绝对路径,ApplicationContextUtil是我自己写的类 String fontPath = ApplicationContextUtil.classpath(FONT_PATH); fontResolver.addFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); renderer.layout(); renderer.createPDF(os); } catch (Exception e) { // 抛出自定义异常 throw GlobalException.newInstance(e); } } public static void html2Pdf(String html, String pdfFile) throws GlobalException { String pdfDir = StringUtils.substringBeforeLast(pdfFile, "/"); File file = new File(pdfDir); if (!file.exists()) { file.mkdirs(); } try (OutputStream os = new FileOutputStream(pdfFile)) { ITextRenderer renderer = new ITextRenderer(); renderer.setDocumentFromString(html); // 解决中文支持 ITextFontResolver fontResolver = renderer.getFontResolver(); // 获取字体绝对路径,ApplicationContextUtil是我自己写的类 String fontPath = ApplicationContextUtil.classpath(FONT_PATH); fontResolver.addFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); renderer.layout(); renderer.createPDF(os); } catch (Exception e) { // 抛出自定义异常 throw GlobalException.newInstance(e); } } } 复制代码
【字体文件】
simsun.tcc 密码:rzw4
以上实现就完成了html转换成pdf的功能,后续就是model转html:
因为我使用的是springboot,所以直接使用以下依赖。小伙伴可以根据自身项目具体情况使用对应的依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> 复制代码
【代码实现】
import com.google.common.collect.Maps; import com.zx.silverfox.common.exception.GlobalException; import com.zx.silverfox.common.util.HtmlUtil; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; import java.util.Map; public abstract class AbstractTemplate { // 使用thymeleaf模版引擎 private TemplateEngine engine; // 模版名称 private String templateName; private AbstractTemplate() {} public AbstractTemplate(TemplateEngine engine,String templateName) { this.engine = engine; this.templateName=templateName; } /** * 模版名称 * * @return */ protected String templateName(){ return this.templateName; } /** * 所有的参数数据 * * @return */ private Map<String, Object> variables(){ // Maps是使用到了guava依赖 Map<String, Object> variables = Maps.newHashMap(); // 对应html模版中的template变量,取值的时候就按照“${template.字段名}”格式,可自行修改 variables.put("template", this); return variables; }; /** * 解析模版,生成html * * @return */ public String process() { Context ctx = new Context(); // 设置model ctx.setVariables(variables()); // 根据model解析成html字符串 return engine.process(templateName(), ctx); } public void parse2Pdf(String targetPdfFilePath) throws GlobalException { String html = process(); // 通过html转换成pdf HtmlUtil.html2Pdf(html, targetPdfFilePath); } } 复制代码
创建模版引擎
@Configuration public class TemplateEngineConfig { // 注入TemplateEngine模版引擎 @Bean public TemplateEngine templateEngine(){ ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver(); // 设置模版前缀,相当于需要在资源文件夹中创建一个html2pdfTemplate文件夹,所有的模版都放在这个文件夹中 resolver.setPrefix("/html2pdfTemplate/"); // 设置模版后缀 resolver.setSuffix(".html"); resolver.setCharacterEncoding("UTF-8"); // 设置模版模型为HTML resolver.setTemplateMode("HTML"); TemplateEngine engine = new TemplateEngine(); engine.setTemplateResolver(resolver); return engine; } } 复制代码
因为我们的依赖是基于springboot的,所以为了不让spring-boot-starter-thymeleaf自动配置,我们需要排除相关的配置类。不想这样做的小伙伴可使用thymeleaf其他依赖,原理上都一样。
@SpringBootApplication(exclude = ThymeleafAutoConfiguration.class) 复制代码
至此,所有的技术准备都做好了,如何使用我们编写好的代码实现model转换pdf文件呢?
【示例】
import lombok.Data; import org.thymeleaf.TemplateEngine; import java.util.List; @Data public class Model extends AbstractTemplate { // 构造函数 public Model(TemplateEngine engine, String templateName) { super(engine, templateName); } // 名称 private String name; // 保险记录 private List<InsuranceInfo> insuranceInfos; } @Data public class InsuranceInfo{ /** 出险日期 */ private String expirationDate; /** 描述 */ private String description; } 复制代码
【报告模版.html】
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>报告模版</title> <style> <!-- 编写css --> </style> </head> <!-- 引入字体 --> <body style="font-family: SimSun;"> <div class="main"> 报告模版 </div> <div class="main2"> <span class="heng" th:text="${template.name}">template.name</span> <table class="tabletype"> <thead> <tr class="recordhead"> <th class="leaf" style="width: 80px;">出险日期</th> <th class="leaf" style="width: 80px;">描述</th> </tr> </thead> <tbody th:if="${template.insuranceInfos}"> <tr th:each="m,var : ${template.insuranceInfos}"> <th class="leaf" th:text="${m.expirationDate}"></th> <th class="leaf" th:text="${m.description}"></th> </tr> </tbody> </table> </div> </body> </html> 复制代码
【测试代码】
@Autowired private TemplateEngine engine; public void test() throws Exception { // 创建model,需要指定模版引擎和具体的模版,“报告模版”指的是资源目录下/html2pdfTemplate/报告模版.html文件。如果是springboot项目,那么就是在resources文件夹下面 Model model = new Model(engine,"报告模版"); model.setName("名称"); List<InsuranceInfo> insuranceInfos = new ArrayList<>(); InsuranceInfo record1 = new InsuranceInfo(); record1.setExpirationDate("2021-01-19"); record1.setDescription("刹车失灵"); insuranceInfos.add(record1); InsuranceInfo record2 = new InsuranceInfo(); record2.setExpirationDate("2021-03-06"); record2.setDescription("挡风玻璃破裂"); insuranceInfos.add(record2); model.setInsuranceInfos(insuranceInfos); //生成pdf,指定目标文件路径 model.parse2Pdf("/home/dev/桌面/test.pdf"); } 复制代码
根据以上理论和实践,我们已经达到了我们的目标,最终完成了数据转换成PDF文件的需求