☰
UReport2集成Spring Boot:报表设计器、数据源与存储避坑手册
2026/11/6 5:44:05 网站建设 项目流程

简介:面向Java后端开发者的一份UReport报表工具实践材料,围绕UReport2的环境搭建与报表制作展开,适合需要在Spring Boot项目中快速接入报表导出能力的初中级开发者参考。压缩包内仅含1个doc文件,体积约1.15MB,正文串联了Maven依赖引入、context.xml与application.yml配置、模板设计、数据源绑定以及与Spring Boot集成的完整流程,并给出PDF、Excel、Word等多格式输出的实现思路。文档中的pom.xml示例明确标注了ureport2-console 2.2.9版本与MySQL驱动、Thymeleaf等配套依赖,便于读者对照自身工程逐项补齐;同时整理了报表生成失败、生成速度偏慢等常见问题的排查方向,以及报表模板设计与数据源配置的最佳实践。目前已有5687人学习下载,可作为UReport入门与落地集成的查阅手册。

1. 一份能跑起来的 UReport2 使用手册:从 demo 到可维护报表

临时接到“把业务数据做成可配置报表、还要能导出 Excel 和 PDF”的需求时,很多团队第一反应是找开源报表工具。UReport 报表这套使用手册配了一个能启动的 demo,价值不在“又有一个报表引擎”,而在于它把 Maven 依赖、Servlet 注册、内置数据源、SpringBean 数据源、本地存储和 FTP 存储这些落地环节都摆出来了。新手可以照着把第一张报表跑通,熟手更该关注它的边界:/ureport/*映射不能改、ureport.disableFileProvider在 yml 里读不到、java -jar启动时默认存储会丢报表。这份资源适合正在做后台管理系统、需要快速嵌入报表设计器的 Java 团队,也适合想判断 UReport2 是否值得接进现有 Spring Boot 项目的工程师。

2. 把 UReport2 塞进 Spring Boot:依赖、Servlet 与启动链路

2.1 pom.xml 里真正不能少的依赖

UReport2 的 demo 用的是 Spring Boot 2.0.0.RELEASE,核心依赖是ureport2-console。这个包不只是控制台,还带了设计器静态资源和 UReport 自己的 Spring 配置。下面这段是能启动的最小骨架,保留输入里的版本,方便对照排查;如果项目已有依赖管理,spring-beans的显式版本可以交给 Boot 管。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.bstek.ureport</groupId> <artifactId>ureport2-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RELEASE</version> </parent> <dependencies> <!-- UReport2 控制台与设计器 --> <dependency> <groupId>com.bstek.ureport</groupId> <artifactId>ureport2-console</artifactId> <version>2.2.9</version> </dependency> <!-- Web 容器与 Servlet 注册能力 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 数据源与 JdbcTemplate 基础能力 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- demo 里带 JPA,实际按需保留 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- 旧项目里常见的连接池依赖,按需保留 --> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>commons-pool</groupId> <artifactId>commons-pool</artifactId> <version>1.5.4</version> </dependency> <!-- MySQL 5.x 旧驱动,换新驱动时类名要一起改 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.46</version> </dependency> </dependencies> <repositories> <repository> <id>sonatype</id> <url>https://oss.sonatype.org/content/groups/public/</url> </repository> </repositories> </project>

这段配置的逻辑是:ureport2-console提供报表引擎和/ureport/*的 Servlet 实现,spring-boot-starter-web提供 Servlet 容器,spring-boot-starter-jdbc提供DataSource给内置数据源复用。参数上最容易被忽略的是mysql-connector-java 5.1.46对应旧驱动类com.mysql.jdbc.Driver;如果换成新驱动,driver-class-name要改成com.mysql.cj.jdbc.Driver。另外,demo 里的TestBean若用了RandomStringUtils和RandomUtils,还需要补commons-lang、commons-lang3,否则编译阶段就翻车。我一般会把模拟数据改成 JDK 的ThreadLocalRandom和UUID,少两个依赖,后续升级也省心。

2.2 context.xml 与 Application.java 的注册顺序

UReport2 不是只加依赖就能用,它需要把ureport-console-context.xml导入 Spring 容器,再把UReportServlet注册到/ureport/*。这两个动作一个是容器初始化,一个是 Servlet 映射,缺一个都会出现“页面 404 但日志没明显报错”的情况。

<?xml version="1.0" encoding="UTF-8"?> <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"> <!-- 导入 UReport2 自己的 Spring 配置 --> <import resource="classpath:ureport-console-context.xml"/> <!-- 如果要用 properties 覆盖 UReport 内部配置,再打开下面这段 --> <!-- <bean id="propertyConfigurer" parent="ureport.props"> <property name="location"> <value>classpath:application.properties</value> </property> </bean> --> </beans>
package com.bstek.ureport.test; import com.bstek.ureport.console.UReportServlet; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ImportResource; @ImportResource("classpath:context.xml") @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Bean public ServletRegistrationBean<UReportServlet> buildUReportServlet() { // /ureport/* 是 UReport2 控制台和设计器的入口,不要随意改成 /report/* return new ServletRegistrationBean<>(new UReportServlet(), "/ureport/*"); } }

@ImportResource("classpath:context.xml")的作用是把 UReport2 的 Bean 定义拉进容器,ServletRegistrationBean则把UReportServlet挂到 Web 容器。参数上唯一不能动的是/ureport/*,输入里也明确写了这个映射一定不能变;改成别的路径后,设计器里的静态资源、请求转发都会对不上。context.xml里注释掉的ureport.props不要随手打开,它会影响 UReport 内部属性文件的读取顺序,后面讲存储开关时会踩到。

2.3 application.yml 的位置与最小配置

demo 把application.yml放在resources/config下,这里有个现实问题:Spring Boot 默认读取的是src/main/resources/application.yml,不是resources/config/application.yml。如果坚持放在 config 目录,启动时要加--spring.config.location=classpath:/config/,或者把文件移到 resources 根目录。数据源配置本身不复杂,但 URL 里的&建议加引号,避免 YAML 解析时出现不必要的歧义。

spring: datasource: url: "jdbc:mysql://127.0.0.1:3306/notified?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull" username: root password: change_me driver-class-name: com.mysql.jdbc.Driver # 如果 application.yml 放在 resources/config 下,启动参数需要补: # --spring.config.location=classpath:/config/

这段配置的逻辑是给 Spring 容器提供一个DataSource,UReport 的内置数据源会从这个DataSource取连接。参数上,useUnicode=true&characterEncoding=UTF-8处理中文,zeroDateTimeBehavior=convertToNull是旧版 MySQL 驱动对零日期的兼容参数;换新驱动后这个参数可能不再需要。注意不要把生产密码直接写进 yml,demo 可以这么干,实际项目用环境变量或配置中心替换。

2.4 启动后的第一轮自检

启动类跑起来后,不要急着做复杂模板,先做三件事:访问/ureport/designer看设计器是否加载;看日志里UReportServlet是否初始化;在设计器里新建一个内存数据源,随便填两行数据预览。常见的通过标准是:页面能打开、左侧数据源列表能看到MyDataSource或testBean、预览不报Connection is null。如果页面 404,先查ServletRegistrationBean的映射;如果页面能开但数据源为空,先查BuildinDatasource实现类有没有被扫描到。这个顺序比一上来调模板样式有效得多。

3. 数据源接法:内置数据源与 SpringBean 数据源怎么选

3.1 BuildinDatasource:复用 Spring 数据源

内置数据源适合“报表直接写 SQL”的场景。实现BuildinDatasource接口,把 Spring 容器里的DataSource注入进来,name()返回设计器里显示的数据源名称,getConnection()从连接池取连接。UReport 会在设计器里把这个名称列出来,报表数据集里写 SQL 就能查库。

package com.bstek.ureport.test; import com.bstek.ureport.definition.datasource.BuildinDatasource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; @Component public class UreportDataSource implements BuildinDatasource { private static final String NAME = "MyDataSource"; private final 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()。name()不是 Bean 名,而是设计器里的数据源显示名,改了就相当于换了一个数据源,已有报表数据集可能找不到它。getConnection()返回的连接由 UReport 负责关闭,但取连接失败时不要只打印日志返回null,否则设计器预览时只会看到“数据集为空”,排查起来像黑匣子。我一般会在 catch 里把数据库 URL、用户名和异常码一起打出来,方便定位是权限问题还是网络问题。

3.2 SpringBean 数据源:方法签名和返回类型

当报表数据不是一条 SQL 能查出来,或者需要做权限过滤、字段脱敏、跨服务聚合时,用 SpringBean 数据源更合适。它的约束很硬:方法必须包含String dsName、String datasetName、Map<String,Object> parameters三个参数,返回List<Map<String,Object>>或List<POJO>。Bean 名就是设计器里选择的数据源名。

package com.bstek.ureport.test; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ThreadLocalRandom; @Component("testBean") public class TestBean { /** * 返回 Map 列表:字段名就是 Map 的 key */ 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> row = new HashMap<>(); row.put("id", i); row.put("name", UUID.randomUUID().toString().replace("-", "").substring(0, 10)); row.put("salary", ThreadLocalRandom.current().nextInt(10000) + i); list.add(row); } return list; } /** * 返回 POJO 列表:字段名来自 getter,报表里用 id/name/salary */ 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 user = new User(); user.setId(i); user.setName(UUID.randomUUID().toString().replace("-", "").substring(0, 10)); user.setSalary(ThreadLocalRandom.current().nextInt(10000) + i); list.add(user); } return list; } }
package com.bstek.ureport.test; public class User { private int id; private String name; private int salary; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getSalary() { return salary; } public void setSalary(int salary) { this.salary = salary; } }

