Java 使用freemarker、wkhtmltopdf根据自定义ftl模板导出html以及pdf

Java使用freemarker、wkhtmltopdf根据自定义ftl模板导出html以及pdf

  • 一、先看一下效果:
    • 1. 控制台输出的:
    • 2. 生成的文件:
    • 3. pdf内容:
  • 二、准备工作
  • 三、项目结构如下:
  • 四、具体实现代码如下:
    • 1. 用到的jar(主要的就是这几个)
    • 2. 将模板生成html (HtmlUtil.java)
    • 3. HtmlToPdfInterceptor.java
    • 4. Html转pdf (HtmlToPdf.java)
    • 5. 测试类(HtmlToPdfMain.java)
    • 6. ftl模板(model.ftl)

这段时间要搞一个导出pdf功能,刚开始想着用模板导出,结果发现不符合要求,因为我要动态的导出pdf,如果用一个pdf模板导出的话就不太灵活,因为你的模板是什么样的你导出的pdf也就是什么样的,所以我费了千辛万苦找到了一个解决办法,利用freemarker的ftl模板导出成html然后通过html导出相应的pdf,因为html里面自己想怎么写就怎么写,这样不就是非常完美了吗,想想就很激动,话不多说,下面我把相应的jar、模板、工具类以及测试类都粘出来了,有兴趣的小伙伴可以搞搞。

我这里用到的是SpringBoot项目演示的

一、先看一下效果:

(把下面的所有文件都弄到自己的项目中,然后运行第五个HtmlToPdfMain.java中的runMain方法就行了)

1. 控制台输出的:

控制台输出的

2. 生成的文件:

在这里插入图片描述

3. pdf内容:

(我这里弄得是一个表格数据,如果有别的需求可以修改model.ftl模板来响应的时间具体页面)

在这里插入图片描述

二、准备工作

1.先在网上下个wkhtmltopdf,地址是:点击下载(如果不想在官网上下载的话、随便找个下载就行)
2.然后安装
3.记住你的安装路径。这个地址后面会用到,在HtmlToPdf.java中的toPdfTool就是你的安装路径拼上你安装的应用程序

安装工作就到这里了,下面来看具体的实现

三、项目结构如下:

在这里插入图片描述

四、具体实现代码如下:

1. 用到的jar(主要的就是这几个)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.30</version>
        </dependency>
        <dependency>
            <groupId>org.apache.ant</groupId>
            <artifactId>ant</artifactId>
            <version>1.10.9</version>
        </dependency>
        <!--PDF-->
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.4.3</version>
        </dependency>

        <!--单元测试-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>compile</scope>
        </dependency>
        <!--单元测试的时候获取request-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.8.RELEASE</version>
        </dependency>

2. 将模板生成html (HtmlUtil.java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.example.demo.pro;


import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

import java.io.*;
import java.util.Map;

/**
 * 使用.ftl模板 生成Html工具类
 */
public class HtmlUtil {<!-- -->
    /**
     * @param modelpath 模板路径 不包含模板名称
     * @param template 模板名称
     * @param dataMap 数据
     * @param descPath 临时路径
     * @param fileName 临时文件名称
     * @throws IOException
     * @throws TemplateException
     */
    public static void createHtml(String modelpath,String template, Map dataMap,String descPath,String fileName) throws IOException, TemplateException {<!-- -->
        Configuration configuration=new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
        //如果你用的是版本低的freemarker 可以用地下的这个创建Configuration
        //Configuration configuration=new Configuration();

        configuration.setDefaultEncoding("utf-8");

        //如果你的模板放到了静态资源下的话就用这个 传入相应的路径就行
        configuration.setDirectoryForTemplateLoading(new File(modelpath));
        //如果模板和HtmlUtil这了类放在同一个文件夹下的话用这个
        //configuration.setClassForTemplateLoading(HtmlUtil.class,"");

        Template t = configuration.getTemplate(template);
        configuration.setClassicCompatible(true);
        File file = new File(descPath+File.separator+fileName);
        String parent = file.getParent();
        File dir = new File(parent);
        if (parent!=null&&!dir.exists()){<!-- -->
            dir.mkdirs();
        }
        boolean exists = file.exists();
        if (!exists){<!-- -->
            file.createNewFile();
        }
        Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file),"UTF-8"));
        t.process(dataMap, out);
        out.flush();
        out.close();
    }
}

3. HtmlToPdfInterceptor.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example.demo.pro;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class HtmlToPdfInterceptor extends Thread {<!-- -->
    private InputStream is;


    public HtmlToPdfInterceptor(InputStream is) {<!-- -->
        // TODO Auto-generated constructor stub
        this.is = is;
    }

    /**
     * 起一个线程执行流的结果 可有可无 想看日志的话可以要
     */
    public void run(){<!-- -->
        try{<!-- -->
            InputStreamReader isr = new InputStreamReader(is, "utf-8");
            BufferedReader br = new BufferedReader(isr);
            String line = null;
            while ((line = br.readLine()) != null) {<!-- -->
                System.out.println("-------*******-------"+line.toString()); //输出内容
            }
        }catch (IOException e){<!-- -->
            e.printStackTrace();
        }
    }

}

