☰
Spring Boot集成UReport报表引擎:从数据集绑定到导出全流程
2026/10/15 4:53:05 网站建设 项目流程

简介:UReport2 是由 Bstek 团队研发的开源 Java 报表引擎,支持灵活生成 PDF、Excel、Word 等多种格式的报表,常被集成到 Spring Boot 等企业级项目中,在企业数据展示与打印场景中应用广泛。压缩包内提供一份完整使用手册和配套示例,面向正在选型或初次接触 UReport2 的 Java 后台开发人员,重点解决 Maven 依赖下载、Spring 配置加载、数据源连接、报表模板绘制等日常开发中的常见问题,降低上手门槛。包体总共只有 1 个 doc 文档,压缩后约 1.15MB,内容包含 pom.xml、context.xml、application.yml 等关键配置片段,还梳理了从新建报表、设计表格样式、绑定数据到预览输出的完整流程,适合初学者按步骤边读边练,也能作为日常开发时随手查阅的速查手册。文档目前已获得 5684 人次浏览学习,不少读者借助它快速搭起 UReport2 报表模块,也验证了这份资料在实际项目中的实用价值。通过学习这份手册,可以掌握 UReport2 与 Spring Boot 集成方法,理解内置设计器与数据源的配合方式,并能针对实际业务定制各类统计报表,减少自行摸索时间,快速完成报表功能的落地。

1. UReport到底在解决什么问题:一张交给老板的报表背后的三件事

业务方隔三差五找过来:“能不能帮我导一份销售明细,按月份、按地区、按产品各出一张?” 手工拼 Excel 重复一个下午,换来一句“字段顺序不对”。UReport 就是在这种场景里被捡起来用的纯 Java 开源报表引擎:报表模板用 XML 定义,通过网页设计器拖拽生成,运行期由 Spring Boot 应用负责渲染数据,最终输出 HTML/PDF/Excel。它解决的不只是“画格子”,而是把“取数逻辑 + 展示模板 + 导出格式”三件事固定成一份可维护的报表文件,业务字段变了改一个地方重跑,胜过每次对着数据源重新写查询。本文面向的正是这类需求:不是要做商业智能大屏,而是在既有业务系统里快速交付一批可复用报表。

2. 搭一个最小可运行工程:把设计器在本地拉起来

2.1 为什么选 Spring Boot 集成,而不是下载安装包

UReport 最早是以 Servlet 应用形态分发的,部署到 Tomcat 能直接用;但它没有独立安装包意义上的“服务端”,本质上是一组需要跟业务应用共用 Spring 容器的 Java 库。用 Spring Boot 集成,数据源可以直接复用项目中已配置的连接池,报表文件目录也由 application.yml 统一管理,这比单独部署一个报表中间件少一层网络交互,也少一套运维。另一个现实原因:UReport 的渲染依赖 Spring 容器里的 DataSource Bean,放不进独立进程里当黑盒用,除非再做一套服务接口。既然最终都要嵌进业务系统,不如从一开始就把依赖和配置写进工程。

还需要接受的一件事是:UReport 2.x 已经停止迭代,社区里没有新的 release,但正因为不再改动,存量项目的坑也被前人踩得差不多,网上能搜到的方案基本都经过验证。只要不走新特性路线,用 2.2.9 这一版做内部报表足够稳。下文的依赖和配置我都以这个版本为例。

2.2 最小 pom 依赖与启动类注解

先建一个空的 Spring Boot 工程,Java 8 以上即可。pom 里加核心依赖:

<dependency> <groupId>com.bstek.up</groupId> <artifactId>ureport2</artifactId> <version>2.2.9</version> </dependency>

这一步的逻辑:ureport2这个 artifact 已经把设计器、渲染引擎、导出组件和 Spring Boot 自动配置都打包进来了,不需要再单独引入 console、font 等模块。如果项目里已经引了较高版本的 commons-lang3、poi,ureport2 传递依赖里带的版本可能跟业务冲突,此时用 Maven 的 exclusions 排除旧的 poi 包,让项目里统一用一个高版本的 Apache POI。否则报表导出时大概率会遇到类冲突。

然后是启动类加包扫描。网上很多教程在这里直接抄@ComponentScan("com.bstek.ureport"),实际项目中我更建议把它放在启动类上:

@SpringBootApplication @ComponentScan(basePackages = {"com.example.demo", "com.bstek.ureport"}) public class ReportApplication { public static void main(String[] args) { SpringApplication.run(ReportApplication.class, args); } }

注意basePackages要显式包含com.bstek.ureport。这是因为 UReport 的自动配置类里有一部分关键 Bean 是靠组件扫描注册的,如果启动类默认只扫自己包,设计器页面能渲染出来但数据源加载会失败。配置类加上后,控制台启动日志里出现UReport console started之类的信息,才算真正拉起来了。

application.yml 里的最小配置如下:

server: port: 8080 ureport: file-store-dir: ./ureport/files debug: false

file-store-dir决定报表模板文件存放在哪个目录,我习惯把它指定到磁盘绝对路径或项目运行目录下的相对路径。这里想提醒的是:不要留空,不要依赖默认值。默认行为会把文件写到 classpath 下的一个临时目录,打包部署后文件会随版本更新被覆盖。后面第 5 章会专门再展开这个坑。

2.3 初始化一个真实数据源,让设计器有东西可用

报表设计器本身画的是模板,真正取数靠的是业务数据库。UReport 设计器页面上能直接看到的数据源,来自 Spring 容器中的 DataSource Bean。所以集成这一步不只是“把库引进来”,还要让这个 DataSource 被容器管起来:

@Configuration public class ReportDataSourceConfig { @Bean(name = "reportDataSource") public DataSource reportDataSource() { HikariDataSource ds = new HikariDataSource(); ds.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/report_demo?useUnicode=true&characterEncoding=utf8"); ds.setUsername("report"); ds.setPassword("report123"); ds.setMaximumPoolSize(10); return ds; } }

参数说明:name = "reportDataSource"是设计器里显示的数据源名称,一个应用可以定义多个数据源 Bean,设计器里按名称选择。HikariDataSource的setJdbcUrl需要先引入对应的 JDBC 驱动依赖,MySQL 就用mysql-connector-j,PostgreSQL 就用对应 driver。最大连接数不要开太大,报表查询一般是批量读,10 个连接够用;如果生产库是核心交易库,建议另开一个只读账号,避免报表 SQL 占用业务连接池。

DataSource 配好后启动应用,浏览器访问http://localhost:8080/ureport/designer,能看到设计器页面和左侧数据源列表。这就算最小环境跑通了。还没到做报表那步,先把设计器打开一次,确认工具链没断,后面所有操作都基于这个页面。

3. 做第一张能交货的报表:从 SQL 数据集到单元格绑定

3.1 在网页设计器里建数据集的完整操作路径

打开设计器页面后,左侧会看到一个工具面板。固定走三步就能建立数据集:

第一步,在“数据源”区域点加号,选择“数据源”下的一个已有数据源;第二步,在新建的数据源节点下点“添加数据集”,类型选“SQL 数据集”;第三步,在弹窗里写查询 SQL,并给数据集起一个名字。这个名字是关键,后面单元格绑定字段全靠它。

我做销售汇总报表时的 SQL 一般写成这样:

select region, month, sum(amount) as total_amount, count(distinct customer_id) as customer_cnt from sale_record where month between :startMonth and :endMonth group by region, month order by region, month

参数说明::startMonth和:endMonth是 UReport 数据集参数,设计器弹窗里有“参数”标签页,测试查询时需要填两个值,比如2024-01和2024-12。这里必须注意:UReport 对参数类型有抽象,日期、字符串、整数要分别选类型,否则预览取数时会被当成字符串比较。设计器里“参数”页填完测试值,点击“测试”按钮能看到查询结果集,确认无误再保存。

操作路径用表格拆开,新手上手更直接:

节点操作必要条件
数据源新增连接Spring 容器中已有 DataSource Bean
数据集新增 SQL 数据集有可执行 SQL 和对应 JDBC 驱动
数据集参数填写测试值参数名要跟 SQL 里:xxx一致
保存设计器保存按钮报表文件落盘到 file-store-dir

数据集建好后,左侧能看到字段列表:region、month、total_amount、customer_cnt。接下来才是报表设计中容易绕路的部分:把字段放进格子。

3.2 单元格绑定字段与聚合函数:A2 的写法

新建一张空白报表,默认会有一块单元格区域。最直接的做法是:在 A1 单元格双击填“地区”,B1 填“月份”,C1 填“金额”,然后在 A2 里写表达式把数据集字段绑进去。

UReport 单元格表达式用${...}包裹,表达式中数据集名用ds作为别名。例如 A2 里写:

${ds.region}

运行时,这个单元格会按region字段的唯一值自动向下展开。同理 B2 写:

${ds.month}

C2 写:

${ds.total_amount}

要注意的是:C2 跟 A2、B2 处于同一行,运行时它会跟随左侧单元格的展开方向对齐,不用手动复制。这个“跟随扩展”是报表引擎最核心的机制,新手经常犯的错是在 C2、C3 分别绑定字段,结果出现笛卡尔积一样的多行重复。

合计行怎么加?在 C3 单元格直接写 UReport 的聚合表达式:

=sum(C2)

这里sum(C2)表示对 C2 这个单元格展开后的所有值求和。表达式不是${...}包裹,而是以=开头,这是 UReport 里计算格和普通数据格的区别。A3 单元格想显示“合计”两个字,直接写文本,其他格子留空即可。如果想按地区加小计,就要用单元格父格的概念:把 A 列设为父格后,=sum(C2)会在每个地区分组内求一次小计,整个地区切换时再求总计。

这块设计器里不需要写代码,拖拽加双击输入就能完成,但理解“父格”是理解报表分组的钥匙。UReport 里选中 C2 单元格,右侧属性面板会显示“父格”设置,默认是“无”。把父格设为 A2,表示 C 列跟随 A 列地区分组展开,此时每个地区下都能看到小计;如果不设父格,所有数据挤在一个维度下,看不出分组层级。

3.3 保存、预览、导出:一条能贴给业务的 URL

报表画完后点设计器右上角保存,输入文件名,如sale_summary.ureport.xml,文件会写入ureport.file-store-dir配置的目录。保存成功后,预览不需要再进设计器,浏览器直接访问:

http://localhost:8080/ureport/preview?_u=file:sale_summary.ureport.xml

_u参数的值由文件类型前缀加文件名组成,file:表示从存储目录读取模板。日志里看到的是这个 URL 对应的渲染请求,报表每次访问都会重新执行 SQL 取数,不存在“缓存了旧数据”的情况。要让业务用起来,把上面 URL 里的 preview 换成 export,再加format参数:

http://localhost:8080/ureport/export?_u=file:sale_summary.ureport.xml&format=pdf

导出支持的格式还有xls、xlsx、doc。我一般按需求选:财务对账用xlsx,给领导发周报用pdf,历史归档用xls。如果导出时要带参数筛选,URL 后面直接用&区域=华东这样拼接,前提是数据集参数名跟 URL 参数名对得上。这部分第 4 章详细说。

4. 报表文件是怎么被解析的:.ureport.xml 的骨架和参数传递

4.1 打开一张真报表文件,它大约长什么样

UReport 的模板文件虽然是由设计器生成的,但它本质是 XML,放在磁盘上可以手工读、可以进版本库。打开刚才保存的sale_summary.ureport.xml,核心结构大致是这个样子:

<ureport> <dataset name="ds" type="normal"> <sql><![CDATA[select region, month, sum(amount) as total_amount ... ]]></sql> <parameter name="startMonth" default-value="2024-01"/> <parameter name="endMonth" default-value="2024-12"/> </dataset> <cell name="A2" expand="Down"> <cell-type> <dataset value="ds" aggregate="select" property="region"/> </cell-type> </cell> <row row-number="2" height="22"/> <column col-number="1" width="100"/> </ureport>

这不是某个版本的完整 schema,但关键层次都在:dataset节点里存了 SQL 和参数默认值;cell节点通过name定位到单元格,expand="Down"表示纵向展开,aggregate="select"表示取明细,还有property="region"指明绑定字段。真正在生产环境维护报表时,大部分改动还是回设计器改,但当你遇到“报表文件在版本库里但同事改错了字段名”这种问题时,能直接打开 XML 搜property往往比重新开设计器更快。

参数默认值值得注意。数据集参数在 XML 里都有default-value,设计器里测试填的值会自动写进去。这个默认值有一个副作用:如果 URL 没传参数,报表渲染时就会用默认值,而不是报错或查全表。所以生产报表一定把默认值设成一个真实存在的合法值,防止领导直接打开预览页时看到空白。

4.2 URL 参数进入 SQL 的绑定过程

参数传递是 UReport 项目里最常见的血泪经验区,很多人第一次做带条件报表都会卡在这里。完整链路是:访问 URL 带参数 → 报表引擎读取_u定位模板 → 从模板解析出 dataset 定义里的 parameter 列表 → 拿 URL 同名参数替换 SQL 中的:xxx→ 执行查询。

看一个带月份筛选的完整 URL:

http://localhost:8080/ureport/preview?_u=file:sale_summary.ureport.xml&startMonth=2024-01&endMonth=2024-12

这里 URL 参数名必须和数据集参数名完全一致。如果 SQL 里写的是:startMonth,URL 里写成startmonth或者start-month,渲染时参数就传不进去,报表会直接采用 XML 里的默认值,且渲染日志里没有报错——这是最坑人的地方,因为从页面上看报表是正常的,只是数据范围错了。

另一种常见做法是把参数写在单元格表达式里,例如:

${ds.region}

这种场景对应“数据集返回多列,单元格按行展开”的普通表格。而像“一个输入框传一个值,每行都拼上这个值”的商务报表,做法是在 SQL 里加参数、设计器里把参数拖到单元格区域,这样不是数据集绑定而是单元格引用。两者不要混:数据集参数解决的是 SQL 取数范围,单元格参数解决的是页面展示逻辑。

参数类型还要对应好。SQL 里where create_time >= :startTime时,设计器数据集参数类型要选“日期”,URL 传参格式就按日期来。如果建数据集时把类型留成字符串,数字类型的字段可能会被隐式转换,索引失效,慢查询就是这么出来的。

4.3 三个必须会用到的单元格函数:sum、日期格式化、条件判断

报表模板里用得最多的计算是三类:聚合求和、日期格式化、条件输出。聚合前面见过,写在一格里就行:

=sum(C2)

这个表达式对单元格做求和,不是对数据集做求和,所以它在父格变化时能生成“小计、合计”两级结果。日期格式化通常要配合参数或数据集字段,UReport 支持在表达式里调用函数来做格式转换,例如把字符串日期转成标准格式,常见写法是:

=format(D2, "yyyy-MM-dd")

这里的format是 UReport 内置函数,第一参数是日期字段所在单元格,第二参数是输出格式。如果直接显示日期字段不格式化,Excel 打开可能出现一串时间戳数字,所以导出的报表我一般会统一在单元格表达式里做一次格式化。

条件输出最常用在状态字段上,等价于业务侧的 if-else:

=if(C2 > 10000, "达标", "未达标")

if函数在报表里能做的不只是写文案,还能配合单元格扩展。比如把 D2 定义成=if(C2 > 10000, "red", "black"),然后给 D2 单元格的字体颜色属性里引用 D2 自身,就能实现达标红、未达标黑的效果。UReport 的函数列表还包含求和、平均、最大值、最小值、计数等常用聚合,以及字符串拼接、子串截取这类数据处理函数。不要试图在 SQL 里把排版相关的事都做完,能用报表函数处理的,尽量留在模板层改,否则每次调样式都要跑一遍数据库。

5. UReport 避坑清单:我们踩过的那几个翻车点

5.1 数据源 Bean 明明配置了,设计器里却空列表

现象:Spring 启动日志正常,DataSource 配置类也执行了,但打开设计器,数据源下拉框是空的。原因有两类:一类是启动类没有@ComponentScan("com.bstek.ureport"),设计器内部的初始化器没拿到容器里所有 DataSource;另一类是项目里有多个 DataSource 配置,且其中一个配了@Primary,UReport 不知道该引用哪个。解决:确认启动类包扫描包含com.bstek.ureport;如果有多个数据源,给报表要用的那个加@Primary,或者直接把它用@Bean(name = "...")单独命名,并在设计器里手动选择。这件事排查时可以打开设计器页面的浏览器控制台,看到数据源加载的接口返回 500,基本就是容器里注入异常。

5.2 报表文件保存成功,重启一次就“丢失”

现象:设计器里保存后能看到.ureport.xml,预览也正常,但只要应用重启,文件就没了,所有报表模板回到空白。原因:ureport.file-store-dir没配置或配置成了 classpath 相对目录,默认落盘到了target/classes下。构建重新打包时target目录被清空,报表文件跟着丢。解决:把file-store-dir固定到应用外部目录,例如:

ureport: file-store-dir: /data/app/ureport/files

并且部署时把这个目录做成持久化盘挂载。某次我们没注意这点,测试环境好好的,发到生产后报表全部失踪,运维查了半天才发现是写到临时目录被回收了。

5.3 导出 Excel 打开是乱码或半张表

现象:用format=xls导出,Excel 打开看到的是满屏乱码,或只有第一页后面全是空行。乱码常见原因是 URL 输出格式参数写错,UReport 导出 Excel 要求format=xls,写成format=excel会返回 HTML 内容。半张表则跟报表分页有关,模板里如果设置了固定行高或分页行数,导出内容被截断。解决:先访问 preview 看 HTML 预览是否完整,排除数据问题;再把format参数值严格写成xls或xlsx;如果内容不完整,回到设计器把报表纸张方向改成“横向”,或者删除分页符相关的单元格设置。文件名中文乱码是另一件事,URL 上的文件名要做 UTF-8 编码。

5.4 取数几万行直接卡死,又是内存溢出

现象:报表数据集 SQL 返回一万行明细,预览时页面转圈,后台java.lang.OutOfMemoryError。原因:UReport 渲染流程会把全部结果集加载到内存再逐单元格展开,数据集带子查询也要先完整执行。它不适合做类 Excel 的百万行明细导出,也不是为此设计的。解决:从数据源头控制,SQL 里先聚合,把报表要展示的粒度算好;如果业务确实需要明细导出,分页或分段查询后自己拼 Excel 更现实。另一个隐蔽点:给数据集加参数时测试值固定了,但生产 URL 参数传了一个极大日期范围,一次拉了几年的数据,性能和内存问题就这么冒出来。限制参数范围这件事,最好在 SQL 里写死一个最大跨度,比如where date >= date_sub(now(), interval 24 month)。

除以上四点外,还有两个容易忽略的地方。一是报表预览 URL 和导出 URL 不能在生产环境裸奔,谁拿到_u就能访问模板内容,建议在 Nginx 层加按路径的访问控制,或写个过滤器校验来源;二是多人同时编辑同一张报表模板,后保存的人会覆盖先保存的人,团队协作时最好约定报表文件一人只维护一张,进 Git 库靠 diff 确认变更。

6. 落地技巧:用一个转发接口把报表嵌进业务系统

直接在业务页面里 iframe 嵌入/ureport/preview是最快的方式,但会遇到两个问题:一是权限,内部系统有统一登录时,报表页面拿不到登录态;二是设计器地址暴露,业务人员误入设计器可能改坏模板。我后来习惯的做法是写一个转发接口,把报表渲染入口收拢到自己 Controller 里:

@GetMapping("/report/{fileName}/preview") public void preview(HttpServletRequest request, HttpServletResponse response, @PathVariable String fileName) throws Exception { // 业务权限校验,基于当前登录用户判断是否有查看该报表的权限 ... String targetUrl = "/ureport/preview?_u=file:" + fileName; request.getRequestDispatcher(targetUrl).forward(request, response); }

参数说明:fileName由 Controller 层控制,白名单里写死允许访问的报表列表,避免任意文件被读取;forward转发后 URL 参数原样透传给 UReport 渲染器,能拿到?startMonth=...这类筛选条件。导出同样转发到/ureport/export,格式参数由前端固定传。这样操作下,业务页面只需要面对一层接口,改动报表模板不影响业务入口。

这个方案还顺手解决了一个权限边界:报表模板里的 SQL 也能看到部分业务表结构,直接暴露给前端不合适,收进 Controller 后只开放“有权限的报表”一类接口。最后再提醒一句:UReport 渲染不是无状态的,报表文件被多人并发访问时,引擎会按模板实时解析,碰上高频访问建议在转发接口里做一层本地缓存,把解析后的报表模板缓存起来,渲染期压力会小很多。

如果只在两台机器上做内部报表,开头那份最小工程已经足够;一旦报表数量上两位数,强烈建议把ureport.file-store-dir纳入 Git 管理,每次模板变更都走一次代码评审,这是我能给出的最实在的经验。报表文件也跟代码一样需要版本管理,等你因为同事改了一个字段导致所有导出结果对不上账时,就会明白这份约束值多少钱。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询