⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 风象南 「风象南」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

在复杂的业务系统中,一个接口往往会执行多条SQL,如何直观地看到这些SQL的调用关系和执行情况?

本文将使用SpringBoot + MyBatis拦截器构建一个SQL调用树可视化系统。

项目背景

在日常开发中,我们经常遇到这样的场景:

  • 复杂查询链路: 一个用户详情接口可能涉及用户基本信息、订单列表、订单详情等多个查询
  • 性能问题排查: 系统响应慢,需要快速定位是哪个SQL影响了性能
  • 开发调试需求: 希望能直观地看到SQL的执行顺序和层次关系

基于这些需求,实现了一个基于SpringBoot + MyBatis的SQL调用树可视化系统。

系统功能特性

该系统具有以下核心功能:

核心功能

MyBatis拦截器:通过拦截器机制捕获SQL执行过程,无需修改业务代码

  • 调用树构建: 自动构建SQL调用的层次关系
  • 可视化展示: 使用D3.js实现树形结构的可视化展示
  • 性能监控: 记录SQL执行时间,自动标识慢SQL
  • 统计分析: 提供SQL执行统计信息和性能分析
  • 数据管理: 支持数据的查询、清理和导出

技术实现

  • 后端技术: Spring Boot 3.4.5 + MyBatis 3.0.3 + H2数据库
  • 前端技术: HTML5 + Tailwind CSS + D3.js v7
  • 配置管理: 支持动态配置慢SQL阈值等参数

项目结构

技术栈

后端技术栈

  • Spring Boot 3.4.5: 应用框架
  • MyBatis 3.0.3: 数据访问层和拦截器
  • H2 Database: 内存数据库(演示用)
  • Lombok: 简化代码编写
  • Jackson: JSON序列化

前端技术栈

  • HTML5 + Tailwind CSS: 页面结构和样式
  • D3.js v7: 数据可视化
  • Font Awesome: 图标库
  • 原生JavaScript: 前端交互逻辑

项目目录结构

springboot-sql-tree/
├── src/main/java/com/example/sqltree/
│ ├── SqlTreeApplication.java # 启动类
│ ├── SqlInterceptor.java # MyBatis拦截器
│ ├── SqlCallTreeContext.java # 调用树上下文管理
│ ├── SqlNode.java # SQL节点数据模型
│ ├── SqlTreeController.java # REST API控制器
│ ├── DemoController.java # 演示API
│ ├── UserService.java # 用户服务(演示用)
│ ├── UserMapper.java # 用户数据访问
│ └── OrderMapper.java # 订单数据访问
├── src/main/resources/
│ ├── application.yml # 应用配置
│ ├── schema.sql # 数据库表结构
│ ├── data.sql # 示例数据
│ └── static/
│ ├── index.html # 前端页面
│ └── sql-tree.js # 前端JavaScript
└── pom.xml # Maven配置

核心实现详解

1. MyBatis拦截器:零侵入的核心

这是整个系统的核心组件,通过MyBatis的插件机制实现SQL执行的无感知拦截:

