前段时间因为相关业务需求需要后台生成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作为我们的最终实现方案
【相关依赖】
com.itextpdf
itextpdf
5.5.13.2
com.itextpdf
itext-asian
5.2.0
com.itextpdf.tool
xmlworker
5.5.13.2
org.xhtmlrenderer
flying-saucer-pdf-itext5
9.1.22
【代码实现】
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,所以直接使用以下依赖。小伙伴可以根据自身项目具体情况使用对应的依赖
org.springframework.boot
spring-boot-starter-thymeleaf
【代码实现】
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 variables(){
Map 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 insuranceInfos;
}
@Data
public class InsuranceInfo{
/** 出险日期 */
private String expirationDate;
/** 描述 */
private String description;
}
【报告模版.html】
报告模版
报告模版
template.name
出险日期
描述
【测试代码】
@Autowired private TemplateEngine engine;
public void test() throws Exception {
// 创建model,需要指定模版引擎和具体的模版,“报告模版”指的是资源目录下/html2pdfTemplate/报告模版.html文件。如果是springboot项目,那么就是在resources文件夹下面
Model model = new Model(engine,"报告模版");
model.setName("名称");
List 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文件的需求