SpringBoot 自研运行时 SQL 调用树,3 分钟定位慢 SQL!

在复杂的业务系统中,一个接口往往会执行多条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:前端交互逻辑

项目目录结构

bash 复制代码
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执行的无感知拦截:

scss 复制代码
@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
    })
})
public class SqlInterceptor implements Interceptor {
    
    @Autowired
    private SqlCallTreeContext sqlCallTreeContext;
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 检查是否启用追踪
        if (!sqlCallTreeContext.isTraceEnabled()) {
            return invocation.proceed();
        }
        
        long startTime = System.currentTimeMillis();
        Object[] args = invocation.getArgs();
        MappedStatement mappedStatement = (MappedStatement) args[0];
        Object parameter = args[1];
        
        // 获取SQL信息
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        String sql = boundSql.getSql();
        String sqlType = mappedStatement.getSqlCommandType().name();
        
        // 获取调用栈信息
        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        String serviceName = extractServiceName(stackTrace);
        String methodName = extractMethodName(stackTrace);
        
        // 创建SQL节点
        SqlNode sqlNode = 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
            Object result = invocation.proceed();
            
            // 记录执行结果
            long executionTime = System.currentTimeMillis() - startTime;
            int affectedRows = 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) {
            String className = 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";
    }
    
    private int calculateAffectedRows(Object result, String sqlType) {
        if ("SELECT".equals(sqlType) && result instanceof List) {
            return ((List<?>) result).size();
        } else if (result instanceof Integer) {
            return (Integer) result;
        }
        return 0;
    }
}

关键特性

  • 🎯 精准拦截:同时拦截查询和更新操作
  • 性能优化:可动态开关,避免生产环境性能影响
  • 🔒 异常安全:确保业务逻辑不受监控影响
  • 📊 丰富信息:自动提取Service调用信息和执行统计

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

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

scss 复制代码
@Component
public class SqlCallTreeContext {
    
    // 线程本地存储
    private final ThreadLocal<Stack<SqlNode>> callStack = new ThreadLocal<Stack<SqlNode>>() {
        @Override
        protected Stack<SqlNode> initialValue() {
            return new Stack<>();
        }
    };
    
    private final ThreadLocal<List<SqlNode>> rootNodes = new ThreadLocal<List<SqlNode>>() {
        @Override
        protected List<SqlNode> initialValue() {
            return new ArrayList<>();
        }
    };
    
    // 全局会话存储
    private final Map<String, List<SqlNode>> globalSessions = new ConcurrentHashMap<>();
    
    // 统计信息
    private final AtomicLong totalSqlCount = new AtomicLong(0);
    private final AtomicLong slowSqlCount = new AtomicLong(0);
    private final AtomicLong errorSqlCount = new AtomicLong(0);
    private final AtomicLong totalExecutionTime = new AtomicLong(0);
    
    // 配置参数
    private volatile long slowSqlThreshold = 1000; // 慢SQL阈值(毫秒)
    private volatile boolean traceEnabled = true; // 追踪开关
    
    /**
     * 进入SQL调用
     */
    public SqlNode enter(SqlNode sqlNode) {
        if (!traceEnabled) {
            return sqlNode;
        }
        
        Stack<SqlNode> stack = callStack.get();
        
        // 设置深度
        sqlNode.setDepth(stack.size() + 1);
        
        // 建立父子关系
        if (!stack.isEmpty()) {
            SqlNode parent = stack.peek();
            parent.addChild(sqlNode);
            sqlNode.setParentId(parent.getNodeId());
        } else {
            // 根节点
            rootNodes.get().add(sqlNode);
        }
        
        // 压入栈
        stack.push(sqlNode);
        
        return sqlNode;
    }
    
    /**
     * 退出SQL调用
     */
    public void exit(SqlNode sqlNode, int affectedRows, String errorMessage) {
        if (!traceEnabled) {
            return;
        }
        
        // 设置结束时间和结果
        sqlNode.setEndTime(LocalDateTime.now());
        sqlNode.setAffectedRows(affectedRows);
        sqlNode.setErrorMessage(errorMessage);
        
        // 计算执行时间
        long executionTime = 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()) {
                String sessionKey = generateSessionKey();
                globalSessions.put(sessionKey, new ArrayList<>(rootNodes.get()));
                rootNodes.get().clear();
            }
        }
    }
    
    /**
     * 获取当前调用深度
     */
    public int getCurrentDepth() {
        return callStack.get().size();
    }
    
    /**
     * 获取当前线程的根节点
     */
    public List<SqlNode> getRootNodes() {
        return new ArrayList<>(rootNodes.get());
    }
    
    /**
     * 获取所有会话
     */
    public Map<String, List<SqlNode>> getAllSessions() {
        return new HashMap<>(globalSessions);
    }
    
    /**
     * 清理会话数据
     */
    public void clearSessions() {
        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方法
    public boolean isTraceEnabled() {
        return traceEnabled;
    }
    
    public void setTraceEnabled(boolean traceEnabled) {
        this.traceEnabled = traceEnabled;
    }
    
    public long getSlowSqlThreshold() {
        return slowSqlThreshold;
    }
    
    public void setSlowSqlThreshold(long slowSqlThreshold) {
        this.slowSqlThreshold = slowSqlThreshold;
    }
}

