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语句和参数。

  • beforeQuerybeforeUpdate方法:在SQL查询或更新前被调用,通过BoundSqlMappedStatement获取SQL语句和参数,然后将其日志输出。
  • logInfo方法:用于格式化并打印SQL语句和参数信息。
  • getSqlshowSql方法:用于构建和展示完整的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类的updatequery方法。

  • 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拦截器的主要作用有以下几点:

  1. 性能优化: 拦截器可以用来缓存SQL查询结果,避免重复执行相同的查询,从而提高数据库操作的效率。

  2. 日志记录: 在执行SQL语句前后记录日志,这对于调试和审计非常有用,可以帮助开发者追踪SQL执行的细节,比如执行时间、参数等。

  3. SQL语句修改: 在发送到数据库之前,拦截器可以动态地修改SQL语句,例如添加或删除某些条件,或者更改SQL语句的结构以适应不同的环境需求。

  4. 事务管理: 拦截器可以协助事务的开启、提交或回滚,确保数据的一致性和完整性。

  5. 安全检查: 可以通过拦截器来检查SQL语句,防止SQL注入等安全问题,确保应用程序的安全性。

  6. 资源管理: 拦截器可以管理数据库连接的获取与释放,确保资源的有效利用和及时回收。

  7. 监控与统计: 对SQL语句的执行进行监控,收集统计信息,如查询次数、平均执行时间等,用于性能分析和优化。

  8. 错误处理: 在SQL执行过程中,拦截器可以捕获异常并进行相应的错误处理,提供更友好的错误反馈机制。

在实际应用中,很多ORM框架(如MyBatis、Hibernate等)都提供了内置的拦截器功能,开发者可以通过配置或扩展这些框架来实现上述功能。使用SQL拦截器可以使代码更加灵活、可维护,并且能够更好地控制和优化数据库操作。

 

posted @ 2024-07-22 17:13  橘子味芬达水  阅读(53)  评论(0编辑  收藏  举报