sql拦截器的应用
问题:在实际开发当中,日志log的打印无法直接对sql层出现的问题进行整而概之的显示,使得开发中对sql层的具体变动无法直观的查看。当出现sql层方法报错后,需要不断地打log逐条分析错误,再进入sql层分析问题。
解决方案:利于mybatis的sql拦截器工具进行对sql的拦截以及日志展示其具体变化。
首先,当没有sql拦截器时,我们的日志输出只有我们打了log的数据传输部分,对于sql的执行状况是没有办法进行直接查看的
引入mybatis的日志打印
当引入后我们也可以看到,sql的变化进行了多行的转换,同时sql语句也是直接利用的?,而填充数据则在换行展示,当sql过长时,这种方式十分的不便同时容易混淆。
这时,我们引入sql拦截器的工具类,为了方便理解与整体项目美观,我们将sql拦截器放在基础设施层infra层下,该层主要为与数据库交互的基础层。
源码如下:
MybatisConfiguration
这是一个Spring的配置类,用于定义和初始化Mybatis-Plus拦截器。
mybatisPlusInterceptor
方法:创建并配置MybatisPlusInterceptor
实例,添加自定义的MybatisPlusAllSqlLog
拦截器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | package com.jingdianjichi.subject.infra.config; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisConfiguration { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(){ MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); mybatisPlusInterceptor.addInnerInterceptor( new MybatisPlusAllSqlLog()); return mybatisPlusInterceptor; } } |
MybatisPlusAllSqlLog
这个类实现InnerInterceptor
接口,是Mybatis-Plus提供的拦截器接口,用于在查询和更新操作前进行额外的处理。它的主要功能是在SQL语句执行前打印详细的SQL语句和参数。
beforeQuery
和beforeUpdate
方法:在SQL查询或更新前被调用,通过BoundSql
和MappedStatement
获取SQL语句和参数,然后将其日志输出。logInfo
方法:用于格式化并打印SQL语句和参数信息。getSql
和showSql
方法:用于构建和展示完整的SQL语句,包括处理占位符?
的替换。getParameterValue
方法:用于获取并格式化参数值,以适配在SQL中的显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | package com.jingdianjichi.subject.infra.config; import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.type.TypeHandlerRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.CollectionUtils; import java.sql.SQLException; import java.text.DateFormat; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; public class MybatisPlusAllSqlLog implements InnerInterceptor { public static final Logger log = LoggerFactory.getLogger( "sys-sql" ); @Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { logInfo(boundSql, ms, parameter); } @Override public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); logInfo(boundSql, ms, parameter); } private static void logInfo(BoundSql boundSql, MappedStatement ms, Object parameter) { try { log.info( "parameter = " + parameter); // 获取到节点的id,即sql语句的id String sqlId = ms.getId(); log.info( "sqlId = " + sqlId); // 获取节点的配置 Configuration configuration = ms.getConfiguration(); // 获取到最终的sql语句 String sql = getSql(configuration, boundSql, sqlId); log.info( "完整的sql:{}" , sql); } catch (Exception e) { log.error( "异常:{}" , e.getLocalizedMessage(), e); } } // 封装了一下sql语句,使得结果返回完整xml路径下的sql语句节点id + sql语句 public static String getSql(Configuration configuration, BoundSql boundSql, String sqlId) { return sqlId + ":" + showSql(configuration, boundSql); } // 进行?的替换 public static String showSql(Configuration configuration, BoundSql boundSql) { // 获取参数 Object parameterObject = boundSql.getParameterObject(); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); // sql语句中多个空格都用一个空格代替 String sql = boundSql.getSql().replaceAll( "[\\s]+" , " " ); if (!CollectionUtils.isEmpty(parameterMappings) && parameterObject != null ) { // 获取类型处理器注册器,类型处理器的功能是进行java类型和数据库类型的转换 TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); // 如果根据parameterObject.getClass()可以找到对应的类型,则替换 if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { sql = sql.replaceFirst( "\\?" , Matcher.quoteReplacement(getParameterValue(parameterObject))); } else { // MetaObject主要是封装了originalObject对象,提供了get和set的方法用于获取和设置originalObject的属性值,主要支持对JavaBean、Collection、Map三种类型对象的操作 MetaObject metaObject = configuration.newMetaObject(parameterObject); for (ParameterMapping parameterMapping : parameterMappings) { String propertyName = parameterMapping.getProperty(); if (metaObject.hasGetter(propertyName)) { Object obj = metaObject.getValue(propertyName); sql = sql.replaceFirst( "\\?" , Matcher.quoteReplacement(getParameterValue(obj))); } else if (boundSql.hasAdditionalParameter(propertyName)) { // 该分支是动态sql Object obj = boundSql.getAdditionalParameter(propertyName); sql = sql.replaceFirst( "\\?" , Matcher.quoteReplacement(getParameterValue(obj))); } else { // 打印出缺失,提醒该参数缺失并防止错位 sql = sql.replaceFirst( "\\?" , "缺失" ); } } } } return sql; } // 如果参数是String,则添加单引号, 如果是日期,则转换为时间格式器并加单引号; 对参数是null和不是null的情况作了处理 private static String getParameterValue(Object obj) { String value; if (obj instanceof String) { value = "'" + obj.toString() + "'" ; } else if (obj instanceof Date) { DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA); value = "'" + formatter.format( new Date()) + "'" ; } else { if (obj != null ) { value = obj.toString(); } else { value = "" ; } } return value; } } |
SqlStatementInterceptor
这是一个实现了MyBatis的Interceptor
接口的类,主要目的是记录SQL语句的执行时间。它通过@Intercepts
注解指定拦截的目标方法,包括Executor
类的update
和query
方法。
intercept
方法:在调用被拦截的方法前后记录时间,计算执行时间,并根据执行时间的不同区间记录日志。plugin
方法:用于将当前拦截器实例应用到目标对象上,这是MyBatis插件机制的一部分。setProperties
方法:默认为空,可以用于从配置文件中读取属性并应用于拦截器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | package com.jingdianjichi.subject.infra.config; import org.apache.ibatis.cache.CacheKey; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.*; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Properties; @Intercepts({ @Signature(type = Executor. class , method = "update" , args = {MappedStatement. class , Object. class }), @Signature(type = Executor. class , method = "query" , args = {MappedStatement. class , Object. class , RowBounds. class , ResultHandler. class , CacheKey. class , BoundSql. class })}) public class SqlStatementInterceptor implements Interceptor { public static final Logger log = LoggerFactory.getLogger( "sys-sql" ); @Override public Object intercept(Invocation invocation) throws Throwable { long startTime = System.currentTimeMillis(); try { return invocation.proceed(); } finally { long timeConsuming = System.currentTimeMillis() - startTime; log.info( "执行SQL:{}ms" , timeConsuming); if (timeConsuming > 999 && timeConsuming < 5000) { log.info( "执行SQL大于1s:{}ms" , timeConsuming); } else if (timeConsuming >= 5000 && timeConsuming < 10000) { log.info( "执行SQL大于5s:{}ms" , timeConsuming); } else if (timeConsuming >= 10000) { log.info( "执行SQL大于10s:{}ms" , timeConsuming); } } } @Override public Object plugin(Object target) { return Plugin.wrap(target, this ); } @Override public void setProperties(Properties properties) { } } |
再次启动项目
这样,我们便可以看到明显的数值传入sql的状态与结果,通过也展示了该sql执行的具体方法以及所在路径,通过此方法能够更好的处理开发过程中sql问题以及数值传输问题。
SQL拦截器(Interceptor)是一个设计模式,通常用于在执行SQL语句前、后进行一些额外的处理。SQL拦截器的主要作用有以下几点:
-
性能优化: 拦截器可以用来缓存SQL查询结果,避免重复执行相同的查询,从而提高数据库操作的效率。
-
日志记录: 在执行SQL语句前后记录日志,这对于调试和审计非常有用,可以帮助开发者追踪SQL执行的细节,比如执行时间、参数等。
-
SQL语句修改: 在发送到数据库之前,拦截器可以动态地修改SQL语句,例如添加或删除某些条件,或者更改SQL语句的结构以适应不同的环境需求。
-
事务管理: 拦截器可以协助事务的开启、提交或回滚,确保数据的一致性和完整性。
-
安全检查: 可以通过拦截器来检查SQL语句,防止SQL注入等安全问题,确保应用程序的安全性。
-
资源管理: 拦截器可以管理数据库连接的获取与释放,确保资源的有效利用和及时回收。
-
监控与统计: 对SQL语句的执行进行监控,收集统计信息,如查询次数、平均执行时间等,用于性能分析和优化。
-
错误处理: 在SQL执行过程中,拦截器可以捕获异常并进行相应的错误处理,提供更友好的错误反馈机制。
在实际应用中,很多ORM框架(如MyBatis、Hibernate等)都提供了内置的拦截器功能,开发者可以通过配置或扩展这些框架来实现上述功能。使用SQL拦截器可以使代码更加灵活、可维护,并且能够更好地控制和优化数据库操作。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南