☰
UReport2 集成 Spring Boot 实战:数据源、存储与避坑指南
2026/11/2 21:30:29 网站建设 项目流程

简介:这份资源面向需要快速上手 UReport2 的 Java 开发者与报表初学者,围绕「使用 demo + 使用手册」展开,帮助读者解决报表工具选型、环境搭建与模板设计中的常见疑问。压缩包内共 1 个 doc 文档,约 1.15MB,以使用手册形式系统整理了 Maven 依赖引入、context.xml 与 application.yml 配置、报表模板制作、与 Spring Boot 集成等关键环节,并附有 pom.xml 完整配置示例,便于对照实践。内容还覆盖报表生成、数据分析、商业智能等应用场景,以及报表生成失败、生成速度慢等常见问题的排查思路,适合作为日常开发中的速查参考。目前已有 5687 人学习下载,对希望低成本掌握 UReport 报表开发的读者具有一定参考价值。

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

接手一个老后台系统时,最怕听到的一句话是“报表这块你顺手改一下”。页面上的表格看着简单,背后往往牵着一堆数据源、模板文件、导出格式和存储路径。最近拆到一份 UReport2 的使用 demo 加使用手册,正好把这条链路从头到尾走了一遍:Maven 依赖怎么引、Servlet 怎么注册、内置数据源和 SpringBean 数据源怎么选、报表文件默认存哪、换成自定义本地目录或 FTP 又该怎么配。UReport 报表这套东西在国内 Java 后台里出现频率不低,基于 Java 的报表工具,能出 PDF、Excel、Word 等格式,适合做统计表、明细表、套打单据这类需求。这份资源的价值不在于教你画表格,而在于把 Spring Boot 2.0 环境下 UReport2 的接入骨架、数据源扩展点和存储策略一次性摆出来,新手能照着跑通,熟手能直接拿去改。下面按“先立住原理、再动手复现、最后盯住坑”的顺序拆。

2. 把 UReport2 接进 Spring Boot:依赖、Servlet 与配置加载

2.1 依赖清单里哪些是必需的,哪些是历史包袱

这份 demo 的 pom 里依赖不少,但真正跟 UReport2 强相关的只有ureport2-console这一个坐标,其余都是它运行所需的宿主环境。先看清楚每个依赖的角色,再决定自己项目里留哪些。

依赖作用是否必需
ureport2-console2.2.9UReport2 控制台与核心引擎必需
spring-boot-starter-web提供 Servlet 容器与 MVC必需
spring-boot-starter-jdbc数据源与 JdbcTemplate用数据库数据源时必需
spring-boot-starter-data-jpaJPA 支持demo 里带了,纯报表可去掉
spring-boot-starter-thymeleaf模板引擎demo 里带了,非必需
commons-dbcp/commons-pool老式连接池可换成 HikariCP
mysql-connector-java5.1.46MySQL 驱动按实际库替换

ureport2-console这个包本身不在 Maven 中央仓库的主路径上,所以 demo 里额外声明了 sonatype 的仓库地址。这一点很关键,很多人第一次引依赖失败就是卡在这里,报的是找不到 artifact,而不是版本冲突。

<dependency> <groupId>com.bstek.ureport</groupId> <artifactId>ureport2-console</artifactId> <version>2.2.9</version> </dependency> <repositories> <repository> <id>sonatype</id> <url>https://oss.sonatype.org/content/groups/public/</url> </repository> </repositories>

逻辑说明:ureport2-console是聚合入口,内部会带进设计器前端资源和报表引擎;repositories段告诉 Maven 去 sonatype 的公共组里找这个坐标。参数上,版本号 2.2.9 是 demo 验证过的,换版本前先确认对应仓库里有没有。如果公司有私服,把 sonatype 的地址代理进私服更稳,避免每次构建都依赖外网。

