当前位置: 首页 > article >正文

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>

CleanShot 2024-07-21 at 10.11.27@2x

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.效果展示

CleanShot 2024-07-21 at 13.18.32@2x

3.SQL优化器升级

1.目录

CleanShot 2024-07-21 at 13.26.27@2x

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.效果展示

CleanShot 2024-07-21 at 13.57.11@2x

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>

CleanShot 2024-07-21 at 14.26.46@2x


http://www.kler.cn/a/442040.html

相关文章:

  • Amazon MSK 开启 Public 访问 SASL 配置的方法
  • 记录node-sass无法安装的问题
  • 嵌入式硬件篇---基本组合逻辑电路
  • 创建 pdf 合同模板
  • 【Linux系统】Ext系列磁盘文件系统二:引入文件系统(续篇)
  • 介绍下常用的前端框架及时优缺点
  • NFT市场回暖:蓝筹项目成为复苏主力,空投潮助推价格上涨
  • Android 13 Aosp SystemServer功能裁剪(PackageManager.hasSystemFeature())
  • Jenkins搭建并与Harbor集成上传镜像
  • 如何查看K8S集群中service和pod定义的网段范围
  • 备战美赛!2025美赛数学建模C题模拟预测!用于大家练手模拟!
  • Python之公共操作篇
  • AirSim 无人机不同视角采集不同场景的图片
  • SEO短视频矩阵系统源码开发概述
  • 域名历史是什么?怎么进行域名历史查询?
  • gorm源码解析(四):事务,预编译
  • Java基础知识(四) -- 面向对象(中)
  • 量化交易实操入门
  • SQLite 安装与使用
  • vue的elementUI 给输入框绑定enter事件失效
  • 【C语言】指针数组和数组指针
  • 25上半年软考《电子商务设计师》,备考大纲已出!
  • 为什么 Teams 中搜索不到 Power Automate
  • 电脑开机提示error loading operating system怎么修复?
  • 新手谷歌浏览器的使用(使用国内的搜索引擎)
  • lc238除自身以外数组的乘积——动态规划前缀积