Spring Boot 集成 FineReport 实现动态报表服务
在企业级数据可视化场景中,FineReport 作为主流报表工具,与 Java 后端服务的整合是常见需求。本文介绍一种基于 RESTful 架构的集成方案,通过 Spring Boot 封装报表调用能力,实现模板渲染、参数传递与前端嵌入的完整链路。
核心设计思路
将 FineReport 部署为独立报表服务,Java 后端承担参数校验、权限控制与请求转发的职责,前端通过 iframe 或 API 方式嵌入渲染结果。该方案解耦了报表设计与业务系统,便于独立维护与扩展。
环境准备
- FineReport 服务器(10.0+ 版本)
- Spring Boot 3.x
- JDK 17+
后端服务实现
配置报表服务器地址,采用 RestClient 替代传统 RestTemplate 进行 HTTP 通信:
@Configuration
public class ReportClientConfig {
@Bean
public RestClient reportRestClient(@Value("${report.server.host}") String baseUrl) {
return RestClient.builder()
.baseUrl(baseUrl)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}
报表调用控制器,支持模板路径、导出格式及查询参数的灵活配置:
@RestController
@RequestMapping("/api/reports")
public class ReportExportController {
private final RestClient reportClient;
public ReportExportController(RestClient reportClient) {
this.reportClient = reportClient;
}
@GetMapping("/preview")
public void previewReport(
@RequestParam String template,
@RequestParam(required = false) String format,
@RequestParam Map<String, String> conditions,
HttpServletResponse response) throws IOException {
// 过滤系统保留参数
Map<String, String> reportParams = conditions.entrySet().stream()
.filter(e -> !e.getKey().equals("template") && !e.getKey().equals("format"))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
// 构建报表请求 URL
String requestPath = UriComponentsBuilder.fromPath("/webroot/decision/view/report")
.queryParam("viewlet", template)
.queryParam("format", Objects.requireNonNullElse(format, "html"))
.queryParamIfPresent("bypagesize", Optional.of("false"))
.build()
.toUriString();
// 转发请求并回写响应
byte[] result = reportClient.get()
.uri(uriBuilder -> {
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(requestPath);
reportParams.forEach(builder::queryParam);
return builder.build().toUri();
})
.retrieve()
.body(byte[].class);
response.setContentType(detectContentType(format));
response.getOutputStream().write(result);
}
private String detectContentType(String fmt) {
return switch (Objects.requireNonNullElse(fmt, "html").toLowerCase()) {
case "pdf" -> MediaType.APPLICATION_PDF_VALUE;
case "excel" -> "application/vnd.ms-excel";
default -> MediaType.TEXT_HTML_VALUE;
};
}
}
前端嵌入方案
使用 Thymeleaf 构建报表容器页面,通过参数化 URL 实现动态加载:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>智能报表中心</title>
<style>
.filter-bar { margin: 16px 0; padding: 12px; background: #f5f5f5; border-radius: 4px; }
.report-frame { width: 100%; height: calc(100vh - 120px); border: none; }
</style>
</head>
<body>
<div class="filter-bar">
<form id="queryForm" th:action="@{/api/reports/preview}" method="get" target="reportView">
<input type="hidden" name="template" th:value="${currentTemplate}">
<label>日期范围:</label>
<input type="date" name="startDate" required>
<input type="date" name="endDate" required>
<label>部门:</label>
<select name="deptId">
<option th:each="dept : ${departments}" th:value="${dept.id}" th:text="${dept.name}"></option>
</select>
<button type="submit">生成报表</button>
<button type="button" onclick="exportExcel()">导出 Excel</button>
</form>
</div>
<iframe name="reportView" class="report-frame"
th:src="@{/api/reports/preview(template=${currentTemplate})}">
</iframe>
<script>
function exportExcel() {
const form = document.getElementById('queryForm');
const originalAction = form.action;
form.action = form.action + '&format=excel';
form.submit();
form.action = originalAction;
}
</script>
</body>
</html>
数据库示例
报表数据源通常关联业务表,以下为用户维度统计表示例:
| 字段 | 类型 | 说明 |
|---|---|---|
| user_id | BIGINT | 主键,自增 |
| username | VARCHAR(64) | 登录账号 |
| dept_code | VARCHAR(32) | 所属部门编码 |
| register_time | DATETIME | 注册时间 |
| status | TINYINT | 状态:0-禁用,1-启用 |
关键注意事项
- 编码统一:URL 参数涉及中文时需进行 UTF-8 编码,避免 FineReport 服务端解析异常
- 会话保持:若报表启用了权限验证,需通过 Cookie 或 Token 传递用户凭证
- 性能优化:大数据量报表建议启用 FineReport 的定时缓存或分页加载机制
- 安全加固:生产环境应对
template参数做白名单校验,防止目录遍历攻击