前段時間因?yàn)橄嚓P(guān)業(yè)務(wù)需求需要后臺生成pdf文件己肮,對于一直crud的程序員來說,這無疑是需要一定時間來做技術(shù)預(yù)研的悲关。下面根據(jù)我的實(shí)踐經(jīng)驗(yàn)總結(jié)一下我是如何使用java生成pdf文件的谎僻。
根據(jù)spring mvc的設(shè)計模式,理論上來說寓辱,我們可以把pdf文件視作一個View視圖艘绍,那么整個mvc模型如下圖:如果按照上圖所示,那么我們要編寫一個pdf視圖解析器秫筏,這無疑是一個有難度的事情诱鞠。但是把思路轉(zhuǎn)換一下,我們可以先把model轉(zhuǎn)換成html这敬,再通過html轉(zhuǎn)換成pdf是不是會更容易一點(diǎn)航夺?
1.如何把model轉(zhuǎn)換成html?
這個問題spring mvc已經(jīng)替我們解決了鹅颊,thymeleaf的實(shí)現(xiàn)無非就是一個活生生的model轉(zhuǎn)換成html的例子敷存。
2.html如何轉(zhuǎn)換成pdf?
基于IText | 基于FlyingSaucer | 基于WKHtmlToPdf | 基于pd4ml | |||||
---|---|---|---|---|---|---|---|---|
跨平臺性 | 跨平臺 | 跨平臺 | 跨平臺 | 跨平臺 | ||||
是否安裝軟件 | 否 | 否 | 需安裝WKHtmlToPdf | 否 | ||||
是否收費(fèi) | 免費(fèi) | 免費(fèi) | 免費(fèi) | 收費(fèi) | ||||
轉(zhuǎn)換Html效率 | 速度快 | 未測 | 速度慢堪伍。相比URL來說锚烦,效率較慢。能忽略一些html語法或資源是否存在問題帝雇。 | 速度快涮俄。部分CSS樣式不支持。 | ||||
效果 | 存在樣式失真問題尸闸。對html語法有一定要求 | 存在樣式失真問題彻亲。對html語法有較高要求。 | 失真情況較小吮廉,大部分網(wǎng)頁能按Chome瀏覽器顯示的頁面轉(zhuǎn)換 | 部分CSS樣式有問題苞尝。 | ||||
轉(zhuǎn)換URL效率 | 未測 | 未測 | 效率不是特別高 | 未測 | ||||
效果 | 未測 | 未測 | 部分網(wǎng)頁由于其限制,或?qū)⒊霈F(xiàn)html網(wǎng)頁不完整宦芦。 | 未測 | ||||
優(yōu)點(diǎn) | 不需安裝軟件宙址、轉(zhuǎn)換速度快 | 不需安裝軟件、轉(zhuǎn)換速度快 | 生成PDF質(zhì)量高 | 不需要安裝軟件调卑、轉(zhuǎn)換速度快 | ||||
缺點(diǎn) | 對html標(biāo)簽嚴(yán)格抡砂,少一個結(jié)束標(biāo)簽就會報錯大咱;服務(wù)器需要安裝字體 | 對html標(biāo)簽嚴(yán)格,少一個結(jié)束標(biāo)簽就會報錯注益;服務(wù)器需要安裝字體 | 需要安裝軟件碴巾、時間效率不高 | 對部分CSS樣式不支持。 | ||||
分頁 | 圖片 | 表格 | 鏈接 | 中文 | 特殊字符 | 整體樣式 | 速度 | |
------------ | ---- | ---- | ---- | ---- | ---- | -------- | -------- | ---- |
IText | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 失真問題 | 快 |
FlyingSaucer | 未知 | 未知 | 未知 | 未知 | 未知 | 未知 | 未知 | 快 |
WKHtmlToPdf | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 很好 | 慢 |
pd4ml | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 失真問題 | 快 |
對比以上各類實(shí)現(xiàn):
1.WKHtmlToPdf因?yàn)檗D(zhuǎn)換速度慢丑搔、需要安裝軟件的缺點(diǎn)被暫時排除在外厦瓢;pd4ml因?yàn)槭鞘召M(fèi)的,并且同樣存在一些常見的樣式失真問題低匙,直接排除旷痕;
2.剩下的就是在IText和FlyingSaucer的實(shí)現(xiàn)方案中做選擇碳锈,對比之下顽冶,選擇IText作為我們的最終實(shí)現(xiàn)方案
【相關(guān)依賴】
<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>
【代碼實(shí)現(xiàn)】
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
以上實(shí)現(xiàn)就完成了html轉(zhuǎn)換成pdf的功能,后續(xù)就是model轉(zhuǎn)html:
因?yàn)槲沂褂玫氖莝pringboot贸人,所以直接使用以下依賴间景。小伙伴可以根據(jù)自身項(xiàng)目具體情況使用對應(yīng)的依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
【代碼實(shí)現(xiàn)】
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;
}
/**
* 所有的參數(shù)數(shù)據(jù)
*
* @return
*/
private Map<String, Object> variables(){
Map<String, Object> variables = Maps.newHashMap();
// 對應(yīng)html模版中的template變量,取值的時候就按照“${template.字段名}”格式艺智,可自行修改
variables.put("template", this);
return variables;
};
/**
* 解析模版倘要,生成html
*
* @return
*/
public String process() {
Context ctx = new Context();
// 設(shè)置model
ctx.setVariables(variables());
// 根據(jù)model解析成html字符串
return engine.process(templateName(), ctx);
}
public void parse2Pdf(String targetPdfFilePath) throws GlobalException {
String html = process();
// 通過html轉(zhuǎn)換成pdf
HtmlUtil.html2Pdf(html, targetPdfFilePath);
}
}
創(chuàng)建模版引擎
@Configuration
public class TemplateEngineConfig {
// 注入TemplateEngine模版引擎
@Bean
public TemplateEngine templateEngine(){
ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver();
// 設(shè)置模版前綴,相當(dāng)于需要在資源文件夾中創(chuàng)建一個html2pdfTemplate文件夾十拣,所有的模版都放在這個文件夾中
resolver.setPrefix("/html2pdfTemplate/");
// 設(shè)置模版后綴
resolver.setSuffix(".html");
resolver.setCharacterEncoding("UTF-8");
// 設(shè)置模版模型為HTML
resolver.setTemplateMode("HTML");
TemplateEngine engine = new TemplateEngine();
engine.setTemplateResolver(resolver);
return engine;
}
}
因?yàn)槲覀兊囊蕾囀腔趕pringboot的封拧,所以為了不讓spring-boot-starter-thymeleaf自動配置,我們需要排除相關(guān)的配置類夭问。不想這樣做的小伙伴可使用thymeleaf其他依賴泽西,原理上都一樣。
@SpringBootApplication(exclude = ThymeleafAutoConfiguration.class)
至此,所有的技術(shù)準(zhǔn)備都做好了,如何使用我們編寫好的代碼實(shí)現(xiàn)model轉(zhuǎn)換pdf文件呢毅桃?
【示例】
import lombok.Data;
import org.thymeleaf.TemplateEngine;
import java.util.List;
@Data
public class Model extends AbstractTemplate {
// 構(gòu)造函數(shù)
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 {
// 創(chuàng)建model昔榴,需要指定模版引擎和具體的模版,“報告模版”指的是資源目錄下/html2pdfTemplate/報告模版.html文件烤低。如果是springboot項(xiàng)目,那么就是在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("擋風(fēng)玻璃破裂");
insuranceInfos.add(record2);
model.setInsuranceInfos(insuranceInfos);
//生成pdf,指定目標(biāo)文件路徑
model.parse2Pdf("/home/dev/桌面/test.pdf");
}
根據(jù)以上理論和實(shí)踐仔涩,我們已經(jīng)達(dá)到了我們的目標(biāo),最終完成了數(shù)據(jù)轉(zhuǎn)換成PDF文件的需求