Java整合Thymeleaf和wkhtmltopdf实现HTML导出PDF

2023年 9月 12日 93.6k 0

前端实现导出 PDF 产品报告,存在几个问题:

  • 是图片版的 PDF;

  • PDF 太大,会卡;

  • 可能会把文字裁剪分页;

  • 无法满足平台提供 Api 接口服务。

  • 核心就是问题3和问题4,于是,考虑后端服务实现导出 PDF 产品报告的方案。

    Java 实现 HTML 转 PDF 技术选型

    推荐使用 wkhtmltopdf, Itext,但 wkhtmltopdf 开源免费,Itext 需要考虑版权

    参考:blog.csdn.net/weixin_4398…

    参考:www.cnblogs.com/IT-study/p/…

    技术实现方案

    技术采用模板引擎 + PDF 插件实现。开发好页面模板,Thymeleaf 模板引擎渲染静态 HTML 文件,wkhtmltopdf 将静态的 HTML 生成 PDF 文件。整体方案流程如下:

  • 后台使用 Thymeleaf 模板生成 Html 报告页面
  • PDF 接口根据 ID 查询报告数据
  • 调用 wkhtmltopdf 工具 将 Html 转为 PDF 文件
  • 关于 wkhtmltopdf

    参数文档: wkhtmltopdf.org/usage/wkhtm…

    wkhtmltopdf 安装

  • Yum 安装(可能是老版本存在bug,不推荐)
  • yum -y install wkhtmltopdf
    
  • rpm 包安装
  • 下载最新按照包,如

    wget https://objects.githubusercontent.com/github-production-release-asset-2e65be/131323182/4c2dd800-ab8e-11ea-95aa-09875726406d?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20230904%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20230904T082059Z&X-Amz-Expires=300&X-Amz-Signature=8eb5914f5551c3f9a454537895ab41c3884fcb447ca4babf6c57e27fefb46b41&X-Amz-SignedHeaders=host&actor_id=39854904&key_id=0&repo_id=131323182&response-content-disposition=attachment%3B%20filename%3Dwkhtmltox-0.12.6-1.centos8.x86_64.rpm&response-content-type=application%2Foctet-stream
    

    先安装依赖包

    yum install -y fontconfig libX11 libXext libXrender libjpeg libpng xorg-x11-fonts-75dpi xorg-x11-fonts-Type1
    

    wkhtmltox 安装

    rpm -ivh wkhtmltox-0.12.6-1.centos8.x86_64.rpm
    

    若需要路径执行,可配置

    cp /usr/local/bin/wkhtmltopdf /usr/bin/wkhtmltopdf
    
  • 内网安装
  • 先下载依赖包到指定目录,例如下载 openssl 依赖包到指定目录

    yum install --downloadonly --downloaddir=/usr/soft/wktooltopdf/ openssl
    

    之后,拷贝依赖包到内网环境,执行命令

    rpm -ivh --force --nodeps *.rpm
    

    rpm -ivh --force --nodeps *.rpm

  • 常见问题
    • 缺少依赖包

    手动安装依赖包

    • FAQ-linux 安装 wkhtmltopdf 中文乱码或者空白解决方法

    参考:www.cnblogs.com/jluo/p/1740…

    安装中文字体,或复制已有字体

    打开windows c:Windowsfontssimsun.ttc
    拷贝到linux服务器/usr/share/fonts/目录下,再次生成pdf中文显示正常
    
    • 出现错误: wkhtmltopdf:cannot connect to X server

    参考:www.jianshu.com/p/2cfc02961…

    需再安装xvfb

    yum install xorg-x11-server-Xvfb
    

    在 /usr/bin/ 目录下生成脚本 wkhtmltopdf.sh 并写入命令

    sudo vim /usr/bin/wkhtmltopdf.sh  
    命令:
    xvfb-run -a --server-args="-screen 0, 1024x768x24" /usr/bin/wkhtmltopdf -q $*
    

    更改文件权限并建立连接

    chmod a+x /usr/bin/wkhtmltopdf.sh
    ln -s /usr/bin/wkhtmltopdf.sh /usr/local/bin/wkhtmltopdf
    

    中文字体安装

    若出现中文乱码,则可能是缺少字体

    阿里巴巴普惠体2.0,免费无版权,好用

    下载地址: done.alibabadesign.com/puhuiti2.0

    介绍说明: fonts.adobe.com/fonts/aliba…

    设置字体集

    font-family: alibaba-puhuiti, sans-serif;
    font-style: normal;
    font-weight: 300;
    

    开发代码及配置

  • *静态资源目录位于 -model 工程下的资源文件,包括以下目录
  • templates/ - 模板文件
    static/ - 静态资源文件
    

    若前端有修改调整,需将更新的文件复制到 *-model 工程下对应目录,

  • 静态资源复制方案1:maven 插件配置, 用于复制公共的资源
  • pom.xml 增加插件配置

      
        maven-resources-plugin
        
            
                copy-resources
                package
                
                    copy-resources
                
                
                    
                        
                            ../../*-model/src/main/resources  
                              
                                templates/**
                                static/**
                            
                        
                    
                    src/main/resources  
                    true  
                
            
        
    
    
  • 静态资源复制方案2:自定义的打包配置,增加资源复制
  • 推荐使用此方法,直接复制资源并打包到目标 zip 包

    路径:/src/main/assemble/package.xml,增加配置

      
          ../../*-model/src/main/resources
          
            
              templates/**
              static/**
          
      
    
  • Java 工具类
  • 由于模板引擎对 JS 的支持有限,固增加 Java 工具类,用于模板中处理数据(模板引擎是在服务端执行,可执行 Java 代码)
    
    参考 HtmlThymeleafHelper 配置, 注意 ModelAndView 中返回工具列
    
  • 后端开发 pom.xml 依赖
  • 
        org.springframework.boot
        spring-boot-starter-thymeleaf
    
    
        org.springframework.boot
        spring-boot-starter-web
    
    
  • 开发模板
  • 
    
    
        
        title
    
    
    Hello Thymeleaf
    张三(离线数据)
    
    
    
  • 后端接口处理 ModelAndView
  • // 接口返回 ModelAndView, 指定模板, 设置数据
    @GetMapping("/template/render")
    public ModelAndView templateReportDetail(HttpServletRequest request, @RequestParam String reportId) {
        this.initJavaEnv(request);
        return this.renderModelAndView("TemplateReportDetail", reportId);
    }
    
    // 通过 Session 设置 Java 对象,用于模板中执行 Java 方法
    private void initJavaEnv(HttpServletRequest request) {
        HtmlThymeleafHelper helper = Singleton.get(HtmlThymeleafHelper.class);
        request.getSession().setAttribute("helper", helper);
    }
    
    
    // 创建一个模型视图对象
    ModelAndView mav = new ModelAndView();
    // 获取到查询的数据
    Object data = ret.getRetObject();
    // 将数据放置到ModelAndView对象view中,第二个参数可以是任何java类型
    mav.addObject("sourceData", data);
    // 放入模板路径
    mav.setViewName("template");
    // 返回ModelAndView对象mav
    return mav;
    
  • SpringBoot yml 配置
  • spring:
      mvc:
        # 添加static文件夹下其他文件夹可访问
        static-path-pattern: /project/static/**
        # 自定义配置项,指定模板路径
        base-template-path: /project/template
      thymeleaf:
        cache: true
        mode: HTML5
        suffix: .html
        prefix: classpath:/templates/
        encoding: UTF-8
        servlet:
          content-type: text/html
    

    关于模板引擎 Thymeleaf

    什么是Thymeleaf?

    • Thymeleaf 官网是这么解释的:Thymeleaf is a modern server-side Java template engine for both web and standalone environments.
    • 译过来就是:Thymeleaf是适用于Web和独立环境的现代服务器端Java模板引擎

    什么是模板引擎?

    • 模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的html文档。从字面上理解模板引擎,最重要的就是模板二字,这个意思就是做好一个模板后套入对应位置的数据,最终以html的格式展示出来,这就是模板引擎的作用。
    • 不仅如此,在Java中模板引擎还有很多,模板引擎是动态网页发展进步的产物,在最初并且流传度最广的jsp它就是一个模板引擎。jsp是早期官方标准的模板,但是由于jsp的缺点比较多也挺严重的,所以很多人弃用jsp选用第三方的模板引擎,市面上开源的第三方的模板引擎也比较多,有Thymeleaf、FreeMaker、Velocity等模板引擎受众较广。
    • 听完了模板引擎的介绍,相信你也很容易明白了模板引擎在web领域的主要作用:让网站实现界面和数据分离,这样大大提高了开发效率,让代码重用更加容易。

    Model、ModelMap、ModelAndView

    Model

    • 一般来说,可以用Model来接收各种类型的数据,如果使用来接收一组数据List,那么这个时候的Model实际上是ModelMap

    ModelMap

    • 主要用于传递控制方法处理数据到结果页面,也就是说我们把结果页面上需要的数据放到ModelMap对象中即可,
    • 他的作用类似于request对象的setAttribute方法的作用:用来在一个请求过程中传递处理的数据
    • ModelMap或者Model通过addAttribute方法向页面传递参数

    ModelAndView

    • 指模型和视图的集合,既包含 模型 又包含 视图
    • Model和 ModelMap 无需用户自己创建,而且需要return 返回指定的页面路径
    Model和 ModelMap 无需用户自己创建,而且需要return 返回指定的页面路径
    
    public String listCategory2(Model model) {
        // 接收查询的信息
        List cs2= categoryService.list();
        // 封装了查询的数据
        model.addAttribute("test", cs2);
        //重要!!需要给出返回model跳转的路径
        return "listCategory2";
    }
    
    
    ModelAndView的实例是需要我们手动new的,这也是和ModelMap的一个区别。
    而且,ModelAndView 可以自己寻址,只需要return 返回其对象即可。
    
    public ModelAndView listCategory(){
      //创建一个模型视图对象
        ModelAndView mav = new ModelAndView();
        //获取到查询的数据
        List cs= categoryService.list();
    
        // //将数据放置到ModelAndView对象view中,第二个参数可以是任何java类型
        mav.addObject("cs", cs);
        // 放入jsp路径
        mav.setViewName("listCategory");
         //返回ModelAndView对象mav
        return mav;
    }
    

    参考:cloud.tencent.com/developer/a…

    Thymeleaf 常用标签

    标签 作用 示例
    th:id 替换id
    th:text 文本替换

    bigsai

    th:utext 支持html的文本替换

    content

    th:object 替换对象
    th:value 替换值
    th:each 迭代
    th:href 替换超链接 超链接
    th:src 替换资源

    七大基础对象:

    • ${#ctx} 上下文对象,可用于获取其它内置对象。
    • ${#vars}: 上下文变量。
    • ${#locale}:上下文区域设置。
    • ${#request}: HttpServletRequest对象。
    • ${#response}: HttpServletResponse对象。
    • ${#session}: HttpSession对象。
    • ${#servletContext}: ServletContext对象。

    常用的工具类:

    • #strings:字符串工具类
    • #lists:List 工具类
    • #arrays:数组工具类
    • #sets:Set 工具类
    • #maps:常用Map方法。
    • #objects:一般对象类,通常用来判断非空
    • #bools:常用的布尔方法。
    • #execInfo:获取页面模板的处理信息。
    • #messages:在变量表达式中获取外部消息的方法,与使用#{...}语法获取的方法相同。
    • #uris:转义部分URL / URI的方法。
    • #conversions:用于执行已配置的转换服务的方法。
    • #dates:时间操作和时间格式化等。
    • #calendars:用于更复杂时间的格式化。
    • #numbers:格式化数字对象的方法。
    • #aggregates:在数组或集合上创建聚合的方法。
    • #ids:处理可能重复的id属性的方法。
    引入css(必须要在标签中加上rel属性)
    
    
    
    引入JavaScript:
    
    
    
    超链接:
    超链接
    
    变量表达式: ${...}
    在Thymeleaf中可以通过${…}进行取值,这点和ONGL表达式语法一致
    
    
    取JavaBean对象:
    使用${对象名.对象属性}或者${对象名['对象属性']}来取值
    如果该JavaBean如果写了get方法,也可以通过get方法取值例如${对象.get方法名}
    
    
    
    
    
    
    取List集合(each):
    因为List集合是个有序列表,要遍历List对其中对象取值,而遍历需要用到标签:th:each,
    具体使用为 ,其中item就相当于遍历每一次的对象名
    
    
    直接取Map: 很多时候我们不存JavaBean而是将一些值放入Map中,再将Map存在Model中,我们就需要对Map取值, 可以 ${Map名['key']} 取值。也可以 ${Map名.key} 取值,当然也可以 ${map.get('key')}(java语法)取值
    place:
    feeling:

    参考:developer.aliyun.com/article/769…

    Thymeleaf 控制处理

    
    
    ? 会判断对象是否为空,如果为空就不会继续取值
    
    
    SPEL处理 null 值
    
    变量为 null 时,显示默认值
    name?:'Unknown'
    当 name 变量为 null 时,显示值 Unknown。等价于 name?name:'Unknown'。
    
    对象为 null 时,避免调用方法或属性出错
    placeOfBirth?.city
    当 placeOfBirth 为 null 时,不再继续调用属性 city。
    
    code?.toUpperCase()
    当 code 为 null 时,不再继续调用方法 toUpperCase。
    
    Map 获取的元素为 null
    当 map 中没有名为 name 的元素时,这样写会报错 map.name。
    安全的写法是这样:map['name']。
    
    如果 map 中的元素为对象时,可以这样写:map['user']?.name。
    
    List 类型数组越界
    数组越界时,错误是这样的:
    
    Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "slist[2].score" (template: "exam/papers/edit" - line 117, col 92)
    Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1025E: The collection has '1' elements, index '2' is invalid
    
    SPEL 是这样的 slist[2].score,但 slist 不够3个元素(EL1025E: The collection has '1' elements, index '2' is invalid),因此数组越界了。
    
    解决办法:添加数组大小的判断。上面的情况下,用 #lists.size(slist)>=3?slist[2]?.score:0 替换 slist[2].score
    

    参考:blog.csdn.net/sayyy/artic…

    参考:zhuanlan.zhihu.com/p/90642654

    Thymeleaf 调用 Java 方法

    1. Java 对象示例存入 Thymeleaf Context 域中,代码层面即为:将实例对象存入Request对象中
    
    
    
     MethodService md = new MethodService();
     mmap.put("methodService",md);
     mmap.put("proofsList",proofsList);
    
    
      
        
    
    

    Thymeleaf 动态添加样式

  • 或者
  • 动态绑定样式
  • Thymeleaf 数组处理

    // 数组长度
    The greatest  continents.
    // 数组包含
    Europe is a continent: .
    // 数组判空
    Array of continents is empty .
    

    Thymeleaf 遍历生成复杂的表格

     
    机构 年份 得分 全球排名
    无排名信息
    th:remove="tag" 它在这的作用是生成表格后把div删除,但不删除子元素 th:if="*{#lists.isEmpty(institution)}" 判断从后台获取的数据为空,空则不渲染 tr 标签 th:if="*{not #lists.isEmpty(institution)}" 判断从后台获取的数据不为空,不为空则渲染 tr 标签
    
        
            
            
            
            
            
            
            
                
            
        
     
        
            
            
            
            
            
                
            
        
    
    
    
    th:remove:会移除该标签行,不会移除其子标签
    
    th:each:迭代集合或者数组
    
    th:with:临时变量的声明
    
    
    colspan 合并单元格 列
    rowspan 合并单元格 行
    

    常见问题

  • wkhtmltopdf 生成 PDF 的表格行内出现分页符、表头重复、截断等
  • 增加表格样式

    thead {
        display: table-row-group;
    }
    tr {
        page-break-before: always;
        page-break-after: always;
        page-break-inside: avoid;
    }
    table {
        word-wrap: break-word;
    }
    table td {
        word-break: break-all;
    }
    

    说明:wkhtmltopdf 对表格的支持很差,会导致文件很大,长表格兼容性等问题

    参考:blog.csdn.net/yellowatumn…

    相关文章

    服务器端口转发,带你了解服务器端口转发
    服务器开放端口,服务器开放端口的步骤
    产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
    如何使用 WinGet 下载 Microsoft Store 应用
    百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
    百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

    发布评论