@Component("testBean")里的testBean就是设计器里看到的数据源名;如果只写@Component,Bean 名会变成类名首字母小写,和文档里的testBean对不上。dsName和datasetName是 UReport 传进来的上下文,parameters是报表参数,比如页面上传了startDate,这里就能用parameters.get("startDate")拿到。返回Map时字段名靠 key,返回 POJO 时字段名靠 getter,大小写要和模板里绑定的字段一致。demo 里造了 1000 条数据,实际报表不要这么干,分页、过滤、聚合尽量在方法内部完成,别把全量数据塞给设计器预览。

3.3 两种数据源怎么选

内置数据源和 SpringBean 数据源不是替代关系,而是两种边界。下面这张表按实际落地场景对比,选型时先看数据从哪里来、要不要权限过滤、报表参数多不多。

对比项BuildinDatasourceSpringBean 数据源
数据来源数据库 SQLJava 方法返回集合
适合场景单库查询、简单关联、运营报表跨服务聚合、权限过滤、字段脱敏
报表参数SQL 里用${参数名}或数据集参数方法第三个参数parameters
字段名来源SQL 别名Map key 或 POJO getter
大数据量依赖 SQL 分页和索引方法内部做分页,别一次性全查
常见故障连接为空、SQL 写错、数据源名对不上方法签名不对、Bean 名不对、返回 null

