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
拦截器。
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中的显示。
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
方法:默认为空,可以用于从配置文件中读取属性并应用于拦截器。
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拦截器可以使代码更加灵活、可维护,并且能够更好地控制和优化数据库操作。