在企业级后台系统中,经常需要把查询结果导出成PDF格式的统计报表。Java生态里,iText是最常用的开源库之一,它提供了从文本、图片到表格、水印的完整排版能力。借助iText,开发者不必关心PDF规范的复杂语法,就能生成符合打印要求的中文报表。

一、环境准备与基础文档创建
在正式写报表逻辑前,先要把iText 7的核心依赖引入项目。Maven工程里添加下面两个坐标即可,其中layout模块包含了表格、段落等高级元素,kernel模块负责底层PDF写入。
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>kernel</artifactId>
<version>7.2.5</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>layout</artifactId>
<version>7.2.5</version>
</dependency>
创建PDF文档的标准流程是:先用PdfWriter绑定输出流,再用PdfDocument包装,最后套上Document对象来方便添加元素。下面代码演示了最简化的初始化过程,实际项目中输出流通常指向HTTP响应或服务器文件。
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import java.io.FileOutputStream;
public class PdfDemo {
public static void main(String[] args) throws Exception {
// 绑定文件输出流
PdfWriter writer = new PdfWriter(new FileOutputStream("report.pdf"));
// 创建PDF文档对象
PdfDocument pdf = new PdfDocument(writer);
// 用布局模块文档便于添加高级元素
Document document = new Document(pdf);
document.add(new com.itextpdf.layout.element.Paragraph("测试报表"));
document.close();
}
}
这种基础写法能跑通,但缺少水印和表格。后面两节会在它之上扩展。值得注意的是,Document关闭时会自动刷新写入器,忘记close会导致文件损坏,因此在Web场景建议放在try-with-resources里。
二、用事件机制添加全局水印
iText 7允许给PdfWriter添加事件处理器,在每页结束渲染时回调。我们自定义一个继承PdfDocumentEvent类的监听器,在onAcceptedEvent方法里拿到当前页的PdfCanvas,然后绘制半透明文本作为水印。这种方式比手动在每页加段落更统一,也不会挤占正文布局。
import com.itextpdf.kernel.events.Event;
import com.itextpdf.kernel.events.IEventHandler;
import com.itextpdf.kernel.events.PdfDocumentEvent;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.colors.ColorConstants;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.layout.Canvas;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.property.TextAlignment;
public class WatermarkHandler implements IEventHandler {
@Override
public void handleEvent(Event event) {
PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
PdfDocument pdfDoc = docEvent.getDocument();
PdfPage page = docEvent.getPage();
Rectangle pageSize = page.getPageSize();
// 在页面中心绘制水印
PdfCanvas canvas = new PdfCanvas(page.newContentStreamAfter(),
page.getResources(), pdfDoc);
canvas.saveState();
canvas.setExtGState(new com.itextpdf.kernel.pdf.extgstate.PdfExtGState()
.setFillOpacity(0.2f));
canvas.setFillColor(ColorConstants.GRAY);
Canvas canvasModel = new Canvas(canvas, pageSize);
Paragraph p = new Paragraph("内部机密");
p.setFontSize(48);
p.setTextAlignment(TextAlignment.CENTER);
canvasModel.showTextAligned(p,
pageSize.getWidth() / 2, pageSize.getHeight() / 2,
pdfDoc.getPageNumber(page),
com.itextpdf.layout.property.TextAlignment.CENTER,
com.itextpdf.layout.property.VerticalAlignment.MIDDLE);
canvasModel.close();
canvas.restoreState();
}
}
注册监听器只需一行:pdfDoc.addEventHandler(PdfDocumentEvent.END_PAGE, new WatermarkHandler())。这样不论后面加多少表格,每一页都会自动带上灰色半透明水印。如果希望水印倾斜,可以在Canvas里用setRotationAngle调整,但注意旋转后文本边界可能超出页面,需要适当缩小字号。
水印层必须画在内容之后(newContentStreamAfter),否则会盖住文字。另外透明度通过PdfExtGState设置,直接改canvas的alpha在部分阅读器不生效。生产环境可将水印文字换成公司名或用户ID,便于追踪泄露源头。
三、表格布局与复杂表头
报表核心往往是数据表。iText的Table类支持按列宽比例分配、单元格合并与边框定制。下面示例构建一个三列报表,第一列是序号,后两列是名称和销售额,并演示如何用setBackgroundColor突出表头。
import com.itextpdf.layout.element.Table;
import com.itextpdf.layout.element.Cell;
import com.itextpdf.layout.property.UnitValue;
import com.itextpdf.kernel.colors.ColorConstants;
import com.itextpdf.layout.element.Paragraph;
public class TableDemo {
public static Table buildTable() {
// 三列,宽度比例1:3:2
Table table = new Table(UnitValue.createPercentArray(new float[]{1, 3, 2}));
table.setWidth(100);
// 表头
Cell h1 = new Cell().add(new Paragraph("序号"));
h1.setBackgroundColor(ColorConstants.LIGHT_GRAY);
Cell h2 = new Cell().add(new Paragraph("产品"));
h2.setBackgroundColor(ColorConstants.LIGHT_GRAY);
Cell h3 = new Cell().add(new Paragraph("销售额"));
h3.setBackgroundColor(ColorConstants.LIGHT_GRAY);
table.addHeaderCell(h1);
table.addHeaderCell(h2);
table.addHeaderCell(h3);
// 数据行
for (int i = 1; i <= 3; i++) {
table.addCell(new Cell().add(new Paragraph(String.valueOf(i))));
table.addCell(new Cell().add(new Paragraph("商品" + i)));
table.addCell(new Cell().add(new Paragraph((i * 100) + "元")));
}
return table;
}
}
addHeaderCell方法会把单元格标记为表头,当表格跨页时自动在每页顶部重复,这对长报表很实用。若需合并单元格,使用new Cell(rowspan, colspan)即可,比如把两行合计行合并成一格。边框粗细通过setBorder方法控制,设为null可去边框做出清爽样式。
中文显示需要在Document初始化时注册字体,否则表格里会出现空白。可以用FontProgramFactory.createFont加载系统宋体或思源字体,再通过PdfFontFactory创建PdfFont传入Paragraph。表格列宽用百分比数组比固定值更能适配不同页面尺寸,避免内容溢出。
四、整合输出完整报表
把前面水印与表格组合,就能得到带背景标识的数据文件。注意Document添加表格后,如果表格很长,iText会自动分页,而我们的END_PAGE事件确保每页都有水印,表头也会重复,整体观感接近Excel打印稿。
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import java.io.FileOutputStream;
public class ReportMain {
public static void main(String[] args) throws Exception {
PdfWriter writer = new PdfWriter(new FileOutputStream("final_report.pdf"));
PdfDocument pdfDoc = new PdfDocument(writer);
// 注册水印事件
pdfDoc.addEventHandler(PdfDocumentEvent.END_PAGE, new WatermarkHandler());
Document doc = new Document(pdfDoc);
doc.add(new com.itextpdf.layout.element.Paragraph("月度销售报表").setFontSize(20));
doc.add(TableDemo.buildTable());
doc.close();
}
}
运行后打开final_report.pdf,能看到每页居中灰色水印,首页有大标题,随后是带浅灰表头的表格。若部署到Web,把FileOutputStream换成ServletResponse的OutputStream,并设好Content-Type为application/pdf,前端就能直接下载。
整体来看,iText用事件解耦了装饰与内容,用Table抽象了排版细节。掌握这两点,常规报表需求基本可在百行内解决,不必引入重量级报表服务器。