我一般会先用内置数据源把 SQL 跑通,确认字段和参数没问题,再把需要业务逻辑的部分迁到 SpringBean 数据源。不要一上来就写复杂 Java 方法,否则报表预览失败时分不清是 SQL 问题还是方法返回值问题。参数传递上,内置数据源的 SQL 参数和 SpringBean 的parameters都来自报表设计器里的参数定义,名字要完全一致,大小写也别错。

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

4.1 默认存储为什么在 java -jar 下会丢

UReport2 默认会把报表文件存在 Web 容器的WEB-INF/ureportfiles目录下。用 Tomcat 部署 war 时,这个目录还在;用java -jar启动 Spring Boot 时,容器的工作目录是临时解压目录,重启后报表文件就没了。输入里还写了一个关键点:ureport.disableFileProvider=true只能在 properties 文件里配置,放在 yml 里读不到。这个坑很隐蔽,配置看起来生效了,实际默认 provider 还在。

# 放在 src/main/resources/application.properties 中,不要只写进 yml ureport.disableFileProvider=true # 自定义本地存储目录,按操作系统写绝对路径 ureport.fileStoreDir=/data/ureport-files

这段配置的逻辑是:ureport.disableFileProvider=true关掉默认文件存储,避免java -jar重启丢报表;ureport.fileStoreDir给自定义FileReportProvider提供根目录。参数上,路径要用应用进程有读写权限的目录,Linux 下不要用/root或没有权限的挂载点。禁用默认 provider 后必须自己提供一个ReportProvider,否则设计器里“保存”会失败,不要以为关掉就万事大吉。

