SpringBoot中使用JasperReports 报表引擎 — 介绍、原理与使用实践

SpringBoot中使用JasperReports 报表引擎 --- 介绍、原理与使用实践


一、JasperReports 是什么

1.1 定位

JasperReports 是 Java 生态中最流行的开源报表引擎,专门用于生成格式化的文档 (PDF、Excel、Word、HTML、CSV 等)。它的核心价值是将数据模板分离,通过模板定义报表样式,运行时填充数据后生成最终文档。

1.2 类比理解

复制代码
Word 邮件合并:
  模板(.docx)+ 数据(Excel联系人) → 批量生成信件

JasperReports:
  模板(.jrxml/.jasper)+ 数据(数据库/Java对象) → 批量生成 PDF/Excel/HTML

1.3 典型应用场景

场景 示例
发货单打印 选中订单 → 生成 PDF 发货单(含条码/表格/签章)
财务报表 月度销售汇总 → 导出 Excel
物流面单 快递单/运单 → 生成固定格式 PDF 批量打印
库存盘点表 仓库商品清单 → 导出打印
对账单 供应商对账 → 生成 PDF 发送邮件

二、核心概念

2.1 关键术语

概念 说明 文件格式
JRXML 报表模板源文件,XML 格式,定义报表布局和样式 .jrxml
Jasper 编译后的模板文件(二进制),运行时直接加载 .jasper
JasperPrint 填充数据后的内存报表对象,可导出为各种格式 内存对象
DataSource 数据源,为报表提供数据(JDBC/Java集合/JSON等) -
Parameter 报表参数,从外部传入的变量(如标题、日期、Logo等) -
Field 数据字段,对应数据源中每条记录的列 -
Band 报表区域/带区(页眉/列头/明细/页脚/汇总等) -

2.2 报表结构

复制代码
┌─────────────────────────────────────────┐
│          Title Band(标题区)             │  ← 整个报表只出现一次
├─────────────────────────────────────────┤
│        Page Header(页眉)               │  ← 每页顶部
├─────────────────────────────────────────┤
│       Column Header(列标题)            │  ← 表格列名
├─────────────────────────────────────────┤
│         Detail Band(明细区)            │  ← 每条数据重复一次
│    ┌────┬──────┬────┬──────┬─────┐     │
│    │序号│ 商品名 │数量│  单价  │ 金额 │     │
│    ├────┼──────┼────┼──────┼─────┤     │
│    │ 1  │冰箱   │ 2  │3999  │7998 │     │
│    │ 2  │洗衣机 │ 1  │2999  │2999 │     │
│    │... │...   │... │...   │...  │     │
│    └────┴──────┴────┴──────┴─────┘     │
├─────────────────────────────────────────┤
│        Column Footer(列汇总)           │  ← 合计行
├─────────────────────────────────────────┤
│        Page Footer(页脚)               │  ← 每页底部(页码等)
├─────────────────────────────────────────┤
│         Summary(总汇总)                │  ← 整个报表最后
└─────────────────────────────────────────┘

三、工作流程

3.1 完整生命周期

复制代码
设计阶段(开发时):
  Jaspersoft Studio 设计模板 → 保存为 .jrxml 文件
       │
       ▼
  编译模板:JasperCompileManager.compileReport() → .jasper 文件
       │
       ▼(部署到项目 resources 目录)

运行阶段(运行时):
  加载 .jasper 模板
       │
       ▼
  填充数据:JasperFillManager.fillReport(模板, 参数, 数据源) → JasperPrint
       │
       ▼
  导出文档:JasperExportManager.exportReportToPdf(jasperPrint) → PDF/Excel/HTML
       │
       ▼
  返回给前端下载/预览/打印

3.2 运行时数据流

复制代码
Controller 接收请求(如:打印发货单)
  │
  ├─→ Service 查询数据库获取发货单数据
  │     └── List<DeliveryOrderDto> 数据集
  │
  ├─→ 组装参数 Map(标题、公司名、打印日期等)
  │
  ├─→ 创建数据源 JRBeanCollectionDataSource(数据集)
  │
  ├─→ 加载模板 .jasper(从 classpath 或数据库)
  │
  ├─→ JasperFillManager.fillReport(模板, 参数, 数据源)
  │     └── → JasperPrint(内存中的完整报表)
  │
  ├─→ 导出为目标格式
  │     ├── PDF:JasperExportManager.exportReportToPdfStream()
  │     ├── Excel:JRXlsxExporter
  │     ├── Word:JRDocxExporter
  │     └── HTML:HtmlExporter
  │
  └─→ 写入 HttpServletResponse 输出流
       └── 前端收到文件下载/预览