2.2 注册 UReportServlet:/ureport/*为什么不能改

UReport2 的控制台和设计器是通过一个独立 Servlet 暴露的,不是普通的 Controller。所以启动类里必须手动注册UReportServlet,并且映射路径固定为/ureport/*。这个路径是引擎内部前端资源请求的约定,改了之后设计器页面能打开但资源全 404,表现就是白屏或者样式错乱。

@ImportResource("classpath:context.xml") @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里的 UReport 配置导入 Spring 容器;ServletRegistrationBean把UReportServlet挂到 Servlet 容器上。参数上,urlMappings必须是/ureport/*,这是硬约束。访问入口一般是http://localhost:8080/ureport/designer,设计器从这里进。

context.xml里只需要一行 import,把 UReport 自带的 Spring 配置拉进来:

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <import resource="classpath:ureport-console-context.xml"/> </beans>

逻辑说明:ureport-console-context.xml是ureport2-console包内自带的配置文件,负责注册引擎内部的一批 Bean。不 import 它,Servlet 能起来但设计器里的数据源、存储提供者都拿不到。demo 里注释掉的那段propertyConfigurer是给老版本用的,新版本走 Spring Boot 的application.yml即可,不用恢复。

2.3 数据源配置:yml 与 properties 的读取边界

demo 里数据源写在resources/config/application.yml,标准 Spring Boot 数据源配置:

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

逻辑说明:这段是给 Spring 容器里的DataSource用的,UReport 的内置数据源实现会通过@Autowired DataSource拿到它。参数上,zeroDateTimeBehavior=convertToNull对报表场景挺实用,避免0000-00-00这种脏日期在导出时炸掉;characterEncoding=UTF-8保证中文不乱码。

这里有个容易翻车的点:UReport 自身的一些开关(比如禁用默认文件存储的ureport.disableFileProvider、自定义存储目录的ureport.fileStoreDir)在 demo 的注释里明确写了“需在 properties 文件中配置,在 yml 中配置是读取不到的”。原因是 UReport 内部用Properties加载器读这些键,不走 Spring 的Environment。所以项目里同时存在application.yml和application.properties是正常的,前者管 Spring,后者管 UReport 自己的开关。我一般会把 UReport 专属的键单独放一个ureport.properties,用@PropertySource引入,边界更清楚。

3. 两种数据源扩展方式:内置数据源与 SpringBean 数据源怎么选

3.1 内置数据源:实现 BuildinDatasource 接口

内置数据源的本质是告诉 UReport“我有一个连接,你拿去执行 SQL”。实现BuildinDatasource接口,把 Spring 管理的DataSource包一层交出去即可。

@Component public class UreportDataSource implements BuildinDatasource { private static final String NAME = "MyDataSource"; private Logger log = LoggerFactory.getLogger(UreportDataSource.class); @Autowired private DataSource dataSource; @Override public String name() { // 这个名字会出现在设计器的数据源下拉里 return NAME; } @Override public Connection getConnection() { try { return dataSource.getConnection(); } catch (SQLException e) { log.error("Ureport 数据源获取连接失败", e); } return null; } }

逻辑说明:name()返回的字符串是设计器里显示的数据源名称,改它等于改用户看到的下拉项;getConnection()每次调用从连接池借一个连接,用完由引擎负责关闭。参数上没什么可调的,重点是@Component必须加,否则 Spring 扫不到,设计器里就看不到这个数据源。适用场景是“数据在关系库里、用 SQL 取数”的常规报表,设计器里直接写 SQL、配数据集,最省事。

3.2 SpringBean 数据源:方法签名是硬约束

当数据不是来自 SQL,而是来自某个 Service 的计算结果、远程接口或者内存集合时,用 SpringBean 数据源。它的约定比内置数据源严格:Bean 名称就是数据源名称,方法必须接收三个参数(String dsName, String datasetName, Map<String,Object> parameters),返回值可以是List<Map<String,Object>>或List<实体>。

@Component("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; } }

逻辑说明:@Component("testBean")里的testBean就是设计器里看到的数据源名;两个方法名loadReportData、buildReport会成为数据集名。三个入参里,parameters是设计器里传进来的报表参数,做条件过滤时从这里取。返回Map时列名就是 key,返回实体时列名来自 getter 的属性名,User的id/name/salary对应三列。参数上要注意:方法必须是public,参数类型和顺序不能变,返回集合不能为null,空结果返回空集合,否则设计器预览会报错。

选型上我的习惯是:能用 SQL 表达的走内置数据源,逻辑简单、维护成本低;需要跨库聚合、调接口、做复杂计算的走 SpringBean 数据源,把逻辑收在 Java 里,报表模板只负责展示。两者可以在同一个项目里共存,设计器里会分别列出来。

4. 报表文件存哪:默认存储、本地目录与 FTP 三种策略

4.1 默认存储的适用边界与禁用方式

UReport 默认把报表文件存在 Web 应用的WEB-INF/ureportfiles下。这个策略在传统 war 包部署里没问题,但换成java -jar启动的微服务就麻烦了:文件落在临时解压目录里,服务一重启就没了,用户辛苦画的模板直接蒸发。这是血泪经验,别问怎么知道的。

禁用默认存储只需在 properties 里加一行:

ureport.disableFileProvider=true

逻辑说明:这个键必须写在application.properties或独立的 properties 文件里,写进 yml 读不到,前面提过原因。禁用后必须自己提供一个ReportProvider实现,否则设计器里保存报表会报“没有可用的存储提供者”。所以禁用不是目的,换存储才是。

4.2 自定义本地目录:实现 ReportProvider

想把报表存到固定磁盘目录,实现ReportProvider接口,核心是loadReport、saveReport、deleteReport、getReportFiles四个方法,外加getPrefix和getName。

@Component public class FileReportProvider implements ReportProvider, ApplicationContextAware { private String prefix = "file:"; @Value("${ureport.fileStoreDir}") private String fileStoreDir; private boolean disabled; @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"); } catch (Exception ex) { throw new ReportException(ex); } } @Override public List<ReportFile> getReportFiles() { File dir = new File(fileStoreDir); List<ReportFile> list = new ArrayList<>(); for (File f : dir.listFiles()) { Calendar c = Calendar.getInstance(); c.setTimeInMillis(f.lastModified()); list.add(new ReportFile(f.getName(), c.getTime())); } list.sort((f1, f2) -> f2.getUpdateDate().compareTo(f1.getUpdateDate())); return list; } @Override public String getPrefix() { return prefix; } @Override public String getName() { return "服务器文件系统"; } @Override public boolean disabled() { return disabled; } @Override public void setApplicationContext(ApplicationContext ctx) throws BeansException { File dir = new File(fileStoreDir); if (!dir.exists()) { dir.mkdirs(); } } }

逻辑说明:prefix是存储前缀,设计器保存时文件名会带上它,加载时再剥掉,保证多存储提供者共存时不冲突;@Value("${ureport.fileStoreDir}")从 properties 读目录,比如ureport.fileStoreDir=D://ftp1;getReportFiles按修改时间倒序,设计器列表里最新的排前面;setApplicationContext里做目录初始化,避免首次保存时目录不存在。参数上,disabled()返回false表示启用,如果项目里同时有多个 Provider,靠prefix区分。

4.3 FTP 存储:工厂类与配置注入的坑

报表要多人共享、多实例部署时,FTP 存储是常见选择。demo 里给了一套FTPClientFactory加连接池的思路,配置分两段:ftp.factory管连接参数,ureport.ftp.provider管 UReport 侧的存储开关。

ftp: factory: hostname: 192.168.6.104 port: 21 username: ftpuser password: 123456 client-timeout: 60000 encoding: UTF-8 passive-mode: true file-type: 2 rename-uploaded: true ureport: ftp: provider: prefix: ftp- disabled: false basePath: /report

逻辑说明:ftp.factory段被@ConfigurationProperties("ftp.factory")的工厂类读取,file-type: 2是二进制传输,报表文件别用 ASCII 模式,否则中文内容会坏;passive-mode: true在跨网段场景下更稳。ureport.ftp.provider段是 UReport 侧的开关,prefix: ftp-决定设计器里这类存储的前缀,basePath是 FTP 上的根目录。

工厂类上有两个必须注意的点。第一,@ConfigurationProperties要生效,类里必须有完整的 getter/setter,demo 注释里专门强调了“一定要在这个类中写 getter 和 setter,否则配置中的属性值无法自动注入”,这是很多人配完发现 hostname 是 null 的直接原因。第二,makeObject()里连接失败要返回null而不是抛异常,让连接池有机会重试;登录失败才抛FTPConnectionClosedException。这两处的异常语义别搞反,否则池化行为会很怪。

5. 避坑与排查:五个真实会卡住你的点

5.1 设计器白屏,控制台一堆 404

现象:访问/ureport/designer页面能返回,但样式和脚本全 404,页面白屏。 原因:ServletRegistrationBean的映射路径被改成了/report/*之类,或者被 Spring MVC 的静态资源拦截器抢先处理了。 解决:映射路径恢复为/ureport/*;如果项目里配了WebMvcConfigurer的静态资源规则,把/ureport/**排除掉,交给 Servlet 处理。

5.2 依赖引不进来,报找不到 ureport2-console

现象:mvn clean package时报Could not find artifact com.bstek.ureport:ureport2-console。 原因:这个坐标不在中央仓库主路径,没配 sonatype 仓库,或者公司私服没代理这个组。 解决:pom 里补上repositories段指向 sonatype 公共组;有私服的让运维把该组代理进来。别去手动下 jar 塞本地仓库,团队协作时会出问题。

5.3 配置写了不生效,开关像没读一样

现象:ureport.disableFileProvider=true写进了application.yml,重启后默认存储还在。 原因:UReport 内部用Properties读这些键,不经过 Spring 的Environment,yml 里的层级结构它读不到。 解决:把 UReport 专属的键挪到application.properties或独立的ureport.properties,用@PropertySource引入。yml 只管 Spring 自己的配置。

5.4 SpringBean 数据源在设计器里看不到

现象:@Component("testBean")加了,方法也写了,设计器数据源下拉里就是没有。 原因:方法签名不符合约定,比如参数少了一个、返回类型用了Collection之外的包装、或者方法不是public。 解决:严格按(String, String, Map<String,Object>)三参数、返回List来写;Bean 名称别用中文或特殊字符;确认启动类扫描范围覆盖到该类所在包。

5.5 微服务重启后报表模板丢失

现象:本地开发时画的模板,服务重启一次就没了,ureportfiles目录也找不到。 原因:用了默认文件存储,而java -jar启动时WEB-INF落在临时目录,重启即清空。 解决:禁用默认存储,实现自定义ReportProvider指向固定磁盘目录,或者切到 FTP 存储。生产环境别依赖默认存储,这是硬伤。

6. 进阶:把报表存储做成可切换的策略,并用一次导出验证全链路

真正把 UReport 用进项目,存储这块最好做成可切换的,而不是写死一种。我的做法是保留ReportProvider接口实现,用 Spring 的@ConditionalOnProperty按配置决定启用本地还是 FTP,这样开发环境用本地目录、生产用 FTP,同一套代码不用改。

@Configuration public class ReportProviderConfig { @Bean @ConditionalOnProperty(name = "ureport.storage.type", havingValue = "local", matchIfMissing = true) public ReportProvider localFileProvider() { return new FileReportProvider(); } @Bean @ConditionalOnProperty(name = "ureport.storage.type", havingValue = "ftp") public ReportProvider ftpProvider() { return new FtpReportProvider(); } }

逻辑说明:@ConditionalOnProperty按ureport.storage.type的值决定装配哪个 Provider,matchIfMissing = true让不配时默认走本地,降低新环境上手成本。参数上,两个 Provider 的prefix要区分开,比如file:和ftp-,避免设计器里同名文件互相覆盖。

配好之后,别急着画复杂模板,先用一个最小数据集跑通“设计 → 保存 → 预览 → 导出”全链路。在设计器里建一个数据集指向testBean的loadReportData,拖两列到单元格,保存后到存储目录里确认文件真的落盘了,再点导出 Excel,打开看中文和数字格式对不对。这一步能同时验证数据源、存储、导出三条链路,比逐个排查快得多。

还有一个容易被忽略的细节:导出格式的支持依赖引擎内部的导出器,PDF 导出对字体有要求,服务器上没有中文字体时导出的 PDF 中文会变方块。常见做法是把字体文件放进resources并在启动参数里指定,或者优先用 Excel 导出绕开字体问题。这个坑在本地开发机上往往复现不了,因为开发机通常装了完整字体,一上服务器就暴露。

从那以后我每次接报表需求,都强制先跑一遍“最小数据集 + 保存 + 导出”的冒烟流程,确认存储和字体这两件事没问题,再开始画正式模板。希望帮到你。

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

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

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

立即咨询