在我们日常开发中,经常会遇到导出pdf这种需求,比如导出合同、导出业务报告等。这中导出功能都有一个特点,导出的pdf中有大量相同的文本布局以及样式,只有涉及到用户本人的信息时出现不同的内容。我们把这些相同的部分称作模版,在模版中放置一些变量来代表用户信息,比如用户姓名、年龄等。这样我们在导出pdf的时候,在数据库中把用户信息查出来,对模版中对应的变量进行替换,再把替换的结果转成pdf文件就可以了。
模版的类型有很多种:html模版、doc模版、excel模版、pdf模版等等。项目中使用哪一种要具体情况具体考虑。
将变量替换后的模版转成pdf文件的工具也有很多,最主流最方面的当然要数itextpdf了。它可以将常见的任何形式的模版转成pdf文件。
前几天俺就遇到一个导出pdf的需求,而且该pdf有点花里胡哨,明显存在大量css样式,所以我们就采用html作为模版,通过itextpdf将html转成pdf。主要步骤如下:
如何对html模版进行变量替换,生成html页面文本,这里向大家提供两个方案,这两个方案各有优缺点,可依个人情况选择。
使用jsoup工具
该工具处理html文本十分友好。你可以直接根据id、class等属性来获取对应的html元素(如:getElementsByAttributeValue("id", "value")),然后对获取的元素通过text()方法设置文本内容。这有点类似python的爬虫工具beautifulSoup,
使用模版引擎,以thymeleaf为例
类似于jsp,thymeleaf支持HTML5作为模版文件,其提供的模版引擎十分强大,而且在spring官方文档中首推的模版引擎就是thymleaf,spring也默认集成了thymleaf,足以可见他的强大。
引入依赖
我们引入spring-boot-starter-web和jsoup的依赖
org.springframework.boot spring-boot-starter-web com.itextpdf styled-xml-parser 7.2.3
创建模版
在resources下新建目录templates,并创建一个html模版文件:StudentReport.html
学生报告
| 学校 | 年级 | 班级 | 学生人数 |
| 姓名 | 性别 | 年龄 | 父亲 | 母亲 |
|---|---|---|---|---|
在浏览器里打开该html模版如下图所示

变量替换的逻辑
如果html模版的结构相对来讲比较简单的话,变量替换的逻辑便不难理解。但若遇到复杂的结构,该逻辑便有点力不从心了,因为它具有一定的局限性,而且针对复杂的结构,变量替换的逻辑相对也会更加复杂。
// 变量替换,src-html模版位置,params-进行变量替换的真实数据,key与html模版中标签的id属性一致,value为真实数据 public static String placeholder(String src, Mapparams) throws IOException { File file = new File(src); // 通过Jsoup创建Document对象,Document就可以表示整个html文本了。 Document document = Jsoup.parse(file, "utf-8"); // 设置内容文本,真正进行变量替换的方法 setText(document, params); // 将变量替换好以后,输出html文本 String outerHtml = document.outerHtml(); System.out.println(outerHtml); return outerHtml; } // 给html模版设置文本数据,document-html模版,params-进行变量替换的真实数据 private static void setText(Document document, Map params) { Set > entrySet = params.entrySet(); for (Map.Entry entry : entrySet) { // 获取最后一个对应的element Element element = document.getElementsByAttributeValue("id", entry.getKey()).last(); if ("tr".equals(element.tagName())) { List
测试
我们写一个Controller,通过接口来测试上面的方法
@RestController
@RequestMapping("/student")
public class StudentController {
@GetMapping("/placehold/jsoup")
public String jsoup() throws IOException {
// 获取html模版文件
File tmpl = new ClassPathResource("templates/StudentReport.html").getFile();
// 模拟数据库中查询的数据
Map params = new HashMap<>();
params.put("school", "家里蹲大学");
params.put("grade", "八年级");
params.put("class", "三班");
params.put("studentNum", 999);
params.put("situation", "这个班的学生相当吊炸天,勿以善小而不为,勿以恶小而为之,关关雎鸠,在水之洲。窈窕淑女,君子好逑。");
List> counselList = new ArrayList<>();
counselList.add(getCounsel("周一", "男", 32, "周一他爸", "周一他妈"));
counselList.add(getCounsel("周二", "女", 42, "周二他爸", "周二他妈"));
counselList.add(getCounsel("周三", "男", 54, "周三他爸", "周三他妈"));
counselList.add(getCounsel("周四", "男", 13, "周四他爸", "周四他妈"));
counselList.add(getCounsel("周五", "女", 43, "周五他爸", "周五他妈"));
counselList.add(getCounsel("周六", "女", 74, "周六他爸", "周六他妈"));
counselList.add(getCounsel("周日", "男", 22, "周日他爸", "周日他妈"));
params.put("studentList", counselList);
String html = JsoupPlaceholdUtil.placeholder(tmpl, params);
return html;
}
private Map getCounsel(String name, String sex, Integer age, String father, String mother) {
Map params = new HashMap<>();
params.put("name", name);
params.put("sex", sex);
params.put("age", age);
params.put("father", father);
params.put("mother", mother);
return params;
}
}
在浏览器中访问该接口,localhost:port/student/placehold/jsoup
从浏览器中我们可以看到,真实数据已经完美地放在html文本中了