四、涉及的技术知识点

4.1 模板设计

知识点 说明
Jaspersoft Studio Eclipse 插件形式的可视化模板设计器(拖拽式)
JRXML 语法 XML 格式的模板描述语言
表达式语言 $F{fieldName}(字段)、$P{paramName}(参数)、$V{varName}(变量)
子报表(Subreport) 报表嵌套,主报表中嵌入子报表
条件样式 根据数据值动态改变颜色/字体/可见性
条码/二维码 内置 Barcode4J 支持各种条码格式
图表 内置 JFreeChart 支持柱状图/折线图/饼图

4.2 数据源类型

数据源 场景
Java Bean 集合 JRBeanCollectionDataSource 最常用,传入 List
JDBC 直连 JRResultSetDataSource 直接执行 SQL
空数据源 JREmptyDataSource 只有参数没有明细数据
Map 集合 JRMapCollectionDataSource List 数据
JSON JsonDataSource JSON 字符串/文件

4.3 导出格式

格式 导出器类 用途
PDF JasperExportManager 打印、归档
Excel (xlsx) JRXlsxExporter 数据分析
Word (docx) JRDocxExporter 文档编辑
HTML HtmlExporter 在线预览
CSV JRCsvExporter 数据交换
图片 (PNG) JRGraphics2DExporter 缩略图

4.4 设计模式

模式 应用
模板方法 编译 → 填充 → 导出的固定流程
策略模式 不同 Exporter 实现不同格式导出
建造者模式 ExporterInput/OutputItem 的构建
工厂模式 JasperCompileManager/JasperFillManager 工厂方法

五、通用示例代码

5.1 pom.xml 依赖

xml 复制代码
<dependencies>
    <!-- JasperReports 核心 -->
    <dependency>
        <groupId>net.sf.jasperreports</groupId>
        <artifactId>jasperreports</artifactId>
        <version>6.20.0</version>
    </dependency>
    <!-- 中文字体支持 -->
    <dependency>
        <groupId>net.sf.jasperreports</groupId>
        <artifactId>jasperreports-fonts</artifactId>
        <version>6.20.0</version>
    </dependency>
    <!-- Excel 导出 -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>5.2.3</version>
    </dependency>
</dependencies>

