Java:实现打印报表(附带源码)

目录

  1. 项目背景详细介绍

  2. 项目需求详细介绍

  3. 相关技术详细介绍

  4. 实现思路详细介绍

  5. 完整实现代码

  6. 代码详细解读

  7. 项目详细总结

  8. 项目常见问题及解答

  9. 扩展方向与性能优化


1. 项目背景详细介绍

在企业信息化建设中,各类业务数据繁杂多样,报表作为关键信息展示与决策依据,发挥着至关重要的作用。无论是财务报表、销售报表,还是运营监控报表,都需要格式规范、内容完整,并且能灵活打印到纸质或导出为 PDF 进行归档与分发。

传统报表生成依赖 Office 自动化、第三方报表引擎(如 JasperReports、BIRT)或前端 HTML 打印。它们或需要额外部署组件、或对运行环境有较高要求、或难以集成到纯 Java 后端服务中。本项目以纯 Java 技术栈为基础,从数据读取、报表布局、样式渲染,到打印服务与文件导出,提供一体化的报表打印解决方案,帮助开发者快速集成后台报表功能,减少运维成本,提高开发效率。


2. 项目需求详细介绍

2.1 功能需求

  1. 数据输入

    • 支持从数据库(通过 JDBC)、CSV、Excel(Apache POI)等多种来源读取报表数据。

  2. 报表布局与样式

    • 固定页眉、页脚与动态数据区域;支持表格、图表、合并单元格等布局。

    • 支持自定义字体、字体大小、颜色、对齐方式、边框样式。

  3. 打印输出

    • 通过 javax.print API 直接打印到物理打印机;支持打印预览。

  4. 文件导出

    • 导出为 PDF(PDFBox)、Excel(Apache POI)、HTML、PNG 等多种格式。

  5. 动态参数

    • 支持运行时传入报表参数(如日期范围、筛选条件、公司抬头等)。

  6. 国际化

    • 支持多语言、多区域设置,动态切换标题与格式。

2.2 非功能需求

  • 性能:数据量 ≤1 万行时,生成与打印总耗时 ≤200ms。

  • 可维护性:模块化、高内聚低耦合,注释全面。

  • 可移植性:纯 Java 实现,兼容 Java 8+;无本地平台依赖。

  • 稳定性:容错处理、异常日志、重试机制。


3. 相关技术详细介绍

  1. Java2D 与 Graphics2D:底层绘图接口,用于无界面渲染报表。

  2. Apache POI:读取与导出 Excel 文件。

  3. Apache PDFBox:生成与编辑 PDF 文档。

  4. JDBC:连接与查询关系型数据库。

  5. JasperReports(可选):作为参考的模板引擎,项目实现中通过自定义布局替代。

  6. Java 打印服务 API (javax.print, java.awt.print):打印机发现、打印任务提交与页面格式设置。

  7. 模板引擎(Freemarker/Velocity,可选):动态生成 HTML 或 Excel 报表。


4. 实现思路详细介绍

  1. 数据访问层

    • ReportDataLoader 通过 JDBC、CSV 或 POI 读取结构化数据,封装为通用的 List<Map<String, Object>>

  2. 报表布局层

    • 设计 ReportTemplate,包含页眉、列头、数据行、页脚等区域。

    • 通过 Java2D 在 BufferedImageGraphics2D 上逐行绘制表格、文本与图表。

  3. 渲染与分页

    • 根据页面可用高度自动分页;保持表头在每页顶端。

  4. 打印与导出

    • PrintableReport 实现 Printable,将渲染好的 BufferedImage 按页绘制到 Graphics2D

    • 使用 PrinterJob 提供预览与打印,

    • PDFReportExporter 结合 PDFBox,将每页图像写入 PDF;

    • ExcelReportExporter 结合 POI,根据模板生成 Excel 文件。

  5. 参数化与国际化

    • 使用 ReportContext 传递参数与资源文件,实现多语言切换与模板复用。


5. 完整实现代码

// ===== pom.xml =====
/*
  Maven 配置:JDBC、Apache POI、PDFBox、commons-csv 等依赖
*/
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>report-printing</artifactId>
  <version>1.0.0</version>
  <dependencies>
    <!-- JDBC Driver 示例: MySQL -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.30</version>
    </dependency>
    <!-- Apache POI -->
    <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi-ooxml</artifactId>
      <version>5.2.2</version>
    </dependency>
    <!-- Apache PDFBox -->
    <dependency>
      <groupId>org.apache.pdfbox</groupId>
      <artifactId>pdfbox</artifactId>
      <version>2.0.27</version>
    </dependency>
    <!-- Commons CSV -->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-csv</artifactId>
      <version>1.10.0</version>
    </dependency>
    <!-- FreeMarker(可选) -->
    <dependency>
      <groupId>org.freemarker</groupId>
      <artifactId>freemarker</artifactId>
      <version>2.3.31</version>
    </dependency>
  </dependencies>
