Java后臺生成pdf文件

前段時間因為相關(guān)業(yè)務(wù)需求需要后臺生成pdf文件,對于一直crud的程序員來說,這無疑是需要一定時間來做技術(shù)預研的。下面根據(jù)我的實踐經(jīng)驗總結(jié)一下我是如何使用java生成pdf文件的。

根據(jù)spring mvc的設(shè)計模式,理論上來說,我們可以把pdf文件視作一個View視圖,那么整個mvc模型如下圖:
image-20210608110457870.png

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

1.如何把model轉(zhuǎn)換成html?

這個問題spring mvc已經(jīng)替我們解決了,thymeleaf的實現(xiàn)無非就是一個活生生的model轉(zhuǎn)換成html的例子。

2.html如何轉(zhuǎn)換成pdf?

基于IText 基于FlyingSaucer 基于WKHtmlToPdf 基于pd4ml
跨平臺性 跨平臺 跨平臺 跨平臺 跨平臺
是否安裝軟件 需安裝WKHtmlToPdf
是否收費 免費 免費 免費 收費
轉(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)點 不需安裝軟件、轉(zhuǎn)換速度快 不需安裝軟件、轉(zhuǎn)換速度快 生成PDF質(zhì)量高 不需要安裝軟件、轉(zhuǎn)換速度快
缺點 對html標簽嚴格,少一個結(jié)束標簽就會報錯;服務(wù)器需要安裝字體 對html標簽嚴格,少一個結(jié)束標簽就會報錯;服務(wù)器需要安裝字體 需要安裝軟件、時間效率不高 對部分CSS樣式不支持。
分頁 圖片 表格 鏈接 中文 特殊字符 整體樣式 速度
------------ ---- ---- ---- ---- ---- -------- -------- ----
IText 支持 支持 支持 支持 支持 支持 失真問題
FlyingSaucer 未知 未知 未知 未知 未知 未知 未知
WKHtmlToPdf 支持 支持 支持 支持 支持 支持 很好
pd4ml 支持 支持 支持 支持 支持 支持 失真問題

對比以上各類實現(xiàn):

1.WKHtmlToPdf因為轉(zhuǎn)換速度慢、需要安裝軟件的缺點被暫時排除在外;pd4ml因為是收費的,并且同樣存在一些常見的樣式失真問題,直接排除;

2.剩下的就是在IText和FlyingSaucer的實現(xiàn)方案中做選擇,對比之下,選擇IText作為我們的最終實現(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>

【代碼實現(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

以上實現(xiàn)就完成了html轉(zhuǎn)換成pdf的功能,后續(xù)就是model轉(zhuǎn)html:

因為我使用的是springboot,所以直接使用以下依賴。小伙伴可以根據(jù)自身項目具體情況使用對應的依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

【代碼實現(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();
        // 對應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è)置模版前綴,相當于需要在資源文件夾中創(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;
    }
}

因為我們的依賴是基于springboot的,所以為了不讓spring-boot-starter-thymeleaf自動配置,我們需要排除相關(guān)的配置類。不想這樣做的小伙伴可使用thymeleaf其他依賴,原理上都一樣。

@SpringBootApplication(exclude = ThymeleafAutoConfiguration.class)

至此,所有的技術(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項目,那么就是在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");
    }

根據(jù)以上理論和實踐,我們已經(jīng)達到了我們的目標,最終完成了數(shù)據(jù)轉(zhuǎn)換成PDF文件的需求

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容