首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 動態生成 PDF 實戰:iText + FreeMarker + JFreeChart 整合方案

     編輯:Java 相關知識

方案選型背景

業務需求:服務端動態生成可配置的 PDF 報表,包含樣式排版、中文字體、頁眉頁腳、業務圖表嵌入、自動分頁。

技術棧組合優勢

組件角色解決痛點
iText 5PDF 生成核心底層 PDF 操作、分頁、水印、加密
FreeMarkerHTML 模板引擎樣式排版、動態數據渲染、前端熟悉語法
JFreeChart圖表生成折線/柱狀/餅圖程序化繪製
Flying Saucer (xhtmlrenderer)HTML→PDF 轉換CSS 2.1 支援、中文斷行、複雜佈局

Maven 依賴配置

<dependencies>
    <!-- iText 核心 -->
    <dependency>
        <groupId>com.itextpdf</groupId>
        <artifactId>itextpdf</artifactId>
        <version>5.5.13.3</version>
    </dependency>
    <dependency>
        <groupId>com.itextpdf.tool</groupId>
        <artifactId>xmlworker</artifactId>
        <version>5.5.13.3</version>
    </dependency>
    <dependency>
        <groupId>com.itextpdf</groupId>
        <artifactId>itext-asian</artifactId>
        <version>5.2.0</version>
    </dependency>
    <!-- Flying Saucer HTML→PDF -->
    <dependency>
        <groupId>org.xhtmlrenderer</groupId>
        <artifactId>flying-saucer-pdf</artifactId>
        <version>9.1.22</version>
    </dependency>
    <!-- FreeMarker -->
    <dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
        <version>2.3.32</version>
    </dependency>
    <!-- JFreeChart -->
    <dependency>
        <groupId>org.jfree</groupId>
        <artifactId>jfreechart</artifactId>
        <version>1.5.4</version>
    </dependency>
</dependencies>

核心模組設計

1. 模板文件(FreeMarker .ftl)

<!DOCTYPE html>
<html xmlns="https://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <style type="text/css">
        @font-face {
            font-family: 'pingfang';
            src: url('file:///fonts/PingFangSC-Light.ttf');
        }
        body { font-family: 'pingfang', sans-serif; font-size: 12px; }
        .page { page-break-after: always; }  /* 關鍵:分頁控制 */
        .center { text-align: center; }
        .header { position: running(header); }
        .footer { position: running(footer); }
        @page {
            @top-center { content: element(header); }
            @bottom-center { content: element(footer); counter-increment: page; }
            @bottom-right { content: "第 " counter(page) " 頁"; }
            margin: 2cm;
        }
    </style>
</head>
<body>
    <div class="header">${reportTitle} - 機密</div>
    <div class="footer">生成時間: ${genTime}</div>

    <!-- 第 1 頁 -->
    <div class="page">
        <h1 class="center">${templateName}</h1>
        <p>iText 官網: <a href="${itextUrl}">${itextUrl}</a></p>
        <p>FreeMarker 官網: <a href="${fmUrl}">${fmUrl}</a></p>
        <h3>評分列表</h3>
        <ul>
        <#list scores as score>
            <li>${score}</li>
        </#list>
        </ul>
    </div>

    <!-- 第 2 頁:圖表嵌入 -->
    <div class="page">
        <h2>外部圖片</h2>
        <img src="${imageUrl}" width="270" height="129"/>
        <h2>動態生成圖表</h2>
        <img src="${chartUrl}" width="500" height="300"/>
    </div>
</body>
</html>

2. FreeMarker 工具類

public class FreeMarkerUtil {
    public static String render(String templateName, Object dataModel) {
        try {
            Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
            cfg.setDefaultEncoding("UTF-8");
            cfg.setClassForTemplateLoading(FreeMarkerUtil.class, "/templates");
            cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
            
            Template template = cfg.getTemplate(templateName);
            StringWriter writer = new StringWriter();
            template.process(dataModel, writer);
            return writer.toString();
        } catch (Exception e) {
            throw new RuntimeException("模板渲染失敗: " + templateName, e);
        }
    }
}

3. JFreeChart 圖表生成服務

@Service
public class ChartService {
    public String generateLineChartBase64(List<Double> data, String title) {
        try {
            // 1. 建構數據集
            DefaultCategoryDataset dataset = new DefaultCategoryDataset();
            for (int i = 0; i < data.size(); i++) {
                dataset.addValue(data.get(i), "數值", "點" + (i+1));
            }
            
            // 2. 創建圖表
            JFreeChart chart = ChartFactory.createLineChart(
                title, "時間", "數值", dataset,
                PlotOrientation.VERTICAL, true, true, false
            );
            
            // 3. 中文顯示修正
            chart.getTitle().setFont(new Font("PingFang SC", Font.BOLD, 16));
            CategoryPlot plot = chart.getCategoryPlot();
            plot.getDomainAxis().setLabelFont(new Font("PingFang SC", Font.PLAIN, 12));
            plot.getRangeAxis().setLabelFont(new Font("PingFang SC", Font.PLAIN, 12));
            plot.getDomainAxis().setTickLabelFont(new Font("PingFang SC", Font.PLAIN, 10));
            
            // 4. 輸出為 Base64 Data URI
            BufferedImage image = chart.createBufferedImage(600, 350);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ImageIO.write(image, "PNG", baos);
            return "data:image/png;base64," + Base64.getEncoder().encodeToString(baos.toByteArray());
            
        } catch (Exception e) {
            throw new RuntimeException("圖表生成失敗", e);
        }
    }
}

4. PDF 生成核心類

@Component
public class PdfGenerator {
    @Autowired private ChartService chartService;