模版引擎我们选择thymleaf的原因是spring天然支持,无需对其集成进行多余的配置,只需要引入依赖就可以使用了。
引入依赖
我们引入spring-boot-starter-web和thymeleaf的依赖
org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-thymeleaf
创建模版
使用thymleaf模版引擎,就需要按照它的要求通过给html标签添加各种th:属性来写html模版。在resources下新建目录templates,并创建一个html模版文件:StudentReportTH.html。
学生报告
| 学校 | 年级 | 班级 | 学生人数 |
| XX学校 | XX年级 | XX班级 | 0 |
| 姓名 | 性别 | 年龄 | 父亲 | 母亲 |
|---|---|---|---|---|
| XXX | XX | XXX | XXX | XXX |
在浏览器里打开该html模版如下图所示

变量替换
有了thymeleaf,变量替换的任何细节我们都不用关心,只需要把模版和数据交给它就可以了。只需要仅仅4行代码。
另外在springboot中已经自动将thymleaf添加到IOC容器中了,我们只需要依赖注入就可以了。
@Autowired
private WebApplicationContext applicationContext;
@Autowired
private LocaleResolver localeResolver;
@Autowired
private SpringTemplateEngine springTemplateEngine;
public String thymeleaf(HttpServletRequest request, HttpServletResponse response) {
// 实际数据
Map params = new HashMap<>();
// 变量替换
Writer writer = new FastStringWriter();
WebExpressionContext context = new WebExpressionContext(springTemplateEngine.getConfiguration(),
request,
response,
applicationContext.getServletContext(),
localeResolver.resolveLocale(request),
params);
// springboot对thymeleaf的默认配置为 prefix="classpath:templates", suffix=".html"
springTemplateEngine.process("StudentReportTH", context,writer);
return s = writer.toString();
}
测试
我们写一个Controller,通过接口来测试上面的方法
@RestController
@RequestMapping("/student")
public class StudentController {
@Autowired
private WebApplicationContext applicationContext;
@Autowired
private LocaleResolver localeResolver;
@Autowired
private SpringTemplateEngine springTemplateEngine;
private Map getCounsel(String name, String sex, Integer age, String father, String mother) {
Map params = new HashMap<>();
params.put("name", name);
params.put("sex", sex);
params.put("age", age);
params.put("father", father);
params.put("mother", mother);
return params;
}
@GetMapping("/placehold/thymeleaf")
public String thymeleaf(HttpServletRequest request, HttpServletResponse response) {
Map params = new HashMap<>();
params.put("school", "家里蹲大学");
params.put("grade", "八年级");
params.put("class", "三班");
params.put("studentNum", 999);
params.put("situation", "这个班的学生相当吊炸天,勿以善小而不为,勿以恶小而为之,关关雎鸠,在水之洲。窈窕淑女,君子好逑。");
List> counselList = new ArrayList<>();
counselList.add(getCounsel("周一", "男", 32, "周一他爸", "周一他妈"));
counselList.add(getCounsel("周二", "女", 42, "周二他爸", "周二他妈"));
counselList.add(getCounsel("周三", "男", 54, "周三他爸", "周三他妈"));
counselList.add(getCounsel("周四", "男", 13, "周四他爸", "周四他妈"));
counselList.add(getCounsel("周五", "女", 43, "周五他爸", "周五他妈"));
counselList.add(getCounsel("周六", "女", 74, "周六他爸", "周六他妈"));
counselList.add(getCounsel("周日", "男", 22, "周日他爸", "周日他妈"));
params.put("studentList", counselList);
Writer writer = new FastStringWriter();
WebExpressionContext context = new WebExpressionContext(springTemplateEngine.getConfiguration(),
request,
response,
applicationContext.getServletContext(),
localeResolver.resolveLocale(request),
params);
// springboot对thymeleaf的默认配置为 prefix="classpath:templates", suffix=".html"
springTemplateEngine.process("StudentReportTH", context,writer);
return s = writer.toString();
}
}
在浏览器中访问该接口,localhost:port/student/placehold/thymeleaf
从浏览器中我们可以看到,真实数据已经完美地放在html文本中了,处理变量替换的逻辑也就四行。

