在Spring boot 中执行 sql 利用 Mybatis 拦截器 获取执行sql语句

需求: 开发 生成 数据库 同步基础数据 但是又不能直接覆盖表 这时候同步起来 很玛法
解决方案:在开发中 添加 更新 删除的 基础数据 执行的 sql 语句 存到开发数据库 需要同步 到线上时
直接执行 存在 开发数据的 sql 语句 即可

需求: 有的时候 需要动态 拼接 sql 比如 数据权限 根据权限不同 拼接不同的 where 条件
也可以在 Mybatis 拦截器 中实现
下面的代码 主要是 拦截 执行的 sql 语句 存储到 数据库

第一步:
定义 Mybatis 拦截器

package com.erp.init.mybatisplus;


import com.erp.init.sqlLog.SqLogUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;

import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.Statement;
import java.util.Map;
import java.util.Properties;
//在mybatis中可被拦截的类型有四种(按照拦截顺序):
//
//        Executor: 拦截执行器的方法。
//        ParameterHandler: 拦截参数的处理。
//        ResultHandler:拦截结果集的处理。
//        StatementHandler: 拦截Sql语法构建的处理。
//@Intercepts({
    
    
//    @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
//    @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
//    @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
//})
//	1. @Intercepts:标识该类是一个拦截器;
//      @Signature:指明自定义拦截器需要拦截哪一个类型,哪一个方法;
//    2.1 type:对应四种类型中的一种;
//    2.2 method:对应接口中的哪类方法(因为可能存在重载方法);
//    2.3 args:对应哪一个方法;
@Intercepts({
    
    
        @Signature(type = StatementHandler.class, method = "update", args = {
    
    Statement.class}),
        @Signature(type = StatementHandler.class, method = "batch", args = {
    
    Statement.class})
})
@Slf4j
@Component  // 必须要交给 spring boot 管理
public class MybatisLogInterceptor implements Interceptor {
    
    

     //这个方法里 是重点  主要是拦截 需要执行的sql  
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
    
    
        // 执行方法
        Object result = invocation.proceed();

        // 获取MapperStatement对象,获取到sql的详细信息
        Object realTarget = realTarget(invocation.getTarget());
        // 获取metaObject对象
        MetaObject metaObject = SystemMetaObject.forObject(realTarget);
        // 获取MappedStatement对象
        MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        // 获取方法的全类名称
        String methodFullName = ms.getId();

        // 判断是否是需要日志记录的方法
        //   我用的是 Mybatis 提供的方法  methodFullName  这个可以获取到 方法 全类名 
        //  如果是 写的 原生 sql 执行的 没测试过  需要 测试一下使用
        Map<String, Object> map = SqLogUtil.verifyRecordLog(methodFullName);
        if (!map.isEmpty() && (boolean) map.get("isRecord")) {
    
    

            Statement statement;
            // 获取方法参数
            Object[] args = invocation.getArgs();
            Object firstArg = args[0];
            if (Proxy.isProxyClass(firstArg.getClass())) {
    
    
                statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement");
            } else {
    
    
                statement = (Statement) firstArg;
            }
             // 这个对象里 有好几个 statement 和 stmt
            // 可以打打断点 看啊看 statement 这个对象里的数据结构
            MetaObject stmtMetaObj = SystemMetaObject.forObject(statement);
            Object stmt = stmtMetaObj.getValue("stmt");
            MetaObject metaObject1 = SystemMetaObject.forObject(stmt);
            Object statement1 = metaObject1.getValue("statement");
            MetaObject metaObject2 = SystemMetaObject.forObject(statement1);
            // mybatis 最后执行的sql 就在 这个对象里
            Object stmt1 = metaObject2.getOriginalObject();
            String finalSql=stmt1.toString();
            //去掉不要的字符串
            finalSql = finalSql.substring(finalSql.indexOf(":") + 1, finalSql.length());

            log.info("最终sql: \n " + finalSql);
              
            String saveLogSql = SqLogUtil.getSaveLogSql(methodFullName, (String) map.get("desc"), finalSql);

            if (StringUtils.isNotBlank(saveLogSql)) {
    
    
                Connection connection = statement.getConnection();

                if (connection.isReadOnly()) {
    
     // 当前事务是只读事务,则重新用不同的Connection对象
                    Connection mysqlConnection = SqLogUtil.getMysqlConnection();
                    if (mysqlConnection != null) {
    
    
                        try {
    
    
                            mysqlConnection.createStatement().execute(saveLogSql);
                        } catch (Exception e) {
    
    
                            e.printStackTrace();
                            log.error("拦截器记录日志出错!", e);
                        } finally {
    
    
                            mysqlConnection.close();//关闭连接
                        }
                    }
                } else {
    
    
                    connection.createStatement().execute(saveLogSql);
                }
            }
        }
        return result;
    }

    @Override
    public Object plugin(Object target) {
    
    
        if (target instanceof StatementHandler) {
    
    
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties prop) {
    
    

    }

    /**
     * <p>
     * 获得真正的处理对象,可能多层代理.
     * </p>
     */
    @SuppressWarnings("unchecked")
    public static <T> T realTarget(Object target) {
    
    
        if (Proxy.isProxyClass(target.getClass())) {
    
    
            MetaObject metaObject = SystemMetaObject.forObject(target);
            return realTarget(metaObject.getValue("h.target"));
        }
        return (T) target;
    }
}

