# SqlInsight **Repository Path**: tangzhongli/sql-insight ## Basic Information - **Project Name**: SqlInsight - **Description**: 零侵入、高性能的 Java SQL 调用链追踪与 API 关联分析工具 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2026-04-24 - **Last Updated**: 2026-04-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SQLInsight:从JDBC底层到API调用的零侵入SQL监控方案 > **项目地址**:[https://gitee.com/sh_wangwanbao/sql-insight](https://gitee.com/sh_wangwanbao/sql-insight) > **上一篇文章**:[SQLInsight:一行依赖,自动追踪API背后的每一条SQL](https://juejin.cn/post/7591513171199295551) ## 开篇 上周写了一篇文章,分享了SQLInsight的设计思路和技术方案,没想到收到了不少反馈。有人问"啥时候能用",有人问"技术细节是怎么实现的"。 花了小一周时间,终于把它实现出来了。今天周末,终于可以还债了。 这篇文章,我们来聊聊SQLInsight的技术实现细节:动态代理是怎么做的?StackTrace是怎么解析的?为什么说零侵入?性能到底如何? 代码已经开源,你可以直接用,也可以学习实现思路。 --- ## 一、技术亮点速览 在开始之前,先说几个核心亮点,让你知道这个项目有什么不一样: ### 1.1 真正的零侵入 不需要在代码里加注解,不需要修改配置文件,甚至不需要加启动参数。加个依赖,启动应用,就能看到所有SQL执行情况。这是怎么做到的?答案是:**在最底层做手脚**。 ### 1.2 完整的调用链自动追踪 你在Controller写了个接口,调用Service,Service调用Mapper,最后执行了SQL。这整个链路,SQLInsight能自动给你串起来: ``` 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借鉴了这个思路,但做得更彻底: ```mermaid 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把这个思路往上延伸了一层: ```mermaid 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 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类只解析一次,后续直接从缓存取 ```mermaid 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的设计天然契合: ```mermaid 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可以无缝配合: ```mermaid 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 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层已经足够了。 ```mermaid 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 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)为代理提供了天然的切入点。 ```mermaid 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. **限制**:限制扫描深度,避免极端情况 **这个平衡的启示**: - 性能优化不是无限制的,要找到合适的度 - 用空间换时间(缓存) - 用异步换同步(后台处理) - 设置合理的边界(扫描深度限制) --- ## 九、使用示例 > **项目地址**:[https://gitee.com/sh_wangwanbao/sql-insight](https://gitee.com/sh_wangwanbao/sql-insight) > 完整的示例代码在`examples`模块中,可以直接运行查看效果。 ### 9.1 引入依赖 ```xml com.surfing sqlinsight-spring-boot-starter 1.0.0 ``` ### 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-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。 --- **项目地址**:[https://gitee.com/sh_wangwanbao/sql-insight](https://gitee.com/sh_wangwanbao/sql-insight) **上一篇文章**:[SQLInsight:一行依赖,自动追踪API背后的每一条SQL](https://juejin.cn/post/7591513171199295551) **开源协议**:MIT License **欢迎反馈**:提Issue、PR,或者留言讨论 --- *写代码的过程,也是学习的过程。希望SQLInsight能帮到你,也欢迎一起完善它。*