Spring Boot3整合FreeMark、itextpdf 5/7 实现pdf文件导出及注意问题

前言

好久没来总结项目经验了,今天刚好有时间总结一下。

最近有一个项目需求实现pdf文件导出,参考了一些网上的方案,真是五花八门,大部分都是直说了丢了一半;在导出pdf文件最重要的是字体文件的问题及在Linux服务器上无法显示中字体问题。好了!废话不多说上代码。

引入依赖包

pom 复制代码
        <!-- iText 7 PDF 核心 (兼容 Spring Boot 3) -->
       <dependency>
           <groupId>com.itextpdf</groupId>
           <artifactId>itext7-core</artifactId>
           <version>8.0.4</version>
           <type>pom</type>
       </dependency>

       <!-- iText 7 html2pdf (HTML 转 PDF) -->
       <dependency>
           <groupId>com.itextpdf</groupId>
           <artifactId>html2pdf</artifactId>
           <version>5.0.4</version>
       </dependency>

       <!-- iText 7 亚洲字体支持 (中文等) -->
       <dependency>
           <groupId>com.itextpdf</groupId>
           <artifactId>font-asian</artifactId>
           <version>8.0.4</version>
       </dependency>

Application.yaml配置文件

yaml 复制代码
ac:
  fonts:
    simsun-path: /usr/share/fonts/chinese/ttf/simsun.ttf
    msyh-path: /usr/share/fonts/chinese/ttf/msyh.ttf

spring:
  ### freemarker
  freemarker:
    suffix: .ftl
    charset: UTF-8
    request-context-attribute: request
    settings:
      number_format: 0.##########
    template-loader-path: classpath:/templates/    

封装工具类

Java 复制代码
import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.html2pdf.resolver.font.DefaultFontProvider;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import freemarker.template.Configuration;
import freemarker.template.Template;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.Map;

@Slf4j
@Component
public class PdfExportUtil {

    private final Configuration freemarkerConfiguration;

    public PdfExportUtil(Configuration freemarkerConfiguration) {
        this.freemarkerConfiguration = freemarkerConfiguration;
    }

    @Value("${ac.fonts.simsun-path:}")
    private String songTi;

    @Value("${ac.fonts.msyh-path:}")
    private String yaHei;

    @SneakyThrows
    public String renderHtml(String templateName, Map<String, Object> dataMap) {
        Template template = freemarkerConfiguration.getTemplate(templateName, "UTF-8");
        StringWriter writer = new StringWriter();
        template.process(dataMap, writer);
        return writer.toString();
    }

    @SneakyThrows
    public void htmlToPdf(String html, HttpServletResponse response, String fileName) {
        response.setContentType("application/pdf;charset=UTF-8");
        response.setHeader("Content-Disposition",
                "attachment;filename=" + java.net.URLEncoder.encode(fileName, "UTF-8"));

        ServletOutputStream outputStream = response.getOutputStream();
        PdfWriter writer = new PdfWriter(outputStream);
        PdfDocument pdf = new PdfDocument(writer);

        ConverterProperties properties = new ConverterProperties();

        // 创建字体提供者,启用系统字体扫描作为基础
        DefaultFontProvider fontProvider = new DefaultFontProvider(true, true, true);
        log.info("已启用系统字体扫描作为基础");

        log.info("开始加载字体配置...");
        log.info("宋体配置路径:{}", songTi);
        log.info("微软雅黑配置路径:{}", yaHei);

        try {
            // 加载宋体
            if (songTi != null && !songTi.isEmpty()) {
                String simsunPath = loadFontFromClasspath(songTi, "SimSun");
                if (simsunPath == null) {
                    File simsunFile = new File(songTi);
                    if (simsunFile.exists()) {
                        simsunPath = songTi;
                    }
                }

                if (simsunPath != null) {
                    File simsunFile = new File(simsunPath);
                    if (simsunFile.exists()) {
                        try {
                            // 使用 PdfFontFactory 创建字体(支持 TTC 格式)
                            // iText 8 API: createFont(String fontProgram, String encoding, boolean embedded)
                            PdfFont simsunFont = PdfFontFactory.createFont(
                                simsunPath,
                                com.itextpdf.io.font.PdfEncodings.IDENTITY_H
                            );
                            fontProvider.addFont(simsunFont.getFontProgram(), com.itextpdf.io.font.PdfEncodings.IDENTITY_H);
                            log.info("✓ 注册宋体字体,路径:{}, 文件大小:{} bytes", simsunPath, simsunFile.length());
                        } catch (Exception e) {
                            log.warn("注册宋体字体失败:{}", e.getMessage());
                        }
                    } else {
                        log.warn("✗ 宋体字体文件不存在:{}", simsunPath);
                    }
                }
            }

            // 加载微软雅黑
            if (yaHei != null && !yaHei.isEmpty()) {
                String msyhPath = loadFontFromClasspath(yaHei, "MicrosoftYaHei");
                if (msyhPath == null) {
                    File msyhFile = new File(yaHei);
                    if (msyhFile.exists()) {
                        msyhPath = yaHei;
                    }
                }

                if (msyhPath != null) {
                    File msyhFile = new File(msyhPath);
                    if (msyhFile.exists()) {
                        try {
                            // 使用 PdfFontFactory 创建字体(支持 TTC 格式)
                            PdfFont msyhFont = PdfFontFactory.createFont(
                                msyhPath,
                                com.itextpdf.io.font.PdfEncodings.IDENTITY_H
                            );
                            fontProvider.addFont(msyhFont.getFontProgram(), com.itextpdf.io.font.PdfEncodings.IDENTITY_H);
                            log.info("✓ 注册微软雅黑字体,路径:{}, 文件大小:{} bytes", msyhPath, msyhFile.length());
                        } catch (Exception e) {
                            log.warn("注册微软雅黑字体失败:{}", e.getMessage());
                        }
                    } else {
                        log.warn("✗ 微软雅黑字体文件不存在:{}", msyhPath);
                    }
                }
            }

            log.info("字体加载完成");

        } catch (Exception e) {
            log.error("字体加载过程中发生异常,继续使用系统字体:{}", e.getMessage(), e);
        }

        properties.setFontProvider(fontProvider);
        properties.setBaseUri("classpath:/static/");

        log.info("开始转换 HTML 为 PDF...");
        HtmlConverter.convertToPdf(html, pdf, properties);
        log.info("PDF 转换完成");

        pdf.close();
        outputStream.flush();
        outputStream.close();
    }

