AOP实现操作日志记录+SQL优化器升级
文章目录
- 1.引入依赖
- 1.sun-dependencies 指定依赖
- 2.将sun-dependencies进行install
- 3.sun-common-log引入依赖
- 2.sun-common-log代码实现
- 1.LogAspect.java(需要更改包时就修改Pointcut的切点表达式即可)
- 2.log4j2-spring.xml
- 3.效果展示
- 3.SQL优化器升级
- 1.目录
- 2.SqlBeautyInterceptor.java
- 3.MybatisConfiguration.java SQL优化器注入Bean容器
- 4.application.yml开启sql优化器
- 5.效果展示
- 4.遇到的bug
- 1.mapstruct失效(在build内配置annotationProcessorPaths)
1.引入依赖
1.sun-dependencies 指定依赖
<spring.aop.version>2.4.2</spring.aop.version>
<gson.version>2.8.6</gson.version>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>${spring.aop.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
2.将sun-dependencies进行install
3.sun-common-log引入依赖
<!-- gson序列化 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<!-- aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.sun-common-log代码实现
1.LogAspect.java(需要更改包时就修改Pointcut的切点表达式即可)
package com.sunxiansheng.log;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Aspect
@Slf4j
@Component
@ConditionalOnProperty(name = "log.aspect.enable", havingValue = "true", matchIfMissing = true)
public class LogAspect {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final String ANSI_RESET = "\u001B[0m";
private static final String ANSI_BLUE = "\u001B[34m";
private static final String ANSI_YELLOW = "\u001B[33m";
private static final String ANSI_GREEN = "\u001B[32m";
private static final String ANSI_PURPLE = "\u001B[35m";
/**
* 配置切点,切Controller和Service的所有方法
*/
@Pointcut("execution(* com.sunxiansheng.*.controller.*Controller.*(..)) || execution(* com.sunxiansheng.*.service.*Service.*(..))")
private void pointCut() {}
/**
* 环绕通知
* @param pjp
*/
@Around("pointCut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 获取参数
Object[] args = pjp.getArgs();
// 序列化为json字符串
String req = GSON.toJson(args);
// 获取方法签名
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
// 获取类名+方法名
String className = methodSignature.getDeclaringType().getName();
String methodName = methodSignature.getName();
// 获取线程信息和时间戳
String threadName = Thread.currentThread().getName();
long threadId = Thread.currentThread().getId();
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// 打印:方法名,请求参数,线程信息,时间戳
log.info("\n========================\n" +
"时间戳:{}{}{}\n" +
"线程:{}{} (ID: {}){}\n" +
"方法名:{}{}{}\n" +
"请求参数:{}{}{}\n" +
"========================",
ANSI_GREEN, timestamp, ANSI_RESET,
ANSI_PURPLE, threadName, threadId, ANSI_RESET,
ANSI_BLUE, className + "." + methodName, ANSI_RESET,
ANSI_YELLOW, req, ANSI_RESET);
long startTime = System.currentTimeMillis();
Object proceed;
try {
// 执行目标方法
proceed = pjp.proceed();
} catch (Throwable throwable) {
// 捕获并记录异常
log.error("\n========================\n" +
"时间戳:{}{}{}\n" +
"线程:{}{} (ID: {}){}\n" +
"方法名:{}{}{}\n" +
"执行异常:{}{}{}\n" +
"堆栈信息:{}{}\n" +
"========================",
ANSI_GREEN, timestamp, ANSI_RESET,
ANSI_PURPLE, threadName, threadId, ANSI_RESET,
ANSI_BLUE, className + "." + methodName, ANSI_RESET,
ANSI_YELLOW, throwable.getMessage(), ANSI_RESET, throwable,
ANSI_RESET);
throw throwable; // 重新抛出异常
}
// 拿到出参
String resp = GSON.toJson(proceed).replace("\n", "\n ");
long endTime = System.currentTimeMillis();
// 打印响应参数和耗时
log.info("\n========================\n" +
"时间戳:{}{}{}\n" +
"线程:{}{} (ID: {}){}\n" +
"方法名:{}{}{}\n" +
"响应参数:{}{}{}\n" +
"耗时:{}{}{} ms\n" +
"========================",
ANSI_GREEN, timestamp, ANSI_RESET,
ANSI_PURPLE, threadName, threadId, ANSI_RESET,
ANSI_BLUE, className + "." + methodName, ANSI_RESET,
ANSI_YELLOW, "\n " + resp, ANSI_RESET,
ANSI_GREEN, endTime - startTime, ANSI_RESET);
// 返回目标方法的执行结果
return proceed;
}
}
2.log4j2-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出 -->
<!-- monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数 -->
<configuration monitorInterval="5">
<!-- 日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!-- 变量配置 -->
<Properties>
<!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符 -->
<!-- %logger{36} 表示 Logger 名字最长36个字符 -->
<property name="LOG_PATTERN"
value="%clr{%d{yyyy-MM-dd HH:mm:ss.SSS}}{faint} %clr{%5p} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%c{1.}.%M(%L)}{cyan} %clr{:}{faint} %m%n%xwEx" />
<!-- 定义日志存储的路径,不要配置相对路径 -->
<property name="FILE_PATH" value="./logs" />
<property name="FILE_NAME" value="sun-user-log" />
</Properties>
<appenders>
<console name="Console" target="SYSTEM_OUT">
<!-- 输出日志的格式 -->
<PatternLayout pattern="${LOG_PATTERN}" />
<!-- 控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY" />
</console>
<!-- 文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用 -->
<File name="Filelog" fileName="${FILE_PATH}/test.log" append="false">
<PatternLayout pattern="${LOG_PATTERN}" />
</File>
<!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档 -->
<RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/info.log"
filePattern="${FILE_PATH}/${FILE_NAME}-INFO-%d{yyyy-MM-dd}_%i.log.gz">
<!-- 控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY" />
<PatternLayout pattern="${LOG_PATTERN}" />
<Policies>
<!-- interval属性用来指定多久滚动一次,默认是1 hour -->
<TimeBasedTriggeringPolicy interval="1" />
<SizeBasedTriggeringPolicy size="10MB" />
</Policies>
<!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖 -->
<DefaultRolloverStrategy max="15" />
</RollingFile>
<!-- 这个会打印出所有的warn及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档 -->
<RollingFile name="RollingFileWarn" fileName="${FILE_PATH}/warn.log"
filePattern="${FILE_PATH}/${FILE_NAME}-WARN-%d{yyyy-MM-dd}_%i.log.gz">
<!-- 控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->
<ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY" />
<PatternLayout pattern="${LOG_PATTERN}" />
<Policies>
<!-- interval属性用来指定多久滚动一次,默认是1 hour -->
<TimeBasedTriggeringPolicy interval="1" />
<SizeBasedTriggeringPolicy size="10MB" />
</Policies>
<!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖 -->
<DefaultRolloverStrategy max="15" />
</RollingFile>
<!-- 这个会打印出所有的error及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档 -->
<RollingFile name="RollingFileError" fileName="${FILE_PATH}/error.log"
filePattern="${FILE_PATH}/${FILE_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz">
<!-- 控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->
<ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY" />
<PatternLayout pattern="${LOG_PATTERN}" />
<Policies>
<!-- interval属性用来指定多久滚动一次,默认是1 hour -->
<TimeBasedTriggeringPolicy interval="1" />
<SizeBasedTriggeringPolicy size="10MB" />
</Policies>
<!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖 -->
<DefaultRolloverStrategy max="15" />
</RollingFile>
</appenders>
<!-- Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等 -->
<!-- 然后定义loggers,只有定义了logger并引入的appender,appender才会生效 -->
<loggers>
<!-- 过滤掉spring和mybatis的一些无用的DEBUG信息 -->
<logger name="org.mybatis" level="info" additivity="false">
<AppenderRef ref="Console" />
</logger>
<!-- 监控系统信息 -->
<!-- 若是additivity设为false,则 子Logger 只会在自己的appender里输出,而不会在 父Logger 的appender里输出 -->
<Logger name="org.springframework" level="info" additivity="false">
<AppenderRef ref="Console" />
</Logger>
<!-- 异步日志 -->
<AsyncLogger name="com.sunxiansheng" level="info" additivity="false">
<AppenderRef ref="RollingFileInfo" />
<AppenderRef ref="Console" />
</AsyncLogger>
<AsyncRoot level="info">
<AppenderRef ref="Console" />
<AppenderRef ref="Filelog" />
<AppenderRef ref="RollingFileInfo" />
<AppenderRef ref="RollingFileWarn" />
<AppenderRef ref="RollingFileError" />
</AsyncRoot>
<!-- 配置异步日志时注释掉这个 -->
<!-- <root level="info"> -->
<!-- <appender-ref ref="Console" /> -->
<!-- <appender-ref ref="Filelog" /> -->
<!-- <appender-ref ref="RollingFileInfo" /> -->
<!-- <appender-ref ref="RollingFileWarn" /> -->
<!-- <appender-ref ref="RollingFileError" /> -->
<!-- </root> -->
</loggers>
</configuration>
3.效果展示
3.SQL优化器升级
1.目录
2.SqlBeautyInterceptor.java
package com.sunxiansheng.inteceptor;
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.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.defaults.DefaultSqlSession.StrictMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.sql.Statement;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
/**
* SQL优化器:显示完整的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})
})
public class SqlBeautyInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(SqlBeautyInterceptor.class);
private static final Set<Class<?>> PRIMITIVE_WRAPPER_CLASSES = new HashSet<>(Arrays.asList(
Byte.class, Short.class, Integer.class, Long.class, Double.class, Float.class, Character.class, Boolean.class));
private static final String ANSI_RESET = "\u001B[0m";
private static final String ANSI_YELLOW = "\u001B[33m";
private static final String ANSI_GREEN = "\u001B[32m";
private static final String ANSI_BLUE = "\u001B[34m";
private static final String ANSI_RED = "\u001B[31m";
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
String mapperMethod = mappedStatement.getId();
long startTime = System.currentTimeMillis();
String threadName = Thread.currentThread().getName();
long threadId = Thread.currentThread().getId();
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
try {
return invocation.proceed();
} catch (Throwable throwable) {
logger.error("SQL execution error: ", throwable);
throw throwable;
} finally {
long endTime = System.currentTimeMillis();
long sqlCost = endTime - startTime;
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappingList = boundSql.getParameterMappings();
String formattedSql = sql;
try {
formattedSql = formatSql(sql, parameterObject, parameterMappingList);
formattedSql = beautifySql(formattedSql);
} catch (Exception e) {
logger.error("Error formatting SQL: ", e);
formattedSql = formatSql(sql, parameterObject, parameterMappingList); // 如果格式化失败,返回未格式化但填充了数据的SQL
}
logger.info("\n========================\n线程信息:{}{} (ID: {}){}\n时间戳:{}{}{}\n堆栈信息:{}\nMapper方法:{}{}{}\nSQL:\n{}{}{}\n执行耗时: {}{}{} ms\n========================",
ANSI_RED, threadName, threadId, ANSI_RESET,
ANSI_GREEN, timestamp, ANSI_RESET,
Arrays.toString(Thread.currentThread().getStackTrace()),
ANSI_BLUE, mapperMethod, ANSI_RESET, ANSI_YELLOW, formattedSql, ANSI_RESET, ANSI_GREEN, sqlCost, ANSI_RESET);
}
}
private String getMapperMethod(StatementHandler statementHandler) {
try {
Field mappedStatementField = statementHandler.getClass().getDeclaredField("mappedStatement");
mappedStatementField.setAccessible(true);
MappedStatement mappedStatement = (MappedStatement) mappedStatementField.get(statementHandler);
return mappedStatement.getId();
} catch (Exception e) {
logger.error("Error getting mapper method: ", e);
return "Unknown method";
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
private String formatSql(String sql, Object parameterObject, List<ParameterMapping> parameterMappingList) {
if (sql == null || sql.trim().isEmpty()) {
return "";
}
if (parameterObject == null || parameterMappingList == null || parameterMappingList.isEmpty()) {
return sql;
}
String sqlWithoutReplacePlaceholder = sql;
try {
if (isStrictMap(parameterObject.getClass())) {
StrictMap<?> strictMap = (StrictMap<?>) parameterObject;
if (isList(strictMap.get("list").getClass())) {
sql = handleListParameter(sql, (List<?>) strictMap.get("list"));
}
} else if (isMap(parameterObject.getClass())) {
sql = handleMapParameter(sql, (Map<?, ?>) parameterObject, parameterMappingList);
} else {
sql = handleCommonParameter(sql, parameterMappingList, parameterObject);
}
} catch (Exception e) {
logger.error("Error formatting SQL: ", e);
return sqlWithoutReplacePlaceholder; // 返回未格式化但填充了数据的SQL
}
return sql;
}
private String handleCommonParameter(String sql, List<ParameterMapping> parameterMappingList, Object parameterObject) throws Exception {
Class<?> parameterObjectClass = parameterObject.getClass();
List<Field> allFields = new ArrayList<>();
while (parameterObjectClass != null) {
allFields.addAll(Arrays.asList(parameterObjectClass.getDeclaredFields()));
parameterObjectClass = parameterObjectClass.getSuperclass();
}
for (ParameterMapping parameterMapping : parameterMappingList) {
String propertyValue = null;
String propertyName = parameterMapping.getProperty();
Field field = allFields.stream().filter(f -> f.getName().equals(propertyName)).findFirst().orElse(null);
if (field != null) {
field.setAccessible(true);
Object value = field.get(parameterObject);
propertyValue = formatParameterValue(value);
logger.debug("Parameter name: {}, value: {}, type: {}", propertyName, propertyValue, value != null ? value.getClass().getSimpleName() : "null");
} else if (isPrimitiveOrPrimitiveWrapper(parameterObject.getClass())) {
propertyValue = parameterObject.toString();
logger.debug("Primitive parameter: {}, value: {}", propertyName, propertyValue);
}
if (propertyValue != null) {
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(propertyValue));
}
}
return sql;
}
private String formatParameterValue(Object value) {
if (value == null) {
return "null";
} else if (value instanceof String) {
return "\"" + value + "\"";
} else if (value instanceof LocalDateTime) {
return "\"" + value.toString() + "\"";
} else if (value instanceof Date) {
return "\"" + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(value) + "\"";
} else if (value instanceof Boolean) {
return (Boolean) value ? "1" : "0";
} else if (value instanceof Number) {
return value.toString();
} else {
return value.toString();
}
}
private String handleMapParameter(String sql, Map<?, ?> paramMap, List<ParameterMapping> parameterMappingList) {
for (ParameterMapping parameterMapping : parameterMappingList) {
String propertyName = parameterMapping.getProperty();
Object propertyValue = getNestedParamValue(paramMap, propertyName);
if (propertyValue == null) {
logger.warn("Parameter '{}' not found in parameter map. Available parameters: {}", propertyName, paramMap.keySet());
continue;
}
String formattedValue = formatParameterValue(propertyValue);
logger.debug("Parameter name: {}, value: {}, type: {}", propertyName, formattedValue, propertyValue.getClass().getSimpleName());
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(formattedValue));
}
return sql;
}
private Object getNestedParamValue(Map<?, ?> paramMap, String propertyName) {
if (propertyName.contains(".")) {
String[] nestedProperties = propertyName.split("\\.");
Object value = paramMap;
for (String prop : nestedProperties) {
if (value instanceof Map) {
value = ((Map<?, ?>) value).get(prop);
} else {
// 如果不是Map,尝试通过反射获取属性值
try {
Field field = value.getClass().getDeclaredField(prop);
field.setAccessible(true);
value = field.get(value);
} catch (NoSuchFieldException | IllegalAccessException e) {
logger.error("Error accessing nested property '{}' on '{}'", prop, value.getClass().getName(), e);
return null;
}
}
if (value == null) {
return null;
}
}
return value;
} else {
return paramMap.get(propertyName);
}
}
private String handleListParameter(String sql, Collection<?> col) {
if (col != null && !col.isEmpty()) {
for (Object obj : col) {
String value = obj.toString();
if (obj.getClass().isAssignableFrom(String.class)) {
value = "\"" + value + "\"";
}
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(value));
}
}
return sql;
}
private String beautifySql(String sql) {
// Remove excess whitespace
sql = sql.replaceAll("[\\s\n]+", " ").trim();
// Format common SQL keywords and clauses
sql = sql.replaceAll("(?i)\\b(SELECT|INSERT INTO|UPDATE|DELETE FROM|FROM|WHERE|SET|VALUES|LEFT JOIN|RIGHT JOIN|INNER JOIN|OUTER JOIN|GROUP BY|ORDER BY|HAVING|LIMIT|OFFSET|UNION|UNION ALL|JOIN|ON|USING|DISTINCT|EXISTS|IN|IS NULL|IS NOT NULL|NOT IN|LIKE|BETWEEN|AND|OR|CASE|WHEN|THEN|ELSE|END)\\b", "\n$1");
// Handle INSERT INTO and VALUES statements and parenthesis formatting
sql = sql.replaceAll("(?i)\\b(INSERT INTO [^\\(]+\\()", "\n$1\n ");
sql = sql.replaceAll("(?i)\\b(VALUES)\\s*\\(", "\n$1\n (");
sql = sql.replaceFirst("\\)\\s*VALUES", "\n)\nVALUES");
// Handle subqueries and nested queries
sql = sql.replaceAll("(?i)\\b(SELECT)\\b", "\n$1");
sql = sql.replaceAll("(?i)\\b(FROM)\\b", "\n$1");
sql = sql.replaceAll("(?i)\\b(WHERE)\\b", "\n$1");
sql = sql.replaceAll("(?i)\\b(GROUP BY)\\b", "\n$1");
sql = sql.replaceAll("(?i)\\b(ORDER BY)\\b", "\n$1");
// Handle indentation for subqueries
sql = sql.replaceAll("\\(\\s*SELECT", "(\n SELECT");
sql = sql.replaceAll("\\)\\s*\\b", ")");
// Split the SQL into lines and format
String[] lines = sql.split("\n");
StringBuilder formattedSql = new StringBuilder();
int indentLevel = 0;
for (String line : lines) {
if (line.matches("(?i)^\\s*(SELECT|INSERT INTO|UPDATE|DELETE FROM|FROM|WHERE|SET|VALUES|LEFT JOIN|RIGHT JOIN|INNER JOIN|OUTER JOIN|GROUP BY|ORDER BY|HAVING|LIMIT|OFFSET|UNION|UNION ALL|JOIN|ON|USING|DISTINCT|EXISTS|IN|IS NULL|IS NOT NULL|NOT IN|LIKE|BETWEEN|AND|OR|CASE|WHEN|THEN|ELSE|END)\\b.*")) {
if (line.matches("(?i)^\\s*(FROM|WHERE|SET|VALUES|LEFT JOIN|RIGHT JOIN|INNER JOIN|OUTER JOIN|GROUP BY|ORDER BY|HAVING|LIMIT|OFFSET|UNION|UNION ALL|JOIN|ON|USING|DISTINCT|EXISTS|IN|IS NULL|IS NOT NULL|NOT IN|LIKE|BETWEEN|AND|OR|CASE|WHEN|THEN|ELSE|END)\\b.*")) {
indentLevel--;
}
formattedSql.append(repeat(" ", indentLevel)).append(line.trim()).append("\n");
if (line.matches("(?i)^\\s*(SELECT|INSERT INTO|UPDATE|DELETE FROM)\\b.*")) {
indentLevel++;
}
} else {
formattedSql.append(repeat(" ", indentLevel)).append(line.trim()).append("\n");
}
}
// Special handling for closing parenthesis
formattedSql = new StringBuilder(formattedSql.toString().replace(") VALUES", ") VALUES"));
formattedSql = new StringBuilder(formattedSql.toString().replaceAll("\\),", "),"));
// Remove trailing spaces and tabs
return formattedSql.toString().replaceAll("\n{2,}", "\n").trim();
}
private String repeat(String str, int count) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < count; i++) {
result.append(str);
}
return result.toString();
}
private boolean isPrimitiveOrPrimitiveWrapper(Class<?> clazz) {
return clazz.isPrimitive() || PRIMITIVE_WRAPPER_CLASSES.contains(clazz);
}
private boolean isStrictMap(Class<?> clazz) {
return StrictMap.class.isAssignableFrom(clazz);
}
private boolean isList(Class<?> clazz) {
return List.class.isAssignableFrom(clazz);
}
private boolean isMap(Class<?> clazz) {
return Map.class.isAssignableFrom(clazz);
}
}
3.MybatisConfiguration.java SQL优化器注入Bean容器
package com.sunxiansheng.config;
import com.sunxiansheng.inteceptor.SqlBeautyInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis配置类:用于将Bean注入容器
*/
@Configuration
public class MybatisConfiguration {
// SQL优化器注入Bean容器
@Bean
// 只有当 sql.beauty.show 的值为 "true" 时,相关的配置或 bean 才会被注册。
// matchIfMissing: 如果设置为 true,这意味着如果在配置中没有找到 name 指定的属性,则条件视为匹配。
@ConditionalOnProperty(name = {"sql.beauty.show"}, havingValue = "true", matchIfMissing = true)
public SqlBeautyInterceptor sqlBeautyInterceptor() {
return new SqlBeautyInterceptor();
}
}
4.application.yml开启sql优化器
# 自定义sql优化器
sql:
beauty:
show: true # 配置true或者不配置,就是启用sql优化器,配置其他的都是不启用
5.效果展示
4.遇到的bug
1.mapstruct失效(在build内配置annotationProcessorPaths)
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<!-- Lombok 注解处理器 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<!-- MapStruct 注解处理器 -->
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>