动态报表
BaskReport的模板文件是以xml存储到数据库的,实现动态报表的关键是在预览报表的时候我们可以提供自定义的xml给报表渲染器,让报表根据我们提供的模板和数据展示报表。
实现步骤
- 实现自定义FileLoader扩展loadContent方法
- 拦截默认的报表预览请求,并将需要实现动态报表的报表文件的缓存关闭
- 创建模板信息实体类和工具类
- 创建前端页面用于配置和提交动态报表
一、实现自定义的FileLoader
如类:com.basksoft.baskreport.demo.dynaview.MockFileLoader
将该类通过SPI配置文件配置到项目中:

然后定义这个类,继承BaskServer默认提供的com.basksoft.core.database.service.file.DefaultFileLoader:
public class MockFileLoader extends DefaultFileLoader {
/**
* 加载文件内容
*
* 优先使用动态模板生成XML内容,如果没有动态模板则使用默认的文件内容
*
* @param fileId 文件ID
* @return 文件内容(XML格式)
*/
public String loadContent(long fileId) {
String content = super.loadContent(fileId);
// 检查是否有动态模板需要处理
TemplateInfo template = TemplateHolder.getTemplate();
if (template != null) {
try {
content = TemplateUtils.buildDynaViewTemplate(
template.getTitle(),
template.getFields(),
template.getDatas()
);
} catch (Exception e) {
// 发生异常时返回原始内容,避免报表完全不可用
logger.error("生成动态报表XML失败: fileId={}", fileId, e);
}
}
return content;
}
}
关键特性:
- 日志记录:使用SLF4J记录调试和错误信息
- 异常处理:生成XML失败时返回原始内容,确保报表可用性
- 回退机制:没有动态模板时使用默认文件内容
二、拦截默认的分页预览
如类:com.basksoft.baskreport.demo.dynaview.MockPageServletHandler
将该类通过SPI配置文件配置到项目中:

然后定义这个类,继承BaskServer默认提供的com.basksoft.report.console.report.preview.PageServletHandler:
public class MockPageServletHandler extends PageServletHandler {
private static final Logger logger = LoggerFactory.getLogger(MockPageServletHandler.class);
// 动态报表的文件ID,可根据实际需求配置
private static final long DYNAMIC_REPORT_FILE_ID = 52001;
@Override
@BaskAuthFile()
public void execute(BaskServletRequestWrapper req, BaskServletResponseWrapper resp) {
FileIdentity identity = ContextHolder.getFile();
try {
long fileId = identity.getId();
// 判断是否为动态报表文件
if (fileId == DYNAMIC_REPORT_FILE_ID) {
handleDynamicReport(identity, req);
}
// 执行默认的预览逻辑
super.execute(req, resp);
} catch (Exception e) {
logger.error("报表预览处理失败: fileId={}",
identity != null ? identity.getId() : "unknown", e);
throw new RuntimeException("报表预览处理失败", e);
} finally {
// 清理ThreadLocal,避免内存泄漏
try {
TemplateHolder.clear();
logger.debug("清理ThreadLocal完成");
} catch (Exception e) {
logger.error("清理ThreadLocal失败", e);
}
}
}
/**
* 处理动态报表的特殊逻辑
*/
private void handleDynamicReport(FileIdentity identity, BaskServletRequestWrapper req) {
// 禁用缓存:设置修改时间为当前时间
BaskFile baskFile = identity.getBaskFile();
if (baskFile != null) {
baskFile.setModifyDate(new Date());
}
// 从请求参数中解析动态报表配置
String parameters = req.getParameter("parameters");
if (StringUtils.isNotBlank(parameters)) {
parseAndSetTemplate(parameters);
}
}
/**
* 解析并设置模板信息
*/
private void parseAndSetTemplate(String parameters) {
// URL解码
String decodedParameters = URLDecoder.decode(parameters, "UTF-8");
// 反序列化为TemplateInfo对象
TemplateInfo templateInfo = JacksonUtils.getObjectMapper()
.readValue(decodedParameters, TemplateInfo.class);
// 参数验证
validateTemplateInfo(templateInfo);
// 存入ThreadLocal
TemplateHolder.setTemplate(templateInfo);
}
/**
* 验证模板信息的有效性
*/
private void validateTemplateInfo(TemplateInfo templateInfo) {
if (templateInfo == null) {
throw new IllegalArgumentException("模板信息不能为空");
}
if (StringUtils.isBlank(templateInfo.getTitle())) {
throw new IllegalArgumentException("报表标题不能为空");
}
if (templateInfo.getFields() == null || templateInfo.getFields().isEmpty()) {
throw new IllegalArgumentException("字段列表不能为空");
}
if (templateInfo.getDatas() == null) {
throw new IllegalArgumentException("数据列表不能为null");
}
// 验证字段配置
for (int i = 0; i < templateInfo.getFields().size(); i++) {
Map<String, Object> field = templateInfo.getFields().get(i);
String fieldName = (String) field.get("name");
if (StringUtils.isBlank(fieldName)) {
throw new IllegalArgumentException("第" + (i + 1) + "个字段的name不能为空");
}
}
}
}
关键特性:
- 模块化设计:将大方法拆分为多个私有方法,提高可维护性
- 常量提取:将动态报表文件ID提取为常量,便于配置
- 参数验证:完整的输入参数验证,提供友好的错误提示
- 日志记录:详细的日志记录,便于问题排查
- 异常处理:完善的异常处理机制
- 资源清理:确保ThreadLocal在finally块中被清理
三、创建模板信息实体类
创建TemplateInfo类来存储动态报表的配置信息:
/**
* 动态报表模板信息实体类
*
* 功能说明:
* 1. 封装动态报表的配置信息,包括标题、字段定义和数据
* 2. 提供getter和setter方法用于属性访问
* 3. 支持序列化和反序列化(通过JSON)
*/
public class TemplateInfo {
/**
* 报表标题
* 显示在报表第一行
*/
private String title;
/**
* 字段列表
* 每个字段包含以下属性:
* - name: 字段名称(数据字段名)
* - label: 字段标签(表头显示名称)
* - type: 字段类型(string/number/boolean)
* - width: 列宽度(像素)
*/
private List<Map<String, Object>> fields;
/**
* 数据列表
* 报表要展示的数据,每个元素代表一行数据
*/
private List<Map<String, Object>> datas;
// getter和setter方法
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public List<Map<String, Object>> getFields() { return fields; }
public void setFields(List<Map<String, Object>> fields) { this.fields = fields; }
public List<Map<String, Object>> getDatas() { return datas; }
public void setDatas(List<Map<String, Object>> datas) { this.datas = datas; }
@Override
public String toString() {
return "TemplateInfo{" +
"title='" + title + '\'' +
", fields=" + (fields != null ? fields.size() : 0) + " items" +
", datas=" + (datas != null ? datas.size() : 0) + " items" +
'}';
}
}
关键特性:
- 完整注释:详细的类和属性注释,说明每个字段的用途
- toString方法:便于调试和日志输出
- 序列化支持:支持JSON序列化和反序列化
四、创建ThreadLocal模板持有器
创建TemplateHolder类,使用ThreadLocal在当前线程中存储模板信息:
/**
* 动态报表模板持有器
*
* 功能说明:
* 1. 使用ThreadLocal在当前线程中存储动态报表模板信息
* 2. 在整个请求处理周期中传递模板信息,从请求拦截到模板生成
* 3. 提供模板信息的设置、获取和清理方法
* 4. 确保在请求结束后清理ThreadLocal,避免内存泄漏
*
* ThreadLocal使用场景:
* - 请求到达时,MockPageServletHandler解析参数并设置模板
* - 文件加载时,MockFileLoader从ThreadLocal获取模板生成XML
* - 请求结束时,清理ThreadLocal释放内存
*/
public class TemplateHolder {
private static final Logger logger = LoggerFactory.getLogger(TemplateHolder.class);
/**
* 使用ThreadLocal存储模板信息,确保线程安全
*/
private static final ThreadLocal<TemplateInfo> template = new ThreadLocal<>();
/**
* 设置当前线程的模板信息
*
* 通常在请求拦截阶段调用,将解析后的模板信息存入ThreadLocal
*
* @param templateInfo 模板信息对象
*/
public static void setTemplate(TemplateInfo templateInfo) {
if (templateInfo == null) {
logger.warn("尝试设置null模板信息,操作已忽略");
return;
}
template.set(templateInfo);
logger.debug("模板信息已设置到ThreadLocal: {}", templateInfo);
}
/**
* 获取当前线程的模板信息
*
* 通常在文件加载阶段调用,从ThreadLocal获取模板信息用于生成XML
*
* @return 当前线程的模板信息,如果未设置则返回null
*/
public static TemplateInfo getTemplate() {
TemplateInfo templateInfo = template.get();
if (templateInfo != null) {
logger.debug("从ThreadLocal获取模板信息: {}", templateInfo);
}
return templateInfo;
}
/**
* 清理当前线程的模板信息
*
* 必须在finally块中调用,确保请求结束后清理ThreadLocal
* 避免线程复用时导致的数据污染和内存泄漏
*
* 建议在MockPageServletHandler的finally块中调用
*/
public static void clear(){
try {
template.remove();
logger.debug("ThreadLocal模板信息已清理");
} catch (Exception e) {
logger.error("清理ThreadLocal模板信息失败", e);
}
}
}
关键特性:
- null检查:防止设置null值导致的问题
- 日志记录:记录模板的设置、获取和清理操作
- 异常处理:清理操作的异常处理
- 使用场景说明:详细说明了ThreadLocal的使用时机
五、创建XML模板工具类
创建TemplateUtils类,用于生成动态报表的XML模板。该类包含以下核心方法:
/**
* 动态报表XML模板工具类
*
* 功能说明:
* 1. 根据报表标题、字段列表和数据列表生成BaskReport规范的XML模板
* 2. 支持动态生成表格结构,包括标题行、列头、数据行
* 3. 自动生成Excel风格的列名(A, B, C, ... AA, AB, ...)
* 4. 提供参数验证和异常处理
* 5. 内置预览配置和导出功能配置
*/
public class TemplateUtils {
private static final Logger logger = LoggerFactory.getLogger(TemplateUtils.class);
// 默认配置常量
private static final String DEFAULT_DATASET_NAME = "employee";
private static final int DEFAULT_TITLE_HEIGHT = 42;
private static final int DEFAULT_ROW_HEIGHT = 20;
private static final int DEFAULT_MIN_COL_WIDTH = 20;
private static final int DEFAULT_MAX_COL_WIDTH = 500;
/**
* 构建动态报表XML模板
*
* @param title 报表标题
* @param fieldList 字段列表,每个字段包含name、label、type、width属性
* @param dataList 数据列表
* @return 完整的XML报表模板
* @throws IllegalArgumentException 参数验证失败时抛出
*/
public static String buildDynaViewTemplate(String title,
List<Map<String, Object>> fieldList,
List<Map<String, Object>> dataList) {
// 参数验证
validateParameters(title, fieldList, dataList);
logger.info("开始构建动态报表XML模板: title={}, fields={}, datas={}",
title, fieldList.size(), dataList != null ? dataList.size() : 0);
StringBuilder sb = new StringBuilder();
// XML头部
sb.append(XML_HEADER);
sb.append(XML_ROOT);
// 标题行
sb.append(buildHeaderTemplate(title, fieldList.size()));
// 标题行空白单元格(用于隐藏标题行的后续单元格)
for (int i = 1; i < fieldList.size(); i++) {
String colName = buildColName(i + 1) + "1";
sb.append(buildHeaderBlankTemplate(i + 1, colName));
}
// 列头行
for (int i = 0; i < fieldList.size(); i++) {
String colName = buildColName(i + 1) + "2";
sb.append(buildColumnHeaderTemplate(i + 1, colName, fieldList.get(i).get("label").toString()));
}
// 数据行
for (int i = 0; i < fieldList.size(); i++) {
String colName = buildColName(i + 1) + "3";
sb.append(buildColumnDataTemplate(i + 1, colName, DEFAULT_DATASET_NAME, fieldList.get(i).get("name").toString()));
}
// 列配置
for (int i = 0; i < fieldList.size(); i++) {
int width = getValidColWidth(fieldList.get(i).get("width"));
sb.append(buildColTemplate(i + 1, width));
}
// 行配置
sb.append(ROW_HEADER_TEMPLATE);
sb.append(buildRowTemplate(2));
sb.append(buildRowTemplate(3));
// 数据集
sb.append(buildDataset(fieldList, dataList));
// 设置
sb.append(SETTING_TEMPLATE);
// XML尾部
sb.append(XML_FOOTER);
logger.info("动态报表XML模板构建完成,长度: {} 字符", sb.length());
return sb.toString();
}
/**
* 验证输入参数
*/
private static void validateParameters(String title, List<Map<String, Object>> fieldList,
List<Map<String, Object>> dataList) {
if (StringUtils.isBlank(title)) {
throw new IllegalArgumentException("报表标题不能为空");
}
if (fieldList == null || fieldList.isEmpty()) {
throw new IllegalArgumentException("字段列表不能为空");
}
if (dataList == null) {
throw new IllegalArgumentException("数据列表不能为null");
}
// 验证字段配置
for (int i = 0; i < fieldList.size(); i++) {
Map<String, Object> field = fieldList.get(i);
if (StringUtils.isBlank((String) field.get("name"))) {
throw new IllegalArgumentException("第" + (i + 1) + "个字段的name不能为空");
}
if (StringUtils.isBlank((String) field.get("label"))) {
throw new IllegalArgumentException("第" + (i + 1) + "个字段的label不能为空");
}
if (StringUtils.isBlank((String) field.get("type"))) {
throw new IllegalArgumentException("第" + (i + 1) + "个字段的type不能为空");
}
}
}
/**
* 获取有效的列宽度
*/
private static int getValidColWidth(Object widthObj) {
if (widthObj == null) {
return DEFAULT_MIN_COL_WIDTH;
}
try {
int width = Integer.parseInt(widthObj.toString());
return Math.max(DEFAULT_MIN_COL_WIDTH, Math.min(DEFAULT_MAX_COL_WIDTH, width));
} catch (NumberFormatException e) {
logger.warn("列宽度格式错误: {}, 使用默认值: {}", widthObj, DEFAULT_MIN_COL_WIDTH);
return DEFAULT_MIN_COL_WIDTH;
}
}
/**
* 构建数据集部分
*/
private static String buildDataset(List<Map<String, Object>> fieldList, List<Map<String, Object>> dataList) {
// 构建字段定义
StringBuilder fields = new StringBuilder();
for (Map<String, Object> field : fieldList) {
fields.append(buildFieldTemplate(
field.get("name").toString(),
field.get("label").toString(),
field.get("type").toString()
));
}
// 序列化数据
String data = "[]";
try {
data = JacksonUtils.getObjectMapper().writeValueAsString(dataList);
} catch (JsonProcessingException e) {
logger.error("数据序列化失败", e);
}
return buildDatasetTemplate(DEFAULT_DATASET_NAME, data, fields.toString());
}
// 私有辅助方法
private static String buildColName(int colNum) {
// 根据colNum生成excel风格的列名称 (1->A, 26->Z, 27->AA, ...)
if (colNum <= 0) return "";
StringBuilder sb = new StringBuilder();
int num = colNum;
while (num > 0) {
num--; // 转换为0-based索引
int remainder = num % 26;
sb.insert(0, (char) ('A' + remainder));
num /= 26;
}
return sb.toString();
}
/**
* XML特殊字符转义
*/
private static String escapeXml(String text) {
if (text == null) return "";
return text.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
}
buildDynaViewTemplate方法生成的XML结构包含:
- XML头部和根节点
- 标题行单元格(使用
colspan合并单元格) - 标题行空白单元格(隐藏多余的单元格)
- 列头单元格(显示字段标签)
- 数据单元格(使用
expand="down"实现向下扩展) - 列配置(宽度,范围20-500像素)
- 行配置(高度,标题行42px,数据行20px)
- 内置数据集(包含字段定义和JSON数据)
- 页面设置和预览配置(支持导出为Excel、Word、PDF、CSV)
关键特性:
- 参数验证:完整的输入参数验证
- 列宽度验证:自动修正超出范围的列宽度
- XML转义:自动转义XML特殊字符
- 异常处理:数据序列化失败的异常处理
- 日志记录:详细的日志记录
- 常量管理:使用常量管理默认配置
六、创建前端配置页面
创建HTML页面(如dynareport.html),用于配置动态报表的字段和数据。页面功能包括:
1. 报表配置

配置报表的基本信息:
- Tenant ID(租户ID):指定报表所属的租户,例如:
basksoft - File ID(文件ID):指定报表文件的ID,例如:
15501 - Report Title(报表标题):设置报表的显示标题,例如:
动态列示例报表
这些配置会在表单提交时作为URL参数传递给预览接口。
2. 字段列表管理
在字段列表表格中可以:
- 添加字段:点击"Add Field"按钮添加新的字段行
- 删除字段:点击每行的"Remove"按钮删除该字段
- 编辑字段属性:
- Name:字段名称(数据库字段名,如:id, username, active)
- Label:字段标签(显示在报表表头的名称,如:ID, Username, Active)
- Type:字段类型(支持:String、Number、Boolean)
- Width:列宽度(单位:像素,范围:20-500)
- 生成Mock数据:点击"Regenerate Mock Data"按钮,根据字段配置自动生成测试数据
- 设置行数:通过"Row Count"输入框控制生成的数据行数
3. 数据编辑器
- JSON格式编辑:在文本框中直接编辑JSON格式的报表数据
- 自动格式化:数据自动格式化为缩进的JSON格式,便于阅读和编辑
- 手动修改:可以手动编辑数据,系统会在提交时验证JSON格式
4. 提交预览
点击"Preview & Save"按钮:
- 将配置信息(标题、字段列表、数据)打包为JSON对象
- 将JSON对象进行URL编码作为
parameters参数 - 通过表单POST提交到预览接口
- 提交URL格式:
/baskserver/baskreport/preview?file=${fileId}&tenantId=${tenantId} - 在新窗口中打开报表预览页面
5. 核心JavaScript功能
// 状态管理
let fields = []; // 字段列表
let reportConfig = { // 报表配置
fileId: 15501,
tenantId: 'basksoft',
title: '动态列示例报表'
};
// 生成Mock数据
function generateMock() {
const rowCount = parseInt(document.getElementById('rowCount').value) || 5;
const mockData = [];
for (let i = 0; i < rowCount; i++) {
const row = {};
fields.forEach(field => {
if (field.type === 'number') {
row[field.name] = Mock.Random.integer(1, 1000);
} else if (field.type === 'boolean') {
row[field.name] = Mock.Random.boolean();
} else {
row[field.name] = Mock.Random.string(5, 10);
}
});
mockData.push(row);
}
document.getElementById('jsonEditor').value = JSON.stringify(mockData, null, 2);
}
// 保存并预览
function saveData(event) {
event.preventDefault();
const jsonContent = document.getElementById('jsonEditor').value;
let parsedData = JSON.parse(jsonContent);
const requestData = {
title: reportConfig.title,
fields: fields,
datas: parsedData
};
const parameters = encodeURIComponent(JSON.stringify(requestData));
document.getElementById('parameters').value = parameters;
const form = document.getElementById('reportForm');
form.action = `/baskserver/baskreport/preview?file=${reportConfig.fileId}&tenantId=${reportConfig.tenantId}`;
form.submit();
}
七、完整的数据流程
- 前端配置:用户通过HTML页面配置报表标题、字段列表和数据
- 提交请求:表单提交到
/baskserver/baskreport/preview,携带parameters参数(JSON格式) - 拦截请求:
MockPageServletHandler拦截请求- 判断是否为动态报表文件(通过fileId判断)
- 设置
BaskFile的修改时间为当前时间,禁用缓存 - 解析parameters参数(URL解码 + JSON反序列化)
- 验证参数有效性
- 创建
TemplateInfo对象并存入ThreadLocal
- 加载模板:
MockFileLoader的loadContent方法被调用- 从
ThreadLocal获取模板信息 - 调用
TemplateUtils.buildDynaViewTemplate生成XML - 如果生成失败,返回原始内容(回退机制)
- 从
- 生成XML:
TemplateUtils.buildDynaViewTemplate方法执行- 验证输入参数
- 构建XML头部和根节点
- 生成标题行、列头行、数据行
- 配置列宽度和行高度
- 构建数据集(字段定义 + JSON数据)
- 添加页面设置和预览配置
- 渲染报表:报表渲染器使用生成的XML和数据进行渲染
- 清理资源:在
finally块中清理ThreadLocal,避免内存泄漏
流程图:
前端页面
↓ 提交配置
MockPageServletHandler (拦截)
↓ 解析参数
TemplateHolder (存入ThreadLocal)
↓ 禁用缓存
MockFileLoader (加载文件)
↓ 获取模板
TemplateUtils (生成XML)
↓ 生成模板
报表渲染器 (渲染)
↓ 清理资源
TemplateHolder (清理ThreadLocal)
八、关键要点
ThreadLocal使用
- 用于在当前线程的请求处理过程中传递模板信息
- 请求开始时设置,请求结束时清理
- 必须在finally块中清理,避免内存泄漏
缓存控制
- 通过设置
BaskFile的修改时间来禁用缓存 - 确保每次都重新加载模板,使用最新的动态配置
- 通过设置
SPI配置
- 通过SPI机制注册自定义的FileLoader和PageServletHandler
- 配置文件路径:
META-INF/services/com.basksoft.core.database.service.file.FileLoader - 配置文件路径:
META-INF/services/com.basksoft.report.console.report.preview.PageServletHandler
XML生成
- 动态生成符合BaskReport规范的XML模板
- 支持参数验证和异常处理
- 提供回退机制,确保报表可用性
资源清理
- 必须在finally块中清理ThreadLocal
- 避免线程复用时导致的数据污染和内存泄漏
- 清理操作也要进行异常处理
数据传递
- 通过URL参数传递JSON格式的配置信息
- 参数需要进行URL编码和解码
- 支持JSON序列化和反序列化
异常处理
- 所有关键操作都有异常处理
- 提供友好的错误提示信息
- 记录详细的日志便于问题排查
日志记录
- 使用SLF4J进行日志记录
- 记录关键操作和异常信息
- 便于调试和问题排查
参数验证
- 完整的输入参数验证
- 提供友好的验证错误提示
- 确保数据的有效性和完整性
常量管理
- 使用常量管理配置值
- 便于统一管理和修改
- 提高代码可维护性