    /**
     * 将 HTML 转换为 PDF 字节数组
     *
     * @param html HTML 内容
     * @return PDF 文件字节数组
     */
    @SneakyThrows
    public byte[] htmlToPdfBytes(String html) {
        java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
        PdfWriter writer = new PdfWriter(baos);
        PdfDocument pdf = new PdfDocument(writer);

        ConverterProperties properties = new ConverterProperties();

        // 创建字体提供者,启用系统字体扫描作为基础
        DefaultFontProvider fontProvider = new DefaultFontProvider(true, true, true);

        try {
            // 加载宋体
            if (songTi != null && !songTi.isEmpty()) {
                String simsunPath = loadFontFromClasspath(songTi, "SimSun");
                if (simsunPath == null) {
                    File simsunFile = new File(songTi);
                    if (simsunFile.exists()) {
                        simsunPath = songTi;
                    }
                }

                if (simsunPath != null) {
                    File simsunFile = new File(simsunPath);
                    if (simsunFile.exists()) {
                        try {
                            // 使用 PdfFontFactory 创建字体(支持 TTC 格式)
                            PdfFont simsunFont = PdfFontFactory.createFont(
                                simsunPath,
                                com.itextpdf.io.font.PdfEncodings.IDENTITY_H
                            );
                            fontProvider.addFont(simsunFont.getFontProgram(), com.itextpdf.io.font.PdfEncodings.IDENTITY_H);
                        } catch (Exception e) {
                            log.warn("注册宋体字体失败:{}", e.getMessage());
                        }
                    }
                }
            }

            // 加载微软雅黑
            if (yaHei != null && !yaHei.isEmpty()) {
                String msyhPath = loadFontFromClasspath(yaHei, "MicrosoftYaHei");
                if (msyhPath == null) {
                    File msyhFile = new File(yaHei);
                    if (msyhFile.exists()) {
                        msyhPath = yaHei;
                    }
                }

                if (msyhPath != null) {
                    File msyhFile = new File(msyhPath);
                    if (msyhFile.exists()) {
                        try {
                            // 使用 PdfFontFactory 创建字体(支持 TTC 格式)
                            PdfFont msyhFont = PdfFontFactory.createFont(
                                msyhPath,
                                com.itextpdf.io.font.PdfEncodings.IDENTITY_H
                            );
                            fontProvider.addFont(msyhFont.getFontProgram(), com.itextpdf.io.font.PdfEncodings.IDENTITY_H);
                        } catch (Exception e) {
                            log.warn("注册微软雅黑字体失败:{}", e.getMessage());
                        }
                    }
                }
            }

        } catch (Exception e) {
            log.error("字体加载过程中发生异常,继续使用系统字体:{}", e.getMessage(), e);
        }

        properties.setFontProvider(fontProvider);
        properties.setBaseUri("classpath:/static/");

        HtmlConverter.convertToPdf(html, pdf, properties);

        pdf.close();
        return baos.toByteArray();
    }

