【springboot系】如何打印完整的执行sql语句?

在项目开发阶段,执行sql语句有利于我们直观排查问题以方便解决问题。但是mybatis默认的方式都是?参数,不方便我们,这个就是默认的方式,需要开启配置

logging:
  level:
    com.baomidou.mybatisplus: debug # MP核心包日志
    com.xxx.scct.dao: debug # 你的Mapper包路径
    org.apache.ibatis: trace # MyBatis原生日志

默认打印的sql,效果如下 

那有没有让我们可以直接看到完整拼接好的sql语句输出打印呢?

第一种方式:p6spy

pom文件添加依赖

  <dependency>
            <groupId>p6spy</groupId>
            <artifactId>p6spy</artifactId>
            <version>3.9.1</version>
        </dependency>

resource目录添加spy.properties

module.log=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 p6spy driver 代理
deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,batch,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 真实JDBC driver , 多个以 逗号 分割 默认为空
#driverlist=org.h2.Driver
driverlist=com.mysql.cj.jdbc.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2

nacos或项目配置文件配置

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.url=jdbc:p6spy:mysql://192.168.6.167:3306/scct?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8

logback-spring.xml配置添加

    <logger name="com.p6spy" level="debug" additivity="false">
        <appender-ref ref="console"/>    <!-- 这里根据自己文件定义控制台的appender名称替换 -->
        <appender-ref ref="flatfile"/>  <!-- 这里根据自己文件定义日志文件的appender名称替换 -->
    </logger>

    <!-- 防止太多日志 -->
    <logger name="com.p6spy.engine.logging" level="INFO"/>
    <logger name="com.p6spy.engine.spy" level="INFO"/>

执行业务代码,控制台打印效果如下 

第二种方式:log4jdbc

pom文件添加依赖

   <dependency>
            <groupId>com.googlecode.log4jdbc</groupId>
            <artifactId>log4jdbc4</artifactId>
        </dependency>

nacos或项目配置文件配置

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=net.sf.log4jdbc.DriverSpy
spring.datasource.url=jdbc:log4jdbc:mysql://192.168.6.167:3306/scct?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8

resource目录添加log4jdbc.properties

# 基本配置
log4jdbc.auto.load.popular.drivers=true
log4jdbc.drivers=com.mysql.cj.jdbc.Driver

# 慢查询相关配置
log4jdbc.sqltiming.enable=true
# 在日志中添加执行时间
log4jdbc.sqltiming.addTime=true
# 警告阈值 (毫秒)
log4jdbc.sqltiming.warn.threshold=300
# 错误阈值 (毫秒)
log4jdbc.sqltiming.error.threshold=2000

# SQL 执行时间显示格式 # "executed in XX ms"
log4jdbc.sqltiming.timeFormat=enabled
# 包括参数值
log4jdbc.sqltiming.logParameter=true

# 结果集限制(防止大结果集占用过多内存)
log4jdbc.dump.sql.datalimit=1000

logback-spring.xml配置添加

  <logger name="jdbc.resultset" additivity="false">
        <level value="error"/>
        <appender-ref ref="console"/>
        <appender-ref ref="flatfile"/>
    </logger>

    <logger name="jdbc.connection" additivity="false">
        <level value="error"/>
        <appender-ref ref="console"/>
        <appender-ref ref="flatfile"/>
    </logger>

    <logger name="jdbc.audit" additivity="false">
        <level value="error"/>
        <appender-ref ref="console"/>
        <appender-ref ref="flatfile"/>
    </logger>

    <!-- log SQL with timing information, post execution -->
    <logger name="jdbc.sqlonly" additivity="false">
        <level value="off"/> <!--这里设置off来禁用sqlonly打印通过,不然就会导致sql重复打印-->
        <appender-ref ref="console"/>
        <appender-ref ref="flatfile"/>
    </logger>

   <logger name="jdbc.sqltiming" additivity="false">
        <level value="info"/> <!--生产环境一般设置warn,info这里为了调试-->
        <appender-ref ref="console"/>
        <appender-ref ref="flatfile"/>
    </logger>

查看执行效果

第三种方式:代码编写sql拦截器