@Component
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {
MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class
}),
@Signature(type = Executor.class, method = "update", args = {
MappedStatement.class, Object.class
})
})
publicclassSqlInterceptorimplementsInterceptor {

@Autowired
private SqlCallTreeContext sqlCallTreeContext;

@Override
public Object intercept(Invocation invocation)throws Throwable {
// 检查是否启用追踪
if (!sqlCallTreeContext.isTraceEnabled()) {
return invocation.proceed();
}

longstartTime= System.currentTimeMillis();
Object[] args = invocation.getArgs();
MappedStatementmappedStatement= (MappedStatement) args[0];
Objectparameter= args[1];

// 获取SQL信息
BoundSqlboundSql= mappedStatement.getBoundSql(parameter);
Stringsql= boundSql.getSql();
StringsqlType= mappedStatement.getSqlCommandType().name();

// 获取调用栈信息
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
StringserviceName= extractServiceName(stackTrace);
StringmethodName= extractMethodName(stackTrace);

// 创建SQL节点
SqlNodesqlNode= SqlNode.builder()
.nodeId(UUID.randomUUID().toString())
.sql(formatSql(sql))
.sqlType(sqlType)
.threadName(Thread.currentThread().getName())
.serviceName(serviceName)
.methodName(methodName)
.startTime(LocalDateTime.now())
.parameters(extractParameters(boundSql, parameter))
.depth(sqlCallTreeContext.getCurrentDepth() + 1)
.build();

// 进入SQL调用
sqlCallTreeContext.enter(sqlNode);

try {
// 执行SQL
Objectresult= invocation.proceed();

// 记录执行结果
longexecutionTime= System.currentTimeMillis() - startTime;
intaffectedRows= calculateAffectedRows(result, sqlType);

sqlCallTreeContext.exit(sqlNode, affectedRows, null);

return result;

} catch (Exception e) {
// 记录异常信息
sqlCallTreeContext.exit(sqlNode, 0, e.getMessage());
throw e;
}
}

private String extractServiceName(StackTraceElement[] stackTrace) {
for (StackTraceElement element : stackTrace) {
StringclassName= element.getClassName();
if (className.contains("Service") && !className.contains("$")) {
return className.substring(className.lastIndexOf('.') + 1);
}
}
return"Unknown";
}

private String extractMethodName(StackTraceElement[] stackTrace) {
for (StackTraceElement element : stackTrace) {
if (element.getClassName().contains("Service")) {
return element.getMethodName();
}
}
return"unknown";
}

privateintcalculateAffectedRows(Object result, String sqlType) {
if ("SELECT".equals(sqlType) && result instanceof List) {
return ((List<?>) result).size();
} elseif (result instanceof Integer) {
return (Integer) result;
}
return0;
}
}

关键特性:

  • 精准拦截: 同时拦截查询和更新操作
  • 异常安全: 确保业务逻辑不受监控影响
  • 丰富信息: 自动提取Service调用信息和执行统计

2. 调用树上下文管理器:线程安全的数据管理

SqlCallTreeContext负责管理SQL调用树的构建和存储,采用线程安全的设计:

@Component
publicclassSqlCallTreeContext {

// 线程本地存储
privatefinal ThreadLocal<Stack<SqlNode>> callStack = newThreadLocal<Stack<SqlNode>>() {
@Override
protected Stack<SqlNode> initialValue() {
returnnewStack<>();
}
};

privatefinal ThreadLocal<List<SqlNode>> rootNodes = newThreadLocal<List<SqlNode>>() {
@Override
protected List<SqlNode> initialValue() {
returnnewArrayList<>();
}
};

// 全局会话存储
privatefinal Map<String, List<SqlNode>> globalSessions = newConcurrentHashMap<>();

// 统计信息
privatefinalAtomicLongtotalSqlCount=newAtomicLong(0);
privatefinalAtomicLongslowSqlCount=newAtomicLong(0);
privatefinalAtomicLongerrorSqlCount=newAtomicLong(0);
privatefinalAtomicLongtotalExecutionTime=newAtomicLong(0);

// 配置参数
privatevolatilelongslowSqlThreshold=1000; // 慢SQL阈值(毫秒)
privatevolatilebooleantraceEnabled=true; // 追踪开关

/**
* 进入SQL调用
*/
public SqlNode enter(SqlNode sqlNode) {
if (!traceEnabled) {
return sqlNode;
}

Stack<SqlNode> stack = callStack.get();

// 设置深度
sqlNode.setDepth(stack.size() + 1);

// 建立父子关系
if (!stack.isEmpty()) {
SqlNodeparent= stack.peek();
parent.addChild(sqlNode);
sqlNode.setParentId(parent.getNodeId());
} else {
// 根节点
rootNodes.get().add(sqlNode);
}

// 压入栈
stack.push(sqlNode);

return sqlNode;
}

/**
* 退出SQL调用
*/
publicvoidexit(SqlNode sqlNode, int affectedRows, String errorMessage) {
if (!traceEnabled) {
return;
}

// 设置结束时间和结果
sqlNode.setEndTime(LocalDateTime.now());
sqlNode.setAffectedRows(affectedRows);
sqlNode.setErrorMessage(errorMessage);

// 计算执行时间
longexecutionTime= Duration.between(sqlNode.getStartTime(), sqlNode.getEndTime()).toMillis();
sqlNode.setExecutionTime(executionTime);

// 标记慢SQL
if (executionTime > slowSqlThreshold) {
sqlNode.setSlowSql(true);
slowSqlCount.incrementAndGet();
}

// 标记错误SQL
if (errorMessage != null) {
errorSqlCount.incrementAndGet();
}

// 更新统计
totalSqlCount.incrementAndGet();
totalExecutionTime.addAndGet(executionTime);

// 弹出栈
Stack<SqlNode> stack = callStack.get();
if (!stack.isEmpty()) {
stack.pop();

// 如果栈为空,说明调用树完成,保存到全局会话
if (stack.isEmpty()) {
StringsessionKey= generateSessionKey();
globalSessions.put(sessionKey, newArrayList<>(rootNodes.get()));
rootNodes.get().clear();
}
}
}

/**
* 获取当前调用深度
*/
publicintgetCurrentDepth() {
return callStack.get().size();
}

/**
* 获取当前线程的根节点
*/
public List<SqlNode> getRootNodes() {
returnnewArrayList<>(rootNodes.get());
}

/**
* 获取所有会话
*/
public Map<String, List<SqlNode>> getAllSessions() {
returnnewHashMap<>(globalSessions);
}

/**
* 清理会话数据
*/
publicvoidclearSessions() {
globalSessions.clear();
rootNodes.get().clear();
callStack.get().clear();
}

/**
* 生成会话键
*/
private String generateSessionKey() {
return Thread.currentThread().getName() + "_" + System.currentTimeMillis();
}

/**
* 获取统计信息
*/
public SqlStatistics getStatistics() {
return SqlStatistics.builder()
.totalSqlCount(totalSqlCount.get())
.slowSqlCount(slowSqlCount.get())
.errorSqlCount(errorSqlCount.get())
.averageExecutionTime(totalSqlCount.get() > 0 ?
totalExecutionTime.get() / totalSqlCount.get() : 0)
.build();
}

// Getter和Setter方法
publicbooleanisTraceEnabled() {
return traceEnabled;
}

publicvoidsetTraceEnabled(boolean traceEnabled) {
this.traceEnabled = traceEnabled;
}

publiclonggetSlowSqlThreshold() {
return slowSqlThreshold;
}

publicvoidsetSlowSqlThreshold(long slowSqlThreshold) {
this.slowSqlThreshold = slowSqlThreshold;
}
}
  • 线程安全: 使用ThreadLocal确保多线程环境下的数据隔离
  • 智能建树: 自动识别父子关系,构建完整调用树
  • 实时统计: 同步更新性能统计信息

3. 数据模型:完整的SQL节点信息

@Data
publicclassSqlNode {
private String nodeId; // 节点唯一标识
private String sql; // SQL语句
private String formattedSql; // 格式化后的SQL
private String sqlType; // SQL类型
privateint depth; // 调用深度
private String threadName; // 线程名称
private String serviceName; // Service类名
private String methodName; // Service方法名
private LocalDateTime startTime; // 开始时间
private LocalDateTime endTime; // 结束时间
privatelong executionTime; // 执行耗时
privateboolean slowSql; // 是否为慢SQL
privateint affectedRows; // 影响行数
private String errorMessage; // 错误信息
private List<Object> parameters; // SQL参数
private List<SqlNode> children; // 子节点

// 智能分析方法
publicbooleanisSlowSql(long threshold) {
return executionTime > threshold;
}

publicintgetTotalNodeCount() {
return1 + children.stream().mapToInt(SqlNode::getTotalNodeCount).sum();
}

publicintgetMaxDepth() {
return children.isEmpty() ? depth :
children.stream().mapToInt(SqlNode::getMaxDepth).max().orElse(depth);
}
}

4. RESTful API:完整的数据接口

SqlTreeController提供完整的REST API接口,支持数据查询、配置管理和系统监控:

@RestController
@RequestMapping("/api/sql-tree")
publicclassSqlTreeController {

@Autowired
private SqlCallTreeContext sqlCallTreeContext;

/**
* 获取当前线程的SQL调用树
*/
@GetMapping("/current")
public ResponseEntity<List<SqlNode>> getCurrentTree() {
List<SqlNode> rootNodes = sqlCallTreeContext.getRootNodes();
return ResponseEntity.ok(rootNodes);
}

/**
* 获取所有会话的SQL调用树
*/
@GetMapping("/sessions")
public ResponseEntity<Map<String, List<SqlNode>>> getAllSessions() {
Map<String, List<SqlNode>> sessions = sqlCallTreeContext.getAllSessions();
return ResponseEntity.ok(sessions);
}

/**
* 获取指定会话的SQL调用树
*/
@GetMapping("/session/{sessionKey}")
public ResponseEntity<List<SqlNode>> getSessionTree(@PathVariable String sessionKey) {
Map<String, List<SqlNode>> sessions = sqlCallTreeContext.getAllSessions();
List<SqlNode> sessionTree = sessions.get(sessionKey);
if (sessionTree != null) {
return ResponseEntity.ok(sessionTree);
} else {
return ResponseEntity.notFound().build();
}
}

/**
* 清理所有调用树数据
*/
@DeleteMapping("/clear")
public ResponseEntity<Map<String, Object>> clearAllTrees() {
sqlCallTreeContext.clearSessions();
Map<String, Object> response = newHashMap<>();
response.put("success", true);
response.put("message", "All SQL trees cleared successfully");
response.put("timestamp", LocalDateTime.now());
return ResponseEntity.ok(response);
}

/**
* 获取统计信息
*/
@GetMapping("/statistics")
public ResponseEntity<Map<String, Object>> getStatistics() {
SqlStatisticsstats= sqlCallTreeContext.getStatistics();
Map<String, Object> response = newHashMap<>();
response.put("totalSqlCount", stats.getTotalSqlCount());
response.put("slowSqlCount", stats.getSlowSqlCount());
response.put("errorSqlCount", stats.getErrorSqlCount());
response.put("averageExecutionTime", stats.getAverageExecutionTime());
response.put("slowSqlThreshold", sqlCallTreeContext.getSlowSqlThreshold());
response.put("traceEnabled", sqlCallTreeContext.isTraceEnabled());
return ResponseEntity.ok(response);
}

/**
* 配置追踪参数
*/
@PostMapping("/config")
public ResponseEntity<Map<String, Object>> updateConfig(@RequestBody Map<String, Object> config) {
Map<String, Object> response = newHashMap<>();

if (config.containsKey("slowSqlThreshold")) {
longthreshold= ((Number) config.get("slowSqlThreshold")).longValue();
sqlCallTreeContext.setSlowSqlThreshold(threshold);
response.put("slowSqlThreshold", threshold);
}

if (config.containsKey("traceEnabled")) {
booleanenabled= (Boolean) config.get("traceEnabled");
sqlCallTreeContext.setTraceEnabled(enabled);
response.put("traceEnabled", enabled);
}

response.put("success", true);
response.put("message", "Configuration updated successfully");
return ResponseEntity.ok(response);
}

/**
* 分析慢SQL
*/
@GetMapping("/analysis/slow-sql")
public ResponseEntity<List<SqlNode>> getSlowSqlAnalysis() {
Map<String, List<SqlNode>> sessions = sqlCallTreeContext.getAllSessions();
List<SqlNode> slowSqlNodes = newArrayList<>();

for (List<SqlNode> sessionNodes : sessions.values()) {
collectSlowSqlNodes(sessionNodes, slowSqlNodes);
}

// 按执行时间降序排序
slowSqlNodes.sort((a, b) -> Long.compare(b.getExecutionTime(), a.getExecutionTime()));

return ResponseEntity.ok(slowSqlNodes);
}

/**
* 导出数据
*/
@GetMapping("/export")
public ResponseEntity<Map<String, Object>> exportData() {
Map<String, Object> exportData = newHashMap<>();
exportData.put("sessions", sqlCallTreeContext.getAllSessions());
exportData.put("statistics", sqlCallTreeContext.getStatistics());
exportData.put("exportTime", LocalDateTime.now());
exportData.put("version", "1.0");

return ResponseEntity.ok(exportData);
}

/**
* 系统状态检查
*/
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> healthCheck() {
Map<String, Object> health = newHashMap<>();
health.put("status", "UP");
health.put("traceEnabled", sqlCallTreeContext.isTraceEnabled());
health.put("slowSqlThreshold", sqlCallTreeContext.getSlowSqlThreshold());
health.put("timestamp", LocalDateTime.now());

return ResponseEntity.ok(health);
}

/**
* 递归收集慢SQL节点
*/
privatevoidcollectSlowSqlNodes(List<SqlNode> nodes, List<SqlNode> slowSqlNodes) {
for (SqlNode node : nodes) {
if (node.isSlowSql()) {
slowSqlNodes.add(node);
}
if (node.getChildren() != null && !node.getChildren().isEmpty()) {
collectSlowSqlNodes(node.getChildren(), slowSqlNodes);
}
}
}
}

