SQLInsight:一行依赖,自动追踪API背后的每一条SQL

起因

前几天看到一篇关于SQL监控的文章,功能挺实用:能追踪API到SQL的调用链,能看到完整的可执行SQL,还能检测N+1查询。我觉得不错,就点了个赞。

结果今天再去看,发现代码库没了。

这让我想起一个问题:开源到底意味着什么?

这些年学习和成长的过程中,我一直在学习各种开源项目的代码,从Spring Boot到MyBatis,从Dubbo到Seata。每次看到优秀的开源项目,都有一种"原来可以这样做"的豁然开朗。更重要的是,开源让我们能够和同行一起交流、一起进步。

所以我一直乐于分享自己的技术积累,最近的JobFlow任务调度项目也是这个想法的延续。帮助别人的过程中,也在充实自己。

既然别人的库没了,那我就自己实现一个,完全开源。

这就是SQLInsight项目的由来。


技术方案

在动手之前,我梳理了几种常见的实现思路。

为什么不用这些方案

方案一:注解 + AOP

这是最常见的想法:

java 复制代码
@SqlMonitor
public class OrderService {
    // ...
}

不采用的原因:

  • 需要在每个类/方法上加注解,侵入性强
  • 开发者容易忘记加
  • 维护成本高

方案二:JavaAgent字节码增强

这个方案技术上很强大,但现在的应用已经有太多Agent了:Jacoco做代码覆盖率、SkyWalking做链路追踪、Arthas做诊断。再加一个Agent,启动参数越来越长,维护越来越复杂。不想给大家增加负担。

方案三:IDEA插件

IDEA插件方案有几个问题:

  1. IDEA本来就慢,装的插件越多,越容易卡顿
  2. 现在AI插件已经很多了,IDE已经够拥挤
  3. 只能开发阶段用,生产环境出了问题无法排查

这个思路不适合生产环境的监控工具。

采用的方案

经过思考,我决定采用:动态代理 + StackTrace

  1. 动态代理:在最底层拦截

思路很简单:不管你用JPA、MyBatis、Hibernate还是JdbcTemplate,最终都要通过JDBC的Connection执行SQL。

所以我们直接在最底层做代理:

markdown 复制代码
应用启动
    ↓
自动拦截DataSource创建
    ↓  
动态代理:DataSource → Connection → PreparedStatement
    ↓
SQL执行时,自动记录

这个思路参考了Seata的实现。Seata也是通过动态代理DataSource来实现分布式事务的。

好处:

  • 一行依赖即可接入
  • 不需要改业务代码
  • 支持所有ORM框架
  • 支持所有DataSource(Druid/HikariCP/C3P0)
  1. StackTrace:自动追踪调用链

传统的API追踪需要在Controller层加AOP,但我们用StackTrace来实现:

java 复制代码
// SQL执行时
StackTraceElement[] stack = Thread.currentThread().getStackTrace();

// 向上查找,找到Controller
for (StackTraceElement element : stack) {
    Class<?> clazz = Class.forName(element.getClassName());
    if (clazz.isAnnotationPresent(RestController.class)) {
        // 找到了,解析API路径
        return parseApiInfo(clazz, element.getMethodName());
    }
}

这样就能自动知道:

scss 复制代码
GET /api/orders/123
    ↓
OrderController.getOrder()
    ↓
OrderService.getById()
    ↓
OrderMapper.selectById()
    ↓
SELECT * FROM orders WHERE id = 123 (15ms)

完整的调用链,自动构建,不需要写任何额外代码。

  1. SQL解析:JSQLParser

SQL解析用JSQLParser 4.9版本,这个库很成熟,PageHelper也在用它。可以用来:

  • 识别SQL类型(SELECT/INSERT/UPDATE/DELETE)
  • 提取表名,统计访问频率
  • SQL格式化
  1. 接入方式

动态代理 + StackTrace的组合,可以让接入变得非常简单。

