SQLInsight:从JDBC底层到API调用的零侵入SQL监控方案

项目地址gitee.com/sh_wangwanb...
上一篇文章SQLInsight:一行依赖,自动追踪API背后的每一条SQL

开篇

上周写了一篇文章,分享了SQLInsight的设计思路和技术方案,没想到收到了不少反馈。有人问"啥时候能用",有人问"技术细节是怎么实现的"。

花了小一周时间,终于把它实现出来了。今天周末,终于可以还债了。

这篇文章,我们来聊聊SQLInsight的技术实现细节:动态代理是怎么做的?StackTrace是怎么解析的?为什么说零侵入?性能到底如何?

代码已经开源,你可以直接用,也可以学习实现思路。


一、技术亮点速览

在开始之前,先说几个核心亮点,让你知道这个项目有什么不一样:

1.1 真正的零侵入

不需要在代码里加注解,不需要修改配置文件,甚至不需要加启动参数。加个依赖,启动应用,就能看到所有SQL执行情况。这是怎么做到的?答案是:在最底层做手脚

1.2 完整的调用链自动追踪

你在Controller写了个接口,调用Service,Service调用Mapper,最后执行了SQL。这整个链路,SQLInsight能自动给你串起来:

sql 复制代码
GET /api/users/123 
  → UserController.getUser()
    → UserService.findById()
      → SELECT * FROM users WHERE id = 123 (15ms)

这不是靠AOP,不是靠埋点,而是靠读取线程栈

1.3 兼容性好到让人惊讶

不管你用的是MyBatis、JPA、Hibernate还是JdbcTemplate,不管你的连接池是Druid、HikariCP还是C3P0,SQLInsight都能工作。为什么?因为它代理的是JDBC标准接口,不依赖具体实现。

1.4 性能影响微乎其微

动态代理?StackTrace扫描?这不会很慢吗?实测下来,单次SQL执行的额外开销在1微秒以内 。怎么做到的?答案是:缓存 + 异步 + 限制扫描深度


二、设计思想的来源

做技术不是闭门造车,SQLInsight的核心设计思想来自两个成熟的开源组件。

2.1 动态代理:学习Seata

Seata是阿里开源的分布式事务框架。它有个很巧妙的设计:通过动态代理DataSource,在JDBC层拦截所有SQL执行,实现分布式事务的透明化。

SQLInsight借鉴了这个思路,但做得更彻底:

graph LR A[应用程序] -->|调用| B[DataSourceProxy] B -->|代理| C[ConnectionProxy] C -->|代理| D[PreparedStatementProxy] D -->|执行| E[真实的JDBC驱动] style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#ffe1f5 style D fill:#e1ffe1 style E fill:#f5e1ff

为什么这个思路好?

  1. 最底层拦截:不管你上层用什么框架,最终都要通过JDBC执行SQL
  2. 标准接口:JDBC是Java标准,不会随便变
  3. 透明性好:对上层业务代码完全透明

Seata代理DataSource是为了加事务控制,SQLInsight代理DataSource是为了监控。本质上是同一个思路的不同应用。

2.2 StackTrace分析:学习MyBatis

MyBatis有个很强大的功能:能把SQL执行日志和Mapper方法对应起来。它怎么做的?答案是:解析StackTrace

当SQL执行时,MyBatis会读取当前线程的调用栈,找到是哪个Mapper接口的哪个方法触发的。

SQLInsight把这个思路往上延伸了一层:

graph LR A[SQL执行] -->|触发| B[获取StackTrace] B --> C{扫描调用栈} C -->|找到| D[RestController注解] D --> E[解析API路径] E --> F[解析方法名] F --> G[构建完整调用链] style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#ffe1f5 style D fill:#e1ffe1 style E fill:#f5e1ff style F fill:#e1f5ff style G fill:#fff4e1

为什么这个思路可行?

