目录
-
项目背景详细介绍
-
项目需求详细介绍
-
相关技术详细介绍
-
实现思路详细介绍
-
完整实现代码
-
代码详细解读
-
项目详细总结
-
项目常见问题及解答
-
扩展方向与性能优化
1. 项目背景详细介绍
在企业信息化建设中,各类业务数据繁杂多样,报表作为关键信息展示与决策依据,发挥着至关重要的作用。无论是财务报表、销售报表,还是运营监控报表,都需要格式规范、内容完整,并且能灵活打印到纸质或导出为 PDF 进行归档与分发。
传统报表生成依赖 Office 自动化、第三方报表引擎(如 JasperReports、BIRT)或前端 HTML 打印。它们或需要额外部署组件、或对运行环境有较高要求、或难以集成到纯 Java 后端服务中。本项目以纯 Java 技术栈为基础,从数据读取、报表布局、样式渲染,到打印服务与文件导出,提供一体化的报表打印解决方案,帮助开发者快速集成后台报表功能,减少运维成本,提高开发效率。
2. 项目需求详细介绍
2.1 功能需求
-
数据输入
-
支持从数据库(通过 JDBC)、CSV、Excel(Apache POI)等多种来源读取报表数据。
-
-
报表布局与样式
-
固定页眉、页脚与动态数据区域;支持表格、图表、合并单元格等布局。
-
支持自定义字体、字体大小、颜色、对齐方式、边框样式。
-
-
打印输出
-
通过
javax.print
API 直接打印到物理打印机;支持打印预览。
-
-
文件导出
-
导出为 PDF(PDFBox)、Excel(Apache POI)、HTML、PNG 等多种格式。
-
-
动态参数
-
支持运行时传入报表参数(如日期范围、筛选条件、公司抬头等)。
-
-
国际化
-
支持多语言、多区域设置,动态切换标题与格式。
-
2.2 非功能需求
-
性能:数据量 ≤1 万行时,生成与打印总耗时 ≤200ms。
-
可维护性:模块化、高内聚低耦合,注释全面。
-
可移植性:纯 Java 实现,兼容 Java 8+;无本地平台依赖。
-
稳定性:容错处理、异常日志、重试机制。
3. 相关技术详细介绍
-
Java2D 与 Graphics2D:底层绘图接口,用于无界面渲染报表。
-
Apache POI:读取与导出 Excel 文件。
-
Apache PDFBox:生成与编辑 PDF 文档。
-
JDBC:连接与查询关系型数据库。
-
JasperReports(可选):作为参考的模板引擎,项目实现中通过自定义布局替代。
-
Java 打印服务 API (
javax.print
,java.awt.print
):打印机发现、打印任务提交与页面格式设置。 -
模板引擎(Freemarker/Velocity,可选):动态生成 HTML 或 Excel 报表。
4. 实现思路详细介绍
-
数据访问层:
-
ReportDataLoader
通过 JDBC、CSV 或 POI 读取结构化数据,封装为通用的List<Map<String, Object>>
。
-
-
报表布局层:
-
设计
ReportTemplate
,包含页眉、列头、数据行、页脚等区域。 -
通过 Java2D 在
BufferedImage
或Graphics2D
上逐行绘制表格、文本与图表。
-
-
渲染与分页:
-
根据页面可用高度自动分页;保持表头在每页顶端。
-
-
打印与导出:
-
PrintableReport
实现Printable
,将渲染好的BufferedImage
按页绘制到Graphics2D
; -
使用
PrinterJob
提供预览与打印, -
PDFReportExporter
结合 PDFBox,将每页图像写入 PDF; -
ExcelReportExporter
结合 POI,根据模板生成 Excel 文件。
-
-
参数化与国际化:
-
使用
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. 扩展方向与性能优化
-
动态图表与报表融合
-
在
ReportTemplate
中集成 JFreeChart 图表,动态展示趋势分析。
-
-
分布式渲染与导出
-
利用微服务架构,将报表渲染与导出任务分发到多节点并行执行。
-
-
缓存与异步
-
对静态报表或常用查询结果进行缓存,结合异步消息队列提升并发性能。
-
-
Web 服务封装
-
基于 Spring Boot 提供 RESTful API,客户端调用即可获取 PDF/Excel/打印任务。
-
-
权限与安全
-
在导出与打印流程中加入鉴权、日志与审计,保障报表数据安全。
-
-
多语言与主题
-
提供多语言资源包及多套主题(色彩、字体),满足国际化与品牌化需求。
-