第二步:
创建一个 sql 存储 工具类

package com.erp.init.sqlLog;

import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.util.JdbcConstants;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * User: Json
 * <p>
 * Date: 2022/11/22
 **/
@Component  // 必须要交给 spring boot 管理
@Slf4j
public class SqLogUtil {
    
    
    /**
     * 判断哪个方法 需要被记录  
     * 这是一种方法 也可以设计成注解的形式 判断注解
     *  我用的是 Mybatis 提供的方法 
     *  如果是 写的 原生 sql 执行的 没测试过  需要 测试一下使用
     */
    public static final String[] DEFAULT_RECORD_METHOD_START = {
    
    
            "com.erp.base.mapper.YsDictMapper.updateById",
            "com.erp.base.mapper.YsDictMapper.insert",
            "com.erp.base.mapper.YsDictMapper.deleteById",
            "com.erp.base.mapper.YsDictSubMapper.insert",
            "com.erp.base.mapper.YsDictSubMapper.updateById",
            "com.erp.base.mapper.YsDictSubMapper.deleteById",
            "com.erp.base.mapper.YsFormMapper.insert",
            "com.erp.base.mapper.YsFormMapper.updateById",
            "com.erp.base.mapper.YsFormMapper.delete"
    };

    /**
     * 默认不记录的操作方法(记录日志的方法)
     * 这个是 执行 Mapper 里的方法
     * YsSqlLogMapper  类里的 方法
     */
    public static final String[] DEFAULT_NOT_RECORED_METHOD = new String[]{
    
    "com.erp.base.mapper.YsSqlLogMapper.saveSqlLog"};

    private static SqLogUtil logUtils;

    /**
     * 注入SqlSessionFactory对象
     */
    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    /**
     * 注入DataSource对象
     */
    @Autowired
    private DataSource mysqlDataSource;

    public SqLogUtil() {
    
    
    }

    /**
     * 给logUtils对象赋值
     */
    @PostConstruct
    public void init() {
    
    
        logUtils = this;
        logUtils.sqlSessionFactory = this.sqlSessionFactory;
        logUtils.mysqlDataSource = this.mysqlDataSource;
    }