5.2 JRXML 模板示例(发货单)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports
              http://jasperreports.sourceforge.net/xsd/jasperreport.xsd"
              name="delivery_order" pageWidth="595" pageHeight="842"
              columnWidth="555" leftMargin="20" rightMargin="20"
              topMargin="20" bottomMargin="20">

    <!-- 参数定义 -->
    <parameter name="companyName" class="java.lang.String"/>
    <parameter name="deliveryCode" class="java.lang.String"/>
    <parameter name="printDate" class="java.lang.String"/>
    <parameter name="customerName" class="java.lang.String"/>
    <parameter name="address" class="java.lang.String"/>

    <!-- 字段定义(对应 Java Bean 属性) -->
    <field name="productCode" class="java.lang.String"/>
    <field name="productName" class="java.lang.String"/>
    <field name="quantity" class="java.lang.Integer"/>
    <field name="price" class="java.math.BigDecimal"/>

    <!-- 变量定义(自动计算) -->
    <variable name="totalAmount" class="java.math.BigDecimal" calculation="Sum">
        <variableExpression>
            <![CDATA[$F{price}.multiply(new java.math.BigDecimal($F{quantity}))]]>
        </variableExpression>
    </variable>

    <!-- 标题区 -->
    <title>
        <band height="80">
            <staticText>
                <reportElement x="0" y="0" width="555" height="30"/>
                <textElement textAlignment="Center">
                    <font size="18" isBold="true" fontName="华文宋体"/>
                </textElement>
                <text><![CDATA[发 货 单]]></text>
            </staticText>
            <textField>
                <reportElement x="0" y="40" width="200" height="20"/>
                <textFieldExpression><![CDATA["单号:" + $P{deliveryCode}]]></textFieldExpression>
            </textField>
            <textField>
                <reportElement x="355" y="40" width="200" height="20"/>
                <textFieldExpression><![CDATA["日期:" + $P{printDate}]]></textFieldExpression>
            </textField>
        </band>
    </title>

    <!-- 列标题 -->
    <columnHeader>
        <band height="25">
            <staticText>
                <reportElement x="0" y="0" width="100" height="25" mode="Opaque" backcolor="#CCCCCC"/>
                <text><![CDATA[商品编码]]></text>
            </staticText>
            <staticText>
                <reportElement x="100" y="0" width="200" height="25" mode="Opaque" backcolor="#CCCCCC"/>
                <text><![CDATA[商品名称]]></text>
            </staticText>
            <staticText>
                <reportElement x="300" y="0" width="80" height="25" mode="Opaque" backcolor="#CCCCCC"/>
                <text><![CDATA[数量]]></text>
            </staticText>
            <staticText>
                <reportElement x="380" y="0" width="80" height="25" mode="Opaque" backcolor="#CCCCCC"/>
                <text><![CDATA[单价]]></text>
            </staticText>
        </band>
    </columnHeader>

    <!-- 明细区(每条数据重复) -->
    <detail>
        <band height="20">
            <textField>
                <reportElement x="0" y="0" width="100" height="20"/>
                <textFieldExpression><![CDATA[$F{productCode}]]></textFieldExpression>
            </textField>
            <textField>
                <reportElement x="100" y="0" width="200" height="20"/>
                <textFieldExpression><![CDATA[$F{productName}]]></textFieldExpression>
            </textField>
            <textField>
                <reportElement x="300" y="0" width="80" height="20"/>
                <textFieldExpression><![CDATA[$F{quantity}]]></textFieldExpression>
            </textField>
            <textField>
                <reportElement x="380" y="0" width="80" height="20"/>
                <textFieldExpression><![CDATA[$F{price}]]></textFieldExpression>
            </textField>
        </band>
    </detail>

    <!-- 汇总区 -->
    <summary>
        <band height="30">
            <textField>
                <reportElement x="300" y="5" width="180" height="20"/>
                <textElement textAlignment="Right">
                    <font isBold="true"/>
                </textElement>
                <textFieldExpression><![CDATA["合计金额:" + $V{totalAmount}]]></textFieldExpression>
            </textField>
        </band>
    </summary>
</jasperReport>

5.3 JasperUtil 工具类封装

java 复制代码
package com.example.utils;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
import net.sf.jasperreports.engine.export.ooxml.JRDocxExporter;
import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter;
import net.sf.jasperreports.export.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * JasperReports 报表工具类.
 * 封装编译、填充、导出的完整流程.
 */
public class JasperUtil {

    private static final Logger log = LoggerFactory.getLogger(JasperUtil.class);

    /**
     * 导出类型枚举.
     */
    public enum DocType {
        PDF, EXCEL, WORD, HTML
    }

    /**
     * 生成报表并写入 HTTP 响应(文件下载).
     *
     * @param templatePath 模板路径(classpath 下的 .jasper 文件)
     * @param params       报表参数
     * @param dataList     数据集合(对应模板中的 Field)
     * @param fileName     下载文件名
     * @param docType      导出格式
     * @param response     HTTP 响应
     */
    public static <T> void exportToResponse(
            String templatePath,
            Map<String, Object> params,
            List<T> dataList,
            String fileName,
            DocType docType,
            HttpServletResponse response) {

        try {
            byte[] bytes = generateReport(templatePath, params, dataList, docType);

            // 设置响应头
            String contentType;
            String extension;
            switch (docType) {
                case PDF:
                    contentType = "application/pdf";
                    extension = ".pdf";
                    break;
                case EXCEL:
                    contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
                    extension = ".xlsx";
                    break;
                case WORD:
                    contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
                    extension = ".docx";
                    break;
                default:
                    contentType = "text/html";
                    extension = ".html";
            }

            response.setContentType(contentType);
            response.setHeader("Content-Disposition",
                "attachment; filename=" + java.net.URLEncoder.encode(fileName + extension, "UTF-8"));
            response.setContentLength(bytes.length);
            response.getOutputStream().write(bytes);
            response.getOutputStream().flush();

        } catch (Exception e) {
            log.error("报表导出失败", e);
            throw new RuntimeException("报表导出失败", e);
        }
    }