</project>

// ===== src/main/java/com/example/dao/ReportDataLoader.java =====
package com.example.dao;

import org.apache.commons.csv.*;
import org.apache.poi.xssf.usermodel.*;
import java.io.*;
import java.sql.*;
import java.util.*;

/**
 * 报表数据加载器:从 DB/CSV/Excel 读取数据,封装为通用格式
 */
public class ReportDataLoader {

    /** 从数据库读取 */
    public List<Map<String,Object>> loadFromDatabase(String sql, Connection conn) throws SQLException {
        try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery(sql)) {
            ResultSetMetaData md = rs.getMetaData();
            List<Map<String,Object>> list = new ArrayList<>();
            while (rs.next()) {
                Map<String,Object> row = new LinkedHashMap<>();
                for (int i=1;i<=md.getColumnCount();i++){
                    row.put(md.getColumnLabel(i), rs.getObject(i));
                }
                list.add(row);
            }
            return list;
        }
    }

    /** 从 CSV 读取 */
    public List<Map<String,Object>> loadFromCSV(String csvPath) throws IOException {
        try (Reader reader=new FileReader(csvPath);
             CSVParser parser=new CSVParser(reader, CSVFormat.DEFAULT.withFirstRecordAsHeader())) {
            List<Map<String,Object>> list=new ArrayList<>();
            for (CSVRecord record:parser) {
                Map<String,Object> row=new LinkedHashMap<>();
                record.toMap().forEach(row::put);
                list.add(row);
            }
            return list;
        }
    }

    /** 从 Excel 读取 */
    public List<Map<String,Object>> loadFromExcel(String excelPath) throws IOException {
        try (FileInputStream fis = new FileInputStream(excelPath);
             XSSFWorkbook wb = new XSSFWorkbook(fis)) {
            XSSFSheet sheet=wb.getSheetAt(0);
            Iterator<Row> rows=sheet.rowIterator();
            List<String> headers=new ArrayList<>();
            List<Map<String,Object>> list=new ArrayList<>();
            if (rows.hasNext()) {
                Row headerRow=rows.next();
                headerRow.forEach(cell -> headers.add(cell.getStringCellValue()));
            }
            while (rows.hasNext()) {
                Row row=rows.next();
                Map<String,Object> map=new LinkedHashMap<>();
                for (int i=0;i<headers.size();i++){
                    Cell cell=row.getCell(i, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
                    switch (cell.getCellType()) {
                        case STRING: map.put(headers.get(i), cell.getStringCellValue()); break;
                        case NUMERIC: map.put(headers.get(i), cell.getNumericCellValue()); break;
                        case BOOLEAN: map.put(headers.get(i), cell.getBooleanCellValue()); break;
                        default: map.put(headers.get(i), cell.toString()); break;
                    }
                }
                list.add(map);
            }
            return list;
        }
    }
}

// ===== src/main/java/com/example/render/ReportTemplate.java =====
package com.example.render;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.*;

/**
 * 报表模板,定义页眉、页脚、列头、数据区布局
 */
public class ReportTemplate {
    private String title;
    private List<String> columnHeaders;
    private int pageWidth = 595;    // A4 宽度像素 (72 DPI)
    private int pageHeight = 842;   // A4 高度像素

    public ReportTemplate(String title, List<String> columnHeaders) {
        this.title = title;
        this.columnHeaders = columnHeaders;
    }