上面我们通过两种方式对html模版进行变量替换并得到html文本内容了。接下来要做的就是把html文本内容转成pdf。
在上面pom.xml的基础上中引入依赖
com.itextpdf itext7-core 7.2.3 com.itextpdf html2pdf 4.0.3
使用itextpdf将html文件转成pdf的过程也是相当简单。
// 设置字体
ConverterProperties converterProperties = new ConverterProperties();
FontSet fontSet = new FontSet();
if (!fontSet.addFont("C:\\Windows\\Fonts\\simhei.ttf")) {
throw new RuntimeException("获取字体失败");
}
converterProperties.setFontProvider(new FontProvider(fontSet));
// html转pdf, 并将pdf作为字节数组保存在bos中
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HtmlConverter.convertToPdf(jsoupHtml, bos, converterProperties);
然后我们对上面两种方式生成的html文本内容进行转换。
对jsoup生成的html文本内容进行转换并测试
@GetMapping("/export/jsoup")
public void exportJsoup(HttpServletResponse response) throws IOException {
String jsoupHtml = jsoup();
// 设置字体
ConverterProperties converterProperties = new ConverterProperties();
FontSet fontSet = new FontSet();
if (!fontSet.addFont("C:\\Windows\\Fonts\\simhei.ttf")) {
throw new RuntimeException("获取字体失败");
}
converterProperties.setFontProvider(new FontProvider(fontSet));
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HtmlConverter.convertToPdf(jsoupHtml, bos, converterProperties);
String fileName = "将jsoup生成的html转换成pdf文件";
// 设置中文文件名
fileName = new String(fileName.getBytes("utf-8"),"iso8859-1");
String encode = URLEncoder.encode(fileName, "iso8859-1");
ServletOutputStream outputStream = response.getOutputStream();
response.setContentType("application/x-download");
response.addHeader("Content-Disposition", "attachment; filename=" + encode + ".pdf");
response.setCharacterEncoding("UTF-8");
outputStream.write(bos.toByteArray());
}
调用接口下载pdf文件

然后打开下载的pdf文件

对thymeleaf生成的html文本内容进行转换并测试
与上面的步骤相同,接口如下
@GetMapping("/export/thymeleaf")
public void exportThymeleaf(HttpServletRequest request, HttpServletResponse response) throws IOException {
String jsoupHtml = thymeleaf(request, response);
// 设置字体
ConverterProperties converterProperties = new ConverterProperties();
FontSet fontSet = new FontSet();
if (!fontSet.addFont("C:\\Windows\\Fonts\\simhei.ttf")) {
throw new RuntimeException("获取字体失败");
}
converterProperties.setFontProvider(new FontProvider(fontSet));
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HtmlConverter.convertToPdf(jsoupHtml, bos, converterProperties);
String fileName = "将thymeleaf生成的html转换成pdf文件";
// 设置中文文件名
fileName = new String(fileName.getBytes("utf-8"),"iso8859-1");
String encode = URLEncoder.encode(fileName, "iso8859-1");
ServletOutputStream outputStream = response.getOutputStream();
response.setContentType("application/x-download");
response.addHeader("Content-Disposition", "attachment; filename=" + encode + ".pdf");
response.setCharacterEncoding("UTF-8");
outputStream.write(bos.toByteArray());
}
同样地通过接口将pdf下载到本机,查看pdf

纸上得来终觉浅,绝知此事要躬行。
————我是万万岁,我们下期再见————