5. 前端可视化实现

前端使用D3.js实现交互式的SQL调用树可视化,主要包含以下功能:

// sql-tree.js - 主要的可视化逻辑
classSqlTreeVisualizer {
constructor() {
this.width = 1200;
this.height = 800;
this.margin = { transform: translateY( 50, right: 150, bottom: 50, left: 150 };

// 初始化SVG容器
this.svg = d3.select('#tree-container')
.append('svg')
.attr('width', this.width)
.attr('height', this.height);

this.g = this.svg.append('g')
.attr('transform', `translate(${this.margin.left},${this.margin.top})`);

// 配置树布局
this.tree = d3.tree()
.size([this.height - this.margin.top - this.margin.bottom,
this.width - this.margin.left - this.margin.right]);

// 初始化工具提示
this.tooltip = d3.select('body').append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
}

/**
* 渲染SQL调用树
*/
render(sessions) {
this.g.selectAll('*').rem)ove();

if (!sessions || Object.keys(sessions).length === 0) {
this.showEmptyState();
return;
}

// 选择第一个会话进行展示
const sessionKey = Object.keys(sessions)[0];
const rootNodes = sessions[sessionKey];

if (rootNodes && rootNodes.length > 0) {
this.renderTree(rootNodes[0]);
}
}

/**
* 渲染单个调用树
*/
renderTree(rootNode) {
// 构建D3层次结构
const root = d3.hierarchy(rootNode, d => d.children);

// 计算节点位置
this.tree(root);

// 绘制连接线
const links = this.g.selectAll('.link')
.data(root.links())
.enter().append('path')
.attr('class', 'link')
.attr('d', d3.linkHorizontal()
.x(d => d.y)
.y(d => d.x))
.style('fill', 'none')
.style('stroke', '#94a3b8')
.style('stroke-width', '2px')
.style('stroke-opacity', 0.6);

// 绘制节点组
const nodes = this.g.selectAll('.node')
.data(root.descendants())
.enter().append('g')
.attr('class', 'node')
.attr('transform', d =>`translate(${d.y},${d.x})`);

// 绘制节点圆圈
nodes.append('circle')
.attr('r', 10)
.style('fill', d =>this.getNodeColor(d.data))
.style('stroke', '#1e293b')
.style('stroke-width', '2px')
.style('cursor', 'pointer');

// 添加节点文本
nodes.append('text')
.attr('dy', '.35em')
.attr('x', d => d.children ? -15 : 15)
.style('text-anchor', d => d.children ? 'end' : 'start')
.style('font-size', '12px')
.style('font-weight', '500')
.style('fill', '#1e293b')
.text(d =>this.getNodeLabel(d.data));

// 添加交互事件
nodes
.on('mouseover', (event, d) =>this.showTooltip(event, d.data))
.on('mouseout', () =>this.hideTooltip())
.on('click', (event, d) =>this.showNodeDetails(d.data));
}

/**
* 获取节点颜色
*/
getNodeColor(data) {
if (data.errorMessage) {
return'#ef4444'; // 错误:红色
}
if (data.slowSql) {
return'#f59e0b'; // 慢SQL:橙色
}
switch (data.sqlType) {
case'SELECT':
return'#10b981'; // 查询:绿色
case'INSERT':
return'#3b82f6'; // 插入:蓝色
case'UPDATE':
return'#8b5cf6'; // 更新:紫色
case'DELETE':
return'#ef4444'; // 删除:红色
default:
return'#6b7280'; // 默认:灰色
}
}

/**
* 获取节点标签
*/
getNodeLabel(data) {
const time = data.executionTime || 0;
return`${data.sqlType} (${time}ms)`;
}

/**
* 显示工具提示
*/
showTooltip(event, data) {
const tooltipContent = `
<div class="font-semibold text-gray-900">${data.sqlType} 操作</div>
<div class="text-sm text-gray-600 mt-1">
<div>执行时间: ${data.executionTime || 0}ms</div>
<div>影响行数: ${data.affectedRows || 0}</div>
<div>服务: ${data.serviceName || 'Unknown'}</div>
<div>方法: ${data.methodName || 'unknown'}</div>
${data.errorMessage ? `<div class="text-red-600">错误: ${data.errorMessage}</div>` : ''}
</div>
`;

this.tooltip.transition()
.duration(200)
.style('opacity', .9);

this.tooltip.html(tooltipContent)
.style('left', (event.pageX + 10) + 'px')
.style('top', (event.pageY - 28) + 'px');
}

/**
* 隐藏工具提示
*/
hideTooltip() {
this.tooltip.transition()
.duration(500)
.style('opacity', 0);
}

/**
* 显示空状态
*/
showEmptyState() {
this.g.append('text')
.attr('x', (this.width - this.margin.left - this.margin.right) / 2)
.attr('y', (this.height - this.margin.top - this.margin.bottom) / 2)
.attr('text-anchor', 'middle')
.style('font-size', '18px')
.style('fill', '#6b7280')
.text('暂无SQL调用数据');
}

/**
* 显示节点详情
*/
showNodeDetails(data) {
// 在侧边栏显示详细信息
const detailsPanel = document.getElementById('node-details');
if (detailsPanel) {
detailsPanel.innerHTML = `
<h3 class="text-lg font-semibold mb-4">SQL详情</h3>
<div class="space-y-2">
<div><span class="font-medium">类型:</span> ${data.sqlType}</div>
<div><span class="font-medium">执行时间:</span> ${data.executionTime || 0}ms</div>
<div><span class="font-medium">影响行数:</span> ${data.affectedRows || 0}</div>
<div><span class="font-medium">服务:</span> ${data.serviceName || 'Unknown'}</div>
<div><span class="font-medium">方法:</span> ${data.methodName || 'unknown'}</div>
<div><span class="font-medium">线程:</span> ${data.threadName || 'unknown'}</div>
${data.sql ? `<div><span class="font-medium">SQL:</span><pre class="mt-1 p-2 bg-gray-100 rounded text-sm">${data.sql}</pre></div>` : ''}
${data.parameters ? `<div><span class="font-medium">参数:</span><pre class="mt-1 p-2 bg-gray-100 rounded text-sm">${data.parameters}</pre></div>` : ''}
${data.errorMessage ? `<div><span class="font-medium text-red-600">错误:</span><div class="mt-1 p-2 bg-red-50 rounded text-sm text-red-700">${data.errorMessage}</div></div>` : ''}
</div>
`;
}
}
}