    public String generate(String templateName, ReportData data, String outputPath) {
        try {
            // 1. 生成圖表 Base64
            String chartBase64 = chartService.generateLineChartBase64(data.getScores(), "評分趨勢");
            data.setChartUrl(chartBase64);
            data.setGenTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")));

            // 2. 渲染 HTML
            String html = FreeMarkerUtil.render(templateName, data);

            // 3. iText + Flying Saucer 轉換
            File outputFile = new File(outputPath);
            outputFile.getParentFile().mkdirs();
            
            try (OutputStream os = new FileOutputStream(outputFile)) {
                ITextRenderer renderer = new ITextRenderer();
                
                // 關鍵:註冊中文字體
                ITextFontResolver fontResolver = renderer.getFontResolver();
                fontResolver.addFont("/fonts/PingFangSC-Light.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
                fontResolver.addFont("/fonts/PingFangSC-Regular.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
                
                renderer.setDocumentFromString(html);
                renderer.layout();
                renderer.createPDF(os);
            }
            return outputPath;
        } catch (Exception e) {
            throw new RuntimeException("PDF 生成失敗", e);
        }
    }
}

5. 頁眉頁腳自定義(iText 事件模型)

public class HeaderFooterEvent implements PdfPageEventHelper {
    private final Font headerFont;
    private final Font footerFont;
    private final String headerText;
    private final String footerText;

    public HeaderFooterEvent(String headerText, String footerText, String fontPath) {
        try {
            BaseFont bf = BaseFont.createFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
            this.headerFont = new Font(bf, 10);
            this.footerFont = new Font(bf, 8);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        this.headerText = headerText;
        this.footerText = footerText;
    }

    @Override
    public void onEndPage(PdfWriter writer, Document document) {
        PdfContentByte cb = writer.getDirectContent();
        Rectangle page = document.getPageSize();
        
        // 頁眉
        ColumnText.showTextAligned(cb, Element.ALIGN_CENTER,
            new Phrase(headerText, headerFont),
            (page.getLeft() + page.getRight()) / 2, page.getTop() - 20, 0);
        
        // 頁腳
        String footer = footerText + " - " + writer.getPageNumber();
        ColumnText.showTextAligned(cb, Element.ALIGN_CENTER,
            new Phrase(footer, footerFont),
            (page.getLeft() + page.getRight()) / 2, page.getBottom() + 20, 0);
    }
}

使用方式:writer.setPageEvent(new HeaderFooterEvent("報表標題", "機密文檔", "/fonts/PingFangSC-Regular.ttf"));

關鍵坑點與解決方案

1. Maven 資源過濾導致字體文件損壞

<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>true</filtering>
            <excludes>
                <exclude>**/*.ttf</exclude>
                <exclude>**/*.otf</exclude>
            </excludes>
        </resource>
    </resources>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-resources-plugin</artifactId>
            <configuration>
                <nonFilteredFileExtensions>
                    <nonFilteredFileExtension>ttf</nonFilteredFileExtension>
                    <nonFilteredFileExtension>otf</nonFilteredFileExtension>
                </nonFilteredFileExtensions>
            </configuration>
        </plugin>
    </plugins>
</build>

2. 中文斷行與字體回退

  • Flying Saucer 需 CSS font-family: 'pingfang', sans-serif; 指定字體族
  • iText FontResolver 必須註冊 IDENTITY_H + EMBEDDED
  • 缺字符時自動回退至 sans-serif

3. 圖片路徑解析

  • 外部圖片:HTTP/HTTPS URL 或 file:/// 協議
  • Base64 Data URI:data:image/png;base64,... 直接嵌入,無需外部文件
  • 相對路徑:需配置 renderer.getSharedContext().setBaseURL()

4. 大數據量分頁效能

  • 模板中使用 .page { page-break-after: always; } 強制分頁
  • 避免單個表格橫跨過多頁,建議拆分為多個小表格
  • 大報表考慮流式生成(iText 直接寫入而非 HTML 轉換)

測試驗證

@SpringBootTest
class PdfGeneratorTest {
    @Autowired private PdfGenerator pdfGenerator;

    @Test
    void testGenerate() {
        ReportData data = new ReportData();
        data.setTemplateName("季度業績報表");
        data.setItextUrl("https://itextpdf.com");
        data.setFmUrl("https://freemarker.apache.org");
        data.setImageUrl("https://example.com/logo.png");
        data.setScores(Arrays.asList(85.5, 92.0, 78.5, 96.0, 88.5));

        String path = pdfGenerator.generate("report.ftl", data, "/tmp/report.pdf");
        assertTrue(new File(path).exists());
        assertTrue(new File(path).length() > 1024);
    }
}

專案結構

src/main/java
├── com.example.pdf
│   ├── component.PdfGenerator
│   ├── component.HeaderFooterEvent
│   ├── service.ChartService
│   ├── util.FreeMarkerUtil
│   └── model.ReportData
src/main/resources
├── templates
│   └── report.ftl
├── fonts
│   ├── PingFangSC-Light.ttf
│   └── PingFangSC-Regular.ttf
└── application.yml

總結

iText + FreeMarker + JFreeChart 組合優勢互補:

  1. FreeMarker 負責業務邏輯與樣式分離,前端協作友好
  2. JFreeChart 程序化生成精確業務圖表
  3. Flying Saucer 以 CSS 2.1 實現複雜佈局、中文斷行、分頁控制
  4. iText 提供底層 PDF 控制(水印、加密、簽名、頁眉頁腳事件)

核心成功要素:字體註冊、Base64 圖片嵌入、CSS 分頁控制、Maven 非文本資源保護。

GitHub 完整示例:https://github.com/superad/pdf-kit

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved