Java后台生成pdf文件

767 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

前段时间因为相关业务需求需要后台生成pdf文件,对于一直crud的程序员来说,这无疑是需要一定时间来做技术预研的。下面根据我的实践经验总结一下我是如何使用java生成pdf文件的。

根据spring mvc的设计模式,理论上来说,我们可以把pdf文件视作一个View视图,那么整个mvc模型如下图:

image-20210608110457870.png

如果按照上图所示,那么我们要编写一个pdf视图解析器,这无疑是一个有难度的事情。但是把思路转换一下,我们可以先把model转换成html,再通过html转换成pdf是不是会更容易一点?

image-20210608130313097.png

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