简介:面向Java开发者与报表开发初学者的UReport实践资料,围绕UReport2在Maven项目及Spring Boot环境中的接入方式展开,可帮助读者理解报表模板设计、数据源配置与PDF、Excel、Word等多格式报表生成流程。压缩包内共1个doc文件,约1.15MB,内容将使用手册与示例配置合并呈现,涉及依赖引入、Spring上下文配置、数据源参数设置、报表制作思路,以及集成过程中的常见问题与优化方向。已有5685人学习,说明其对入门和项目预研具有一定参考价值。读者可据此快速熟悉UReport2的基本用法,对照配置排查依赖缺失、数据源连接和报表生成异常,并借助模板设计、集成示例与排错要点建立从环境搭建到报表输出的整体认识,适合初中级Java开发者按需查阅。
1. UReport 报表落地:一份能直接跑的 demo 手册,卡点到底在哪
接手“同一份报表要能在线预览、要能导 PDF、还要让业务自己拖字段”这类需求时,多数人第一反应是用代码拼表头,真到字段一变就要改版发版,才知道纯代码方案顶不住。UReport 报表这套东西的价值就在这儿:一个浏览器里的设计器,加一份能直接跑的 Spring Boot demo 手册,把依赖、Servlet、数据源、存储四件事按顺序摆开。这份资源适合两类人:一类是要在一两天内把报表模块塞进现有 Java 项目的后端;一类是接手了前人留下的 UReport 工程、想搞清楚ureport2-console到底读哪些配置文件、为什么改了不生效的维护者。下面按手册的代码顺序拆,手册里一句话带过的地方,我补上原因和参数。
2. 依赖与工程骨架:pom 里那几个坐标的真实分工
先把工程跑起来这件事说清楚。UReport2 不是一个“加个 starter 就完事”的库,它是按传统 Servlet + Spring XML 那套思路设计的,只是被打包成了 jar 供 Spring Boot 引用。这就决定了接入动作分三步:引依赖、导 XML、注册 Servlet。三步缺任何一步,表现都是页面打不开或者打开是空白,而错误日志里往往只有一行 404,没有更多线索。
2.1 ureport2-console 一个包带进来什么
手册里最核心的一行就是ureport2-console。这个包不是纯后端逻辑库,它把设计器的前端静态资源、报表引擎、Servlet 实现、以及一份ureport-console-context.xml全塞在一起了。所以你不用单独去引什么前端资源包,也不用自己写 Controller 去映射设计器页面——Servlet 注册对了,页面自然就出来了。
<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.example.ureportdemo</groupId> <artifactId>ureport-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> <!-- Web 容器,Servlet 注册必须依赖它 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- UReport2 核心控制台包:设计器 + 引擎 + Servlet + 内置 spring xml --> <dependency> <groupId>com.bstek.ureport</groupId> <artifactId>ureport2-console</artifactId> <version>2.2.9</version> </dependency> <!-- 内置数据源要靠 Spring 的 DataSource,jdbc starter 负责这个 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- 连接池:ureport 内部取连接时会走它 --> <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> <!-- 数据库驱动,版本要与驱动类名对应 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.46</version> </dependency> <!-- 模板引擎:手册模板里带了,纯报表场景其实用不上 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> <!-- 兜底仓库:部分坐标在中央仓库解析不到时从 sonatype 取 --> <repositories> <repository> <id>sonatype</id> <url>https://oss.sonatype.org/content/groups/public/</url> </repository> </repositories> </project>这段 pom 有几个参数值得逐条掰开。spring-boot-starter-parent锁在 2.0.0,意味着整个工程的 Spring 版本基线是 5.0.x,UReport2 2.2.9 的 Servlet 注册方式和这个基线是匹配的;如果你把 parent 升到 2.6 以上,ServletRegistrationBean的构造参数会变,代码要跟着改。mysql-connector-java用 5.1.46,对应的驱动类名是com.mysql.jdbc.Driver,不要顺手写成com.mysql.cj.jdbc.Driver——那是 8.x 的写法,5.1 的驱动包里有这个类但不推荐,连接串参数也会对不上。commons-dbcp和commons-pool是一对,后者是前者的底层对象池,只引前者会缺类。
至于spring-boot-starter-thymeleaf和spring-boot-starter-data-jpa,手册模板里带着,但纯做报表的话这两个可以砍掉。JPA 留着会让启动时初始化EntityManagerFactory,多花两三秒,日志刷一屏,而报表模块一个字都用不上它。我一般新建工程时先按手册原样跑通,确认链路没问题之后,再删掉这两个依赖重跑一次,能省不少后续排查时的干扰信息。
2.2 context.xml:一行 import 干了什么
resources下的context.xml手册里只有一行有效内容,就是 import 了 jar 里的ureport-console-context.xml。这一行是整个接入的关键:UReport 的 bean(报表引擎、各种 provider、配置解析器)全在那个 XML 里定义,你不 import,Spring 容器里就没有这些 bean,Servlet 初始化时会因为拿不到引擎实例而报空指针。
<?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"> <!-- 关键:把 jar 内 ureport-console-context.xml 中的 bean 全部并入当前容器 --> <import resource="classpath:ureport-console-context.xml"/> <!-- 手册里注释掉的一段:把 ureport 的配置来源从默认文件换成 application.properties --> <!-- <bean id="propertyConfigurer" parent="ureport.props"> <property name="location"> <value>classpath:application.properties</value> </property> </bean> --> </beans>被注释掉的那段不是废代码,它是理解后面一堆“配置写了不生效”问题的钥匙。UReport 内部有一个 id 为ureport.props的PropertyPlaceholderConfigurer,默认去 classpath 找它自己的配置文件。你把它注释掉,意味着这个解析器的 location 保持默认,那么凡是它负责解析的键,就必须写在它默认认的那个文件里,写在application.yml里它是看不见的。而如果你把这段打开、把 location 指向application.properties,解析器就改吃 Spring Boot 的标准配置文件了——这才是这段注释真正的用法,不是“留着以后用”。
@ImportResource("classpath:context.xml")挂在启动类上,作用是把这份 XML 加载进 Spring 上下文。注意它是 classpath 根路径下的context.xml,不是 Tomcat 的context.xml,名字撞了容易看晕,我一般会把它改名成ureport-context.xml之类的,避免后来人误解。
2.3 application.yml 与启动类:数据源和 Servlet 两个入口
数据源配置走 Spring Boot 标准写法就行,UReport 的内置数据源实现里注入的就是 Spring 的DataSource,所以连接池参数、字符集、时区这些都按你项目平时的习惯配。
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/reportdb?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull username: report_user password: change_me driver-class-name: com.mysql.jdbc.Driver连接串里三个参数别省:useUnicode和characterEncoding决定中文能不能正常进出,zeroDateTimeBehavior=convertToNull处理 MySQL 的零日期,报表里做日期分组时遇到0000-00-00会直接抛异常,提前转成 null 能让报表跑完只是缺值,而不是整页报错。
启动类本身很短,但两个注解一个都不能少:
package com.example.ureportdemo; 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; @SpringBootApplication @ImportResource("classpath:context.xml") // 加载 UReport 的 bean 定义 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } /** * 注册报表 Servlet * urlMappings 必须是 "/ureport/*",改了设计器和预览全部 404 */ @Bean public ServletRegistrationBean<UReportServlet> buildUReportServlet() { return new ServletRegistrationBean<>(new UReportServlet(), "/ureport/*"); } }/ureport/*这个映射不是随便起的约定,UReport 的 Servlet 内部按路径前缀分流:设计器、预览、导出、资源加载都是在这一个 Servlet 里根据请求路径判断的。你改成/report/*,Servlet 本身能收到请求,但它内部拼资源路径时还是按自己的规则来,结果就是页面加载到一半卡住,静态资源 404。这是很多人第一天的翻车点,记住这个路径是死的。
3. 数据源接法:内置、Spring Bean 与取数方式的取舍
报表能不能出数,取决于数据源这一步。UReport2 给了两条主要路子:内置数据源(实现BuildinDatasource)和 Spring Bean 数据源(靠反射调你写的方法)。两者在设计器里的表现完全不同,选错了后面改起来很别扭。
3.1 内置数据源:name() 和 getConnection() 两个方法就够了
内置数据源的实现就是告诉 UReport“我有一个数据源,名字叫这个,连接这样拿”。它适合数据在数据库里、并且你希望在设计器里直接写 SQL 的场景。
package com.example.ureportdemo.datasource; 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 static final Logger log = LoggerFactory.getLogger(UreportDataSource.class); @Autowired private DataSource dataSource; /** 设计器数据源下拉里显示的名字 */ @Override public String name() { return NAME; } /** UReport 取连接时调这里,异常必须吞掉返回 null,不能往外抛 */ @Override public Connection getConnection() { try { return dataSource.getConnection(); } catch (SQLException e) { log.error("Ureport 数据源获取连接失败", e); return null; } } }name()的返回值就是设计器里新建数据集时下拉看到的选项,改名等于用户侧看到的名字变了,已有报表里绑定的数据源名对不上会直接报错,所以这个名字上线后别动。getConnection()里那个return null是刻意为之:UReport 拿到 null 会给出“数据源不可用”的提示,而你把异常抛出去,异常会穿透到 Servlet 层,页面表现为一个没有任何信息的 500。宁可吞掉记日志,也不要让它炸出去。
还有一个隐性的坑在连接池。这个实现每次调用都从 Spring 的DataSource取连接,用完之后 UReport 负责关,正常情况下没问题;但如果你的报表数据集很多、并发预览的人多,默认连接池 8 个连接很快就不够,表现是设计器里预览转圈,日志里Cannot get a connection。生产环境把spring.datasource.tomcat.max-active或对应连接池的最大连接数调上去,别用默认值硬扛。
3.2 Spring Bean 数据源:三参数签名不能改
数据不直接从库里来、而是要走一段业务逻辑再算出来的场景,用 Spring Bean 数据源。手册里给的两个方法很有代表性:一个返回List<Map<String,Object>>,一个返回List<User>实体列表,两种返回类型 UReport 都能识别,前者把 key 当字段名,后者靠实体类的 getter 推断字段名。
package com.example.ureportdemo.datasource; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Component("testBean") public class TestBean { /** * 方法签名固定三参数:数据源名、数据集名、报表参数 * 返回 List<Map>,map 的 key 就是报表里可用的字段名 */ public List<Map<String, Object>> loadReportData(String dsName, String datasetName, Map<String, Object> parameters) { List<Map<String, Object>> list = new ArrayList<>(); // 真实场景这里替换成业务查询,parameters 里是设计器传过来的查询条件 Object startDate = parameters.get("startDate"); for (int i = 0; i < 1000; i++) { Map<String, Object> m = new HashMap<>(); m.put("id", i); m.put("name", "user_" + i); m.put("salary", 5000 + i); list.add(m); } return list; } /** 返回实体列表也行,字段名取 getter 去掉 get 后首字母小写 */ 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("user_" + i); u.setSalary(5000 + i); list.add(u); } return list; } }三个参数的顺序和类型是硬约束:String, String, Map<String,Object>。少一个参数、把 Map 换成Map<String,String>、或者在中间插一个参数,设计器里调用时会直接反射失败,报的是NoSuchMethodException之类的信息,跟“数据源配置错误”八竿子打不着,很容易查错方向。返回类型上,List<Map>最灵活,字段名改了不用改 Java 类;实体列表类型安全,但字段名跟着 getter 走,把getUserName()改成getName(),报表里绑定的userName就全空了。
@Component("testBean")里的 bean 名要和设计器里填的一致。设计器新建 Spring Bean 数据集时会让你填 bean id 和方法名,填错一个字母就是“找不到 bean”。
3.3 两种数据源怎么选
| 维度 | 内置数据源(BuildinDatasource) | Spring Bean 数据源 |
|---|---|---|
| 数据来源 | 数据库,设计器里直接写 SQL | 任意 Java 方法,可查库也可算 |
| 改字段成本 | 改 SQL 即可,不用发版 | 改方法签名要重新编译发布 |
| 参数传递 | SQL 里用:param占位 | 方法第三个参数Map里取 |
| 适合场景 | 报表口径稳定、DBA 能直接调 SQL | 需要权限过滤、跨库聚合、调用外部接口 |
| 排查难度 | SQL 报错信息直白 | 反射异常信息晦涩,见 5.4 |
选型上我给个朴素的判断:如果这个报表的数据能在一条 SQL 里写出来,就用内置数据源,改起来快;如果取数前要先算一段权限、或者要拼多个库的数据,用 Spring Bean,但一定要在方法里自己 try-catch 把业务异常包成带业务语义的返回值,不然用户看到的就是一坨堆栈。
4. 报表文件存哪:默认路径的失效边界与三种替代
报表设计完得存下来,这一步是整个接入里最容易在“开发环境好好的、上线就丢”的地方翻车。默认存储方式把文件写在 Web 应用的WEB-INF下面,这个路径在打成 jar 用java -jar启动时是临时的。
4.1 默认 FileProvider 为什么只在 war 包里成立
默认存储位置是应用部署目录下的WEB-INF/ureportfiles。war 包部署到外部容器时,这个目录是磁盘上的真实目录,重启还在;而 Spring Boot 打成可执行 jar 之后,ServletContext.getRealPath("/")返回的是容器解压出来的临时目录,进程一停就清空。手册里那句“微服务重启后保存的报表将丢失”说的就是这个。
要禁掉默认文件存储,配置项是ureport.disableFileProvider=true。注意这个键是 UReport 内部那个配置解析器读的,写在application.yml里它看不见,得写在它认的文件里(默认是它自带的那份 properties,或者你按 2.2 里那段注释把 location 指到application.properties)。这个区别我在 5.3 单独展开。
4.2 自定义本地目录:前缀、目录创建与相对路径的坑
不想用 FTP、又想避开默认路径的临时目录问题,就自己实现一个ReportProvider。手册里的实现是标准写法,重点在三个地方:前缀、目录创建时机、以及相对路径的处理。
package com.example.ureportdemo.provider; import com.bstek.ureport.exception.ReportException; import com.bstek.ureport.provider.report.ReportFile; import com.bstek.ureport.provider.report.ReportProvider; import org.apache.commons.io.IOUtils; 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.util.*; @Component public class FileReportProvider implements ReportProvider, ApplicationContextAware { /** 前缀决定报表标识长什么样:file:xxx.ureport.xml */ private String prefix = "file:"; /** 从配置里读存储目录,yml 和 properties 都能被 @Value 解析到 */ @Value("${ureport.fileStoreDir}") private String fileStoreDir; private boolean disabled; @Override public InputStream loadReport(String file) { String fullPath = fileStoreDir + "/" + stripPrefix(file); try { return new FileInputStream(fullPath); } catch (FileNotFoundException e) { throw new ReportException(e); } } @Override public void saveReport(String file, String content) { String fullPath = fileStoreDir + "/" + stripPrefix(file); FileOutputStream out = null; try { out = new FileOutputStream(new File(fullPath)); IOUtils.write(content, out, "utf-8"); } catch (Exception ex) { throw new ReportException(ex); } finally { if (out != null) { try { out.close(); } catch (IOException ignored) { } } } } @Override public void deleteReport(String file) { File f = new File(fileStoreDir + "/" + stripPrefix(file)); if (f.exists()) { f.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; // 目录不存在时 listFiles 返回 null,别直接遍历 for (File f : files) { 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 getName() { return "服务器文件系统"; } @Override public boolean disabled() { return disabled; } @Override public String getPrefix() { return prefix; } /** 相对路径按 Web 根目录展开,并保证目录存在 */ @Override public void setApplicationContext(ApplicationContext ctx) throws BeansException { File dir = new File(fileStoreDir); if (dir.exists()) return; if (ctx instanceof WebApplicationContext) { ServletContext sc = ((WebApplicationContext) ctx).getServletContext(); fileStoreDir = sc.getRealPath("/") + fileStoreDir; new File(fileStoreDir).mkdirs(); } } private String stripPrefix(String file) { return file.startsWith(prefix) ? file.substring(prefix.length()) : file; } }几个参数得说清楚。prefix是报表标识的前缀,UReport 同时存在多个 provider 时靠它区分,设计器里保存的报表名会带上file:前缀;你把前缀改成local-,已有报表的引用全部失效,所以这玩意定下来就别动。getName()返回的是设计器里“保存到”下拉里显示的选项名,和前缀是两回事,改它只影响显示。disabled()返回 false 才会启用,如果你想临时禁用某个 provider,改这个返回值比删类安全。
最坑的是setApplicationContext里那段:如果配置的是相对路径(比如ureport.fileStoreDir=ureportfiles),它会拼到getRealPath("/")后面,jar 启动时又回到临时目录,重启照样丢。所以这个字段一定要配绝对路径,/data/ureport/files这种。另外listFiles()在目录不存在时返回 null,不判空就是启动后第一次打开设计器列表时一个空指针,日志里看不出跟目录有啥关系。
4.3 FTP 存储:工厂类的配置注入条件
需要多实例共享报表、或者容器随时重建的场景,把报表放 FTP 上是常见做法。手册给的是一个FTPClientFactory实现PoolableObjectFactory<FTPClient>,负责创建、校验、销毁 FTP 连接对象,配合@ConfigurationProperties("ftp.factory")从 yml 的ftp.factory前缀下读配置。
package com.example.ureportdemo.ftp; import org.apache.commons.net.ftp.FTPClient; 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; @Component @ConfigurationProperties(prefix = "ftp.factory") public class FTPClientFactory implements PoolableObjectFactory<FTPClient> { private String hostname; private int port = 21; private String username; private String password; private int clientTimeout = 60000; private String encoding = "UTF-8"; private int fileType = 2; // 2 = 二进制,传文件必须用它 private boolean passiveMode = true; @Override public FTPClient makeObject() throws Exception { FTPClient client = new FTPClient(); client.setConnectTimeout(clientTimeout); client.connect(hostname, port); if (!FTPReply.isPositiveCompletion(client.getReplyCode())) { client.disconnect(); return null; } if (!client.login(username, password)) { throw new IllegalStateException("FTP 登录失败"); } client.setFileType(fileType); client.setControlEncoding(encoding); if (passiveMode) { client.enterLocalPassiveMode(); } return client; } @Override public boolean validateObject(FTPClient client) { try { return client.sendNoOp(); // NOOP 探活,比 isConnected 靠谱 } catch (Exception e) { return false; } } @Override public void destroyObject(FTPClient client) throws Exception { if (client != null && client.isConnected()) { client.logout(); client.disconnect(); } } @Override public void activateObject(FTPClient client) { } @Override public void passivateObject(FTPClient client) { } // getter / setter 省略,但一个都不能少 }@ConfigurationProperties的注入条件是类上必须有对应属性的 setter。手册里那段代码手写了一大串 getter/setter,同时又挂着 Lombok 的@Data,看着重复,但这正是踩过坑之后的写法:Lombok 在某些 IDE 或构建配置下没生效,编译出来的 class 里没有 setter,配置就全是 null,连接时connect(null, 0)直接抛异常。你如果确认 Lombok 正常,@Data就够;不确定就老老实实手写,这东西写一次管很久。
关于参数的几个提醒:fileType默认 2(二进制),传报表 XML 其实文本模式也行,但一旦报表里有导出图片的引用,文本模式会把换行符改掉,文件就坏了,所以固定二进制;passiveMode在容器网络里基本都要开,不开的话数据连接经常卡住然后超时,表现是保存转圈几十秒后报连接失败,很容易误判成 FTP 服务端问题;encoding要和 FTP 服务端的字符集一致,中文报表名乱码就是这里对不上。
yml 那边的配置按手册的结构来,ureport.ftp.provider下面三个键控制 provider 是否启用、前缀是什么、以及 basePath:
ftp: factory: hostname: 192.168.10.20 port: 21 username: ftpuser password: change_me client-timeout: 60000 ureport: ftp: provider: prefix: ftp- disabled: false basePath: /report4.4 三种存储方式对比
| 方式 | 数据在哪 | 重启是否丢 | 适用场景 | 主要成本 |
|---|---|---|---|---|
| 默认 FileProvider | WEB-INF/ureportfiles | jar 启动会丢 | 本地跑通、war 部署 | 几乎零成本 |
| 自定义本地目录 | 指定绝对路径 | 不丢 | 单机、容器挂载卷 | 要保证目录可写、路径为绝对路径 |
| FTP provider | 远端 FTP | 不丢,多实例共享 | 多副本部署、报表统一管理 | 工厂类 + 网络稳定性 |
单机部署我倾向自定义本地目录,简单、可控、出问题能直接上服务器看文件;多副本就必须 FTP,不然 A 实例保存的报表 B 实例读不到,用户看到的是“报表时有时无”,这种玄学问题最难查。
5. 避坑与排查:五条血泪记录
下面这几条都是接入过程中真实会撞上的,每条按现象、原因、解决写,遇到了直接对号入座。
5.1 访问设计器返回 404
现象:启动日志一切正常,访问/ureport/designer返回 404,或者返回 Spring 的默认错误页。
原因:ServletRegistrationBean没注册,或者注册的路径不是/ureport/*。还有一种情况是启动类上漏了@ImportResource("classpath:context.xml"),容器里没有 UReport 的 bean,Servlet 初始化失败但异常被容器吞了,表现也是 404。
解决:先看启动日志里有没有UReportServlet相关的注册信息,再确认注解和路径。路径这块别自作主张改,/ureport/*是硬要求,Servlet 内部按这个前缀拼资源地址。
5.2 设计器能打开,保存时报错或报表重启后消失
现象:设计器正常打开,也能画报表,点保存要么报错,要么提示成功但重启后列表空了。
原因:用的是默认 FileProvider,jar 启动时落在临时解压目录。保存成功只是写进了那个临时目录,进程一停就没了。
解决:要么按 4.2 实现自定义 provider 并配绝对路径,要么接 FTP。短期验证可以先把ureport.disableFileProvider=true加上,确认默认存储被禁掉之后问题消失,再换成正式方案。
5.3 配置写在 application.yml 里不生效
现象:ureport.disableFileProvider=true写在 yml 里,默认存储照样生效;ureport.fileStoreDir写 yml 里,自定义 provider 里读到了,但 UReport 内部行为没变。
原因:UReport 内部有一批配置键是通过它自己的PropertyPlaceholderConfigurer(bean id 为ureport.props)解析的,默认只读它自己那份 properties 文件,Spring Boot 的 yml 环境不参与。而你在自定义类里写的@Value("${ureport.fileStoreDir}")走的是 Spring Environment,yml 能读到。两套解析体系,同一个键名,很容易让人以为“配了但没生效”。
解决:需要被 UReport 内部消费的键,放到它认的那个 properties 文件里;同时如果自定义类也用@Value读同一个键,两个地方都要写一份。或者按 2.2 里那段注释,把ureport.props的 location 指到application.properties,统一到一个文件,但注意它只认 properties 不认 yml。
5.4 Spring Bean 数据源下拉里看不到 bean
现象:设计器里新建 Spring Bean 数据集,bean id 填了,方法名填了,点确定报找不到 bean 或方法。
原因:三类。一是@Component的名字和填的不一致;二是方法签名不是(String, String, Map),反射找不到匹配方法;三是方法所在类没被扫描到,比如包路径在启动类的兄弟目录之外。
解决:先把 bean 名和方法名对着代码逐字符核一遍,再看启动日志里有没有这个 bean 的注册记录。方法签名用javap或者 IDE 的反射视图确认一下实际参数类型,泛型擦除之后Map<String,Object>就是Map,但如果你写成了HashMap就匹配不上。类扫描范围这个最隐蔽,@SpringBootApplication默认扫它所在包及子包,你把数据源类放到平级包里就扫不到。
5.5 预览慢、日志刷 Cannot get a connection
现象:单人预览正常,几个人同时点就转圈,日志里出现取连接失败的提示。
原因:内置数据源每次取连接都走 Spring 的 DataSource,连接池默认最大连接数偏小,多个并发预览叠加报表里的多个数据集,很快就打满。
解决:先把连接池最大连接数调上去,再看是不是有报表数据集里写了全表扫描。真的顶不住就考虑给报表加查询条件,让用户先选时间范围再查,别一上来就拉全量。这条不是配置能完全解决的,属于报表设计规范问题。
6. 上线前的自检套路:五步把链路验一遍
接入做完不等于能用,我一般会按固定顺序过一遍,每一步都有明确的失败信号,比漫无目的地乱点快得多。
第一步,启动后直接打 Servlet 的入口,确认 servlet 注册生效:
# 设计器页面,返回 200 且内容里有设计器相关脚本,说明 servlet 通了 curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/ureport/designer # 故意打一个不存在的子路径,正常应返回 UReport 自己的错误页而不是 Spring 的 Whitelabel curl -s http://127.0.0.1:8080/ureport/not-exist | head -20第二条命令的价值在于区分“servlet 通了”和“请求被 Spring MVC 接走了”。如果返回的是 Spring Boot 默认错误页,说明请求根本没到 UReport 的 Servlet,问题在映射不在业务。
第二步,在设计器里新建一个内置数据源数据集,只写select 1 as a,预览。这一步只验连接,不验业务 SQL,能快速把数据源问题和 SQL 问题分开。
第三步,保存一张空报表,然后去服务器上看文件到底落在哪:
# 换成你配置的绝对路径 ls -lt /data/ureport/files | head文件在预期目录里,说明 provider 生效;文件出现在别的地方,说明你配的相对路径被getRealPath("/")拼走了。
第四步,重启进程,再打开设计器看报表列表。这一步是 4.1 那个坑的唯一有效验证方式,光看保存成功提示没用。
第五步,导一次 Excel 和 PDF,重点看中文和日期列。中文乱码回去查 2.3 里的连接串参数,日期列变成一串数字通常是单元格格式没设,跟数据源无关。
这几步我在不同项目里重复了不知道多少次,最省时间的是第二步——很多人一上来就写复杂 SQL,报错了分不清是连接问题还是 SQL 问题,来回折腾半天。从那以后我每次接新的报表模块,都强制走一遍这五步,先在select 1上把链路跑绿,再往里灌业务。希望帮到你。
本文还有配套的精品资源,点击获取