核心特性:

  • 树形布局: 清晰展示SQL调用层次关系
  • 颜色编码: 绿色(正常)、红色(慢SQL)
  • 交互操作: 点击节点查看详情,悬停显示提示
  • 智能筛选: 支持按执行时间、SQL类型等条件筛选
  • 实时刷新: 支持自动/手动刷新数据

快速开始

环境要求

  • Java 21+
  • Maven 3.6+
  • 现代浏览器(支持ES6+)

访问系统

启动成功后,可以通过以下地址访问:

  • 可视化界面: http://localhost:8080/index.html
  • H2数据库控制台: http://localhost:8080/h2-console
JDBC URL: `jdbc:h2:mem:testdb`
用户名: `sa`
密码: (空)

项目配置

核心依赖(pom.xml)

<dependencies>
<!-- Spring Boot 3.4.5 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.4.5</version>
</dependency>

<!-- MyBatis 3.0.3 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>

<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>

<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

应用配置(application.yml)

server:
port:8080

spring:
application:
name:springboot-sql-tree
datasource:
url:jdbc:h2:mem:testdb
driver-class-name:org.h2.Driver
username:sa
password:
schema:classpath:schema.sql
data:classpath:data.sql
h2:
console:
enabled:true
path:/h2-console

mybatis:
mapper-locations:classpath:mapper/*.xml
type-aliases-package:com.example.sqltree.entity
configuration:
map-underscore-to-camel-case:true
lazy-loading-enabled:true
cache-enabled:true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

实际应用场景

开发调试场景

场景1:复杂查询性能分析

当调用/api/demo/user/1/detail 接口时,系统会自动捕获以下SQL调用链:

UserService.getUserDetailWithOrders()
├── SELECT * FROM users WHERE id = ? (2ms)
└── SELECT * FROM orders WHERE user_id = ? (15ms)
└── SELECT * FROM order_items WHERE order_id IN (...) (45ms)

通过可视化界面可以清晰看到:

  • 总执行时间:62ms
  • SQL调用深度:2层
  • 性能瓶颈:order_items查询耗时最长

场景2:慢SQL识别

系统自动标识执行时间超过阈值(默认1000ms)的SQL:

{
"nodeId":"uuid-123",
"sql":"SELECT * FROM orders o JOIN users u ON o.user_id = u.id WHERE o.status = ?",
"executionTime":1250,
"slowSql":true,
"serviceName":"OrderService",
"methodName":"getOrdersWithUserInfo"
}

数据监控

统计信息示例

{
"totalSqlCount":1247,
"slowSqlCount":23,
"errorSqlCount":5,
"averageExecutionTime":35.6,
"slowSqlThreshold":1000,
"traceEnabled":true
}

慢SQL分析报告

系统提供按执行时间排序的慢SQL列表:

[
{
"sql":"SELECT COUNT(*) FROM orders WHERE created_at BETWEEN ? AND ?",
"executionTime":2150,
"serviceName":"ReportService",
"methodName":"generateDailyReport",
"affectedRows":1
},
{
"sql":"UPDATE users SET last_login = ? WHERE id IN (...)",
"executionTime":1890,
"serviceName":"UserService",
"methodName":"batchUpdateLastLogin",
"affectedRows":156
}
]

总结

这个项目展示了如何结合Spring Boot生态和前端技术,构建一个实用的SQL监控工具,为日常开发和性能优化提供有力支持。

https://github.com/yuboon/java-examples/tree/master/springboot-sql-tree

文章目录
  1. 1. 项目背景
  2. 2. 系统功能特性
    1. 2.0.1. 核心功能
    2. 2.0.2. 技术实现
  • 3. 项目结构
    1. 3.0.1. 技术栈
      1. 3.0.1.1. 后端技术栈
      2. 3.0.1.2. 前端技术栈
    2. 3.0.2. 项目目录结构
  • 4. 核心实现详解
    1. 4.0.1. 1. MyBatis拦截器:零侵入的核心
    2. 4.0.2. 2. 调用树上下文管理器:线程安全的数据管理
    3. 4.0.3. 3. 数据模型:完整的SQL节点信息
    4. 4.0.4. 4. RESTful API:完整的数据接口
    5. 4.0.5. 5. 前端可视化实现
      1. 4.0.5.1. 核心特性:
  • 5. 快速开始
    1. 5.0.1. 环境要求
    2. 5.0.2. 访问系统
    3. 5.0.3. 项目配置
  • 6. 实际应用场景
    1. 6.0.1. 开发调试场景
      1. 6.0.1.1. 场景1:复杂查询性能分析
      2. 6.0.1.2. 场景2:慢SQL识别
    2. 6.0.2. 数据监控
      1. 6.0.2.1. 统计信息示例
      2. 6.0.2.2. 慢SQL分析报告
  • 7. 总结