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...

相关推荐
二闹1 分钟前
后端的请求体你选对了吗?
后端
lichenyang45325 分钟前
Mongodb(文档数据库)的安装与使用(文档的增删改查)
后端
创码小奇客27 分钟前
架构师私藏:SpringBoot 集成 Hera,让日志查看从 “找罪证” 变 “查答案”
spring boot·spring cloud·trae
雨落倾城夏未凉27 分钟前
8.被free回收的内存是立即返还给操作系统吗?为什么?
c++·后端
数新网络27 分钟前
LevelDB 辅助工具类
后端
Code_Artist29 分钟前
[Go]结构体实现接口类型静态校验——引用类型和指针之间的关系
后端·面试·go
onejason30 分钟前
《利用 Python 爬虫获取 Amazon 商品详情实战指南》
前端·后端·python
雨落倾城夏未凉30 分钟前
6.new和malloc的区别
c++·后端
程序员清风1 小时前
跳表的原理和时间复杂度,为什么还需要字典结构配合?
java·后端·面试
用户298698530141 小时前
C#合并/拆分PDF文档的3种方法(Spire.PDF实战示例)
后端