    /**
     * 验证方法是否需要日志记录
     *
     * @param methodFullName
     * @return
     */
    public static Map<String, Object> verifyRecordLog(String methodFullName) {
    
    
        Map<String, Object> resultMap = new HashMap<>();

        for (int i = 0; i < DEFAULT_NOT_RECORED_METHOD.length; i++) {
    
    
            if (methodFullName.equals(DEFAULT_NOT_RECORED_METHOD[i])) {
    
    
                return resultMap;
            }
        }

        boolean isRecord = false;

        String desc = "";
        int flag = methodFullName.lastIndexOf(".");
        String classPath = methodFullName.substring(0, flag);
        String methodName = methodFullName.substring(flag + 1);
        Class<?> clazz = null;
        try {
    
    
            clazz = Class.forName(classPath);
        } catch (ClassNotFoundException e) {
    
    
            e.printStackTrace();
            log.error("判断是否需要记录日志异常!", e);
        }
        if (clazz != null) {
    
    
            if (verifyMethodName(methodFullName)) {
    
    
                isRecord = true;
            }
        }
        resultMap.put("isRecord", isRecord); // 是否记录
        resultMap.put("desc", desc); // 方法描述
        return resultMap;
    }


    /**
     * 判断方法名是否满足日志记录格式
     *
     * @param methodName
     * @return
     */
    public static boolean verifyMethodName(String methodName) {
    
    
        boolean methodNameFlag = false;
        for (int i = 0; i < DEFAULT_RECORD_METHOD_START.length; i++) {
    
    
            if (methodName.startsWith(DEFAULT_RECORD_METHOD_START[i])) {
    
    
                methodNameFlag = true;
                break;
            }
        }
        return methodNameFlag;
    }


    /**
     * 填充日记记录SQL参数
     *
     * @param methodFullName
     * @param desc
     * @param originalSql
     * @return
     */
    private static List<Object> getParamList(String methodFullName, String desc, String originalSql) {
    
    
        List<Object> paramList = new ArrayList<>();
        // 完整SQL语句
        paramList.add(handlerSql(originalSql));
        //时间
        paramList.add(LocalDateTime.now().toString());
        return paramList;
    }

    /**
     * 处理SQL语句
     *
     * @param originalSql
     * @return
     */
    private static String handlerSql(String originalSql) {
    
    
      //  String sql = originalSql.substring(originalSql.indexOf(":") + 1);
        // 将原始sql中的空白字符(\s包括换行符,制表符,空格符)替换为" "
        return originalSql.replaceAll("[\\s]+", " ");
    }

    /**
     * 获取日志保存SQL
     *
     * @param methodFullName
     * @param desc
     * @param originalSql
     * @return
     */
    public static String getSaveLogSql(String methodFullName, String desc, String originalSql) {
    
    
        String sql = logUtils.sqlSessionFactory.getConfiguration()
                .getMappedStatement(DEFAULT_NOT_RECORED_METHOD[0]).getBoundSql(null).getSql();
        List<Object> paramList = getParamList(methodFullName, desc, originalSql);
        // paramList  是你需要存到数据库的 数据  
        sql = paramList != null && !paramList.isEmpty() ? SQLUtils.format(sql, JdbcConstants.MYSQL, paramList) : null;
        return sql;
    }

    /**
     * 获取mysql Connection对象
     *
     * @return
     */
    public static Connection getMysqlConnection() {
    
    
        Connection conn = null;
        try {
    
    
            conn = logUtils.mysqlDataSource.getConnection();
        } catch (SQLException e) {
    
    
            e.printStackTrace();
            log.error("保存日志时获取Connection对象异常!", e);
        }

        return conn;
    }


}

第三步:
创建一个 存储sql 语句的 Mapper 方法

package com.erp.base.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.erp.api.entities.base.base.YsSqlLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

/**
 * <p>
 * sql日志表 Mapper 接口
 * </p>
 *
 * @author Json
 * @since 2022-11-22
 */
@Mapper
public interface YsSqlLogMapper extends BaseMapper<YsSqlLog> {
    
    


    @Insert({
    
    "insert into ys_sql_log(sql_info,date) values(#{ysSqlLog.sqlInfo},#{ysSqlLog.createTime})"})
    int saveSqlLog(YsSqlLog ysSqlLog);

}

最后再执行 sql 的时候 数据库表里 就会记录 执行的 sql 语句
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Drug_/article/details/127995504