add 增加 自定义sql日志输出 性能比p6spy更好更详细 可以精确定位mapper具体位置

This commit is contained in:
疯狂的狮子Li
2026-06-17 10:44:01 +08:00
parent 0c2b409304
commit 06d4e6d7f1
10 changed files with 418 additions and 41 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ Topiam IAM/IDaaS身份管理平台 - https://www.topiam.cn/ <br>
| Redis客户端 | 采用 Redisson Redis官方推荐 基于Netty的客户端工具<br/>支持Redis 90%以上的命令 底层优化规避很多不正确的用法 例如: keys被转换为scan<br/>支持单机、哨兵、单主集群、多主集群等模式 | Lettuce + RedisTemplate 支持模式少 工具使用繁琐<br/>连接池采用 common-pool Bug多经常性出问题 |
| 缓存注解 | 采用 Spring-Cache 注解 对其扩展了实现支持了更多功能<br/>例如 过期时间 最大空闲时间 组最大长度等 只需一个注解即可完成数据自动缓存 | 需手动编写Redis代码逻辑 |
| ORM框架 | 采用 Mybatis-Plus 基于对象几乎不用写SQL全java操作 功能强大插件众多<br/>例如分页插件 乐观锁插件等等 | 采用 Mybatis 基于XML需要手写SQL |
| SQL监控 | 采用 p6spy 可输出完整SQL与执行时间监控 | log输出 需手动拼接sql与参数无法快速查看调试问题 |
| SQL监控 | 内置 MyBatis 完整 SQL 输出工具,可输出完整SQL、Mapper ID与执行时间监控 | log输出 需手动拼接sql与参数无法快速查看调试问题 |
| 数据分页 | 采用 Mybatis-Plus 分页插件<br/>框架对其进行了扩展 对象化分页对象 支持多种方式传参 支持前端多排序 复杂排序 | 采用 PageHelper 仅支持单查询分页 参数只能从param传 只能单排序 功能扩展性差 体验不好 |
| 数据权限 | 采用 Mybatis-Plus 插件 自行分析拼接SQL 无感式过滤<br/>只需为Mapper设置好注解条件 支持多种自定义 不限于部门角色 | 采用 注解+aop 实现 基于部门角色 生成的sql兼容性差 不支持其他业务扩展<br/>生成sql后需手动拼接到具体业务sql上 对于多个Mapper查询不起作用 |
| 数据脱敏 | 采用 注解 + jackson 序列化期间脱敏 支持不同模块不同的脱敏条件<br/>支持多种策略 如身份证、手机号、地址、邮箱、银行卡等 可自行扩展 | 无 |
-8
View File
@@ -30,7 +30,6 @@
<mybatis-plus.version>3.5.16</mybatis-plus.version>
<mybatis-plus-join.version>1.5.7</mybatis-plus-join.version>
<dynamic-ds.version>4.5.0</dynamic-ds.version>
<p6spy.version>3.9.1</p6spy.version>
<anyline.version>8.7.3-20260306</anyline.version>
<easy-es.version>3.0.2</easy-es.version>
<elasticsearch-client.version>7.17.28</elasticsearch-client.version>
@@ -242,13 +241,6 @@
<version>${mybatis-plus-join.version}</version>
</dependency>
<!-- sql性能分析插件 -->
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>${p6spy.version}</version>
</dependency>
<!-- AWS SDK for Java 2.x -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
@@ -61,14 +61,20 @@ snail-ai:
read-timeout-ms: 60000
chat-timeout-ms: 300000
--- # MyBatis Plus 配置
mybatis-plus:
sql-log:
# 完整 SQL 输出开关
enabled: true
# 输出方式,可选 console、log
output: console
--- # 数据源配置
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
# 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
dynamic:
# 性能分析插件(有性能损耗 不建议生产环境使用)
p6spy: true
# 设置默认的数据源或者数据源组,默认值即为 master
primary: master
# 严格模式 匹配不到数据源则报错
@@ -64,14 +64,20 @@ snail-ai:
read-timeout-ms: 60000
chat-timeout-ms: 300000
--- # MyBatis Plus 配置
mybatis-plus:
sql-log:
# 完整 SQL 输出开关
enabled: false
# 输出方式,可选 console、log
output: console
--- # 数据源配置
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
# 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
dynamic:
# 性能分析插件(有性能损耗 不建议生产环境使用)
p6spy: false
# 设置默认的数据源或者数据源组,默认值即为 master
primary: master
# 严格模式 匹配不到数据源则报错
@@ -58,11 +58,6 @@
<artifactId>mybatis-plus-join-boot-starter</artifactId>
</dependency>
<!-- sql性能分析插件 -->
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
</dependency>
</dependencies>
</project>
@@ -10,12 +10,16 @@ import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInt
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.dromara.common.core.factory.YmlPropertySourceFactory;
import org.dromara.common.mybatis.aspect.DataPermissionPointcutAdvisor;
import org.dromara.common.mybatis.config.properties.SqlLogProperties;
import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler;
import org.dromara.common.mybatis.handler.MybatisExceptionHandler;
import org.dromara.common.mybatis.handler.PlusPostInitTableInfoHandler;
import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor;
import org.dromara.common.mybatis.interceptor.SqlLogInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.annotation.Role;
@@ -30,6 +34,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
@EnableTransactionManagement(proxyTargetClass = true)
@MapperScan("${mybatis-plus.mapperPackage}")
@PropertySource(value = "classpath:common-mybatis.yml", factory = YmlPropertySourceFactory.class)
@EnableConfigurationProperties(SqlLogProperties.class)
public class MybatisPlusConfig {
/**
@@ -82,6 +87,15 @@ public class MybatisPlusConfig {
return new OptimisticLockerInnerInterceptor();
}
/**
* 完整 SQL 日志拦截器
*/
@Bean
@ConditionalOnProperty(prefix = "mybatis-plus.sql-log", name = "enabled", havingValue = "true")
public SqlLogInterceptor sqlLogInterceptor(SqlLogProperties sqlLogProperties) {
return new SqlLogInterceptor(sqlLogProperties);
}
/**
* 元对象字段填充控制器
*/
@@ -0,0 +1,25 @@
package org.dromara.common.mybatis.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* SQL 日志配置。
*
* @author Lion Li
*/
@Data
@ConfigurationProperties(prefix = "mybatis-plus.sql-log")
public class SqlLogProperties {
/**
* 是否开启完整 SQL 输出。
*/
private Boolean enabled = false;
/**
* 输出方式,可选 console、log。
*/
private String output = "console";
}
@@ -0,0 +1,361 @@
package org.dromara.common.mybatis.interceptor;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.dromara.common.mybatis.config.properties.SqlLogProperties;
import java.lang.reflect.InvocationTargetException;
import java.sql.Statement;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.TemporalAccessor;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock;
/**
* 完整 SQL 日志拦截器。
*
* @author Lion Li
*/
@Slf4j(topic = "SQL_FULL")
@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}),
@Signature(type = StatementHandler.class, method = "queryCursor", args = {Statement.class})
})
public class SqlLogInterceptor implements Interceptor {
/**
* 单条日志分片长度。
*/
private static final int CHUNK_SIZE = 8000;
/**
* SQL 空白字符匹配。
*/
private static final String BLANK_REGEX = "\\s+";
/**
* 日期时间格式。
*/
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 日期格式。
*/
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/**
* 时间格式。
*/
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
/**
* 控制台输出锁,避免多线程 SQL 日志互相穿插。
*/
private static final ReentrantLock CONSOLE_LOCK = new ReentrantLock();
/**
* SQL 日志配置。
*/
private final SqlLogProperties sqlLogProperties;
public SqlLogInterceptor(SqlLogProperties sqlLogProperties) {
this.sqlLogProperties = sqlLogProperties;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
BoundSql boundSql = statementHandler.getBoundSql();
MappedStatement mappedStatement = PluginUtils.mpStatementHandler(statementHandler).mappedStatement();
long startTime = System.currentTimeMillis();
try {
Object result = invocation.proceed();
printSql(mappedStatement, boundSql, System.currentTimeMillis() - startTime, null);
return result;
} catch (Throwable e) {
printSql(mappedStatement, boundSql, System.currentTimeMillis() - startTime, e);
throw e;
}
}
/**
* 输出 SQL。
*
* @param mappedStatement 映射语句
* @param boundSql 绑定 SQL
* @param elapsedTime 执行耗时
* @param throwable 执行异常
*/
private void printSql(MappedStatement mappedStatement, BoundSql boundSql, long elapsedTime, Throwable throwable) {
String fullSql = buildFullSql(mappedStatement.getConfiguration(), boundSql);
String message = buildLogMessage(mappedStatement, elapsedTime, fullSql, throwable);
printChunk(message);
}
/**
* 构建日志内容。
*
* @param mappedStatement 映射语句
* @param elapsedTime 执行耗时
* @param fullSql 完整 SQL
* @param throwable 执行异常
* @return 日志内容
*/
private String buildLogMessage(MappedStatement mappedStatement, long elapsedTime, String fullSql, Throwable throwable) {
if (isLogOutput()) {
String message = StrUtil.format("Consume Time{} ms {} Mapper ID{} Execute SQL{}",
elapsedTime, DateUtil.now(), mappedStatement.getId(), fullSql);
String errorMessage = formatThrowable(throwable);
if (StrUtil.isNotBlank(errorMessage)) {
message = message + " Execute Error" + errorMessage;
}
return message;
}
String message = StrUtil.format("Consume Time{} ms {}\nMapper ID{}\nExecute SQL{}",
elapsedTime, DateUtil.now(), mappedStatement.getId(), fullSql);
String errorMessage = formatThrowable(throwable);
if (StrUtil.isNotBlank(errorMessage)) {
message = message + "\nExecute Error" + errorMessage;
}
return message;
}
/**
* 格式化执行异常。
*
* @param throwable 执行异常
* @return 异常信息
*/
private String formatThrowable(Throwable throwable) {
if (throwable == null) {
return StrUtil.EMPTY;
}
Throwable realThrowable = unwrapThrowable(throwable);
String message = realThrowable.getMessage();
if (StrUtil.isBlank(message)) {
return StrUtil.EMPTY;
}
return realThrowable.getClass().getName() + ": " + message;
}
/**
* 解包反射调用异常。
*
* @param throwable 执行异常
* @return 实际异常
*/
private Throwable unwrapThrowable(Throwable throwable) {
if (throwable instanceof InvocationTargetException invocationTargetException
&& invocationTargetException.getTargetException() != null) {
return invocationTargetException.getTargetException();
}
return throwable;
}
/**
* 构建完整 SQL。
*
* @param configuration MyBatis 配置
* @param boundSql 绑定 SQL
* @return 完整 SQL
*/
private String buildFullSql(Configuration configuration, BoundSql boundSql) {
String sql = boundSql.getSql().replaceAll(BLANK_REGEX, " ").trim();
List<String> parameters = buildParameterValues(configuration, boundSql);
return replacePlaceholders(sql, parameters);
}
/**
* 构建参数值集合。
*
* @param configuration MyBatis 配置
* @param boundSql 绑定 SQL
* @return 参数值集合
*/
private List<String> buildParameterValues(Configuration configuration, BoundSql boundSql) {
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
List<String> parameters = new ArrayList<>(parameterMappings.size());
Object parameterObject = boundSql.getParameterObject();
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() == ParameterMode.OUT) {
continue;
}
String propertyName = parameterMapping.getProperty();
Object value;
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else if (metaObject != null && metaObject.hasGetter(propertyName)) {
value = metaObject.getValue(propertyName);
} else {
value = null;
}
parameters.add(formatParameter(value));
}
return parameters;
}
/**
* 替换 SQL 占位符。
*
* @param sql SQL 模板
* @param parameters 参数集合
* @return 完整 SQL
*/
private String replacePlaceholders(String sql, List<String> parameters) {
if (parameters.isEmpty()) {
return sql;
}
StringBuilder builder = new StringBuilder(sql.length() + parameters.size() * 8);
int parameterIndex = 0;
boolean inSingleQuote = false;
boolean inDoubleQuote = false;
for (int i = 0; i < sql.length(); i++) {
char current = sql.charAt(i);
if (current == '\'' && !inDoubleQuote) {
inSingleQuote = !inSingleQuote;
} else if (current == '"' && !inSingleQuote) {
inDoubleQuote = !inDoubleQuote;
}
if (current == '?' && !inSingleQuote && !inDoubleQuote && parameterIndex < parameters.size()) {
builder.append(parameters.get(parameterIndex++));
} else {
builder.append(current);
}
}
return builder.toString();
}
/**
* 格式化参数值。
*
* @param value 参数值
* @return SQL 参数文本
*/
private String formatParameter(Object value) {
if (value == null) {
return "null";
}
if (value instanceof Number || value instanceof Boolean) {
return value.toString();
}
if (value instanceof Date date) {
return quote(DateUtil.formatDateTime(date));
}
if (value instanceof LocalDateTime localDateTime) {
return quote(localDateTime.format(DATE_TIME_FORMATTER));
}
if (value instanceof LocalDate localDate) {
return quote(localDate.format(DATE_FORMATTER));
}
if (value instanceof LocalTime localTime) {
return quote(localTime.format(TIME_FORMATTER));
}
if (value instanceof TemporalAccessor) {
return quote(value.toString());
}
if (value instanceof Enum<?> enumValue) {
return quote(enumValue.name());
}
return quote(value.toString());
}
/**
* 包装字符串参数。
*
* @param value 字符串值
* @return SQL 字符串参数
*/
private String quote(String value) {
return "'" + value.replace("'", "''") + "'";
}
/**
* 分片输出日志,避免日志链路截断超长 SQL。
*
* @param message 日志内容
*/
private void printChunk(String message) {
if (!isLogOutput()) {
printConsole(message);
return;
}
if (message.length() <= CHUNK_SIZE) {
print(message);
return;
}
String sqlLogId = UUID.randomUUID().toString();
int total = (message.length() + CHUNK_SIZE - 1) / CHUNK_SIZE;
for (int i = 0; i < total; i++) {
int start = i * CHUNK_SIZE;
int end = Math.min(start + CHUNK_SIZE, message.length());
print(StrUtil.format("sqlLogId={} part={}/{} {}", sqlLogId, i + 1, total, message.substring(start, end)));
}
}
/**
* 输出 SQL 日志。
*
* @param message 日志内容
*/
private void print(String message) {
if (isLogOutput()) {
log.info(message);
return;
}
printConsole(message);
}
/**
* 输出控制台日志。
*
* @param message 日志内容
*/
private void printConsole(String message) {
CONSOLE_LOCK.lock();
try {
System.err.println(message);
System.err.println();
} finally {
CONSOLE_LOCK.unlock();
}
}
/**
* 是否使用日志系统输出。
*
* @return true 使用日志系统输出,false 使用控制台输出
*/
private boolean isLogOutput() {
return StrUtil.equalsIgnoreCase("log", sqlLogProperties.getOutput());
}
}
@@ -13,9 +13,7 @@ mybatis-plus:
# MyBatis 自动映射时未知列或未知属性处理策
# NONE:不做处理 WARNING:打印相关警告 FAILING:抛出异常和详细信息
autoMappingUnknownColumnBehavior: NONE
# 更详细的日志输出 会有性能损耗 org.apache.ibatis.logging.stdout.StdOutImpl
# 关闭日志记录 (可单纯使用 p6spy 分析) org.apache.ibatis.logging.nologging.NoLoggingImpl
# 默认日志输出 org.apache.ibatis.logging.slf4j.Slf4jImpl
# 关闭默认日志输出 org.apache.ibatis.logging.nologging.NoLoggingImpl
logImpl: org.apache.ibatis.logging.nologging.NoLoggingImpl
global-config:
# 是否打印 Logo banner
@@ -1,20 +0,0 @@
# p6spy 性能分析插件配置文件
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,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
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,commit,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# SQL语句打印时间格式
databaseDialectTimestampFormat=yyyy-MM-dd HH:mm:ss
# 是否过滤 Log
filter=true
# 过滤 Log 时所排除的 sql 关键字,以逗号分隔
exclude=