问题:在实际开发当中,日志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 {@Beanpublic 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");@Overridepublic void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {logInfo(boundSql, ms, parameter);}@Overridepublic 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语句的idString 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)) {// 该分支是动态sqlObject 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");@Overridepublic 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);}}}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic 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拦截器可以使代码更加灵活、可维护,并且能够更好地控制和优化数据库操作。