跳到主要内容Spring Boot 接入 ECharts 的数据可视化实践 | 极客日志Javajava
Spring Boot 接入 ECharts 的数据可视化实践
在 Spring Boot 项目中集成 ECharts 时,可以用 Thymeleaf 将后端产品数据直接注入页面,先把列表和饼图跑通。示例中通过 Web、Thymeleaf 依赖搭建页面,用内存 List 模拟 CRUD,再在模板里生成图表数据。这个方案适合后台管理、销售报表、用户分析等场景,优点是接入快、排障直观,但要注意图表类型和数据粒度要匹配,别为了图表而图表。
Spring Boot 接入 ECharts 的数据可视化实践

在 Java 项目里,数据通常先落在表格里,看起来很完整,真要判断趋势却不太顺手。把后端数据交给图表展示,阅读成本会低很多,尤其是销量、订单量、地区分布这类本来就适合'看形状'的数据。这里我用 Spring Boot + Thymeleaf + ECharts 走一遍最直接的集成方式,目标不是堆架构,而是把页面先跑起来。
为什么我会选 ECharts
做 Web 可视化时,可选方案不少。Highcharts 上手快,但授权和使用边界要先看清;D3.js 更灵活,代价是代码量和学习成本都高。ECharts 的好处很现实:文档够全,图表类型多,前端接入也不重。对大多数 Spring Boot 后台页面来说,它是一个省事且不容易踩坑的选择。
搭一个能跑的 Spring Boot 页面
先建一个标准的 Spring Boot Web 项目,pom.xml 里引入 Web 和 Thymeleaf 依赖。这里没有上前后端分离,原因很简单:这个场景只是想快速把服务端数据塞进页面里,Thymeleaf 够用。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot
spring-boot-starter-test
test
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
</groupId>
<artifactId>
</artifactId>
<scope>
</scope>
</dependency>
</dependencies>
application.properties 里把模板缓存关掉,调试时省很多刷新成本:
server.port=8080
spring.thymeleaf.cache=false
spring.thymeleaf.mode=HTML
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.suffix=.html
spring.thymeleaf.prefix=classpath:/templates/
先把产品数据模型立住
为了演示,直接做一个产品实体。字段不复杂,id、productId、productName、price、sales 足够支撑表格和图表。这里的重点不是建模优雅不优雅,而是后面页面渲染时字段名别乱。
public class Product {
private Long id;
private String productId;
private String productName;
private double price;
private int sales;
public Product() {}
public Product(Long id, String productId, String productName, double price, int sales) {
this.id = id;
this.productId = productId;
this.productName = productName;
this.price = price;
this.sales = sales;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
public String getProductName() { return productName; }
public void setProductName(String productName) { this.productName = productName; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public int getSales() { return sales; }
public void setSales(int sales) { this.sales = sales; }
@Override
public String toString() {
return "Product{" +
"id=" + id +
", productId='" + productId + '\'' +
", productName='" + productName + '\'' +
", price=" + price +
", sales=" + sales +
'}';
}
}
用内存仓库先把流程跑通
这里没有接数据库,直接用一个内存 List 模拟 CRUD。这个做法不花哨,但对验证图表联动很实用:不用先处理 SQL、Mapper、事务这些外部变量,先把页面和数据流走通。
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Repository
public class ProductRepository {
private List<Product> products = new ArrayList<>();
public ProductRepository() {
products.add(new Product(1L, "P001", "手机", 1000.0, 100));
products.add(new Product(2L, "P002", "电脑", 5000.0, 50));
products.add(new Product(3L, "P003", "电视", 3000.0, 80));
products.add(new Product(4L, "P004", "手表", 500.0, 200));
products.add(new Product(5L, "P005", "耳机", 300.0, 150));
}
public List<Product> getAllProducts() {
return products;
}
public Product getProductById(Long id) {
return products.stream().filter(p -> p.getId().equals(id)).findFirst().orElse(null);
}
public void addProduct(Product product) {
product.setId((long) (products.size() + 1));
products.add(product);
}
public void updateProduct(Product product) {
Product existing = getProductById(product.getId());
if (existing != null) {
existing.setProductId(product.getProductId());
existing.setProductName(product.getProductName());
existing.setPrice(product.getPrice());
existing.setSales(product.getSales());
}
}
public void deleteProduct(Long id) {
products.removeIf(p -> p.getId().equals(id));
}
}
Service 层还是简单透传,Controller 负责把数据塞进模型,页面再拿去渲染:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Controller
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/")
public String getAllProducts(Model model) {
List<Product> products = productService.getAllProducts();
model.addAttribute("products", products);
return "product-list";
}
@GetMapping("/{id}")
public String getProductById(@PathVariable Long id, Model model) {
Product product = productService.getProductById(id);
model.addAttribute("product", product);
return "product-detail";
}
@GetMapping("/add")
public String addProductForm(Model model) {
model.addAttribute("product", new Product());
return "product-form";
}
@PostMapping("/add")
public String addProduct(@ModelAttribute Product product) {
productService.addProduct(product);
return "redirect:/api/products/";
}
@GetMapping("/edit/{id}")
public String editProductForm(@PathVariable Long id, Model model) {
Product product = productService.getProductById(id);
model.addAttribute("product", product);
return "product-form";
}
@PostMapping("/edit/{id}")
public String editProduct(@PathVariable Long id, @ModelAttribute Product product) {
product.setId(id);
productService.updateProduct(product);
return "redirect:/api/products/";
}
@GetMapping("/delete/{id}")
public String deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return "redirect:/api/products/";
}
}
把 Thymeleaf 和 ECharts 接起来
真正麻烦的地方在这里。图表本身不难,难的是后端数据怎么稳妥地进到 JavaScript 里。这个示例直接用 Thymeleaf 在模板中拼出数组,简单、直接,但也意味着你要注意输出内容的格式,别把引号和逗号弄乱。
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>产品列表</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
</head>
<body>
<h1>产品列表</h1>
<a href="/api/products/add">添加产品</a>
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>产品 ID</th>
<th>产品名称</th>
<th>价格</th>
<th>销量</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="product : ${products}">
<td th:text="${product.id}"></td>
<td th:text="${product.productId}"></td>
<td th:text="${product.productName}"></td>
<td th:text="${product.price}"></td>
<td th:text="${product.sales}"></td>
<td>
<a th:href="@{/api/products/edit/{id}(id=${product.id})}">编辑</a>
<a th:href="@{/api/products/delete/{id}(id=${product.id})}">删除</a>
</td>
</tr>
</tbody>
</table>
<h2>产品销量图表</h2>
<div id="salesChart" style="width: 800px;height: 400px;"></div>
<script>
var chartDom = document.getElementById('salesChart');
var myChart = echarts.init(chartDom);
var option;
var productNames = [];
var productSales = [];
<th:block th:each="product : ${products}">
productNames.push('<span th:text="${product.productName}"></span>');
productSales.push(<span th:text="${product.sales}"></span>);
</th:block>
option = {
title: {
text: '产品销量图表',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center'
},
series: [{
name: '销量',
type: 'pie',
radius: [, ],
: ,
: {
: ,
: ,
:
},
: {
: ,
:
},
: {
: {
: ,
: ,
:
}
},
: {
:
},
: [
]
}]
};
option && myChart.(option);
</script>
</body>
</html>
页面访问 http://localhost:8080/api/products/ 后,就能同时看到表格和饼图。这个例子比较朴素,但路径是通的:后端准备数据,Thymeleaf 输出到页面,ECharts 接手渲染。只要数据结构没变,后面替换成真实数据库也不需要改太多。
这种做法适合什么场景
这套方式比较适合管理后台、运营看板、报表页这类页面。比如用户分布、订单趋势、销售占比,都能直接套进去。它不追求前后端解耦到极致,但胜在开发快,页面和数据在同一个项目里时,排障也更直接。
真正需要注意的是图表类型和数据粒度要匹配。饼图适合占比,折线图适合趋势,柱状图适合对比。如果只是为了'看起来高级'硬上图表,最后往往比表格更难读。
'40%'
'70%'
avoidLabelOverlap
false
itemStyle
borderRadius
10
borderColor
'#fff'
borderWidth
2
label
show
false
position
'center'
emphasis
label
show
true
fontSize
20
fontWeight
'bold'
labelLine
show
false
data
<th:block th:each="product : ${products}">
{value: <span th:text="${product.sales}"></span>, name: '<span th:text="${product.productName}"></span>'},
</th:block>
setOption