    /**
     * 生成报表字节数组.
     */
    public static <T> byte[] generateReport(
            String templatePath,
            Map<String, Object> params,
            List<T> dataList,
            DocType docType) throws Exception {

        // 1. 加载编译好的模板
        InputStream templateStream = JasperUtil.class.getClassLoader()
            .getResourceAsStream(templatePath);
        JasperReport jasperReport = (JasperReport) JRLoader.loadObject(templateStream);

        // 2. 创建数据源
        JRDataSource dataSource;
        if (dataList != null && !dataList.isEmpty()) {
            dataSource = new JRBeanCollectionDataSource(dataList);
        } else {
            dataSource = new JREmptyDataSource();
        }

        // 3. 填充数据 → 生成 JasperPrint
        JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, params, dataSource);

        // 4. 导出为目标格式
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        switch (docType) {
            case PDF:
                JasperExportManager.exportReportToPdfStream(jasperPrint, outputStream);
                break;
            case EXCEL:
                exportToExcel(jasperPrint, outputStream);
                break;
            case WORD:
                exportToWord(jasperPrint, outputStream);
                break;
            default:
                JasperExportManager.exportReportToHtmlFile(jasperPrint, outputStream.toString());
        }
        return outputStream.toByteArray();
    }

    private static void exportToExcel(JasperPrint jasperPrint, ByteArrayOutputStream out) throws Exception {
        JRXlsxExporter exporter = new JRXlsxExporter();
        exporter.setExporterInput(new SimpleExporterInput(jasperPrint));
        exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(out));
        SimpleXlsxReportConfiguration config = new SimpleXlsxReportConfiguration();
        config.setOnePagePerSheet(false);
        config.setDetectCellType(true);
        exporter.setConfiguration(config);
        exporter.exportReport();
    }

    private static void exportToWord(JasperPrint jasperPrint, ByteArrayOutputStream out) throws Exception {
        JRDocxExporter exporter = new JRDocxExporter();
        exporter.setExporterInput(new SimpleExporterInput(jasperPrint));
        exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(out));
        exporter.exportReport();
    }
}

5.4 业务代码使用示例

java 复制代码
@RestController
public class DeliveryPrintController {

    @Resource
    private DeliveryService deliveryService;

    /**
     * 打印发货单(导出PDF).
     */
    @GetMapping("/api/delivery/print")
    public void printDeliveryOrder(
            @RequestParam Integer deliveryId,
            HttpServletResponse response) {

        // 1. 查询发货单数据
        DeliveryOrderDto order = deliveryService.getDeliveryOrder(deliveryId);
        List<DeliveryItemDto> items = deliveryService.getDeliveryItems(deliveryId);

        // 2. 组装报表参数
        Map<String, Object> params = new HashMap<>();
        params.put("companyName", "xxx科技");
        params.put("deliveryCode", order.getDeliveryCode());
        params.put("printDate", DateUtil.formatStandardDate(new Date()));
        params.put("customerName", order.getCustomerName());
        params.put("address", order.getShipToAddress());

        // 3. 导出 PDF
        JasperUtil.exportToResponse(
            "print/delivery_order.jasper",  // classpath 下的模板
            params,
            items,                           // 明细数据
            "发货单_" + order.getDeliveryCode(),
            JasperUtil.DocType.PDF,
            response);
    }