Java的线程栈里包含了完整的方法调用链。通过扫描栈帧,可以找到:

  • 哪个Controller被调用了(有@RestController注解)
  • 哪个Service方法被调用了
  • 哪个Mapper方法被调用了

这样就能把API调用和SQL执行串起来,形成完整的链路。


三、核心技术实现

3.1 动态代理的三层结构

JDBC的核心接口有三个:DataSource、Connection、Statement/PreparedStatement。SQLInsight为每一层都创建了代理。

3.1.1 第一层:代理DataSource

这一层是入口。Spring Boot启动时,会创建DataSource Bean。我们通过BeanPostProcessor拦截这个过程:

java 复制代码
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
    if (bean instanceof DataSource) {
        // 把原始DataSource包装成代理
        return DataSourceProxy.create((DataSource) bean, collector, handler);
    }
    return bean;
}

关键点BeanPostProcessor是Spring提供的扩展点,能在Bean创建后、初始化后做一些处理。这是Spring生态常用的插件化机制。

3.1.2 第二层:代理Connection

当业务代码调用dataSource.getConnection()时,我们返回一个代理的Connection:

java 复制代码
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if ("getConnection".equals(method.getName())) {
        // 调用原始DataSource获取真实Connection
        Connection realConnection = (Connection) method.invoke(target, args);
        // 返回代理Connection
        return ConnectionProxy.createProxy(realConnection, collector, handler);
    }
    return method.invoke(target, args);
}

这里用到了Java动态代理的核心机制InvocationHandler。每次调用代理对象的方法时,都会进入invoke方法,我们可以在这里做拦截。

3.1.3 第三层:代理PreparedStatement

这一层是真正执行SQL的地方。当业务代码调用connection.prepareStatement(sql)时:

java 复制代码
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if ("prepareStatement".equals(method.getName())) {
        String sql = (String) args[0];
        PreparedStatement realStmt = (PreparedStatement) method.invoke(target, args);
        // 返回代理PreparedStatement,并把SQL传进去
        return PreparedStatementProxy.createProxy(realStmt, sql, collector, handler);
    }
    return method.invoke(target, args);
}

这里的细节:我们把SQL语句传给了PreparedStatement代理。这样在执行SQL时,代理就知道要执行的是什么SQL了。

3.2 SQL执行的拦截

PreparedStatement代理的核心逻辑在这里:

java 复制代码
private final Map<Integer, Object> parameters = new ConcurrentHashMap<>();

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 1. 拦截参数设置方法(setString、setInt等)
    if (method.getName().startsWith("set") && args.length >= 2) {
        int index = (int) args[0];
        Object value = args[1];
        parameters.put(index, value);  // 记录参数值
    }
    
    // 2. 拦截执行方法(execute、executeQuery等)
    if (method.getName().startsWith("execute")) {
        long start = System.currentTimeMillis();
        Object result = method.invoke(target, args);  // 执行真实SQL
        long duration = System.currentTimeMillis() - start;
        
        // 3. 收集SQL执行信息
        collectSqlExecution(sql, parameters, duration);
        return result;
    }
    
    return method.invoke(target, args);
}

这里有两个关键点

  1. 参数拦截 :PreparedStatement的参数是通过setXxx(index, value)方法设置的。我们拦截这些方法,把参数记下来。
  2. 执行拦截 :当调用execute()方法时,我们记录开始时间,执行SQL,然后记录结束时间,算出执行耗时。

3.3 StackTrace解析:找到API调用者

SQL执行时,我们需要知道是哪个API触发的。这里用到了StackTrace分析:

