首页 > 数据库 >sql拦截器的应用

sql拦截器的应用

时间:2024-07-22 17:22:12浏览次数:10  
标签:拦截器 org sql 应用 SQL apache import

问题:在实际开发当中,日志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拦截器可以使代码更加灵活、可维护,并且能够更好地控制和优化数据库操作。

 

标签:拦截器,org,sql,应用,SQL,apache,import
From: https://www.cnblogs.com/azwz/p/18316459

相关文章

  • 如何攻克LLM应用的调试?教你本地化部署与使用一款免费的LLM应用工程化平台
    随着LLM应用的不断成熟,特别是在B端企业场景中的逐渐落地,其不再停留在原型与验证阶段,将面临着更高的工程化要求,无论是输出的稳定性、性能、以及成本控制等,都需要实现真正的“生产就绪”;但由于大量的应用基于LangChain、LlamaIndex等框架开发,更多的抽象与封装使得应用难以跟踪......
  • sql server 事务日志释放空间
    您收到的错误消息表明数据库'EastRiver'的事务日志已满,导致数据库操作失败。要解决这个问题,可以按照以下步骤操作:1.备份事务日志首先,备份事务日志以释放空间:BACKUPLOG[EastRiver]TODISK=N'C:\Backup\EastRiver_log.bak'GO2.收缩事务日志文件备份日志后,可以使用DBCC......
  • Mysql的主从同步时延问题
    主从同步延迟可以通过一些策略减少,从而减低对业务的影响避免大事务和长事务大事务和长事务会严重影响主从同步的效率,例如大规模的DELETE操作会生成大量的binlog,导致从库复制速度跟不上。解决:通过分批操作缓解,例如一次1000条,减少每次操作的时间,减少对主从同步的影响。启用从库......
  • CentOS 7 安装MySQL
    一、前言这篇博客使用CentOS7系统进行操作,安装过程参考本人另一篇博客CentOS7的下载与安装所有操作都在root用户下进行二、下载MySQL的下载地址:https://downloads.mysql.com/archives/community/版本选择随意,这里以MySQL:8.0.28为例系统选择RedHatEnter......
  • Day07 MySQL 面试题 (三)
    MySQL面试题22、MySQL里记录货币用什么字段类型好NUMERIC和DECIMAL类型被MySQL实现为同样的类型,这在SQL92标准允许。他们被用于保存值,该值的准确精度是极其重要的值,例如与金钱有关的数据。当声明一个类是这些类型之一时,精度和规模的能被(并且通常是)指定。例......
  • 【Gin】精准应用:Gin框架中工厂模式的现代软件开发策略与实施技巧(下)
    【Gin】精准应用:Gin框架中工厂模式的现代软件开发策略与实施技巧(下)大家好我是寸铁......
  • k8s-deployment:应用生命周期管理流程
    deployment:应用生命周期管理流程 应用程序->部署->升级->回滚->删除1部署deployment###kubectlapply-fweb1-deployp.yaml kubectlcreatedeploymentweb--image=nginx:1.16--replicas=3#web1-deploy.yaml ......
  • sqlalchemy.exc.InvalidRequestError
    我收到此错误,但我不明白如何解决它。当我使用uvicorn运行FastApi时出现此错误。要注意利用循环模型导入的错误,我使用了TYPE_CHEKINGsqlalchemy.ext.InvalidRequestError:Oneormoremappersfailedtoinitialize-can'tproceedwithinitializationofotherm......
  • 用SqlBulkCopy批量插入数据 遇到的错误
    原文链接:https://www.cnblogs.com/wz327/archive/2011/07/05/2098356.html错误一:来自数据源的String类型的给定值不能转换为指定目标列的类型nvarchar。还有其他的错误如:AddTime不能为DBNull(这个应该是目标表中AddTime要求不许为null) 可能的原因有两种可能是有"'"(单引......
  • Linux下MySQL的安装部署
    文章目录前言一、MySQL是什么?二、MySQL安装部署(两种)1.手动安装MySQL(1)下载MySQL安装包(2)解压压缩包至opt文件夹下(3)重命名mysql-8.0.33-linux-glibc2.12-x86_64文件夹(4)创建用户组和密码(5)为mysql用户授权(6)创建数据文件的存储位置(7)初始化MySQL数据库(8)编辑MySQL的配置文件(9)......