4. Html转pdf (HtmlToPdf.java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.example.demo.pro;


import java.io.File;
import java.io.IOException;

/**
 * html转pdf
 */
public class HtmlToPdf {<!-- -->
    // wkhtmltopdf在系统中安装的路径 第一个wkhtmltopdf是安装的路径 后面是安装后文件中的exe
    private static  String toPdfTool = "D:\\Exe\\wkhtmltopdf\\bin\\wkhtmltopdf.exe";

    /**
     * html转pdf
     *
     * @param srcPath
     *            html路径,可以是硬盘上的路径,也可以是网络路径
     * @param destPath
     *            pdf保存路径
     * @return 转换成功返回true
     */
    public static boolean convert(String srcPath, String destPath) throws IOException {<!-- -->
        File file = new File(destPath);
        File parent = file.getParentFile();
        // 如果pdf保存路径不存在,则创建路径
        if (!parent.exists()) {<!-- -->
            parent.mkdirs();
        }
        if (!file.exists()){<!-- -->
            file.createNewFile();
        }
        StringBuilder cmd = new StringBuilder();
        if (System.getProperty("os.name").indexOf("Windows") == -1) {<!-- -->
            // 非windows 系统
            toPdfTool = "/inco/usr/local/bin/wkhtmltopdf";
        }
        cmd.append(toPdfTool);
        cmd.append(" ");
        cmd.append("  --header-line");// 页眉下面的线
        cmd.append(" --header-center Pdf标头 ");//页眉中间内容
        cmd.append("  --margin-top 3cm ");// 设置页面上边距 (default 10mm)
        //cmd.append(" --header-html  file:///" + "https://blog.csdn.net/x6582026/article/details/53835835");// (添加一个HTML页眉,后面是网址)
        cmd.append(" --header-spacing 5 ");// (设置页眉和内容的距离,默认0)
        cmd.append(" --footer-center 第[page]页/共[topage]页");//设置在中心位置的页脚内容
        //cmd.append(" --footer-html  file:///" + "https://blog.csdn.net/x6582026/article/details/53835835");// (添加一个HTML页脚,后面是网址)
        cmd.append(" --footer-line");// * 显示一条线在页脚内容上)
        cmd.append(" --footer-spacing 5 ");// (设置页脚和内容的距离)
        File file1 = new File(srcPath);
        cmd.append(file1.getAbsolutePath());
        cmd.append(" ");
        String absolutePath = file.getAbsolutePath();
        cmd.append(absolutePath);
        boolean result = true;
        try {<!-- -->
            Process proc = Runtime.getRuntime().exec(cmd.toString());
            //起一个线程执行流的结果 可有可无 想看日志的话可以要 不想的话可以删除当前行的下面四行
            HtmlToPdfInterceptor error = new HtmlToPdfInterceptor(proc.getErrorStream());
            HtmlToPdfInterceptor output = new HtmlToPdfInterceptor(proc.getInputStream());
            error.start();
            output.start();
            proc.waitFor();
        } catch (Exception e) {<!-- -->
            result = false;
            e.printStackTrace();
        }

        return result;
    }
}

5. 测试类(HtmlToPdfMain.java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package com.example.demo.pro;

import freemarker.template.TemplateException;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class HtmlToPdfMain {<!-- -->

    //声明request变量
    private MockHttpServletRequest request = new MockHttpServletRequest();

    //run
    @Test
    public void runMain() throws IOException, TemplateException {<!-- -->
        //创建request对象并设置字符编码
        request.setCharacterEncoding("UTF-8");
        //如果是linux系统的话将\\改为File.separator来代替
        //生成html
        //第一个参数 模板路径,  第二个参数 模板名称, 第三个参数 数据源, 第四个参数 保存路径, 第五个参数 保存名称
        HtmlUtil.createHtml(ToPath(request),"model.ftl",DataMap(),"E:\","model.html");
        //将html转换为Pdf
        HtmlToPdf.convert("E:\\model.html","E:\\model.pdf");
    }




    /**
     * 获取路径
     * @param request
     * @return
     */
    public static String ToPath(MockHttpServletRequest request){<!-- -->
        //path1 根据class的地址获取项目编译后的路径
        //如果这种获取的方法在linux环境上获取的地址不对,那就使用request的方法获取路径 具体写法请看path
        String path = request.getServletContext().getRealPath("/templates/");
        System.out.println("Request获取Path = " + path);

        String path1 = (String.valueOf(HtmlToPdfMain.class.getResource("/templates/"))).replaceAll("file:/", "").replaceAll("%20", " ").trim();
        System.out.println("类名获取Path1 = " + path1);

        String path2 = (String.valueOf(HtmlToPdfMain.class.getResource(""))).replaceAll("file:/", "").replaceAll("%20", " ").trim();
        System.out.println("获取当前类的所在路径Path2 = " + path2);

        String path3 = (String.valueOf(HtmlToPdfMain.class.getResource("/"))).replaceAll("file:/", "").replaceAll("%20", " ").trim();
        System.out.println("获取当前类的所在路径Path3 = " + path3);

        return path;
    }

    /**
     * 数据源
     * @return
     */
    public static Map DataMap(){<!-- -->
        Map map = new HashMap();
        map.put("jsxm", "甄士隐 ");
        map.put("ssyx", "北京师范学院 ");
        map.put("sksj", "2020-11-08 4444 ");

        List<Map> user = new ArrayList<>();
        Map a = new ConcurrentHashMap();
        a.put("tmlx","教师教学状态(80分)");
        a.put("tmmc","思政育人(10分)");
        a.put("tmxx","A.在教学");
        a.put("tmfz","10");//分数
        a.put("sfxz","0");//0是没有选着 1是选着了
        a.put("tmlxkhsl","4");//题目类型跨行数量
        a.put("tmmckhsl","2");//题目名称跨行数量
        user.add(a);
        a = new ConcurrentHashMap();
        a.put("tmlx","教师教学状态(80分)");
        a.put("tmmc","思政育人(10分)");
        a.put("tmxx","B.在教学");
        a.put("tmfz","8");
        a.put("sfxz","1");
        a.put("tmlxkhsl","4");//题目类型跨行数量
        a.put("tmmckhsl","2");//题目名称跨行数量
        user.add(a);
        a = new ConcurrentHashMap();
        a.put("tmlx","教师教学状态(80分)");
        a.put("tmmc","精神状态(10分)");
        a.put("tmxx","A.精神饱满");
        a.put("tmfz","10");
        a.put("sfxz","0");
        a.put("tmlxkhsl","4");//题目类型跨行数量
        a.put("tmmckhsl","2");//题目名称跨行数量
        user.add(a);
        a = new ConcurrentHashMap();
        a.put("tmlx","教师教学状态(80分)");
        a.put("tmmc","精神状态(10分)");
        a.put("tmxx","B.精神饱满");
        a.put("tmfz","8");
        a.put("sfxz","1");
        a.put("tmlxkhsl","4");//题目类型跨行数量
        a.put("tmmckhsl","2");//题目名称跨行数量
        user.add(a);
        map.put("userList",user);
        return map;
    }
}

6. ftl模板(model.ftl)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        *{<!-- -->
            margin: 0;
            padding: 0;
        }
    </style>
</head>
<body style="text-align: center;">
<#--   这里些相应的html就行-->
<#--   这里我把freemarker经常用到的几个标签写在了下面-->

<#--assign 类似 c:set 一样 就是初始化一个变量-->
<#--list 类似 c:forEach 一样 循环list-->
<#--if 类似 c:if 一样 判断条件 -->
<#--在html中取值的话统一用${字段名} 在freemarker中取值是直接写字段名就行-->

<div class="tab-main">
    <table border="1" style="margin: auto;">
        <tr>
            <td style="width:10%;">教师姓名</td>
            <td style="width:15%;">${<!-- -->jsxm}</td>
            <td style="width:10%;">所属院部</td>
            <td style="width:35%;">${<!-- -->ssyx}</td>
            <td style="width:10%;">上课时间</td>
            <td style="width:20%;" colspan="2">${<!-- -->sksj}</td>
        </tr>
        <tr>
            <td rowspan="2" colspan="2">评价内容</td>
            <td colspan="6">评价结果</td>
        </tr>
        <tr>
            <td colspan="3">评价标准</td>
            <td>分值</td>
            <td>评价</td>
        </tr>

        <#--    循环开始-->
        <#assign tmlx = '' tmmc = '' tmxx = '' tmfz = '' sfxz = '' tmlxkhsl ='' tmmckhsl=''/><#--这里定义了几个变量-->
        <#list userList as user>
            <tr>
                <#if tmlx??><#--这个代表tmlx不等于空-->
                    <#if tmlx == user.tmlx >
                    <#else>
                        <#assign tmlx = user.tmlx/>
                        <td rowspan="${user.tmlxkhsl}">${<!-- -->user.tmlx}</td>
                    </#if>
                <#else><#--否者 如果还有if的话就是 <#elseif 判断条件> -->
                    <#assign tmlx = user.tmlx/>
                    <td rowspan="${user.tmlxkhsl}">${<!-- -->user.tmlx}</td>
                </#if>

                <#if tmmc??>
                    <#if tmmc == user.tmmc >
                    <#else>
                        <#assign tmmc = user.tmmc/>
                        <td rowspan="${user.tmmckhsl}">${<!-- -->user.tmmc}</td>
                    </#if>
                <#else>
                    <#assign tmmc = user.tmmc/>
                    <td rowspan="${user.tmmckhsl}">${<!-- -->user.tmmc}</td>
                </#if>

                <td colspan="3">${<!-- -->user.tmxx}</td>
                <td >${<!-- -->user.tmfz}</td>

                <#if user.sfxz?? && user.sfxz == '1' >
                    <td>√</td>
                <#else>
                    <td></td>
                </#if>
            </tr>
        </#list>
        <#--    //结束-->

    </table>
</div>
</body>
</html>