java 复制代码
public static ApiInfo extractApiInfo() {
    StackTraceElement[] stack = Thread.currentThread().getStackTrace();
    
    // 限制扫描深度,避免性能问题
    int maxDepth = Math.min(stack.length, 50);
    
    for (int i = 0; i < maxDepth; i++) {
        String className = stack[i].getClassName();
        
        // 先查缓存
        if (controllerCache.containsKey(className)) {
            return controllerCache.get(className);
        }
        
        // 加载类,检查是否有@RestController或@Controller注解
        Class<?> clazz = Class.forName(className);
        if (clazz.isAnnotationPresent(RestController.class) || 
            clazz.isAnnotationPresent(Controller.class)) {
            ApiInfo apiInfo = parseApiInfo(clazz, stack[i].getMethodName());
            controllerCache.put(className, apiInfo);  // 缓存起来
            return apiInfo;
        }
    }
    
    return ApiInfo.unknown();
}

性能优化点

  1. 限制扫描深度:只扫描前50层调用栈,避免深度递归
  2. 缓存Controller信息:同一个Controller类只解析一次,后续直接从缓存取
graph LR A[SQL执行] --> B{读取线程栈} B --> C[第1层: SQLInsight代理] C --> D[第2层: MyBatis] D --> E[第3层: Service] E --> F[第4层: Controller] F --> G{找到RestController?} G -->|是| H[解析API信息] G -->|否| I[继续向上扫描] H --> J[返回API路径和方法] style A fill:#e1f5ff style F fill:#e1ffe1 style H fill:#fff4e1 style J fill:#ffe1f5

3.4 API信息解析

找到Controller后,怎么解析出API路径呢?

java 复制代码
private static ApiInfo parseApiInfo(Class<?> controllerClass, String methodName) {
    // 1. 获取类级别的@RequestMapping
    RequestMapping classMapping = controllerClass.getAnnotation(RequestMapping.class);
    String basePath = classMapping != null ? classMapping.value()[0] : "";
    
    // 2. 找到对应的方法
    for (Method method : controllerClass.getDeclaredMethods()) {
        if (method.getName().equals(methodName)) {
            // 3. 获取方法级别的@GetMapping/@PostMapping等
            GetMapping getMapping = method.getAnnotation(GetMapping.class);
            if (getMapping != null) {
                String path = basePath + getMapping.value()[0];
                return new ApiInfo(path, "GET", methodName);
            }
            // ... 处理PostMapping、PutMapping等
        }
    }
    
    return ApiInfo.unknown();
}

这样就能从注解中提取出完整的API路径,比如/api/users/123


四、设计模式的应用

4.1 代理模式

这是SQLInsight的核心。Java的动态代理基于接口,这和JDBC的设计天然契合:

graph LR A[业务代码] -->|调用接口| B[代理对象] B -->|拦截处理| C[收集SQL信息] B -->|转发调用| D[真实对象] D -->|执行SQL| E[数据库] C -.->|异步| F[日志输出] style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#ffe1f5 style D fill:#e1ffe1 style E fill:#f5e1ff style F fill:#fff4e1

代理模式的好处

  • 对业务代码透明
  • 可以在不修改原有代码的情况下增加功能
  • 符合开闭原则(对扩展开放,对修改关闭)

4.2 观察者模式

SQL执行信息收集后,可能有多种处理方式:输出到控制台、写入日志文件、持久化到数据库、发送到监控系统等。

SQLInsight用观察者模式来处理这个问题:

java 复制代码
public interface SqlExecutionHandler {
    void handle(SqlExecution execution);
}

public class ConsoleSqlExecutionHandler implements SqlExecutionHandler {
    public void handle(SqlExecution execution) {
        System.out.println(formatSqlLog(execution));
    }
}

public class LoggingSqlExecutionHandler implements SqlExecutionHandler {
    public void handle(SqlExecution execution) {
        logger.info(formatSqlLog(execution));
    }
}

这样可以灵活组合不同的处理器,满足不同场景的需求。

4.3 工厂模式

DataSourceProxy的创建使用了工厂模式:

java 复制代码
public static DataSource create(DataSource target, 
                                 SqlExecutionCollector collector, 
                                 SqlExecutionHandler handler) {
    return (DataSource) Proxy.newProxyInstance(
        DataSource.class.getClassLoader(),
        new Class<?>[] { DataSource.class },
        new DataSourceProxy(target, collector, handler)
    );
}

为什么要用工厂模式?

动态代理的创建比较复杂,涉及类加载器、接口数组等参数。通过工厂方法封装这些细节,让使用者更方便。


五、兼容性设计

5.1 与Druid的兼容

Druid是阿里开源的数据库连接池,自带监控功能。SQLInsight和Druid可以无缝配合:

graph LR A[应用程序] --> B[SQLInsight代理] B --> C[Druid连接池] C --> D[MySQL驱动] B -.->|收集| E[SQL执行信息] C -.->|收集| F[连接池监控] style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#ffe1f5 style D fill:#e1ffe1 style E fill:#f5e1ff style F fill:#fff4e1

为什么能兼容?

SQLInsight代理的是标准JDBC接口,不关心底层实现。Druid连接池也实现了JDBC接口,所以可以直接代理。

两者的监控数据是互补的:

  • Druid监控:连接池状态、连接获取耗时、SQL执行统计
  • SQLInsight监控:SQL与API的关联、完整调用链、慢SQL检测

5.2 与Seata的兼容

Seata也会代理DataSource,这就涉及到代理链的顺序问题

理想的顺序是:应用程序 → Seata → SQLInsight → 连接池

为什么?因为Seata需要在最外层控制事务,SQLInsight只做监控,应该在内层。

如何保证顺序?通过调整BeanPostProcessor的优先级:

java 复制代码
@Bean
public SqlInsightDataSourceBeanPostProcessor processor() {
    SqlInsightDataSourceBeanPostProcessor processor = 
        new SqlInsightDataSourceBeanPostProcessor(collector, handler);
    processor.setOrder(Ordered.LOWEST_PRECEDENCE);  // 设置最低优先级
    return processor;
}

这样Seata会先执行,SQLInsight后执行,代理顺序就对了。


六、性能优化策略

6.1 缓存Controller信息

第一次解析Controller信息后,缓存起来:

java 复制代码
private static final Map<String, ApiInfo> controllerCache = new ConcurrentHashMap<>();

public static ApiInfo extractApiInfo() {
    // 先查缓存
    if (controllerCache.containsKey(className)) {
        return controllerCache.get(className);
    }
    // 解析并缓存
    ApiInfo apiInfo = parseApiInfo(clazz, methodName);
    controllerCache.put(className, apiInfo);
    return apiInfo;
}

效果:首次解析约50微秒,缓存命中后小于1微秒。

6.2 异步处理

SQL执行信息的处理(输出日志、持久化等)是异步的:

java 复制代码
public void handle(SqlExecution execution) {
    executor.submit(() -> {
        // 处理SQL执行信息
        persistence.save(execution);
        logger.info(formatSqlLog(execution));
    });
}

这样不会阻塞业务SQL的执行。

6.3 限制StackTrace扫描深度

只扫描前50层调用栈:

java 复制代码
int maxDepth = Math.min(stack.length, 50);
for (int i = 0; i < maxDepth; i++) {
    // 扫描栈帧
}

实际测试中,Controller一般在前10层就能找到,50层已经足够了。

graph LR A[SQL执行] --> B{读取栈深度} B -->|深度<=50| C[扫描栈帧] B -->|深度>50| D[只扫描前50层] C --> E[查找Controller] D --> E E --> F{找到了?} F -->|是| G[返回API信息] F -->|否| H[返回Unknown] style A fill:#e1f5ff style C fill:#e1ffe1 style E fill:#fff4e1 style G fill:#ffe1f5

七、核心代码剖析

7.1 PreparedStatementProxy的精妙之处

这是整个项目最核心的一段代码:

java 复制代码
public class PreparedStatementProxy implements InvocationHandler {
    private final PreparedStatement target;
    private final String sql;
    private final List<Object> parameters = new ArrayList<>();
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        
        // 拦截参数设置方法
        if (methodName.startsWith("set") && args.length >= 2) {
            int index = (int) args[0];
            Object value = args[1];
            
            // 扩展参数列表
            while (parameters.size() < index) {
                parameters.add(null);
            }
            parameters.set(index - 1, value);  // JDBC参数索引从1开始
            
            return method.invoke(target, args);
        }
        
        // 拦截执行方法
        if (methodName.startsWith("execute")) {
            long start = System.currentTimeMillis();
            Object result = method.invoke(target, args);
            long duration = System.currentTimeMillis() - start;
            
            // 收集SQL执行信息
            collectSqlExecution(sql, parameters, duration);
            
            return result;
        }
        
        // 其他方法直接转发
        return method.invoke(target, args);
    }
}

这段代码的精妙之处

  1. 拦截参数设置 :PreparedStatement的参数是通过setXxx(index, value)方法设置的,而且索引从1开始(JDBC规范)。代码中用parameters.set(index - 1, value)处理了这个细节。

  2. 动态扩展参数列表 :参数可能不是按顺序设置的,比如先设置第3个参数,再设置第1个参数。代码用while循环动态扩展列表,保证不会越界。

  3. 精确测量执行时间 :在invoke方法里包裹真实的SQL执行,前后记录时间戳,得到精确的执行耗时。

  4. 保持原有行为 :所有拦截后,都要调用method.invoke(target, args),确保原有功能不受影响。

7.2 StackTrace分析的优雅实现

java 复制代码
public static ApiInfo extractApiInfo() {
    StackTraceElement[] stack = Thread.currentThread().getStackTrace();
    int maxDepth = Math.min(stack.length, 50);
    
    for (int i = 0; i < maxDepth; i++) {
        try {
            String className = stack[i].getClassName();
            
            // 缓存命中
            if (controllerCache.containsKey(className)) {
                return controllerCache.get(className);
            }
            
            // 加载类并检查注解
            Class<?> clazz = Class.forName(className);
            if (isController(clazz)) {
                ApiInfo apiInfo = parseApiInfo(clazz, stack[i].getMethodName());
                controllerCache.put(className, apiInfo);
                return apiInfo;
            }
        } catch (ClassNotFoundException e) {
            // 忽略,继续扫描下一层
        }
    }
    
    return ApiInfo.unknown();
}

private static boolean isController(Class<?> clazz) {
    return clazz.isAnnotationPresent(RestController.class) || 
           clazz.isAnnotationPresent(Controller.class);
}

这段代码的优雅之处

  1. 异常处理得当Class.forName()可能抛出ClassNotFoundException,但这不是错误,只是说明这个类不在当前ClassLoader里。代码用try-catch优雅地处理了这个问题。

  2. 提前返回:一旦找到Controller,立即返回,不再继续扫描。这是性能优化的关键。

  3. 缓存策略:用类名作为key缓存,而不是用完整的ApiInfo。这样即使方法不同,类名相同也能命中缓存。


八、设计思想的升华

做完这个项目后,我有一些更深层次的思考。

8.1 最少知识原则

SQLInsight遵循了"最少知识原则"(Law of Demeter)。它只和JDBC接口打交道,不关心:

  • 你用的是什么ORM框架
  • 你的连接池是什么
  • 你的数据库是MySQL还是PostgreSQL

这个原则的好处

  • 降低耦合
  • 提高适应性
  • 减少维护成本

在设计系统时,我们应该尽量依赖抽象(接口),而不是具体实现。JDBC是一个很好的抽象层。

8.2 分层架构的威力

JDBC的分层设计(DataSource → Connection → Statement)为代理提供了天然的切入点。

