ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

UReport2 集成 Spring Boot 实战:数据源配置、存储方案与避坑指南

UReport2 集成 Spring Boot 实战:数据源配置、存储方案与避坑指南

简介:这份资源面向需要为 Java 项目补充报表能力的开发者,尤其适合正在使用 Spring Boot 并希望快速上手 UReport2 的中初级工程师。内容围绕报表工具的实际落地展开,涵盖 Maven 依赖引入、context.xml 与 application.yml 配置、报表模板设计、数据源接入,以及与 Spring Boot 的集成方式,并延伸至报表生成、数据分析、商业智能等应用场景和常见问题排查思路。资源包共 1 个文件,为 doc 格式的使用手册,压缩包约 1.15MB,篇幅集中、便于按章节查阅,可当作入门参考与配置对照清单使用。目前已有 5687 人学习下载,说明其在同类报表工具资料中具备一定参考价值。读者可从中获取依赖配置示例、模板制作流程与集成要点,减少自行摸索成本,适合作为项目初期选型与快速验证的辅助材料。

1. 从一份能跑起来的 UReport2 集成 Demo 说起

接手一个后台系统,运营要的报表从「导出 Excel」变成「在线拖拽设计、随时改字段、还要能打印」,这时候自己手写 POI 拼表格基本就是给自己挖坑。这份资源就是一个把 UReport2 塞进 Spring Boot 的最小可运行 Demo,外加一份使用手册,覆盖 Maven 依赖、Servlet 注册、内置数据源、SpringBean 数据源、本地文件存储和 FTP 存储几条主线。它适合两类人:一是第一次接触 UReport 报表、想先跑通再谈定制的后端;二是已经用过、但被存储方式和数据源配置卡住、需要一份能对照的工程骨架的人。下面按「能干什么 → 怎么跑 → 怎么改」的顺序拆。

2. 工程骨架与启动链路:一个 UReportServlet 撑起整个设计器

2.1 依赖清单与版本约束

这个 Demo 的 pom 结构很典型:Spring Boot 2.0.0.RELEASE 做父工程,核心只有一条ureport2-console依赖,其余都是为它服务的配角。ureport2-console这个 artifact 自带报表设计器的前端静态资源和后端 Servlet,所以引入它之后不需要再单独引ureport2-core。

<!-- 报表引擎与设计器控制台,一个依赖全包 --> <dependency> <groupId>com.bstek.ureport</groupId> <artifactId>ureport2-console</artifactId> <version>2.2.9</version> </dependency> <!-- Spring 封装的 JDBC,内置数据源要靠它拿 DataSource --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- 模板引擎,Demo 里用来渲染页面 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- MySQL 驱动,版本压得比较低,跟 5.x 服务端对齐 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.46</version> </dependency>

几个参数值得说清楚。spring-beans被显式钉在 5.0.4.RELEASE,是因为 UReport2 内部对 Spring 的 Bean 生命周期有依赖,版本漂太多容易在启动阶段报找不到类;commons-dbcp和commons-pool是连接池的老组合,Demo 用它而不是 HikariCP,主要是为了跟 UReport 内置数据源的取连接逻辑兼容。repositories里配了 sonatype 仓库,因为ureport2-console早期版本并不总在中央仓库,加这个源能少一次「依赖拉不下来」的翻车。

2.2 启动类与 Servlet 注册