    /**
     * 批量打印(多个发货单合并为一个PDF).
     */
    @PostMapping("/api/delivery/batch-print")
    public void batchPrint(
            @RequestBody List<Integer> deliveryIds,
            HttpServletResponse response) {

        List<JasperPrint> prints = new ArrayList<>();
        for (Integer id : deliveryIds) {
            DeliveryOrderDto order = deliveryService.getDeliveryOrder(id);
            List<DeliveryItemDto> items = deliveryService.getDeliveryItems(id);

            Map<String, Object> params = new HashMap<>();
            params.put("deliveryCode", order.getDeliveryCode());
            // ... 其他参数

            // 生成每个发货单的 JasperPrint
            InputStream template = getClass().getClassLoader()
                .getResourceAsStream("print/delivery_order.jasper");
            JasperReport report = (JasperReport) JRLoader.loadObject(template);
            JasperPrint print = JasperFillManager.fillReport(report, params,
                new JRBeanCollectionDataSource(items));
            prints.add(print);
        }

        // 合并导出为一个 PDF
        response.setContentType("application/pdf");
        response.setHeader("Content-Disposition", "attachment; filename=batch_delivery.pdf");
        JRPdfExporter exporter = new JRPdfExporter();
        exporter.setExporterInput(SimpleExporterInput.getInstance(prints));
        exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(response.getOutputStream()));
        exporter.exportReport();
    }
}

5.5 模板 DTO 示例

java 复制代码
/**
 * 发货单明细DTO(字段名需与JRXML中的Field名称一致).
 */
@Data
public class DeliveryItemDto {
    private String productCode;    // 对应 $F{productCode}
    private String productName;    // 对应 $F{productName}
    private Integer quantity;      // 对应 $F{quantity}
    private BigDecimal price;      // 对应 $F{price}
}

六、开发流程

6.1 模板设计(使用 Jaspersoft Studio)

复制代码
1. 下载安装 Jaspersoft Studio(免费,基于 Eclipse)
2. 新建 Jasper Report → 选择模板尺寸(A4/自定义)
3. 拖拽组件到 Band 中:
   - Static Text:固定文本
   - Text Field:动态字段 $F{xxx}
   - Image:图片/Logo
   - Barcode:条码
   - Line/Rectangle:线条/边框
4. 定义 Parameters(外部传入的参数)
5. 定义 Fields(对应数据源的字段)
6. 预览效果 → 保存为 .jrxml
7. 编译为 .jasper → 放到项目 resources/print/ 目录

6.2 项目中的文件组织

复制代码
src/main/resources/
├── print/
│   ├── delivery_order.jrxml      ← 模板源文件(用于修改)
│   ├── delivery_order.jasper     ← 编译后模板(运行时加载)
│   ├── outbound_order.jasper     ← 出库单模板
│   └── stock_report.jasper       ← 库存报表模板
├── font/
│   └── simsun.ttf                ← 中文字体文件
└── jasperreports.properties       ← JasperReports 配置

七、关键设计总结

设计要点 实现方式 收益
数据与模板分离 .jasper 模板 + List 数据 修改样式不改代码,修改逻辑不改模板
预编译模板 .jrxml → .jasper(开发时编译) 运行时直接加载,无需编译开销
多格式输出 同一模板导出 PDF/Excel/Word/HTML 一次设计,多种输出
批量合并 多个 JasperPrint 合并为一个 PDF 批量打印只需下载一个文件
中文支持 嵌入字体文件到项目中 避免服务器字体缺失导致中文乱码
参数化 $P{paramName} 动态传入 同一模板适配不同场景(换标题/Logo/签章)
子报表嵌套 Subreport 组件 主从报表(如订单主信息 + 商品明细)
表达式计算 $V{variable} + calculation=Sum 自动计算合计/平均/计数
相关推荐
swipe1 小时前
04|(前端转后全栈)前端状态为什么不够用?从页面数据到 MySQL 持久化
前端·后端·全栈
花生了什么事o1 小时前
DDD 分层架构:六层分层架构
java·架构·ddd
苏三说技术1 小时前
为什么越来越多人使用WebFlux?
后端
武子康1 小时前
Ask/Allow 不是安全边界:企业 Coding Agent 必须建立四层治理(Policy / Scoped Credential / Sandbox / Provenance)
人工智能·后端·agent
Wang's Blog1 小时前
Go-Zero项目开发17: IM私聊功能实现与消息存储设计
开发语言·后端·golang
她的男孩2 小时前
一行配置,把 Spring Boot 后台变成 AI Agent 的工具箱:Forge MCP Server 插件源码拆解
人工智能·后端
qq_150841992 小时前
SQL的insert和update二合一指令
java·数据库·sql
2601_964702892 小时前
Claude Opus 5 API 开发实战:对话、文本生成与结构化输出
java·服务器·前端