graph LR A[业务层
MyBatis/JPA] --> B[JDBC抽象层
标准接口] B --> C[驱动实现层
MySQL/Oracle驱动] D[SQLInsight] -.->|代理| B style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#ffe1f5 style D fill:#e1ffe1

分层架构的好处

  • 每一层都有明确的职责
  • 层与层之间通过接口通信
  • 可以在任何一层插入新功能(如监控、事务控制)

这启发我们:在设计系统时,合理的分层能带来极大的扩展性。

8.3 组合优于继承

SQLInsight没有通过继承来扩展功能,而是通过组合:

java 复制代码
public class DataSourceProxy implements InvocationHandler {
    private final DataSource target;  // 组合原始DataSource
    private final SqlExecutionCollector collector;  // 组合收集器
    private final SqlExecutionHandler handler;  // 组合处理器
}

为什么组合优于继承?

  1. 灵活性:可以动态替换组件(比如换一个不同的handler)
  2. 低耦合:各个组件可以独立演化
  3. 可测试:可以单独测试每个组件

这是设计模式中的经典原则,值得反复体会。

8.4 开闭原则的实践

SQLInsight对扩展开放,对修改关闭:

java 复制代码
public interface SqlExecutionHandler {
    void handle(SqlExecution execution);
}

// 新增一个handler,不需要修改核心代码
public class CustomHandler implements SqlExecutionHandler {
    public void handle(SqlExecution execution) {
        // 自定义处理逻辑
    }
}

想增加新功能?实现一个新的Handler就行了,不需要改动核心代码。

这给我们的启示

  • 设计接口时要想清楚扩展点在哪里
  • 核心逻辑要稳定,扩展逻辑要灵活
  • 通过接口和抽象隔离变化

8.5 性能与功能的平衡

SQLInsight面临一个矛盾:

  • 功能需求:需要扫描StackTrace,解析注解,收集信息
  • 性能需求:不能影响业务SQL的执行速度

解决方案是:

  1. 缓存:减少重复计算
  2. 异步:把耗时操作移到后台
  3. 限制:限制扫描深度,避免极端情况

这个平衡的启示

  • 性能优化不是无限制的,要找到合适的度
  • 用空间换时间(缓存)
  • 用异步换同步(后台处理)
  • 设置合理的边界(扫描深度限制)

九、使用示例

项目地址gitee.com/sh_wangwanb...

完整的示例代码在examples模块中,可以直接运行查看效果。

9.1 引入依赖

xml 复制代码
<dependency>
    <groupId>com.surfing</groupId>
    <artifactId>sqlinsight-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

9.2 配置(可选)

properties 复制代码
# 启用SQLInsight(默认true)
sqlinsight.enabled=true

# 慢SQL阈值(默认1000ms)
sqlinsight.slow-sql-threshold=50

# N+1查询检测阈值(默认10)
sqlinsight.n-plus-one-threshold=10

9.3 输出示例

sql 复制代码
[SQL-Insight] GET /api/users/123 → UserController.findById
   SQL: SELECT * FROM users WHERE id = 123 (15ms) [Type: SELECT]

[SQL-Insight] GET /api/users/orders → UserController.getUserOrders
   [SLOW] SQL: SELECT u.*, o.* FROM users u LEFT JOIN orders o ON u.id = o.user_id (56ms) [Type: SELECT]

十、总结与展望

10.1 技术总结

SQLInsight通过三个核心技术实现了零侵入的SQL监控:

  1. 动态代理:在JDBC层拦截SQL执行(学习自Seata)
  2. StackTrace分析:自动追踪API调用链(学习自MyBatis)
  3. BeanPostProcessor:自动代理DataSource(Spring的扩展机制)

这三个技术的组合,实现了:

  • 零配置集成
  • 透明监控
  • 完整的调用链追踪
  • 良好的兼容性

10.2 设计原则的践行

