Java后臺生成pdf文件

前段時間因?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模型如下圖:
image-20210608110457870.png

如果按照上圖所示,那么我們要編寫一個pdf視圖解析器秫筏,這無疑是一個有難度的事情诱鞠。但是把思路轉(zhuǎn)換一下,我們可以先把model轉(zhuǎn)換成html这敬,再通過html轉(zhuǎn)換成pdf是不是會更容易一點(diǎn)航夺?
image-20210608130313097.png

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文件的需求

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谋竖,一起剝皮案震驚了整個濱河市红柱,隨后出現(xiàn)的幾起案子承匣,更是在濱河造成了極大的恐慌,老刑警劉巖锤悄,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件韧骗,死亡現(xiàn)場離奇詭異,居然都是意外死亡零聚,警方通過查閱死者的電腦和手機(jī)袍暴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來隶症,“玉大人政模,你說我怎么就攤上這事÷旎幔” “怎么了淋样?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長胁住。 經(jīng)常有香客問我趁猴,道長,這世上最難降的妖魔是什么彪见? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任儡司,我火速辦了婚禮,結(jié)果婚禮上余指,老公的妹妹穿的比我還像新娘捕犬。我一直安慰自己,他們只是感情好酵镜,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布碉碉。 她就那樣靜靜地躺著,像睡著了一般笋婿。 火紅的嫁衣襯著肌膚如雪誉裆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天缸濒,我揣著相機(jī)與錄音足丢,去河邊找鬼。 笑死庇配,一個胖子當(dāng)著我的面吹牛斩跌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播捞慌,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼耀鸦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起袖订,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤氮帐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后洛姑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體上沐,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年楞艾,在試婚紗的時候發(fā)現(xiàn)自己被綠了参咙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡硫眯,死狀恐怖蕴侧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情两入,我是刑警寧澤净宵,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站谆刨,受9級特大地震影響塘娶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜痊夭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望脏里。 院中可真熱鬧她我,春花似錦、人聲如沸迫横。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽矾踱。三九已至恨狈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間呛讲,已是汗流浹背禾怠。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留贝搁,地道東北人吗氏。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像雷逆,于是被迫代替她去往敵國和親弦讽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容