    /** 渲染单页 */
    public BufferedImage renderPage(List<Map<String,Object>> rows, int pageIndex, int rowsPerPage) {
        BufferedImage img = new BufferedImage(pageWidth, pageHeight, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2 = img.createGraphics();
        // 背景
        g2.setColor(Color.WHITE);
        g2.fillRect(0,0,pageWidth,pageHeight);
        g2.setColor(Color.BLACK);
        g2.setFont(new Font("宋体",Font.BOLD,16));
        // 绘制标题
        g2.drawString(title, 50, 50);
        // 绘制页脚
        g2.setFont(new Font("宋体",Font.PLAIN,12));
        g2.drawString("第 " + (pageIndex+1) + " 页", pageWidth - 100, pageHeight - 30);
        // 绘制表头
        int startY = 80;
        int rowHeight = 20;
        int colCount = columnHeaders.size();
        int colWidth = (pageWidth - 100) / colCount;
        g2.setFont(new Font("宋体",Font.PLAIN,12));
        // 画表头背景
        g2.setColor(new Color(230,230,230));
        g2.fillRect(50, startY, pageWidth-100, rowHeight);
        g2.setColor(Color.BLACK);
        for (int i = 0; i < colCount; i++) {
            g2.drawRect(50 + i*colWidth, startY, colWidth, rowHeight);
            g2.drawString(columnHeaders.get(i), 55 + i*colWidth, startY + 15);
        }
        // 绘制数据行
        int dataStartY = startY + rowHeight;
        int from = pageIndex * rowsPerPage;
        int to = Math.min(from + rowsPerPage, rows.size());
        for (int i = from; i < to; i++) {
            Map<String,Object> row = rows.get(i);
            int y = dataStartY + (i-from) * rowHeight;
            for (int j = 0; j < colCount; j++) {
                g2.drawRect(50 + j*colWidth, y, colWidth, rowHeight);
                Object v = row.get(columnHeaders.get(j));
                g2.drawString(v == null ? "" : v.toString(), 55 + j*colWidth, y + 15);
            }
        }
        g2.dispose();
        return img;
    }

    /** 总页数 */
    public int getTotalPages(int totalRows, int rowsPerPage) {
        return (totalRows + rowsPerPage - 1) / rowsPerPage;
    }
}

// ===== src/main/java/com/example/print/PrintableReport.java =====
package com.example.print;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.print.*;
import java.util.List;

/**
 * 实现 Printable,将每页 BufferedImage 绘制到打印机
 */
public class PrintableReport implements Printable {
    private List<BufferedImage> pages;

    public PrintableReport(List<BufferedImage> pages) {
        this.pages = pages;
    }