开发者只需要:

java 复制代码
// 1. 添加依赖
<dependency>
    <groupId>com.github</groupId>
    <artifactId>sqlinsight-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

// 2. 启动类加个注解(如果需要的话)
@SpringBootApplication
@EnableSqlInsight  // 这一行
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

如果采用Spring Boot自动配置,甚至连注解都可以省略。


关于监控系统集成

很多人可能会问:为什么不直接集成Prometheus?

我的想法是:建议业务系统自己接入Prometheus,但SQLInsight作为一个工具,不应该渗透到业务系统的监控体系中。

原因:

  1. 每个公司的监控体系不一样,有的用Prometheus,有的用其他方案
  2. 作为一个通用工具,不应该强制用户使用某种监控方案
  3. 保持工具的纯粹性和简洁性

不过,SQLInsight会提供标准的输出接口:

  • 日志输出(可以对接ELK)
  • 数据库持久化(可以自己做可视化)
  • 事件回调(可以自己集成任何监控系统)

用户可以根据自己的需求,灵活接入自己的监控体系。


数据持久化

SQLInsight支持可选的持久化功能:

yaml 复制代码
sqlinsight:
  persistence:
    enabled: true
    type: mysql  # 或者 postgresql

持久化内容:

  • API路径和方法
  • SQL语句和参数
  • 执行时间
  • 调用链路
  • 时间戳

持久化的好处:

  • 可以做历史查询
  • 可以做趋势分析
  • 可以做慢SQL统计
  • 后续可以做Web页面查看

核心功能

1. API到SQL的完整追踪

sql 复制代码
输入:/api/orders/123
输出:
  └─ OrderController.getOrder()
      └─ OrderService.getById()
          └─ SQL: SELECT * FROM orders WHERE id = 123 (15ms)

2. SQL执行统计

每个API执行了多少条SQL:

sql 复制代码
GET /api/orders/123
├─ SELECT * FROM orders WHERE id = ? (15ms)
├─ SELECT * FROM order_items WHERE order_id = ? (8ms)  
└─ SELECT * FROM users WHERE id = ? (5ms)
总计: 3条SQL,28ms

3. SQL执行时间

sql 复制代码
慢SQL检测:
[警告] SELECT * FROM orders WHERE status = 'PAID' ORDER BY created_at DESC
        执行时间: 3500ms
        API: GET /api/orders/list
        建议: 添加索引 (status, created_at)

4. 完整的可执行SQL

不再是带问号的SQL,而是可以直接复制到数据库执行的完整SQL:

sql 复制代码
原始: SELECT * FROM orders WHERE id = ? AND user_id = ?
完整: SELECT * FROM orders WHERE id = 123 AND user_id = 456

这对于排查问题非常有用,可以直接拿到数据库执行,看看结果是否符合预期。

5. N+1查询检测

自动检测循环查询问题:

sql 复制代码
[警告] 检测到N+1查询
       API: GET /api/orders/list
       SQL数量: 23条SELECT
       建议: 使用JOIN或批量查询优化

开发计划

第一阶段:核心功能

  • 动态代理DataSource
  • 动态代理Connection
  • 动态代理PreparedStatement
  • StackTrace解析
  • API信息提取
  • SQL参数绑定
  • 控制台输出
  • 日志输出

第二阶段:增强功能

  • 数据库持久化
  • 慢SQL检测
  • N+1查询检测
  • 性能优化(缓存)
  • 配置项支持

性能考虑

很多人会担心:这样监控,会不会影响性能?

影响可以忽略不计。

性能优化策略:

  1. 缓存Controller信息
java 复制代码
// 解析一次后缓存
Map<String, ControllerInfo> cache = new ConcurrentHashMap<>();

首次解析:约50微秒 缓存命中:<1微秒

  1. 异步记录
java 复制代码
// SQL信息异步写入
executor.submit(() -> {
    persistence.save(sqlExecution);
});
  1. 限制栈深度