项目体现了多个经典设计原则:

  • 单一职责:每个类只做一件事
  • 开闭原则:对扩展开放,对修改关闭
  • 依赖倒置:依赖抽象(JDBC接口),不依赖具体实现
  • 最少知识:只和JDBC接口打交道
  • 组合优于继承:用组合构建功能

10.3 未来展望

SQLInsight还有很多可以优化的地方:

  1. N+1查询检测:自动识别循环查询问题
  2. Web控制台:提供可视化的监控界面
  3. 智能建议:根据SQL执行情况给出优化建议
  4. 分布式追踪:集成OpenTelemetry,支持分布式链路追踪
  5. SQL审计:记录所有SQL操作,用于安全审计

但核心思想不会变:在最底层做拦截,保持对业务代码的零侵入


十一、写在最后

做这个项目,最大的收获不是写了多少行代码,而是理解了设计思想的传承

Seata的动态代理、MyBatis的StackTrace分析,这些都是前人智慧的结晶。我们站在巨人的肩膀上,把这些思想组合起来,创造出新的价值。

技术的本质不是炫技,而是解决问题。SQLInsight解决的问题很简单:让开发者知道每个API执行了哪些SQL,用了多长时间。但实现这个简单的目标,需要深入理解JDBC、动态代理、反射、线程栈等多个技术点。

好的技术方案,应该是简单的。

对使用者来说,只需要加一个依赖。对设计者来说,需要考虑性能、兼容性、扩展性、可维护性等多个维度。

这就是技术的魅力:表面简单,内在复杂;对外易用,对内精巧。

希望这篇文档能帮助你理解SQLInsight的设计思路,也希望这些设计思想能对你未来的项目有所启发。


十二、开发记录

从想法到实现,花了小一周时间。

刚开始写代码的时候,遇到了不少坑:

  • PreparedStatement的参数索引从1开始,而不是0(JDBC规范)
  • StackTrace扫描时要注意ClassNotFoundException
  • 动态代理要完整实现接口的所有方法,不然会NPE
  • 缓存策略要做好,不然性能会受影响

调试的过程中,对JDBC标准、Java动态代理、反射机制有了更深的理解。看似简单的"拦截SQL执行",背后涉及的技术点比想象中要多。

今天周末,终于把坑都填完了,代码也开源了。算是还了上周文章的债。

如果你在使用过程中遇到问题,或者有什么想法建议,欢迎提Issue或PR。


项目地址gitee.com/sh_wangwanb...
上一篇文章SQLInsight:一行依赖,自动追踪API背后的每一条SQL
开源协议 :MIT License
欢迎反馈:提Issue、PR,或者留言讨论


写代码的过程,也是学习的过程。希望SQLInsight能帮到你,也欢迎一起完善它。

相关推荐
小宇的天下15 小时前
Calibre nmDRC Results(19-1)
服务器·数据库·oracle
程序员iteng16 小时前
AI一键图表生成、样式修改的绘图开源工具【easy-draw】
spring boot·开源·node.js
Yeats_Liao16 小时前
MindSpore开发之路(二十五):融入开源:如何为MindSpore社区贡献力量
人工智能·分布式·深度学习·机器学习·华为·开源
superman超哥16 小时前
Rust HashSet与BTreeSet的实现细节:集合类型的底层逻辑
开发语言·后端·rust·编程语言·rust hashset·rust btreeset·集合类型
Web项目开发16 小时前
Mysql创建索引的SQL脚本,复制粘贴即可使用
数据库·sql·mysql
万岳科技系统开发16 小时前
开源外卖系统源码的整体架构设计与核心功能实现
开源
晴天¥17 小时前
Oracle如何在DBeaver上进行登录
数据库·oracle
FIT2CLOUD飞致云17 小时前
操作教程|DataEase企业总-分公司数据填报场景搭建实践
数据分析·开源·数据可视化·dataease·bi
2301_8002561117 小时前
事务处理-同步与调度-两阶段锁-隔离级别
数据库·oracle