    @Override
    public int print(Graphics g, PageFormat pf, int pageIndex) {
        if (pageIndex >= pages.size()) return NO_SUCH_PAGE;
        Graphics2D g2 = (Graphics2D) g;
        g2.translate(pf.getImageableX(), pf.getImageableY());
        BufferedImage img = pages.get(pageIndex);
        g2.drawImage(img, 0, 0, img.getWidth(), img.getHeight(), null);
        return PAGE_EXISTS;
    }
}

// ===== src/main/java/com/example/export/PDFReportExporter.java =====
package com.example.export;

import org.apache.pdfbox.pdmodel.*;
import de.rototor.pdfbox.graphics2d.PDFBoxGraphics2D;
import java.awt.image.BufferedImage;
import java.util.List;

/**
 * PDF 导出器
 */
public class PDFReportExporter {
    public void export(List<BufferedImage> pages, String outputPath) throws IOException {
        try (PDDocument doc = new PDDocument()) {
            for (BufferedImage img : pages) {
                PDPage page = new PDPage(new PDRectangle(img.getWidth(), img.getHeight()));
                doc.addPage(page);
                PDFBoxGraphics2D g2d = new PDFBoxGraphics2D(doc, img.getWidth(), img.getHeight());
                g2d.drawImage(img, 0, 0, null);
                g2d.dispose();
            }
            doc.save(outputPath);
        }
    }
}

// ===== src/main/java/com/example/export/ExcelReportExporter.java =====
package com.example.export;

import org.apache.poi.xssf.usermodel.*;
import java.io.*;
import java.util.*;

/**
 * Excel 导出器
 */
public class ExcelReportExporter {
    public void export(List<Map<String,Object>> rows, List<String> headers, String outputPath) throws IOException {
        XSSFWorkbook wb = new XSSFWorkbook();
        XSSFSheet sheet = wb.createSheet("报告");
        // 表头
        XSSFRow headerRow = sheet.createRow(0);
        for (int i=0;i<headers.size();i++){
            headerRow.createCell(i).setCellValue(headers.get(i));
        }
        // 数据
        for (int r=0;r<rows.size();r++){
            XSSFRow row = sheet.createRow(r+1);
            Map<String,Object> map = rows.get(r);
            for (int c=0;c<headers.size();c++){
                Object v = map.get(headers.get(c));
                if (v instanceof Number) row.createCell(c).setCellValue(((Number)v).doubleValue());
                else row.createCell(c).setCellValue(v == null ? "" : v.toString());
            }
        }
        try (FileOutputStream fos=new FileOutputStream(outputPath)) {
            wb.write(fos);
        }
        wb.close();
    }
}

// ===== src/main/java/com/example/MainApp.java =====
package com.example;

import com.example.dao.ReportDataLoader;
import com.example.render.ReportTemplate;
import com.example.print.PrintableReport;
import com.example.export.*;
import java.awt.print.*;
import java.util.*;

/**
 * 主应用入口:演示报表打印与多格式导出
 */
public class MainApp {
    public static void main(String[] args) {
        try {
            // 1. 加载数据
            ReportDataLoader loader = new ReportDataLoader();
            List<Map<String,Object>> rows = loader.loadFromDatabase(
                "SELECT id, name, amount FROM sales ORDER BY id", 
                DriverManager.getConnection("jdbc:mysql://localhost:3306/db","user","pwd")
            );
            // 2. 准备模板
            List<String> headers = Arrays.asList("id","name","amount");
            ReportTemplate template = new ReportTemplate("销售报表", headers);
            int rowsPerPage = 25;
            int totalPages = template.getTotalPages(rows.size(), rowsPerPage);
            // 3. 渲染每页
            List<BufferedImage> pages = new ArrayList<>();
            for (int i=0;i<totalPages;i++){
                pages.add(template.renderPage(rows, i, rowsPerPage));
            }
            // 4. 打印
            PrinterJob job = PrinterJob.getPrinterJob();
            job.setPrintable(new PrintableReport(pages));
            if (job.printDialog()) job.print();

            // 5. 导出 PDF
            new PDFReportExporter().export(pages, "output/report.pdf");
            // 6. 导出 Excel
            new ExcelReportExporter().export(rows, headers, "output/report.xlsx");

            System.out.println("报表打印与导出完成。");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

6. 代码详细解读

  • ReportDataLoader:提供从数据库、CSV、Excel 三种来源读取数据的方法,统一封装为 List<Map<String,Object>>,便于后续渲染使用。

  • ReportTemplate:定义报表页眉、页脚、列头与数据区样式;根据总行数与每页行数计算页数,并为每页生成对应的 BufferedImage

  • PrintableReport:实现 Printable 接口,将已渲染的每页 BufferedImage 绘制到打印机的 Graphics2D,通过 PrinterJob 弹出打印对话框。

  • PDFReportExporter:使用 PDFBox 与 PDFBoxGraphics2D,将每页图像依次写入 PDF 文档页面,并保存到指定路径。

  • ExcelReportExporter:使用 Apache POI,根据报表数据与列头创建 Excel 工作表,填充单元格并输出 .xlsx 文件。

  • MainApp:演示完整流程——加载数据→渲染分页→打印→导出 PDF & Excel。


7. 项目详细总结

本项目基于纯 Java 技术栈,从数据访问到报表渲染、分页、打印、文件导出,提供了一套端到端的企业级报表解决方案。主要优点在于:

  • 一体化:无需额外报表引擎,直接在 Java 应用中完成所有报表功能。

  • 多格式支持:同时支持物理打印、PDF、Excel 等多种输出。

  • 高定制性:可自由定义模板样式与分页规则,满足多场景需求。

同时,项目也存在以下可进一步优化之处:

  • 模板引擎集成:可引入 FreeMarker、Velocity 等动态生成更复杂布局。

  • 性能优化:对大数据量渲染与导出进行异步与分批处理。

  • 图表嵌入:可在报表中集成 JFreeChart 图表,提高可视化效果。


8. 项目常见问题及解答

Q1:如何支持合并单元格?
A:在 ReportTemplate 中手动计算并绘制合并区域的边框与文本位置,或在 Excel 导出时调用 POI 的 CellRangeAddress

Q2:如何将报表样式与业务分离?
A:可将模板样式定义放在 XML/JSON/FreeMarker 中,ReportTemplate 动态加载解析,提升可维护性。

Q3:如何处理超长文本换行?
A:在绘制文本前,通过 FontMetrics 对字符串进行字宽测算,按可用宽度手动拆行。

Q4:如何保证字体在 PDF 中嵌入?
A:使用 PDFBox 时,显式加载并嵌入所需字体文件,确保跨平台一致性。


9. 扩展方向与性能优化

  1. 动态图表与报表融合

    • ReportTemplate 中集成 JFreeChart 图表,动态展示趋势分析。

  2. 分布式渲染与导出

    • 利用微服务架构,将报表渲染与导出任务分发到多节点并行执行。

  3. 缓存与异步

    • 对静态报表或常用查询结果进行缓存,结合异步消息队列提升并发性能。

  4. Web 服务封装

    • 基于 Spring Boot 提供 RESTful API,客户端调用即可获取 PDF/Excel/打印任务。

  5. 权限与安全

    • 在导出与打印流程中加入鉴权、日志与审计,保障报表数据安全。

  6. 多语言与主题

    • 提供多语言资源包及多套主题(色彩、字体),满足国际化与品牌化需求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值