相关拦截器代码

@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SqlPrinterInterceptor implements Interceptor {

    private static final Logger logger = LoggerFactory.getLogger("SQL_LOGGER");
    private static final Pattern REPLACE_PATTERN = Pattern.compile("\\?");

    // 配置开关(可通过配置中心动态更新)
    private volatile boolean enabled = true;
    
    // SQL模板缓存(提升性能)
    private final Map<String, String> sqlCache = new ConcurrentHashMap<>(256);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (!enabled || !logger.isDebugEnabled()) {
            return invocation.proceed(); // 修正方法名:proceed()
        }
        
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = handler.getBoundSql();
        MappedStatement mappedStatement = getMappedStatement(handler);
        
        try {
            // 获取原始SQL模板
            String originalSql = boundSql.getSql();
            String cacheKey = mappedStatement.getId() + "@" + originalSql.hashCode();
            String formattedSql = sqlCache.computeIfAbsent(cacheKey, k -> 
                originalSql.replaceAll("[\\s\n]+", " ")
            );
            
            // 获取实际参数值
            Object paramObj = boundSql.getParameterObject();
            
            // 处理批量操作场景
            if (isBatchOperation(boundSql)) {
                handleBatchOperation(invocation, handler, boundSql, formattedSql);
                return invocation.proceed(); // 确保调用继续
            }
            
            // 生成完整SQL
            String completeSql = formatSql(formattedSql, boundSql, getConfiguration(handler));
            logger.debug("执行SQL: {}", completeSql);
        } catch (Exception e) {
            logger.warn("SQL打印失败,不中断操作", e);
        }
        return invocation.proceed();
    }

    private String formatSql(String sql, BoundSql boundSql, Configuration configuration) {
        if (boundSql.getParameterObject() == null) {
            return sql;
        }
        
        List<ParameterMapping> mappings = boundSql.getParameterMappings();
        if (mappings == null || mappings.isEmpty()) {
            return sql;
        }

        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        Object paramObject = boundSql.getParameterObject();
        String preSql = sql;
        
        // 使用更精确的替换方式避免误替换
        StringBuffer result = new StringBuffer();
        Matcher matcher = REPLACE_PATTERN.matcher(sql);
        int index = 0;
        
        while (matcher.find()) {
            String replacement = "?";
            if (index < mappings.size()) {
                ParameterMapping mapping = mappings.get(index++);
                String property = mapping.getProperty();
                Object value = getParamValue(property, paramObject, boundSql, typeHandlerRegistry);
                replacement = formatValue(value);
            }
            matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
        }
        matcher.appendTail(result);
        
        return result.toString();
    }

    private Object getParamValue(String property, Object paramObject, BoundSql boundSql, 
                                TypeHandlerRegistry registry) {
        if (paramObject == null) {
            return null;
        }
        
        if (registry.hasTypeHandler(paramObject.getClass())) {
            return paramObject;
        }
        
        MetaObject metaObject = SystemMetaObject.forObject(paramObject);
        if (metaObject.hasGetter(property)) {
            return metaObject.getValue(property);
        }
        
        // 处理Map类型的参数
        if (paramObject instanceof Map) {
            return ((Map<?, ?>) paramObject).get(property);
        }
        
        // 处理额外参数
        if (boundSql.hasAdditionalParameter(property)) {
            return boundSql.getAdditionalParameter(property);
        }
        
        return null;
    }

    private String formatValue(Object value) {
        if (value == null) return "NULL";
        
        if (value instanceof String || value instanceof Date) {
            return "'" + value.toString() + "'";
        }
        if (value instanceof Iterable) {
            return formatIterable((Iterable<?>) value);
        }
        return value.toString();
    }

    private String formatIterable(Iterable<?> iterable) {
        StringBuilder sb = new StringBuilder("(");
        Iterator<?> it = iterable.iterator();
        while (it.hasNext()) {
            Object item = it.next();
            sb.append(formatValue(item));
            if (it.hasNext()) sb.append(",");
        }
        return sb.append(")").toString();
    }

    private boolean isBatchOperation(BoundSql boundSql) {
        Object paramObj = boundSql.getParameterObject();
        return paramObj instanceof Collection && boundSql.getSql().toUpperCase().contains("IN"); 
    }

    private void handleBatchOperation(Invocation invocation, StatementHandler handler, 
                                    BoundSql boundSql, String formattedSql) throws Throwable {
        Collection<?> batchParams = (Collection<?>) boundSql.getParameterObject();
        Configuration configuration = getConfiguration(handler);
        
        if (!batchParams.isEmpty()) {
            int batchCount = 0;
            for (Object param : batchParams) {
                String completeSql = formatSql(formattedSql, boundSql, configuration);
                logger.debug("批量操作[{}] SQL: {}", ++batchCount, completeSql);
                // 只打印前3条避免日志过大
                if (batchCount >= 3) break;
            }
            if (batchParams.size() > 3) {
                logger.debug("...省略批量操作中的{}条记录", batchParams.size() - 3);
            }
        }
    }

    // 获取Configuration的正确方式
    private Configuration getConfiguration(StatementHandler handler) {
        MetaObject metaObject = SystemMetaObject.forObject(handler);
        StatementHandler delegate = (StatementHandler) metaObject.getValue("delegate");
        if (delegate != null) {
            metaObject = SystemMetaObject.forObject(delegate);
        }
        return (Configuration) metaObject.getValue("configuration");
    }
    
    private MappedStatement getMappedStatement(StatementHandler handler) {
        MetaObject metaObject = SystemMetaObject.forObject(handler);
        StatementHandler delegate = (StatementHandler) metaObject.getValue("delegate");
        if (delegate != null) {
            metaObject = SystemMetaObject.forObject(delegate);
        }
        return (MappedStatement) metaObject.getValue("mappedStatement");
    }

    // 动态开关
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

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

    @Override
    public void setProperties(Properties properties) {
        // 从配置初始化
    }
}