设计亮点

  • 🧵 线程安全:使用ThreadLocal确保多线程环境下的数据隔离
  • 🌳 智能建树:自动识别父子关系,构建完整调用树
  • 📊 实时统计:同步更新性能统计信息

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

arduino 复制代码
@Data
public class SqlNode {
    private String nodeId;              // 节点唯一标识
    private String sql;                 // SQL语句
    private String formattedSql;        // 格式化后的SQL
    private String sqlType;             // SQL类型
    private int depth;                  // 调用深度
    private String threadName;          // 线程名称
    private String serviceName;         // Service类名
    private String methodName;          // Service方法名
    private LocalDateTime startTime;    // 开始时间
    private LocalDateTime endTime;      // 结束时间
    private long executionTime;         // 执行耗时
    private boolean slowSql;            // 是否为慢SQL
    private int affectedRows;           // 影响行数
    private String errorMessage;        // 错误信息
    private List<Object> parameters;    // SQL参数
    private List<SqlNode> children;     // 子节点
    
    // 智能分析方法
    public boolean isSlowSql(long threshold) {
        return executionTime > threshold;
    }
    
    public int getTotalNodeCount() {
        return 1 + children.stream().mapToInt(SqlNode::getTotalNodeCount).sum();
    }
    
    public int getMaxDepth() {
        return children.isEmpty() ? depth : 
               children.stream().mapToInt(SqlNode::getMaxDepth).max().orElse(depth);
    }
}

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

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

swift 复制代码
@RestController
@RequestMapping("/api/sql-tree")
public class SqlTreeController {
    
    @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 = new HashMap<>();
        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() {
        SqlStatistics stats = sqlCallTreeContext.getStatistics();
        Map<String, Object> response = new HashMap<>();
        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 = new HashMap<>();
        
        if (config.containsKey("slowSqlThreshold")) {
            long threshold = ((Number) config.get("slowSqlThreshold")).longValue();
            sqlCallTreeContext.setSlowSqlThreshold(threshold);
            response.put("slowSqlThreshold", threshold);
        }
        
        if (config.containsKey("traceEnabled")) {
            boolean enabled = (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 = new ArrayList<>();
        
        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 = new HashMap<>();
        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 = new HashMap<>();
        health.put("status", "UP");
        health.put("traceEnabled", sqlCallTreeContext.isTraceEnabled());
        health.put("slowSqlThreshold", sqlCallTreeContext.getSlowSqlThreshold());
        health.put("timestamp", LocalDateTime.now());
        
        return ResponseEntity.ok(health);
    }
    
    /**
     * 递归收集慢SQL节点
     */
    private void collectSlowSqlNodes(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调用树可视化,主要包含以下功能:

kotlin 复制代码
// sql-tree.js - 主要的可视化逻辑
class SqlTreeVisualizer {
    constructor() {
        this.width = 1200;
        this.height = 800;
        this.margin = { top: 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('*').remove();
        
        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+)

访问系统

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

    • JDBC URL: jdbc:h2:mem:testdb
    • 用户名: sa
    • 密码: (空)

项目配置

核心依赖(pom.xml)

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)

yaml 复制代码
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调用链:

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:

json 复制代码
{
  "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"
}

数据监控

统计信息示例

yaml 复制代码
{
  "totalSqlCount": 1247,
  "slowSqlCount": 23,
  "errorSqlCount": 5,
  "averageExecutionTime": 35.6,
  "slowSqlThreshold": 1000,
  "traceEnabled": true
}

慢SQL分析报告

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

css 复制代码
[  {    "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  }]

技术特点

零侵入设计

  • 基于MyBatis拦截器实现,无需修改现有业务代码
  • 通过注解和配置即可启用SQL监控功能
  • 支持动态开启/关闭追踪功能

线程安全

  • 使用ThreadLocal确保多线程环境下的数据隔离
  • ConcurrentHashMap保证全局会话存储的线程安全
  • 无锁设计,避免性能瓶颈

内存友好

  • 会话级别的数据存储,避免全局数据累积
  • 支持手动清理和自动过期机制
  • 轻量级数据结构,内存占用小

总结

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

github.com/yuboon/java...

相关推荐
间彧几秒前
Redis缓存穿透、缓存雪崩、缓存击穿详解与代码实现
后端
摸鱼的春哥4 分钟前
【编程】是什么编程思想,让老板对小伙怒飙英文?Are you OK?
前端·javascript·后端
计算机毕业设计小帅37 分钟前
【2026计算机毕业设计】基于Springboot的校园失物招领小程序
spring boot·小程序·课程设计
Max81244 分钟前
Agno Agent 服务端文件上传处理机制
后端
调试人生的显微镜1 小时前
苹果 App 怎么上架?从开发到发布的完整流程与使用 开心上架 跨平台上传
后端
顾漂亮1 小时前
Spring AOP 实战案例+避坑指南
java·后端·spring
间彧1 小时前
Redis Stream相比阻塞列表和发布订阅有哪些优势?适合什么场景?
后端
间彧1 小时前
Redis阻塞弹出和发布订阅模式有什么区别?各自适合什么场景?
后端
苏三说技术1 小时前
统计接口耗时的6种常见方法
后端
SimonKing2 小时前
Mybatis-Plus的竞争对手来了,试试 MyBatis-Flex
java·后端·程序员