UReport 的设计器不是 Controller,而是一个 Servlet,所以启动类里必须手动注册它,并且 URL 映射写死为/ureport/*。

@ImportResource("classpath:context.xml") // 加载 UReport 的 Spring 配置 @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Bean public ServletRegistrationBean buildUReportServlet() { // 第二个参数 "/ureport/*" 是硬约定,改了设计器就打不开 return new ServletRegistrationBean(new UReportServlet(), "/ureport/*"); } }

@ImportResource指向的context.xml里只有一行关键配置:<import resource="classpath:ureport-console-context.xml"/>,把 UReport 自带的 Bean 定义拉进容器。这里有个容易忽略的点——context.xml里那段被注释掉的propertyConfigurer不要随手打开,它会把application.properties再加载一遍,和 Spring Boot 自己的配置加载打架。启动后访问http://localhost:8080/ureport/designer能看到设计器界面,就说明 Servlet 链路通了。

2.3 数据源配置的两种落点

application.yml里配的是业务库连接,UReport 的内置数据源会复用它:

spring: datasource: url: jdbc:mysql://localhost:3306/notified?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull username: root password: Admin123@ driver-class-name: com.mysql.jdbc.Driver

zeroDateTimeBehavior=convertToNull这个参数别删,报表里遇到0000-00-00这类脏日期时,没有它查询会直接抛异常。数据源本身由UreportDataSource这个类暴露给 UReport,它实现BuildinDatasource接口,name()返回的名字就是设计器数据源下拉框里显示的名称,getConnection()直接从注入的DataSource取连接。换句话说,业务库和报表库在这里是同一个,如果要做读写分离,改的是这个类里的取连接逻辑,而不是 UReport 的配置。

3. 数据源接入实战:内置数据源与 SpringBean 数据源怎么选

3.1 内置数据源:直接吃数据库连接

内置数据源适合「报表 SQL 直接打在业务库上」的场景。实现类只有两个方法要写,name()决定设计器里的显示名,getConnection()负责给 UReport 一个可用连接。

@Component public class UreportDataSource implements BuildinDatasource { private static final String NAME = "MyDataSource"; @Autowired private DataSource dataSource; // Spring Boot 自动装配的数据源 @Override public String name() { return NAME; // 设计器数据源列表里看到的就是这个名字 } @Override public Connection getConnection() { try { return dataSource.getConnection(); } catch (SQLException e) { log.error("Ureport 数据源 获取连接失败!"); return null; } } }

这里返回null而不是抛异常,是 Demo 的写法;生产里我一般会直接抛出去,让设计器明确提示连接失败,而不是给一个空连接让后续查询报更晦涩的错。另外注意getConnection()拿到的连接由 UReport 负责关闭,不要在方法里自己 close,否则会出现「连接已关闭」的连锁报错。

3.2 SpringBean 数据源:方法即数据集

当数据不是一条 SQL 能查出来,而是要调服务、拼缓存、做计算时,用 SpringBean 数据源更合适。规则很死:Bean 名就是数据源名,方法必须接收(String dsName, String datasetName, Map<String,Object> parameters)三个参数,返回值可以是List<Map>也可以是List<实体>。

@Component("testBean") // Bean 名 testBean 就是设计器里的数据源名 public class TestBean { // 返回 Map 列表,字段名即列名 public List<Map<String, Object>> loadReportData(String dsName, String datasetName, Map<String, Object> parameters) { List<Map<String, Object>> list = new ArrayList<>(); for (int i = 0; i < 1000; i++) { Map<String, Object> m = new HashMap<>(); m.put("id", i); m.put("name", RandomStringUtils.random(10, true, false)); m.put("salary", RandomUtils.nextInt() + i); list.add(m); } return list; } // 返回实体列表,靠 getter 映射列 public List<User> buildReport(String dsName, String datasetName, Map<String, Object> parameters) { List<User> list = new ArrayList<>(); for (int i = 0; i < 1000; i++) { User u = new User(); u.setId(i); u.setName(RandomStringUtils.random(10, true, false)); u.setSalary(RandomUtils.nextInt() + i); list.add(u); } return list; } }

parameters这个 Map 是设计器里传进来的查询参数,做条件过滤就靠它,比如parameters.get("deptId")。返回实体时,UReport 通过反射读 getter,字段名要和报表里配的列名对上,User类里id/name/salary三个属性对应三列。方法名本身不重要,重要的是参数签名和返回值类型,设计器会扫描这个 Bean 的所有公开方法让你选。

3.3 两种数据源的取舍

维度内置数据源SpringBean 数据源
数据来源数据库连接 + SQLJava 方法返回值
适合场景单表/多表查询、聚合跨服务取数、计算逻辑复杂
参数传递SQL 里写${参数}方法第三个参数 Map
性能关注点SQL 索引、连接池方法内循环与远程调用

简单判断:能在 SQL 里写完的用内置数据源,需要走 Java 逻辑的用 SpringBean。两者可以在同一个报表里混用,设计器里分别建数据集即可。

4. 报表存储方案:默认文件、本地目录与 FTP 三条路

4.1 默认存储的坑

UReport 默认把报表文件存在WEB-INF/ureportfiles下。打成 war 部署到 Tomcat 时没问题,但用java -jar启动微服务时,这个目录在临时解压路径里,服务一重启报表就没了。禁用默认存储的开关是ureport.disableFileProvider=true,注意它必须写在application.properties里,写在 yml 里读不到——这是 Demo 手册里专门标出来的一条,我踩过。

4.2 自定义本地存储

要换存储目录,配ureport.fileStoreDir,然后实现ReportProvider接口。核心是四个方法:loadReport、saveReport、deleteReport、getReportFiles,外加getPrefix()返回file:作为文件标识前缀。

@Component public class FileReportProvider implements ReportProvider, ApplicationContextAware { private String prefix = "file:"; @Value("${ureport.fileStoreDir}") private String fileStoreDir; @Override public InputStream loadReport(String file) { if (file.startsWith(prefix)) { file = file.substring(prefix.length()); // 去掉前缀再拼路径 } try { return new FileInputStream(fileStoreDir + "/" + file); } catch (FileNotFoundException e) { throw new ReportException(e); } } @Override public void saveReport(String file, String content) { if (file.startsWith(prefix)) { file = file.substring(prefix.length()); } try (FileOutputStream out = new FileOutputStream(fileStoreDir + "/" + file)) { IOUtils.write(content, out, "utf-8"); // 报表 XML 统一 UTF-8 } catch (Exception ex) { throw new ReportException(ex); } } @Override public String getPrefix() { return prefix; } // deleteReport / getReportFiles / getName 略 }

setApplicationContext里做了一件事:如果配置的是相对路径,就基于servletContext.getRealPath("/")补全并mkdirs()。这个逻辑在 war 部署下是对的,jar 部署下getRealPath可能返回临时目录,所以生产里我一般直接配绝对路径,比如ureport.fileStoreDir=/data/ureport,省掉这层推断。

4.3 FTP 存储与连接工厂

报表要多人共享、多实例部署时,本地目录就不够了,得上 FTP。配置分两段,一段是 FTP 连接参数,一段是 UReport 的 provider 开关:

ftp: factory: hostname: 192.168.6.104 port: 21 username: ftpuser password: 123456 client-timeout: 60000 ureport: ftp: provider: prefix: ftp- disabled: false basePath: /

FTPClientFactory实现PoolableObjectFactory<FTPClient>,负责创建、校验、销毁连接。makeObject()里连上之后先看FTPReply.isPositiveCompletion(replyCode),不通过就断开返回 null,登录失败则抛FTPConnectionClosedException。这里有个血泪经验:@ConfigurationProperties("ftp.factory")绑定的类必须有完整的 getter/setter,少一个 setter 对应字段就注入不进来,而且不报错,只是值为 null,排查起来很费时间。prefix配成ftp-是为了和本地存储的file:区分,UReport 靠前缀决定用哪个 provider 读写。

5. 避坑与排查:启动、存储、数据源的高频翻车点

5.1 设计器 404,Servlet 没生效

现象是访问/ureport/designer返回 404。原因通常是ServletRegistrationBean的映射写成了/ureport而不是/ureport/*,或者启动类漏了@ImportResource("classpath:context.xml"),导致ureport-console-context.xml没被加载。解决方式是先确认映射字符串完全一致,再在启动日志里搜UReportServlet,没有注册日志就回头查注解。

5.2 报表保存后重启就丢

现象是设计器里保存成功,服务重启后列表空了。原因是用了默认文件存储且以 jar 方式启动,文件落在临时目录。解决办法二选一:配ureport.disableFileProvider=true并实现自定义ReportProvider,或者直接把ureport.fileStoreDir指到固定绝对路径。注意这个开关只认 properties 文件。

5.3 数据源下拉框里看不到自定义数据源

现象是写了BuildinDatasource实现类,设计器里却没有。原因多半是类上漏了@Component,或者name()返回了空字符串。SpringBean 数据源则是 Bean 名和方法签名的问题——方法参数不是三个、或者返回值不是 List,都会被过滤掉。排查时可以在启动后打日志遍历容器里的BuildinDatasource实现,确认注册成功。

5.4 FTP 连接一直为 null

现象是 FTP 存储模式下保存报表报空指针。原因是FTPClientFactory的配置属性没注入,字段全是 null,connect直接失败。检查@ConfigurationProperties前缀是否和 yml 层级一致,以及每个字段是否有 setter。另外client-timeout设太小、网络稍慢就会连接超时,60000 毫秒是个稳妥值。

5.5 报表里日期字段查询报错

现象是带日期条件的报表执行时报 SQL 异常。原因是库里存在0000-00-00这类非法日期,JDBC 默认处理策略会抛错。解决方式是在连接串里保留zeroDateTimeBehavior=convertToNull,让驱动把非法日期转成 null,报表侧再做空值判断。

6. 二次开发:把 Demo 改成能上生产的报表服务

拿到这份 Demo 之后,真正要动的地方集中在三处,我按改造顺序说。

第一处是存储。Demo 的本地存储和 FTP 存储都是「能用」级别,生产里我一般会再实现一个数据库存储的ReportProvider,把报表 XML 存进一张report_file表,字段就name、content、update_time三个。好处是多实例部署天然共享,坏处是报表文件大了之后单行字段会撑爆,所以我会加一个大小阈值,超过 1MB 的走对象存储。改造时只实现ReportProvider接口,getPrefix()返回db:,UReport 会自动按前缀路由,不用改引擎代码。

第二处是数据源权限。Demo 里UreportDataSource直接把业务库连接交出去,等于设计器里能写任意 SQL。生产里我会在getConnection()外面包一层,或者干脆禁用内置数据源,全部走 SpringBean 数据源,在方法内部做参数校验和字段白名单。下面这个写法可以限制只能查指定表:

public List<Map<String, Object>> queryOrder(String dsName, String datasetName, Map<String, Object> parameters) { // 只允许按部门查,参数缺失直接返回空,不给全表扫描的机会 Object deptId = parameters.get("deptId"); if (deptId == null) { return Collections.emptyList(); } return jdbcTemplate.queryForList( "select id, amount, create_time from orders where dept_id = ?", deptId); }

第三处是验证方法。改完存储和数据源后,别急着在设计器里点,先写一个集成测试:启动上下文,断言容器里存在你的ReportProvider且disabled()返回 false,再调一次saveReport后loadReport比对内容。这一步能挡住大部分「配置写了但没生效」的问题。我自己的习惯是每次换存储实现,都先跑这个往返测试,再开设计器——从那以后,报表丢失这类问题再没在联调阶段出现过。希望这份拆解帮到你,把 Demo 跑通只是起点,按上面的顺序改,它离一个能上生产的报表服务就不远了。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进