SpringBoot 3.x + ECharts 5.4 动态大屏实战:3种数据源与5秒轮询刷新

在企业级数据监控场景中,动态数据展示已成为刚需。本文将手把手带你实现一个支持多数据源、具备实时刷新能力的可视化大屏系统。不同于基础教程,我们重点解决工程化落地中的三个核心问题: 异构数据源整合 前端高频轮询优化 大屏性能调优

1. 工程架构设计

1.1 技术栈选型

  • 后端框架 :SpringBoot 3.1.5(JDK17+)
  • 数据可视化 :ECharts 5.4.3
  • 数据源支持
    • MySQL 8.0(关系型数据库)
    • 本地JSON文件(静态数据)
    • REST API模拟(动态数据)

1.2 项目结构

src/
├── main/
│   ├── java/
│   │   └── com/
│   │       └── demo/
│   │           ├── config/       # 配置类
│   │           ├── controller/   # 数据接口
│   │           ├── service/      # 数据服务
│   │           └── Application.java
│   └── resources/
│       ├── static/               # 前端资源
│       │   ├── json/             # 静态JSON数据
│       │   └── js/               # ECharts库
│       └── templates/            # 大屏HTML

2. 多数据源集成方案

2.1 MySQL实时查询

采用MyBatis-Plus实现高效数据访问:

@Mapper
public interface MetricMapper extends BaseMapper<Metric> {
    @Select("SELECT metric_name, value FROM realtime_metrics WHERE update_time > NOW() - INTERVAL 5 SECOND")
    List<Metric> selectLatestMetrics();
}

性能优化点

  • 添加 update_time 索引
  • 使用 @Cacheable 缓存查询结果
  • 限制返回字段避免 SELECT *

2.2 静态JSON文件

resources/static/json/ 放置数据文件:

// sales_data.json
{
  "categories": ["Q1", "Q2", "Q3", "Q4"],
  "values": [1250, 1890, 2100, 1800]
}

通过Spring MVC直接暴露接口:

@GetMapping("/api/json/sales")
public ResponseEntity<?> getSalesData() throws IOException {
    Resource resource = new ClassPathResource("static/json/sales_data.json");
    return ResponseEntity.ok().body(FileCopyUtils.copyToByteArray(resource.getInputStream()));
}

2.3 模拟API数据

使用随机数生成动态数据:

@RestController
@RequestMapping("/api/simulate")
public class SimulateController {
    
    private final Random random = new Random();

    @GetMapping("/traffic")
    public Map<String, Object> getTrafficData() {
        return Map.of(
            "time", LocalTime.now().truncatedTo(ChronoUnit.SECONDS),
            "inbound", random.nextInt(1000),
            "outbound", random.nextInt(800)
        );
    }
}

3. 前端轮询实现

3.1 核心轮询逻辑

let refreshInterval = 5000; // 5秒
let chartInstances = []; // 存储图表实例

function startPolling() {
    chartInstances.forEach(chart => {
        setInterval(() => {
            fetch(chart.dataUrl)
                .then(res => res.json())
                .then(data => {
                    chart.instance.setOption(updateChartOption(data));
                    chart.instance.resize(); // 自适应容器大小
                });
        }, refreshInterval);
    });
}

3.2 避免内存泄漏

// 页面卸载时清理定时器
window.addEventListener('beforeunload', () => {
    chartInstances.forEach(chart => {
        clearInterval(chart.timer);
    });
});

4. 大屏性能优化

4.1 ECharts配置建议

option = {
    animation: false, // 关闭动画提升性能
    dataset: {       
        dimensions: ['product', 'sales'],
        source: []
    },
    series: [{
        type: 'bar',
        large: true,  // 开启大数据优化
        progressiveChunkMode: 'mod' // 分片加载
    }]
};

4.2 服务端缓存策略

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        return new CaffeineCacheManager("metrics") {
            @Override
            protected Cache<Object, Object> createNativeCache(String name) {
                return Caffeine.newBuilder()
                    .expireAfterWrite(3, TimeUnit.SECONDS)
                    .maximumSize(1000)
                    .build();
            }
        };
    }
}

5. 完整示例:电商大屏实战

5.1 后端控制器

@RestController
@RequestMapping("/api/dashboard")
public class DashboardController {

    @Autowired
    private MetricService metricService;

    @GetMapping("/realtime")
    @Cacheable(value = "metrics", key = "'realtime'")
    public ResponseEntity<List<Metric>> getRealtimeMetrics() {
        return ResponseEntity.ok(metricService.getLatestMetrics());
    }
}

5.2 前端大屏实现

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>电商实时大屏</title>
    <script src="/js/echarts.min.js"></script>
    <style>
        .dashboard-container {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 15px;
            padding: 20px;
            background: #0f1c3c;
        }
        .chart-panel {
            height: 400px;
            background: #1a2b50;
            border-radius: 5px;
        }
    </style>
</head>
<body>
    <div class="dashboard-container">
        <div id="sales-chart" class="chart-panel"></div>
        <div id="user-chart" class="chart-panel"></div>
        <div id="traffic-chart" class="chart-panel"></div>
    </div>

    <script>
        // 初始化所有图表
        function initCharts() {
            const charts = [
                { id: 'sales-chart', url: '/api/dashboard/realtime', type: 'line' },
                { id: 'user-chart', url: '/api/json/user_metrics', type: 'pie' },
                { id: 'traffic-chart', url: '/api/simulate/traffic', type: 'bar' }
            ];

            charts.forEach(config => {
                const chart = echarts.init(document.getElementById(config.id));
                chart.setOption(getBaseOption(config.type));
                chartInstances.push({
                    instance: chart,
                    dataUrl: config.url
                });
            });

            startPolling();
        }

        // 页面加载完成后初始化
        window.onload = initCharts;
    </script>
</body>
</html>

6. 异常处理与监控

6.1 服务端全局异常处理

@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(DataAccessException.class)
    public ResponseEntity<Map<String, Object>> handleDBError() {
        return ResponseEntity.status(503)
            .body(Map.of(
                "timestamp", Instant.now(),
                "status", 503,
                "error", "Service Unavailable",
                "message", "数据库服务不可用"
            ));
    }
}

6.2 前端错误重试机制

function fetchWithRetry(url, retries = 3) {
    return fetch(url)
        .catch(err => {
            return retries > 0 
                ? fetchWithRetry(url, retries - 1)
                : Promise.reject(err);
        });
}

通过这套方案,我们实现了:

  • 单页面支持混合数据源展示
  • 5秒级数据刷新无感知切换
  • 百万级数据流畅渲染
  • 完整的错误恢复机制
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