java 复制代码
// 只扫描前50个栈帧
for (int i = 0; i < Math.min(stack.length, 50); i++) {
    // ...
}

预期性能影响:微秒级,可忽略。


使用示例

控制台输出

bash 复制代码
2026-01-05 10:30:45.123 [SQL] GET /api/orders/123
  └─ OrderController.getOrder() 
      └─ SELECT * FROM orders WHERE id = 123 (15ms)
      
2026-01-05 10:30:45.145 [SQL] GET /api/orders/123
  └─ OrderController.getOrder()
      └─ SELECT * FROM order_items WHERE order_id = 123 (8ms)

2026-01-05 10:30:45.850 [SLOW-SQL] GET /api/reports/summary
  └─ ReportController.getSummary()
      └─ SELECT COUNT(*), SUM(amount) FROM orders 
         WHERE created_at > '2025-01-01' GROUP BY user_id (3500ms)

日志输出(JSON格式,ELK友好)

json 复制代码
{
  "timestamp": "2026-01-05T10:30:45.123Z",
  "api": {
    "path": "/api/orders/123",
    "method": "GET",
    "controller": "OrderController.getOrder()"
  },
  "sql": {
    "statement": "SELECT * FROM orders WHERE id = ?",
    "parameters": [123],
    "executableSql": "SELECT * FROM orders WHERE id = 123",
    "duration": 15,
    "type": "SELECT"
  },
  "callStack": "OrderController.getOrder() → OrderService.getById() → OrderMapper.selectById()"
}

Web页面查询(计划中)

vbnet 复制代码
┌─────────────────────────────────────────────────────┐
│ SQL监控查询                                           │
├─────────────────────────────────────────────────────┤
│ 查询条件:                                            │
│   API路径: /api/orders/*                             │
│   时间范围: 最近1小时                                 │
│   执行时间: > 1000ms                                 │
│                                    [查询]             │
├─────────────────────────────────────────────────────┤
│ 结果:                                                │
│                                                      │
│ GET /api/orders/list                     3500ms     │
│   SELECT * FROM orders WHERE status = 'PAID'...     │
│   执行时间: 3500ms                                   │
│   调用链: OrderController → OrderService             │
│                                                      │
│ GET /api/orders/export                   2800ms     │
│   SELECT * FROM orders WHERE...                     │
│   执行时间: 2800ms                                   │
│   调用链: OrderController → ExportService            │
└─────────────────────────────────────────────────────┘

写在最后

做SQLInsight这个项目,不是为了跟谁竞争,也不是为了证明什么。只是觉得技术应该开放共享,而不是闭门造车。

如果这个工具能帮助到一些同行,让大家少踩一些坑,少加一些班,那就值了。

项目采用MIT协议,可以自由使用、修改、商用和分发。

相关推荐
毕设源码-钟学长21 小时前
【开题答辩全过程】以 基于SpringBoot的智能书城推荐系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
beginner.zs1 天前
注意力革命:Transformer架构深度解析与全景应用
深度学习·架构·transformer
阿基米东1 天前
基于 C++ 的机器人软件框架(具身智能)开源通信库选型分析
c++·机器人·开源
青春男大1 天前
Redis和RedisTemplate快速上手
java·数据库·redis·后端·spring·缓存
大厂技术总监下海1 天前
从“使用AI服务”到“拥有AI助手”:Clawdbot,你的个人AI基础设施
人工智能·ai·开源
张张努力变强1 天前
C++ 类和对象(四):const成员函数、取地址运算符重载全精讲
开发语言·数据结构·c++·后端
舰长1151 天前
文件存储NAS使用架构
架构
不吃香菜学java1 天前
springboot左脚踩右脚螺旋升天系列-整合开发
java·spring boot·后端·spring·ssm
奋进的芋圆1 天前
Java 锁事详解
java·spring boot·后端