springboot项目实现导出pdf功能,这也太简单了吧
作者:mmseoamin日期:2023-12-20

往期文章

  • springcloud整合knike4j聚合微服务接口文档

  • spring源码 - 条件注解@ConditionnalOnClass的原理分析

  • 用最简单的话讲最明白的红黑树

    文章目录

    • 往期文章
    • 一、介绍
    • 二、使用html模版生成html页面文本
      • 1. 使用jsoup工具生成html页面文本
      • 2. 使用模版引擎生成html页面文本
      • 三、将html页面文本转成pdf文件

        一、介绍

        在我们日常开发中,经常会遇到导出pdf这种需求,比如导出合同、导出业务报告等。这中导出功能都有一个特点,导出的pdf中有大量相同的文本布局以及样式,只有涉及到用户本人的信息时出现不同的内容。我们把这些相同的部分称作模版,在模版中放置一些变量来代表用户信息,比如用户姓名、年龄等。这样我们在导出pdf的时候,在数据库中把用户信息查出来,对模版中对应的变量进行替换,再把替换的结果转成pdf文件就可以了。

        模版的类型有很多种:html模版、doc模版、excel模版、pdf模版等等。项目中使用哪一种要具体情况具体考虑。

        将变量替换后的模版转成pdf文件的工具也有很多,最主流最方面的当然要数itextpdf了。它可以将常见的任何形式的模版转成pdf文件。

        前几天俺就遇到一个导出pdf的需求,而且该pdf有点花里胡哨,明显存在大量css样式,所以我们就采用html作为模版,通过itextpdf将html转成pdf。主要步骤如下:

        1. 将html模版生成html页面文本,对模版进行变量替换
        2. 将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的依赖

                
                    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, Map params) 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> counselList = (List>)entry.getValue();
                			// 设置行,就是把列表数据设置到html的表格行中
                            setRowsText(document, element, counselList);
                        } else {
                            // 对html元素设置文本
                            element.text(entry.getValue().toString());
                        }
                    }
                }
                // 把列表数据设置到html的表格行中,document-html模版,element-表示一行的元素,即tr标签。list-真实列表数据
                private static void setRowsText(Document document, Element element, List> list) {
                    if (list.isEmpty()) {
                        return;
                    }
                    Iterator> iterator = list.iterator();
                    do {
                        Map counsel = iterator.next();
                        // 设置文本数据
                        setText(document, counsel);
                        if (iterator.hasNext()) {
                            // 追加一行
                            appendTableRow(element);
                        }
                    } while (iterator.hasNext());
                    // 如果list集合中还有元素,则复制当前element追加到当前element后面,并循环到前面一步,
                    // 如果list集合中没有元素了,则说明内容已经写完了,返回即可
                }
                // 扩展一行
                private static void appendTableRow(Element element) {
                    Node parent = element.parent();
                    Element tbody = (Element) parent;
                    tbody.appendChild((Node) element.clone());
                }
                
              • 测试

                我们写一个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文本中了

                在这里插入图片描述

                2. 使用模版引擎生成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页面文本转成pdf文件

                  上面我们通过两种方式对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

                    在这里插入图片描述

                    纸上得来终觉浅,绝知此事要躬行。

                    ————我是万万岁,我们下期再见————