简介:这份文档面向需要快速上手 UReport 报表开发的 Java 开发者与 Spring Boot 项目实践者,围绕 UReport 的使用示例与操作手册展开,重点解决报表模板设计、数据源配置、报表生成及与 Spring Boot 集成等常见问题。压缩包内仅含 1 个 doc 文件,体积约 1.15MB,内容以图文说明和配置示例为主,便于按章节查阅 Maven 依赖引入、context.xml 与 application.yml 配置、模板制作和集成流程。目前已有 5685 人学习,适合作为入门查阅与项目落地时的参考。读者可从中了解 UReport 生成 PDF、Excel、Word 等格式报表的基本思路,掌握报表模板设计、数据源配置、生成优化及常见故障排查方向,并借助示例快速搭建可运行的报表 Demo,减少从零摸索配置的成本。
1. UReport 使用 demo 到底难在哪:一份能照着改的使用手册起点
很多人搜「UReport 使用 demo」,想要的其实不只是一个能启动的工程,而是一份能对着改的使用手册:依赖怎么引、设计器怎么打开、数据集和单元格表达式怎么配合、上线后分页和导出为什么又开始出错。UReport 走的是「单元格即报表」的路子,一张模板就是一个 Excel 样式的网格,每个格子挂一段表达式,引擎按表达式把数据填进去。它最适合的是那种字段固定、格式要求严格、业务方还要自己微调列宽和表头的报表场景。下面按环境、设计器、集成、排查、进阶的顺序走一遍,每一步都给到能直接抄改的参数和命令。
2. 把 UReport 跑起来:最小 demo 的依赖、配置与设计器入口
动手前要先想清楚一件事:这个 demo 是「验环境」还是「验方案」。只验环境,一个空工程加设计器就够;验方案,就得把数据源、样本表、导出链路一起配好,否则设计器里画得再漂亮,接业务库时照样会出问题。我的习惯是一开始就按生产结构建目录,宁可多引一个包,也不要在上线前再回头改路径。
2.1 依赖与目录结构:一个能跑的最小工程长什么样
报表引擎本身不含界面,设计器是独立的一份前端资源,所以至少要引两个构件:引擎核心和设计器控制台。前者负责表达式解析、数据填充、导出,后者把浏览器里的可视化设计器挂到某个 URL 上。如果你用的是带自动装配能力的框架,再加一个集成包来省掉手写 Servlet 注册和静态资源映射。
<!-- 引擎核心:表达式、数据集、导出 --> <dependency> <groupId>com.bstek.ureport</groupId> <artifactId>ureport2-core</artifactId> <version>${ureport.version}</version> </dependency> <!-- 设计器:把可视化编辑界面挂到 /ureport/designer --> <dependency> <groupId>com.bstek.ureport</groupId> <artifactId>ureport2-console</artifactId> <version>${ureport.version}</version> </dependency>版本统一用一个属性变量管住,两个包必须同版本,混版本最容易出现的情况是设计器能打开、点保存没反应,或者预览报类找不到。这个坑后面还会提,先记住一条:ureport.version只写一处。
工程目录建议一开始就分开,报表模板不跟代码混在一起:
src/main/ ├── java/ │ └── com/example/report/ │ ├── ReportApplication.java # 启动类 │ ├── config/ # 数据源、安全放行配置 │ └── service/ # 报表权限与参数处理 └── resources/ ├── application.properties # 报表引擎配置 └── ureport/ # 本地模板目录(仅开发期) └── demo_order.ureport.xml模板文件是 XML,扩展名固定为.ureport.xml,内部记录单元格坐标、表达式、样式、数据集定义。它本质是一份可版本化的文本,放进 Git 管理比塞数据库里更利于追溯改动,这一点在多人协作时差异非常明显。
2.2 配置项逐条拆:报表存储、设计器开关与数据源
UReport 的配置项不多,但每一项都直接决定「能不能用」。下面这几个是启动阶段必须明确的,写成 properties 形式,带自动装配的框架会把它绑到引擎的配置对象上。
# 是否禁用文件存储提供者。false 表示启用,模板会落在本地目录 ureport.disableFileProvider=false # 模板落盘目录,必须是进程有读写权限的绝对路径 ureport.fileStoreDir=/data/ureport/reports # 关闭会话级报表缓存,多人同时预览时避免互相串数据 ureport.disableHttpSessionReportCache=false # 打开调试,表达式报错时会带上更完整的堆栈 ureport.debug=true几项参数的实际作用差别很大,配错的表现也各不相同:
| 配置项 | 作用 | 建议值 | 配错的典型表现 |
|---|---|---|---|
disableFileProvider | 控制模板是否走本地文件存储 | false | 设为true后设计器保存无效,刷新即丢 |
fileStoreDir | 模板落盘根目录 | 独立数据盘绝对路径 | 用相对路径时随启动目录漂移,重启找不到模板 |
disableHttpSessionReportCache | 是否关掉会话缓存 | 生产建议true | 开着时高并发预览会拿到别人的查询结果 |
debug | 表达式异常详细程度 | 开发true,生产false | 关掉后报错只有一行,定位不到具体单元格 |
fileStoreDir这一项尤其要注意,容器化部署时如果不挂持久卷,重启一次模板全没了,业务方会以为是自己误删。我一般会在启动日志里打一行当前生效的目录,方便排障时第一时间确认。
数据源部分不用单独配,引擎会复用应用已有的 JDBC 数据源。如果你的项目里有多个数据源,就得自己实现一个数据源提供者,按名字返回对应的DataSource,否则设计器里配 SQL 数据集时选不到目标库。
2.3 启动并打开设计器:三个验证动作确认环境没歪
启动之后不要急着画报表,先做三个验证,能把绝大部分环境问题挡在门外。
第一个动作,访问设计器入口。默认路径是/ureport/designer,如果返回 404,先看控制台包有没有真正进到依赖树里,再看安全框架有没有把/ureport/**拦掉。企业项目里十有八九是后者。
# 只看这个路径能不能通,不关心返回内容 curl -o /dev/null -s -w "%{http_code}\n" http://localhost:8080/ureport/designer第二个动作,新建一个内置数据集,随便造两行数据,拖到单元格里预览。这一步验证的是表达式引擎和渲染链路,跟数据库无关,能过说明核心没问题。
第三个动作,保存模板,然后去fileStoreDir看有没有生成.ureport.xml文件。文件在,说明存储链路通;文件不在但页面提示保存成功,那多半是目录权限问题,进程写不进去又不报错。
# 保存后立刻确认文件是否落盘,注意时间戳 ls -lh /data/ureport/reports/*.ureport.xml三个动作都过,环境就算立住了。后面所有问题都可以归到「表达式写错」或「数据不对」这两类,排查范围一下子小了很多。
3. 使用手册第一课:在设计器里把数据集、单元格表达式和参数串起来
设计器里最容易让人卡住的不是画表格,而是搞不清「数据从哪来、往哪填、按什么条件筛」这三件事的先后顺序。正确的顺序是先定数据集,再排单元格布局,最后补参数。反过来做,你会发现改一个查询条件就得动半张表。
3.1 数据源与数据集:SQL 数据集怎么配、参数怎么占位
数据集是报表的数据入口,最常用的是 SQL 数据集。写 SQL 的时候有个原则:只查报表真正要的列,别图省事写select *。报表模板里字段一多,单元格表达式就容易绑错列,而且导出时多出来的列会让列宽全乱。
-- 订单明细报表的数据集,参数用 ${} 占位,由报表参数面板注入 select o.order_no, o.customer_name, o.amount, o.created_at from t_order o where o.created_at >= ${beginDate} and o.created_at < ${endDate} and o.status = 'PAID' order by o.created_at desc, o.order_no这段 SQL 有两个细节值得说。一是结束时间用<而不是<=,避免当天的数据因为时分秒被漏掉,这是报表里最常见的「少一天」问题。二是参数占位符不要自己拼字符串,引擎会把参数值按类型绑定,拼字符串既容易被注入,也会让日期类型在部分数据库上直接报错。
数据集命名也有讲究,名字会直接变成表达式里的对象名。上面这个数据集如果命名为orderDs,那么字段访问就是orderDs.customer_name。名字尽量短、语义清楚,别用中文或者带空格的名字,后面写表达式时会很别扭。
除了 SQL 数据集,还有两种常见入口:Spring Bean 数据集和内置数据集。前者适合数据来自远程接口或复杂计算的场景,你只需要提供一个返回集合的方法,把方法名填进数据集配置即可;后者用于调试,手工填几行固定数据,验证布局时特别快。
3.2 单元格表达式:从简单字段绑定到分组聚合
单元格表达式是这张报表的核心。它没有「公式」和「脚本」的区分,所有逻辑都写在格子里,以=开头。刚上手的人容易把它当成模板引擎的变量替换,实际上它更接近一个带上下文的表达式求值器,#代表当前单元格,周围格子的值可以直接引用。
| 使用场景 | 表达式写法 | 关键说明 |
|---|---|---|
| 取数据集第一条记录的字段 | =orderDs.customer_name | 单行数据集直接取,多行会只取第一条 |
| 展开明细行 | =orderDs.select(orderDs.order_no) | 单元格向下自动扩展成多行 |
| 按客户分组 | =orderDs.group(orderDs.customer_name) | 生成分组行,配合下方明细使用 |
| 组内金额合计 | =sum(D3) | 对 D3 及其向下扩展的区域求和 |
| 引用报表参数 | =param("beginDate") | 与 SQL 里的${beginDate}对应 |
| 条件显示 | =orderDs.amount > 1000 ? "重点" : "" | 三元表达式,注意空值判断 |
写表达式时有两条经验。第一,分组和明细必须成对出现:group后面紧跟的那一行放select,引擎才知道哪些行属于这个组,顺序反了就会出现每组只显示一行。第二,聚合函数里的坐标写起始格就行,不要写区域范围,引擎会顺着扩展方向自己算,写死范围反而在数据量变化时错位。
调试表达式不要一次写完一整张表。先在一个格子里验证数据集能不能取到值,再验证分组,最后加聚合。因为报错信息通常只告诉你「第几行第几列表达式异常」,不会告诉你缺了哪个字段,逐个格子验证是唯一的低成本办法。
3.3 参数面板与查询表单:把硬编码 SQL 变成可查询报表
报表一旦要交给业务方自己用,参数就是必须的。参数定义在报表的「参数」面板里,每个参数有名字、类型、默认值和数据来源。名字必须和 SQL 里的占位符完全一致,大小写也算,这是新手最常踩的坑之一。
参数的数据来源有几种:手工填默认值、从 SQL 数据集取值做下拉、从静态字典取值。做下拉时建议单独配一个只返回value和label两列的轻量数据集,别复用主数据集,否则每次打开参数面板都要跑一遍大查询,速度会很难看。
-- 下拉参数专用数据集,只要两个字段,避免拖慢参数面板加载 select c.customer_id as value, c.customer_name as label from t_customer c where c.enabled = 1 order by c.customer_name默认值怎么设也有讲究。日期类参数一般默认「本月第一天」到「今天」,靠表达式算而不是写死;如果业务方习惯先看全量数据再筛,就把默认值留空,并在 SQL 里用条件判断让空值不参与过滤。这一点处理不好,用户第一次打开报表看到空白页,会直接认为报表坏了。
参数传进来之后还有一个隐性问题:空值和空字符串不是一回事。部分数据库里''和NULL的比较行为不同,参数没填时到底是NULL还是空串,最好在 SQL 里显式判断,别指望引擎帮你兜底。
4. 从设计器到业务系统:Spring Boot 集成与报表的保存、预览、导出
设计器里画出来的东西,最终要变成业务系统里的一个菜单项。这一步的难点不在技术,而在「谁有权看哪张报表、看哪些行」这件事上。把权限放在前端判断等于没做,必须在服务端把参数和路由一起管住。
4.1 报表文件的存储方式:文件、数据库还是对象存储
模板存哪儿,是个需要提前定的问题。三种方式各有适用面,选错了后面迁移成本很高。
| 存储方式 | 实现方式 | 优点 | 适用场景 |
|---|---|---|---|
| 本地文件 | 引擎自带的文件存储 | 零开发,设计器直接可用 | 单机部署、报表数量少 |
| 数据库 | 自定义存储提供者,把 XML 存进一张表 | 多实例共享,可做版本记录 | 集群部署、需要审计 |
| 对象存储 | 自定义存储提供者,读写远端对象 | 容量无上限,便于备份 | 报表体量大、跨环境同步 |
单机部署用本地文件完全够用,但要记得把目录挂到持久卷上,并且定期备份。集群部署就得换数据库方案,否则用户在设计器里保存的模板只落在其中一台机器上,下次请求打到另一台就找不到文件,表现是「时好时坏」,这类问题排查起来极其费劲。
走数据库方案时,表结构不用复杂,主键、报表名、内容、更新时间四个字段就够,内容字段用大文本类型。读取时按名字查出一条记录,转成输入流转给引擎,写入时覆盖更新。要注意的是并发保存:两个人同时编辑同一张报表,后保存的会覆盖先保存的,要么加乐观锁,要么在设计器入口就限制同一张报表只能一个人编辑。
4.2 预览与导出接口:Controller 层怎么接、参数怎么传
引擎自带预览和导出端点,但业务系统一般不会让前端直接调,中间至少要加一层鉴权和参数过滤。我的做法是写一个很薄的转发层:先校验用户对这张报表的读权限,再把参数过一遍白名单,最后重定向到引擎端点。
@RestController @RequestMapping("/api/report") public class ReportAccessController { @Autowired private ReportPermissionService permissionService; // 业务侧自行实现 @GetMapping("/{reportId}/preview") public void preview(@PathVariable String reportId, @RequestParam Map<String, String> params, HttpServletResponse response) throws IOException { // 1. 校验当前用户是否有这张报表的读权限 permissionService.assertReadable(reportId); // 2. 参数白名单:只放行报表声明过的参数,防止被塞入额外条件 Map<String, String> safe = permissionService.filterParams(reportId, params); // 3. 拼查询串并重定向到引擎端点,导出把 preview 换成 excel / pdf 即可 String query = safe.entrySet().stream() .map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8)) .collect(Collectors.joining("&")); String target = "/ureport/preview?_u=file:" + reportId + ".ureport.xml" + (query.isEmpty() ? "" : "&" + query); response.sendRedirect(target); } }_u参数是引擎的模板定位符,file:前缀表示走文件存储;如果换成了数据库存储,前缀要改成对应的提供者标识,具体值取决于你实现的存储提供者返回的标识,改之前先在设计器里点一次预览,看地址栏里实际拼的是什么,照着抄最稳。
参数白名单这一步看着麻烦,实际上省掉了后面大量的排查。有些用户会手动改 URL 里的参数,把不该看的客户 ID 塞进去,没有白名单的话数据就漏出去了。过滤逻辑很简单:报表定义里声明过哪些参数,就只放行哪些,其余一律丢弃。
4.3 权限与多租户:报表可见性和数据行级过滤
权限分两层,别混在一起做。第一层是报表级:这张报表归哪个部门、哪些角色能看,通常存在业务表里,鉴权时查一次就行。第二层是数据级:同一张报表,不同的人看到不同的行。
数据级过滤有两种实现路径。一种是在 SQL 里加条件,把当前登录人的组织 ID 作为隐藏参数传进去;另一种是在数据集层面做拦截,读取当前会话上下文,动态拼条件。前者直观但要把条件写进每一张报表的 SQL,后者集中但要求你对引擎的数据集扩展点足够熟。中小项目我一般选前者,改动可控,出问题也容易定位。
用隐藏参数时注意不要暴露在 URL 上。做法是参数面板里不显示,服务端转发时从会话里取用户信息注入,前端传的参数直接忽略同名项。这一步如果偷懒,用户改一下 URL 就能看到别人的数据,属于典型的越权问题。
5. 避坑与排查:UReport 上线前最容易出现问题的 5 个地方
下面这几条都是我在实际项目里遇到过、且每次都花了半天以上才定位的问题。现象写清楚,原因和解决办法一并给,照着对一遍能省不少时间。
5.1 设计器打开是空白页,控制台一堆 404
现象:访问设计器路径,浏览器标签转了半天,页面一片白,开发者工具里能看到静态资源全是 404。
原因:控制台包没进依赖树,或者被安全框架的拦截器挡在了静态资源之前。还有一种情况是网关层做了路径重写,/ureport/前缀被剥掉了。
解决:先用依赖树命令确认控制台包存在,再检查拦截器放行规则,把/ureport/**加到白名单。网关层的问题看转发日志里最终到达服务的路径,跟预期比对。这几步走完基本能定位,剩下的就是配置改动。
5.2 预览正常,导出 Excel 却出现方框和乱码
现象:浏览器里预览中文完全正常,点导出 Excel 或 PDF 后,标题里的中文变成方框,或者整段乱码。
原因:Excel 一般不会乱码而是字体问题,PDF 更常见的是缺字体文件。中文在 PDF 里必须有对应的字体嵌入,服务端环境没有装中文字体,导出就会退化成方框。
解决:把字体文件打进镜像或放到固定目录,在引擎的字体配置里指过去。同时确认导出响应头里的字符集是 UTF-8,链路上任何一层做了转码都会前功尽弃。导出的文件名如果带中文,还要做一次 URL 编码,否则部分浏览器会显示成乱码文件名。
5.3 分页错乱,表头重复出现或者行被截断
现象:数据超过一页后,第二页开头又出现一遍完整表头,或者某一行被硬生生切成两半,跨页后内容对不上。
原因:重复表头的范围设置过大,把明细行也圈进去了;跨页断行的处理和合并单元格冲突,引擎不知道在哪里切。
解决:重复区域只框表头那几行,不要包含明细行。合并单元格尽量放在表头,明细行里少用纵向合并。分页设置里有一项控制是否允许行内断页,默认关闭,如果误开了就会出现半行。改完在预览里翻到第二页确认,别只看第一页。
5.4 数据量一大就超时或者内存打满
现象:测试库几万条数据导出正常,生产上百万行时接口超时,服务端内存曲线一路飙高。
原因:引擎默认会把结果集整体加载到内存再渲染,数据量一上来就是成倍的内存占用。加上导出时还要在内存里拼工作簿对象,双重叠加。
解决:优先在 SQL 层做限制,能不查全量就不查全量,加时间范围或分页。必须全量导出的场景,改成异步任务:提交后后台跑,跑完给下载链接,避免 HTTP 连接长时间挂着。同时对单次导出行数设上限,超过就提示用户缩小范围,这比事后扩容便宜得多。
5.5 表达式报错只给一行提示,定位不到具体格子
现象:预览时报「表达式异常」,日志里只有一行堆栈,几十个格子不知道是哪一个出的问题。
原因:调试开关没开,或者异常被统一处理层吞掉了原始信息。
解决:开发期把调试开关打开,报错会带上单元格坐标。生产期则要在统一异常处理里把原始异常记进日志,别只返回一句「系统繁忙」。另外一个实用技巧是先把可疑区域的表达式清空,逐块加回去,二分定位比看日志快。
6. 进阶用法与验证:自定义函数、缓存与一份可回归的报表清单
报表做到一定数量后,真正麻烦的不是画表,而是「改了一处,别处坏了」。我后来固定了两个习惯:把重复的格式化逻辑收进自定义函数,把每次改动后的验证项固化成一份清单。
自定义函数适合处理金额大写、证件号脱敏、状态码转中文这类到处都要用的逻辑。实现一个函数类,注册成容器里的组件,表达式里就能直接按名字调用。
@Component("moneyUpper") public class MoneyUpperFunction implements Function { @Override public String getName() { return "moneyUpper"; // 表达式里写 =moneyUpper(E5) } @Override public Object execute(List<Expression> args, Context ctx) { if (args.isEmpty()) { return ""; } Object value = args.get(0).evaluate(ctx); if (value == null) { return ""; // 空值必须返回空串,返回 null 会让整格渲染异常 } BigDecimal amount = new BigDecimal(value.toString()); return toChineseUpper(amount); // 转换逻辑单独抽方法,便于单测 } }三个参数值得留意:函数名要和表达式里写的一致,大小写敏感;参数求值用evaluate而不是取原始文本,否则传进来的是字段名而不是字段值;空值一定返回空串,返回null在部分渲染路径上会直接抛异常,这个坑我在两个项目里都遇到过。
缓存这块要克制。引擎本身有会话级缓存,多人同时预览时容易串数据,生产环境我一般直接关掉,靠数据库查询优化和结果集行数限制来保性能。真要加缓存,也只缓存那些数据变化不频繁的字典类数据集,业务明细不要缓存,否则用户看到的是昨天的数字,解释起来很麻烦。
验证清单我固定成这么几项,每次改完报表跑一遍:
| 验证项 | 怎么验 | 不通过说明什么 |
|---|---|---|
| 参数为空 | 清空所有参数直接预览 | SQL 空值判断没做,或默认值失效 |
| 边界日期 | 查当月第一天和最后一天 | 时间范围用了闭区间,漏数据 |
| 大数据量 | 用十倍于日常的数据量导出 | 内存或超时风险 |
| 导出格式 | Excel、PDF 各导一次 | 字体、列宽、分页问题 |
| 越权访问 | 换个无权限账号直接调接口 | 鉴权只在页面做了 |
这套清单我贴在项目文档最上面,新人接手时第一件事就是照着跑一遍。血泪经验是:报表这种东西,改一个字段位置都可能让分页错位,靠肉眼检查根本靠不住,只有固定动作能保证不翻车。希望帮到你。
本文还有配套的精品资源,点击获取