配置拦截器bean注册

@Configuration
public class MyBatisConfig {


    @Bean
    public SqlPrinterInterceptor sqlPrinterInterceptor() {
        SqlPrinterInterceptor interceptor = new SqlPrinterInterceptor();
        interceptor.setEnabled(true); // 默认开启,生产环境建议默认关闭
        return interceptor;
    }


}

开启日志配置

logging:
  level:
    SQL_LOGGER: DEBUG # 控制SQL打印

执行效果

### 如何配置 Spring Boot 自动打印 MyBatis SQL 语句 为了使 Spring Boot 应用程序能够自动打印由 MyBatis 生成的 SQL 语句,可以通过多种方式进行设置。以下是几种常见方法: #### 方法一:通过 `application.yml` 文件配置 可以在项目的 `application.yml` 或者 `application.properties` 中添加特定于 MyBatis 的属性来启用 SQL 日志记录功能。 对于 YAML 格式的配置文件,可以这样写入: ```yaml mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl ``` 此配置项会指示 MyBatis 使用标准输出流作为日志实现工具[^2]。 #### 方法二:修改 MyBatis XML 配置文件中的 `<settings>` 节点 如果项目中有单独定义的 MyBatis XML 配置文件,则可在该文件内加入如下片段以激活 SQL 打印功能: ```xml <configuration> <settings> <!-- 启用SQL日志 --> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> </configuration> ``` 这种方式同样能有效触发 SQL 查询的日志输出至控制台[^4]。 #### 方法三:利用内置的日志框架进行更灵活的日志管理 考虑到实际生产环境中可能需要更加精细地控制日志级别以及目的地等因素,推荐采用基于现有 Java 日志库(如 Logback、Log4j 等)的方式来处理这个问题。例如,在使用 SLF4J 和 Logback 组合的情况下,只需调整相应的 logger 设置即可满足需求: ```properties logging.level.com.example.mapper=DEBUG ``` 这里假设所有的 Mapper 接口都位于包路径 `com.example.mapper` 下面;将这些接口的日志等级设为 DEBUG 就可以让它们发出详细的执行信息,包括完整SQL 文本[^5]。 以上三种方案均可达成目的,具体选用哪一种取决于个人偏好和技术栈的选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值