往期文章
- springcloud整合knike4j聚合微服务接口文档
- spring源码 - 条件注解@ConditionnalOnClass的原理分析
- 用最简单的话讲最明白的红黑树
文章目录
一、介绍
在我们日常开发中,经常会遇到导出pdf这种需求,比如导出合同、导出业务报告等。这中导出功能都有一个特点,导出的pdf中有大量相同的文本布局以及样式,只有涉及到用户本人的信息时出现不同的内容。我们把这些相同的部分称作模版,在模版中放置一些变量来代表用户信息,比如用户姓名、年龄等。这样我们在导出pdf的时候,在数据库中把用户信息查出来,对模版中对应的变量进行替换,再把替换的结果转成pdf文件就可以了。
模版的类型有很多种:html模版、doc模版、excel模版、pdf模版等等。项目中使用哪一种要具体情况具体考虑。
将变量替换后的模版转成pdf文件的工具也有很多,最主流最方面的当然要数itextpdf了。它可以将常见的任何形式的模版转成pdf文件。
前几天俺就遇到一个导出pdf的需求,而且该pdf有点花里胡哨,明显存在大量css样式,所以我们就采用html作为模版,通过itextpdf将html转成pdf。主要步骤如下:
- 将html模版生成html页面文本,对模版进行变量替换
- 将html页面文本转成pdf文件
二、使用html模版生成html页面文本
如何对html模版进行变量替换,生成html页面文本,这里向大家提供两个方案,这两个方案各有优缺点,可依个人情况选择。
- 使用jsoup工具该工具处理html文本十分友好。你可以直接根据id、class等属性来获取对应的html元素(如:
getElementsByAttributeValue("id", "value")
),然后对获取的元素通过text()
方法设置文本内容。这有点类似python的爬虫工具beautifulSoup
,- 优点:只要知道html模版的结构就行。- 缺点:处理复杂的结构如表格时,可能会对模版的html标签结构进行修改,因此处理逻辑较为复杂。 - 使用模版引擎,以thymeleaf为例类似于
jsp
,thymeleaf支持HTML5作为模版文件,其提供的模版引擎十分强大,而且在spring官方文档中首推的模版引擎就是thymleaf,spring也默认集成了thymleaf,足以可见他的强大。- 优点:利用thymleaf模版引擎,只需三四行代码就可以完成整个html模版的变量替换。- 缺点:需要对thymleaf的使用有基本的了解。
1. 使用jsoup工具生成html页面文本
- 引入依赖我们引入
spring-boot-starter-web
和jsoup
的依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 处理xml风格的工具包,对html有效,包含Jsoup --><dependency><groupId>com.itextpdf</groupId><artifactId>styled-xml-parser</artifactId><version>7.2.3</version></dependency>
- 创建模版在resources下新建目录
templates
,并创建一个html模版文件:**StudentReport.html
**<!DOCTYPEhtml><html><head><metacharset="utf-8"><title></title></head><body><h1style="text-align: center;">学生报告</h1><!-- 班级信息 --><tableborder="1"style="text-align: center"><tr><td>学校</td><td>年级</td><td>班级</td><td>学生人数</td></tr><tr><!-- 变量:学校 --><tdid="school"></td><!-- 变量:年级 --><tdid="grade"></td><!-- 变量:班级 --><tdid="class"></td><!-- 变量:学生人数 --><tdid="studentNum"></td></tr></table><h3> 班级概况: </h3><!-- 变量:班级概况 --><pid="situation"style="text-indent: 2em;"></p><h3> 学生列表: </h3><!-- 学生列表 --><tableborder="1"><tbody><!-- 表头 --><tr><th>姓名</th><th>性别</th><th>年龄</th><th>父亲</th><th>母亲</th></tr><!-- 学生数据 --><trid="studentList"><!-- 变量:姓名 --><tdid="name"></td><!-- 变量:性别 --><tdid="sex"></td><!-- 变量:年龄 --><tdid="age"></td><!-- 变量:父亲 --><tdid="father"></td><!-- 变量:母亲 --><tdid="mother"></td></tr></tbody></table></body></html>
在浏览器里打开该html模版如下图所示 - 变量替换的逻辑如果html模版的结构相对来讲比较简单的话,变量替换的逻辑便不难理解。但若遇到复杂的结构,该逻辑便有点力不从心了,因为它具有一定的局限性,而且针对复杂的结构,变量替换的逻辑相对也会更加复杂。
// 变量替换,src-html模版位置,params-进行变量替换的真实数据,key与html模版中标签的id属性一致,value为真实数据publicstaticStringplaceholder(String src,Map<String,Object> params)throwsIOException{File file =newFile(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-进行变量替换的真实数据privatestaticvoidsetText(Document document,Map<String,Object> params){Set<Map.Entry<String,Object>> entrySet = params.entrySet();for(Map.Entry<String,Object> entry : entrySet){// 获取最后一个对应的elementElement element = document.getElementsByAttributeValue("id", entry.getKey()).last();if("tr".equals(element.tagName())){List<Map<String,Object>> counselList =(List<Map<String,Object>>)entry.getValue();// 设置行,就是把列表数据设置到html的表格行中setRowsText(document, element, counselList);}else{// 对html元素设置文本 element.text(entry.getValue().toString());}}}// 把列表数据设置到html的表格行中,document-html模版,element-表示一行的元素,即tr标签。list-真实列表数据privatestaticvoidsetRowsText(Document document,Element element,List<Map<String,Object>> list){if(list.isEmpty()){return;}Iterator<Map<String,Object>> iterator = list.iterator();do{Map<String,Object> counsel = iterator.next();// 设置文本数据setText(document, counsel);if(iterator.hasNext()){// 追加一行appendTableRow(element);}}while(iterator.hasNext());// 如果list集合中还有元素,则复制当前element追加到当前element后面,并循环到前面一步,// 如果list集合中没有元素了,则说明内容已经写完了,返回即可}// 扩展一行privatestaticvoidappendTableRow(Element element){Node parent = element.parent();Element tbody =(Element) parent; tbody.appendChild((Node) element.clone());}
- 测试我们写一个
Controller
,通过接口来测试上面的方法@RestController@RequestMapping("/student")publicclassStudentController{@GetMapping("/placehold/jsoup")publicStringjsoup()throwsIOException{// 获取html模版文件File tmpl =newClassPathResource("templates/StudentReport.html").getFile();// 模拟数据库中查询的数据Map<String,Object> params =newHashMap<>(); params.put("school","家里蹲大学"); params.put("grade","八年级"); params.put("class","三班"); params.put("studentNum",999); params.put("situation","这个班的学生相当吊炸天,勿以善小而不为,勿以恶小而为之,关关雎鸠,在水之洲。窈窕淑女,君子好逑。");List<Map<String,Object>> counselList =newArrayList<>(); 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;}privateMap<String,Object>getCounsel(String name,String sex,Integer age,String father,String mother){Map<String,Object> params =newHashMap<>(); 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文本中了
2. 使用模版引擎生成html页面文本
模版引擎我们选择thymleaf的原因是spring天然支持,无需对其集成进行多余的配置,只需要引入依赖就可以使用了。
- 引入依赖我们引入
spring-boot-starter-web
和thymeleaf
的依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- thymeleaf模版引擎 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>
- 创建模版使用thymleaf模版引擎,就需要按照它的要求通过给html标签添加各种th:属性来写html模版。在resources下新建目录
templates
,并创建一个html模版文件:**StudentReportTH.html
**。<!DOCTYPEhtml><htmlxmlns:th="http://www.thymeleaf.org"lang="en"><head><metacharset="utf-8"><title></title></head><body><h1style="text-align: center;">学生报告</h1><!-- 班级信息 --><tableborder="1"style="text-align: center"><tr><td>学校</td><td>年级</td><td>班级</td><td>学生人数</td></tr><tr><tdth:text="${school}">XX学校</td><tdth:text="${grade}">XX年级</td><tdth:text="${class}">XX班级</td><tdth:text="${studentNum}">0</td></tr></table><h3> 班级概况:</h3><pid="situation"style="text-indent: 2em;"></p><h3> 学生列表:</h3><!-- 学生列表 --><tableborder="1"><tbody><!-- 表头 --><tr><th>姓名</th><th>性别</th><th>年龄</th><th>父亲</th><th>母亲</th></tr><!-- 学生数据 --><trth:each="student in ${studentList}"th:if="${studentList}"><tdth:text="${student.name}">XXX</td><tdth:text="${student.sex}">XX</td><tdth:text="${student.age}">XXX</td><tdth:text="${student.father}">XXX</td><tdth:text="${student.mother}">XXX</td></tr></tbody></table></body></html>
在浏览器里打开该html模版如下图所示 - 变量替换有了thymeleaf,变量替换的任何细节我们都不用关心,只需要把模版和数据交给它就可以了。只需要仅仅4行代码。另外在springboot中已经自动将thymleaf添加到IOC容器中了,我们只需要依赖注入就可以了。
@AutowiredprivateWebApplicationContext applicationContext;@AutowiredprivateLocaleResolver localeResolver;@AutowiredprivateSpringTemplateEngine springTemplateEngine;publicStringthymeleaf(HttpServletRequest request,HttpServletResponse response){// 实际数据Map<String,Object> params =newHashMap<>();// 变量替换Writer writer =newFastStringWriter();WebExpressionContext context =newWebExpressionContext(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")publicclassStudentController{@AutowiredprivateWebApplicationContext applicationContext;@AutowiredprivateLocaleResolver localeResolver;@AutowiredprivateSpringTemplateEngine springTemplateEngine;privateMap<String,Object>getCounsel(String name,String sex,Integer age,String father,String mother){Map<String,Object> params =newHashMap<>(); 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")publicStringthymeleaf(HttpServletRequest request,HttpServletResponse response){Map<String,Object> params =newHashMap<>(); params.put("school","家里蹲大学"); params.put("grade","八年级"); params.put("class","三班"); params.put("studentNum",999); params.put("situation","这个班的学生相当吊炸天,勿以善小而不为,勿以恶小而为之,关关雎鸠,在水之洲。窈窕淑女,君子好逑。");List<Map<String,Object>> counselList =newArrayList<>(); 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 =newFastStringWriter();WebExpressionContext context =newWebExpressionContext(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页面文本转成pdf文件
上面我们通过两种方式对html模版进行变量替换并得到html文本内容了。接下来要做的就是把html文本内容转成pdf。
在上面
pom.xml
的基础上中引入依赖
<dependencies><!-- itext核心包 --><dependency><groupId>com.itextpdf</groupId><artifactId>itext7-core</artifactId><version>7.2.3</version></dependency><!-- html转pdf,包含类似于jsoup依赖的操作html文档的依赖 --><dependency><groupId>com.itextpdf</groupId><artifactId>html2pdf</artifactId><version>4.0.3</version></dependency></dependencies>
使用itextpdf将html文件转成pdf的过程也是相当简单。
// 设置字体ConverterProperties converterProperties =newConverterProperties();FontSet fontSet =newFontSet();if(!fontSet.addFont("C:\\Windows\\Fonts\\simhei.ttf")){thrownewRuntimeException("获取字体失败");}
converterProperties.setFontProvider(newFontProvider(fontSet));// html转pdf, 并将pdf作为字节数组保存在bos中ByteArrayOutputStream bos =newByteArrayOutputStream();HtmlConverter.convertToPdf(jsoupHtml, bos, converterProperties);
然后我们对上面两种方式生成的html文本内容进行转换。
- 对jsoup生成的html文本内容进行转换并测试
@GetMapping("/export/jsoup")publicvoidexportJsoup(HttpServletResponse response)throwsIOException{String jsoupHtml =jsoup();// 设置字体ConverterProperties converterProperties =newConverterProperties();FontSet fontSet =newFontSet();if(!fontSet.addFont("C:\\Windows\\Fonts\\simhei.ttf")){thrownewRuntimeException("获取字体失败");} converterProperties.setFontProvider(newFontProvider(fontSet));ByteArrayOutputStream bos =newByteArrayOutputStream();HtmlConverter.convertToPdf(jsoupHtml, bos, converterProperties);String fileName ="将jsoup生成的html转换成pdf文件";// 设置中文文件名 fileName =newString(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")publicvoidexportThymeleaf(HttpServletRequest request,HttpServletResponse response)throwsIOException{String jsoupHtml =thymeleaf(request, response);// 设置字体ConverterProperties converterProperties =newConverterProperties();FontSet fontSet =newFontSet();if(!fontSet.addFont("C:\\Windows\\Fonts\\simhei.ttf")){thrownewRuntimeException("获取字体失败");} converterProperties.setFontProvider(newFontProvider(fontSet));ByteArrayOutputStream bos =newByteArrayOutputStream();HtmlConverter.convertToPdf(jsoupHtml, bos, converterProperties);String fileName ="将thymeleaf生成的html转换成pdf文件";// 设置中文文件名 fileName =newString(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
纸上得来终觉浅,绝知此事要躬行。
————我是万万岁,我们下期再见————
版权归原作者 理想万岁万万岁 所有, 如有侵权,请联系我们删除。