    private String loadFontFromClasspath(String fontPath, String fontName) throws Exception {
        String resourcePath = fontPath.startsWith("/") ? fontPath.substring(1) : fontPath;
        if (resourcePath.startsWith("classpath:")) {
            resourcePath = resourcePath.substring(10);
        }

        Resource resource = new ClassPathResource(resourcePath);

        if (!resource.exists()) {
            log.debug("字体资源不存在:{}", fontPath);
            return null;
        }

        File tempDir = new File(System.getProperty("java.io.tmpdir"), "ac-fonts");
        if (!tempDir.exists()) {
            tempDir.mkdirs();
        }

        // 根据文件扩展名确定临时文件名
        String fileName = resource.getFilename();
        String extension = ".ttf"; // 默认扩展名
        if (fileName != null && fileName.contains(".")) {
            extension = fileName.substring(fileName.lastIndexOf("."));
        }

        File tempFontFile = new File(tempDir, fontName + extension);

        if (tempFontFile.exists() && tempFontFile.length() > 0) {
            log.debug("使用缓存的字体文件:{}", tempFontFile.getAbsolutePath());
            return tempFontFile.getAbsolutePath();
        }

        try (InputStream inputStream = resource.getInputStream();
             FileOutputStream outputStream = new FileOutputStream(tempFontFile)) {

            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
        }

        log.info("字体文件已解压到临时目录:{}", tempFontFile.getAbsolutePath());
        return tempFontFile.getAbsolutePath();
    }

}

工具类有些冗余,可自行优化调整,这里是为了方面测试是否有成功读取到字体文件。

应用

java 复制代码
@Slf4j
@RestController
public class DownloadCenterController {
    @Autowired
    private PdfExportUtil pdfExportUtil;

    public void test(HttpServletResponse response) {
        // 1. 构造模板数据
        Map<String, Object> data = new HashMap<>();
        // 2. 渲染模板生成 HTML
        String renderHtml = pdfExportUtil.renderHtml("ftl/nvt-template.ftl", data);
        // 3. 输出 PDF
        String fileName = "test.pdf";
        pdfExportUtil.htmlToPdf(renderHtml, response, fileName);
    }
}

到此,本地idea跑项目是没有问题了。

注意!注意!注意! 重要的事情说三遍!

打包发布生产环境运行的问题。在发布一定要确保服务器系统上一定要有对应的字体文件,字体文件后缀问题,一定要使用TTF的文件后缀,如果使用TTC后缀会出现没有中文字体(这里也是我是使用ttc文件始终无法出现中文字体的原因所在)。

Linux上安装字体

这里主要安装宋体、微软雅黑

操作系统Ubuntu

首先,我们在windows系统找对应字段,C:\Windows\Fonts;找TTF文件,如果没有将TTC文件拷贝服务器,在服务器中从TTC文件提取TTF文件,操作如下:

bash 复制代码
# 1. 安装字体提取工具
apt-get update && apt-get install -y fontforge

# 2. 创建输出目录
mkdir -p /usr/share/fonts/chinese/ttf

# 3. 从 simsun.ttc 中提取宋体
fontforge -lang=ff -c 'Open("/usr/share/fonts/chinese/simsun.ttc"); Generate("/usr/share/fonts/chinese/ttf/simsun.ttf")'

# 4. 从 msyh.ttc 中提取微软雅黑
fontforge -lang=ff -c 'Open("/usr/share/fonts/chinese/msyh.ttc"); Generate("/usr/share/fonts/chinese/ttf/msyh.ttf")'

# 5. 设置权限
chmod 644 /usr/share/fonts/chinese/ttf/*.ttf

# 6. 刷新字体缓存
fc-cache -fv

# 7. 验证新字体
fc-list | grep -E "simsun\.ttf|msyh\.ttf"

在 CentOS/RHEL 系统使用 yum 工具安装。

将文件拷贝到Docker容器内

方式一: Dockerfile

dockerfile 复制代码
FROM your-base-image

# 在Dockerfile文件所在目录下
COPY fonts/simsun.ttf /usr/share/fonts/chinese/
COPY fonts/msyh.ttf /usr/share/fonts/chinese/
RUN fc-cache -fv

方式二:docker-compose.yaml

在配置挂在数据卷的volumes映射到容器内。

yml 复制代码
services:
	api:
		volumes:
			- /usr/share/fonts:/usr/share/fonts

映射路径根据你的实际使用做配置。

相关推荐
大数据新鸟2 小时前
微服务之Spring Cloud LoadBalancer
java·spring cloud·微服务
杜子不疼.2 小时前
AI Agent 智能体开发入门:AutoGen 多智能体协作实战教程
java·人工智能·spring
樽酒ﻬق2 小时前
构筑容器化基石:Docker 稳定版本抉择、极速安装与配置全解
java·docker·运维开发
星辰_mya2 小时前
深度全面学习负载均衡Ribbon/Spring Cloud LoadBalancer
后端·spring cloud·面试·负载均衡·架构师
weisian1512 小时前
Java并发编程--29-分布式ID的6种方案:从单机到分库分表的“身份证”设计
java·分布式·雪花算法·美团leaf·百度uid
美式请加冰2 小时前
最短路径问题
java·数据结构·算法
小江的记录本2 小时前
【JEECG Boot】 JEECG Boot 数据字典管理——六大核心功能(内含:《JEECG Boot 数据字典开发速查清单》)
java·前端·数据库·spring boot·后端·spring·mybatis
小江的记录本2 小时前
【JEECG Boot】 JEECG Boot——Online表单 系统性知识体系全解
java·前端·spring boot·后端·spring·低代码·mybatis
都说名字长不会被发现2 小时前
Spring 线程池最佳实践:如何优雅管理多线程任务
java·spring·线程池·并发编程