4.2 自定义本地存储 FileReportProvider

自定义本地存储的核心是实现ReportProvider,把报表文件读写落到固定目录。下面这段保留输入里的关键方法,做了两个实战修正:prefix用file:,setApplicationContext里先判断路径是不是绝对路径,避免basePath + /data/ureport拼出错误路径。

package com.bstek.ureport.test; import com.bstek.ureport.exception.ReportException; import com.bstek.ureport.provider.report.ReportFile; import com.bstek.ureport.provider.report.ReportProvider; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; import org.springframework.web.context.WebApplicationContext; import javax.servlet.ServletContext; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.List; @Component public class FileReportProvider implements ReportProvider, ApplicationContextAware { private String prefix = "file:"; @Value("${ureport.fileStoreDir:./ureport-files}") private String fileStoreDir; private boolean disabled; @Override public InputStream loadReport(String file) { try { return new FileInputStream(resolve(file)); } catch (FileNotFoundException e) { throw new ReportException(e); } } @Override public void saveReport(String file, String content) { File target = resolve(file); File parent = target.getParentFile(); if (parent != null && !parent.exists()) { parent.mkdirs(); } try { Files.write(target.toPath(), content.getBytes(StandardCharsets.UTF_8)); } catch (IOException e) { throw new ReportException(e); } } @Override public void deleteReport(String file) { File target = resolve(file); if (target.exists()) { target.delete(); } } @Override public List<ReportFile> getReportFiles() { File dir = new File(fileStoreDir); List<ReportFile> list = new ArrayList<>(); File[] files = dir.listFiles(); if (files == null) { return list; } for (File f : files) { if (f.isFile()) { list.add(new ReportFile(f.getName(), new Date(f.lastModified()))); } } list.sort(Comparator.comparing(ReportFile::getUpdateDate).reversed()); return list; } @Override public String getName() { return "服务器文件系统"; } @Override public boolean disabled() { return disabled; } @Override public String getPrefix() { return prefix; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { File dir = new File(fileStoreDir); if (dir.isAbsolute()) { dir.mkdirs(); return; } if (applicationContext instanceof WebApplicationContext) { WebApplicationContext context = (WebApplicationContext) applicationContext; ServletContext servletContext = context.getServletContext(); String basePath = servletContext.getRealPath("/"); fileStoreDir = new File(basePath, fileStoreDir).getAbsolutePath(); new File(fileStoreDir).mkdirs(); } } private File resolve(String file) { String name = file.startsWith(prefix) ? file.substring(prefix.length()) : file; return new File(fileStoreDir, name); } }

这段代码的逻辑是:所有报表文件都以file:开头,resolve去掉前缀后拼到fileStoreDir根目录;saveReport自动创建父目录,避免第一次保存时报“路径不存在”;getReportFiles按修改时间倒序,设计器里最近改过的报表排在前面。参数上,prefix必须和设计器里存储 provider 的识别前缀一致,默认file:已经能和 UReport 默认文件存储衔接。disabled()返回false表示启用;如果项目里同时有多个 provider,prefix 不要重复,否则保存时可能落到另一个实现里。

4.3 FTP 存储的工厂类与配置

当报表需要多节点共享,或者不想把文件放本机磁盘时,FTP 存储是 demo 里给出的另一条路。它分两层:FTPClientFactory负责创建、校验、销毁 FTP 连接;ReportProvider实现负责按ftp-前缀读写报表。配置上,@ConfigurationProperties("ftp.factory")要求工厂类必须有 getter/setter,否则 yml 里的值注入不进来,这是输入里特别强调的点。

ftp: factory: hostname: 192.168.1.100 port: 21 username: ftpuser password: change_me client-timeout: 60000 encoding: UTF-8 passive-mode: true file-type: 2 rename-uploaded: false ureport: ftp: provider: prefix: ftp- disabled: false basePath: /ureport
package com.bstek.ureport.test.ftp; import lombok.Data; import org.apache.commons.net.ftp.FTP; import org.apache.commons.net.ftp.FTPClient; import org.apache.commons.net.ftp.FTPClientConfig; import org.apache.commons.net.ftp.FTPReply; import org.apache.commons.pool.PoolableObjectFactory; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @Component @ConfigurationProperties("ftp.factory") public class FTPClientFactory implements PoolableObjectFactory<FTPClient> { private String hostname; private int port = 21; private String username; private String password; private boolean passiveMode = true; private String encoding = "UTF-8"; private int clientTimeout = 60000; private int fileType = FTP.BINARY_FILE_TYPE; @Override public FTPClient makeObject() throws Exception { FTPClient client = new FTPClient(); client.setConnectTimeout(clientTimeout); client.setControlEncoding(encoding); client.connect(hostname, port); int reply = client.getReplyCode(); if (!FTPReply.isPositiveCompletion(reply)) { client.disconnect(); throw new IllegalStateException("FTP 服务端拒绝连接,reply=" + reply); } if (!client.login(username, password)) { client.disconnect(); throw new IllegalStateException("FTP 登录失败"); } FTPClientConfig conf = new FTPClientConfig(FTPClientConfig.SYST_NT); conf.setServerLanguageCode("zh"); client.configure(conf); if (passiveMode) { client.enterLocalPassiveMode(); } client.setFileType(fileType); return client; } @Override public void destroyObject(FTPClient client) throws Exception { if (client != null && client.isConnected()) { client.logout(); client.disconnect(); } } @Override public boolean validateObject(FTPClient client) { try { return client != null && client.isConnected() && client.sendNoOp(); } catch (Exception e) { return false; } } @Override public void activateObject(FTPClient client) { } @Override public void passivateObject(FTPClient client) { } }

这段工厂类的逻辑是:makeObject建立连接并登录,destroyObject退出并断开,validateObject用NOOP判断连接是否还活着。参数上,passive-mode: true在很多内网 FTP 环境里是必须的,否则数据连接建不起来;client-timeout不要设得太短,报表文件大时上传会超时;file-type: 2对应二进制传输,避免 Excel/PDF 内容被文本模式改坏。FTP 的ReportProvider实现要按loadReport、saveReport、deleteReport、getReportFiles四个方法落到 FTP 命令上,其中loadReport取到输入流后不要立刻断开连接,先把流读完再归还连接,否则会出现“预览为空、下载一半断流”的问题。凭据不要写死在 yml 里,demo 可以,实际项目用环境变量覆盖。

5. 避坑排查:启动失败、数据源为空、报表丢失怎么查

5.1 启动期:404 与 Servlet 映射

现象:应用启动成功,访问/ureport/designer返回 404。原因通常是ServletRegistrationBean没注册,或者映射被改成了/report/*。解决方式是回到Application.java,确认new ServletRegistrationBean<>(new UReportServlet(), "/ureport/*")存在,并且启动类在父包或能扫描到配置类。另一个常见原因是@ImportResource没加,ureport-console-context.xml没进容器,Servlet 初始化时拿不到必要 Bean。

现象:设计器页面能打开,但静态资源 404,样式全乱。原因多半是应用加了全局路径前缀,比如server.servlet.context-path,或者安全框架拦截了/ureport/**。解决时先看浏览器 Network 里静态资源的实际请求路径,再确认安全框架是否放行/ureport/**和/ureport/designer。不要只改 Servlet 映射,映射改了反而更乱。

现象:启动日志报NoClassDefFoundError或ClassNotFoundException。先查ureport2-console是否真的从仓库拉下来了,再看有没有和 Spring 版本冲突。demo 显式写了spring-beans 5.0.4.RELEASE,如果项目父 POM 管理了不同版本,容易出现类冲突。解决方式是先跑mvn dependency:tree看spring-beans、spring-web的版本,必要时删掉显式版本,交给 Boot 统一管理。

5.2 运行期:数据源和参数为空

现象:设计器里数据源列表为空,看不到MyDataSource。原因一般是UreportDataSource没加@Component,或者启动类扫描包不包含它。解决时先确认类上有@Component,再看启动类所在包是否能覆盖com.bstek.ureport.test。如果用了多模块,检查@ComponentScan有没有漏掉子模块。

现象:选中testBean后预览报“方法不存在”或数据集为空。原因通常是方法签名不对。UReport 要求方法包含String、String、Map三个参数,返回List。少一个参数、参数顺序反了、返回null,都会让设计器认为数据集不可用。解决时把方法头固定成public List<Map<String,Object>> loadReportData(String dsName, String datasetName, Map<String,Object> parameters),先在 Java 里直接调用一次,确认能返回数据,再回设计器绑定。

现象:报表里参数传不进去,SQL 条件一直为空。原因可能是报表参数名和 SQL 里的${}名称不一致,或者 SpringBean 方法里没从parameters取值。解决时在设计器里看参数定义的大小写,SQL 里先用固定值跑通,再替换成参数;SpringBean 里打印parameters.keySet(),确认前端传进来的 key 到底是什么。不要靠猜,参数名大小写在报表里很敏感。

5.3 存储期:重启丢报表与路径权限

现象:java -jar启动后保存的报表,重启就没了。原因是用了默认文件存储,落到了临时解压目录。解决方式是在application.properties里加ureport.disableFileProvider=true,并提供一个自定义ReportProvider。注意这个开关写在 yml 里读不到,必须放 properties;context.xml里如果没打开ureport.props,UReport 内部属性读取也可能不生效。

现象:自定义本地存储保存时报“路径不存在”或“权限不足”。原因可能是ureport.fileStoreDir配了相对路径,setApplicationContext里又拼了basePath,最后落到了容器临时目录;也可能是进程用户没有写权限。解决时先把目录配成绝对路径,手动mkdir -p并chmod给应用用户,再在代码里判断File.isAbsolute(),绝对路径不再拼basePath。

现象:FTP 存储预览正常,保存或下载大文件时断流。原因通常是 FTP 被动模式没开、client-timeout太短,或者loadReport返回流之后连接被提前归还。解决时先开passive-mode: true,把超时调到 60 秒以上;再检查 provider 的loadReport是否先把流复制到内存或临时文件,再关闭连接。大报表不要直接把 FTP 输入流交给上层,连接池回收时机很难控。

5.4 上线前检查表

检查项检查位置通过标准
Servlet 映射Application.java路径为/ureport/*,未被安全框架拦截
UReport 上下文context.xml已导入ureport-console-context.xml
数据源名称BuildinDatasource.name()设计器里能看到且与报表数据集一致
SpringBean 签名数据源方法三参数、返回 List、Bean 名与设计器一致
默认存储开关application.propertiesureport.disableFileProvider=true生效
自定义存储目录ureport.fileStoreDir绝对路径、进程可读写、重启后文件还在
FTP 连接ftp.factory配置登录成功、被动模式可用、大文件不中断
导出格式设计器预览与导出Excel/PDF 字段完整、中文不乱码

这张表建议在每次部署前走一遍,尤其是换容器、换启动方式、换数据源的时候。UReport2 的很多问题不是代码写错,而是配置落点不对;把检查项固定下来,比事后翻日志快得多。

6. 进阶验证:用一份最小报表跑通设计、预览与导出

最小验证不要拿业务大表,造一张三行数据的数据集就够。内置数据源里写select 1 as id, 'A' as name, 100 as salary union all select 2, 'B', 200,在设计器里新建数据集、绑定字段、拖到模板单元格,预览能出三行再谈样式。这个动作能同时验证数据源连接、SQL 别名、字段绑定和模板渲染四件事。导出时分别点 Excel 和 PDF,重点看中文、数字格式和空值列;Excel 看表头是否合并错位,PDF 看字体是否缺字。

验证动作观察点失败时先查
设计器打开左侧数据源列表Servlet 映射、context.xml
数据集预览返回行数和字段名SQL 别名、数据源名称
模板预览单元格绑定是否生效字段大小写、表达式写法
导出 Excel合并单元格、中文模板样式、字体
导出 PDF分页、缺字PDF 字体配置、列宽

我一般会把这份最小报表留在工程里,作为升级依赖后的回归用例。从那以后我每次接 UReport2,都先把/ureport/*、数据源名、存储目录这三件事写进启动检查单,再谈模板美观;这三处不稳,后面调样式全是白费。希望帮到你。

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

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

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

立即咨询