weixin_40812348 2025-11-20 19:38 采纳率: 0%
浏览 9

log4j2自定义插件导致控制台日志重复打印

SpringBoots3.5.7版本整合log4j2,配置自定义日志插件用于处理关键字脱敏后,控制台日志打印了两遍,一遍是自定义格式,一遍是默认格式,找了半天也没找到问题在哪里,求解决方案
相关maven依赖如下:

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

log4j2.xml配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration status="INFO" monitorInterval="30">
    <!--<Configuration status="WARN" monitorInterval="30"> -->
    <properties>
        <property name="LOG_HOME">./service-logs</property>
    </properties>
    <Appenders>
        <!--*********************控制台日志***********************-->
        <Console name="consoleAppender" target="SYSTEM_OUT">
            <!--设置日志格式及颜色-->
            <!--            <PatternLayout-->
            <!--                    pattern="%d %p %m%n %style{%d{ISO8601}}{bright,green} %highlight{%-5level} [%style{%t}{bright,blue}] %style{%C{}}{bright,yellow}: %msg%n%style{%throwable}{red}"-->
            <!--                    disableAnsi="false" noConsoleNoAnsi="false"/>-->
            <CustomPatternLayout
                    pattern="%d %p %m%n %style{%d{ISO8601}}{bright,green} %highlight{%-5level} [%style{%t}{bright,blue}] %style{%C{}}{bright,yellow}: %msg%n%style{%throwable}{red}"
                    disableAnsi="false" noConsoleNoAnsi="false" charset="UTF-8"/>
        </Console>

        <!--*********************文件日志***********************-->
        <!--debug级别日志-->
        <RollingFile name="debugFileAppender"
                     fileName="${LOG_HOME}/debug.log"
                     filePattern="${LOG_HOME}/$${date:yyyy-MM-dd}/debug-%d{yyyy-MM-dd}-%i.log.gz">
            <Filters>
                <!--过滤掉info及更高级别日志-->
                <ThresholdFilter level="info" onMatch="DENY" onMismatch="NEUTRAL"/>
            </Filters>
            <!--设置日志格式-->
            <CustomPatternLayout
                    pattern="%d %p %m%n %style{%d{ISO8601}}{bright,green} %highlight{%-5level} [%style{%t}{bright,blue}] %style{%C{}}{bright,yellow}: %msg%n%style{%throwable}{red}"
                    disableAnsi="false" noConsoleNoAnsi="false" charset="UTF-8"/>
            <Policies>
                <!-- 设置日志文件切分参数 -->
                <!--<OnStartupTriggeringPolicy/>-->
                <!--设置日志基础文件大小,超过该大小就触发日志文件滚动更新-->
                <SizeBasedTriggeringPolicy size="100 MB"/>
                <!--设置日志文件滚动更新的时间,依赖于文件命名filePattern的设置-->
                <TimeBasedTriggeringPolicy/>
            </Policies>
            <!--设置日志的文件个数上限,不设置默认为7个,超过大小后会被覆盖;依赖于filePattern中的%i-->
            <DefaultRolloverStrategy max="100"/>
        </RollingFile>

        <!--info级别日志-->
        <RollingFile name="infoFileAppender"
                     fileName="${LOG_HOME}/info.log"
                     filePattern="${LOG_HOME}/$${date:yyyy-MM-dd}/info-%d{yyyy-MM-dd}-%i.log.gz">
            <Filters>
                <!--过滤掉warn及更高级别日志-->
                <ThresholdFilter level="warn" onMatch="DENY" onMismatch="NEUTRAL"/>
            </Filters>
            <!--设置日志格式-->
            <CustomPatternLayout
                    pattern="%d %p %m%n %style{%d{ISO8601}}{bright,green} %highlight{%-5level} [%style{%t}{bright,blue}] %style{%C{}}{bright,yellow}: %msg%n%style{%throwable}{red}"
                    disableAnsi="false" noConsoleNoAnsi="false" charset="UTF-8"/>
            <Policies>
                <!-- 设置日志文件切分参数 -->
                <!--<OnStartupTriggeringPolicy/>-->
                <!--设置日志基础文件大小,超过该大小就触发日志文件滚动更新-->
                <!--            <SizeBasedTriggeringPolicy size="100 MB"/>-->
                <!--设置日志文件滚动更新的时间,依赖于文件命名filePattern的设置-->
                <TimeBasedTriggeringPolicy interval="6" modulate="true"/>
            </Policies>
            <!--设置日志的文件个数上限,不设置默认为7个,超过大小后会被覆盖;依赖于filePattern中的%i-->
            <!--<DefaultRolloverStrategy max="100"/>-->
        </RollingFile>

        <!--warn级别日志-->
        <RollingFile name="warnFileAppender"
                     fileName="${LOG_HOME}/warn.log"
                     filePattern="${LOG_HOME}/$${date:yyyy-MM-dd}/warn-%d{yyyy-MM-dd}-%i.log.gz">
            <Filters>
                <!--过滤掉error及更高级别日志-->
                <ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/>
            </Filters>
            <!--设置日志格式-->
            <CustomPatternLayout
                    pattern="%d %p %m%n %style{%d{ISO8601}}{bright,green} %highlight{%-5level} [%style{%t}{bright,blue}] %style{%C{}}{bright,yellow}: %msg%n%style{%throwable}{red}"
                    disableAnsi="false" noConsoleNoAnsi="false" charset="UTF-8"/>
            <Policies>
                <!-- 设置日志文件切分参数 -->
                <!--<OnStartupTriggeringPolicy/>-->
                <!--设置日志基础文件大小,超过该大小就触发日志文件滚动更新-->
                <SizeBasedTriggeringPolicy size="100 MB"/>
                <!--设置日志文件滚动更新的时间,依赖于文件命名filePattern的设置-->
                <TimeBasedTriggeringPolicy/>
            </Policies>
            <!--设置日志的文件个数上限,不设置默认为7个,超过大小后会被覆盖;依赖于filePattern中的%i-->
            <DefaultRolloverStrategy max="100"/>
        </RollingFile>

        <!--error及更高级别日志-->
        <RollingFile name="errorFileAppender"
                     fileName="${LOG_HOME}/error.log"
                     filePattern="${LOG_HOME}/$${date:yyyy-MM-dd}/error-%d{yyyy-MM-dd}-%i.log.gz">
            <!--设置日志格式-->
            <CustomPatternLayout
                    pattern="%d %p %m%n %style{%d{ISO8601}}{bright,green} %highlight{%-5level} [%style{%t}{bright,blue}] %style{%C{}}{bright,yellow}: %msg%n%style{%throwable}{red}"
                    disableAnsi="false" noConsoleNoAnsi="false" charset="UTF-8"/>
            <Policies>
                <!-- 设置日志文件切分参数 -->
                <!--<OnStartupTriggeringPolicy/>-->
                <!--设置日志基础文件大小,超过该大小就触发日志文件滚动更新-->
                <SizeBasedTriggeringPolicy size="100 MB"/>
                <!--设置日志文件滚动更新的时间,依赖于文件命名filePattern的设置-->
                <TimeBasedTriggeringPolicy/>
            </Policies>
            <!--设置日志的文件个数上限,不设置默认为7个,超过大小后会被覆盖;依赖于filePattern中的%i-->
            <DefaultRolloverStrategy max="100"/>
        </RollingFile>

    </Appenders>

    <Loggers>

        <!--spring日志-->
        <!--        <Logger name="org.springframework" level="info"/>-->
        <!-- mybatis日志 -->
        <Logger name="com.mybatis" level="info"/>
        <Logger name="org.hibernate" level="info"/>
        <Logger name="com.zaxxer.hikari" level="info"/>
        <Logger name="org.quartz" level="info"/>
        <logger name="com.scb.comgm" level="debug"/>
        <!-- 根日志设置 -->
        <Root level="info">
    
            <AppenderRef ref="consoleAppender" level="info"/>
            <AppenderRef ref="debugFileAppender" level="debug"/>
            <AppenderRef ref="infoFileAppender" level="info"/>
            <AppenderRef ref="warnFileAppender" level="warn"/>
            <AppenderRef ref="errorFileAppender" level="error"/>

        </Root>
    </Loggers>

</configuration>

自定义插件内容如下:


package com.scb.comgm.config.plugs;


import cn.hutool.core.map.MapUtil;
import com.scb.comgm.config.SensitiveDataRule;
import com.scb.comgm.util.StringUtils;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Node;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.layout.AbstractStringLayout;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.apache.logging.log4j.util.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * log4j2 脱敏插件
 * 继承AbstractStringLayout
 **/
@Plugin(name = "CustomPatternLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
public class CustomPatternLayout extends AbstractStringLayout {

    public final static Logger logger = LoggerFactory.getLogger(CustomPatternLayout.class);
    /**
     * 要匹配的正则表达式map
     */
    private static final Map<String, Pattern> REG_PATTERN_MAP = new HashMap<>();
    private static final Map<String, String> KEY_REG_MAP = new HashMap<>();
    private final PatternLayout patternLayout;

    protected CustomPatternLayout(Charset charset, String pattern, Boolean disableAnsi, Boolean noConsoleNoAnsi) {
        super(charset);

        patternLayout = PatternLayout.newBuilder().withDisableAnsi(disableAnsi)
                .withNoConsoleNoAnsi(noConsoleNoAnsi).withPattern(pattern).build();
    }

    /**
     * 创建插件
     */
    @PluginFactory
    public static Layout createLayout(@PluginAttribute(value = "pattern") final String pattern,
                                      @PluginAttribute(value = "charset") final Charset charset,
                                      @PluginAttribute(value = "disableAnsi") final Boolean disableAnsi,
                                      @PluginAttribute(value = "noConsoleNoAnsi") final Boolean noConsoleNoAnsi) {
        return new CustomPatternLayout(charset, pattern, disableAnsi, noConsoleNoAnsi);
    }

    private void initRule() {
        try {
            Map<String, String> regularMap = SensitiveDataRule.regularMap;

            if (MapUtil.isEmpty(regularMap)) {
                return;
            }
            regularMap.forEach((a, b) -> {
                if (Strings.isNotBlank(a)) {
                    Map<String, String> collect = Arrays.stream(a.split(",")).collect(Collectors.toMap(c -> c, w -> b, (key1, key2) -> key1));
                    KEY_REG_MAP.putAll(collect);
                }
                Pattern compile = Pattern.compile(b);
                REG_PATTERN_MAP.put(b, compile);
            });

        } catch (Exception e) {
            logger.info(">>>>>> 初始化日志脱敏规则失败 ERROR:{0}", e);
        }

    }

    /**
     * 处理日志信息,进行脱敏
     * 1.判断配置文件中是否已经配置需要脱敏字段
     * 2.判断内容是否有需要脱敏的敏感信息
     * 2.1 没有需要脱敏信息直接返回
     * 2.2 处理: 身份证 ,姓名,手机号,地址敏感信息
     */
    public String hideMarkLog(String logStr) {
        try {
            //若初始化日志失败,则重新初始化一次
            if (MapUtil.isEmpty(KEY_REG_MAP) || MapUtil.isEmpty(REG_PATTERN_MAP)) {
                initRule();
            }
            //1.判断配置文件中是否已经配置需要脱敏字段
            if (Strings.isBlank(logStr) || MapUtil.isEmpty(KEY_REG_MAP) || MapUtil.isEmpty(REG_PATTERN_MAP)) {
                return logStr;
            }
            //2.判断内容是否有需要脱敏的敏感信息
            Set<String> charKeys = KEY_REG_MAP.keySet();
            for (String key : charKeys) {
                if (logStr.contains(key)) {
                    String regExp = KEY_REG_MAP.get(key);
                    logStr = matchingAndEncrypt(logStr, regExp, key);
                }
            }
            return logStr;
        } catch (Exception e) {
            logger.info(">>>>>>>>> 脱敏处理异常 ERROR:{0}", e);
            //如果抛出异常为了不影响流程,直接返回原信息
            return logStr;
        }
    }

    /**
     * 正则匹配对应的对象。
     *
     * @param msg    日志对象
     * @param regExp 正则匹配
     * @param key    字段
     * @return 找到对应对象
     */
    private String matchingAndEncrypt(String msg, String regExp, String key) {
        Pattern pattern = Pattern.compile(regExp);
        Matcher matcher = pattern.matcher(msg);
        Integer hideType = SensitiveDataRule.hideStrTypeMap.entrySet().stream().filter(entry ->
                entry.getKey().contains(key)).findFirst().get().getValue();
        int length = key.length() + 5;
//        boolean names = Log4j2Rule.USER_NAME_STR.contains(key);
        String hiddenStr = "***";
        StringBuffer result = new StringBuffer(msg);
        int i = 0;
        while (matcher.find()) {
            String originStr = matcher.group();
            // 计算关键词和需要脱敏词的距离小于5。
            i = msg.indexOf(originStr, i);
            if (i < 0) {
                continue;
            }
            int span = i - length;
            int startIndex = Math.max(span, 0);
            String substring = msg.substring(startIndex, i);
            if (Strings.isBlank(substring) || !substring.contains(key)) {
                i += key.length();
                continue;
            }
            //日志脱敏只需要对密码和手机号
            hiddenStr = StringUtils.hideMarkStr(originStr, hideType);

            msg = result.replace(i, i + originStr.length(), hiddenStr).toString();
        }
        return msg;
    }

    @Override
    public String toSerializable(LogEvent event) {
        return hideMarkLog(patternLayout.toSerializable(event));
    }

}

现在控制台日志如下:

img

  • 写回答

4条回答 默认 最新

  • 檀越@新空间 2025-11-20 19:45
    关注

    晚上好🌙🌙🌙
    本答案参考通义千问

    在使用 Spring Boot 3.5.7 整合 Log4j2 并自定义插件(如 CustomPatternLayout)时,控制台日志重复打印 是一个常见问题。你提到的“一遍是自定义格式,一遍是默认格式”,说明可能存在两个不同的 Appender 同时输出日志。


    ✅ 问题分析

    1. Log4j2 的默认 Appender

    Spring Boot 默认会配置一个 ConsoleAppender,用于输出日志到控制台。如果你手动配置了另一个 ConsoleAppender(例如 consoleAppender),那么日志就会被打印两次。

    2. 自定义 PatternLayout 导致冲突

    你使用了 <CustomPatternLayout> 而不是标准的 <PatternLayout>,这可能与 Spring Boot 自带的日志系统不兼容,导致重复输出。

    3. 依赖冲突或版本不一致

    虽然你排除了 spring-boot-starter-logging,但若其他依赖引入了 Logback 或其他日志框架,也可能造成冲突。


    ✅ 解决方案

    一、移除重复的 Console Appender

    检查你的 log4j2.xml 文件,确保只保留一个 ConsoleAppender,并将其设置为默认输出。

    修改后的 log4j2.xml 示例:

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration status="INFO" monitorInterval="30">
        <properties>
            <property name="LOG_HOME">./service-logs</property>
        </properties>
    
        <Appenders>
            <!-- 控制台日志 -->
            <Console name="consoleAppender" target="SYSTEM_OUT">
                <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
            </Console>
    
            <!-- 文件日志 -->
            <RollingFile name="debugFileAppender"
                         fileName="${LOG_HOME}/debug.log"
                         filePattern="${LOG_HOME}/$${date:yyyy-MM-dd}/debug-%d{yyyy-MM-dd}-%i.log.gz">
                <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
                <Policies>
                    <SizeBasedTriggeringPolicy size="100 MB"/>
                    <TimeBasedTriggeringPolicy/>
                </Policies>
                <DefaultRolloverStrategy max="100"/>
            </RollingFile>
    
            <RollingFile name="infoFileAppender"
                         fileName="${LOG_HOME}/info.log"
                         filePattern="${LOG_HOME}/$${date:yyyy-MM-dd}/info-%d{yyyy-MM-dd}-%i.log.gz">
                <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
                <Policies>
                    <SizeBasedTriggeringPolicy size="100 MB"/>
                    <TimeBasedTriggeringPolicy/>
                </Policies>
                <DefaultRolloverStrategy max="100"/>
            </RollingFile>
        </Appenders>
    
        <Loggers>
            <Root level="info">
                <AppenderRef ref="consoleAppender"/>
                <AppenderRef ref="debugFileAppender"/>
                <AppenderRef ref="infoFileAppender"/>
            </Root>
        </Loggers>
    </configuration>
    

    重点说明:

    • 不要使用 <CustomPatternLayout>,而是使用标准的 <PatternLayout>
    • 确保只有一个 ConsoleAppender,避免重复输出。
    • 如果需要自定义格式,建议使用 <PatternLayout>pattern 属性。

    二、检查依赖是否冲突

    确保你没有引入多个日志框架(如 Logback 和 Log4j2)。查看 pom.xml 中的依赖关系,确认以下几点:

    ✅ 正确的依赖结构如下:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- 排除默认的日志框架 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
    <!-- 使用 Log4j2 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>
    

    重点说明:

    • 确保只保留 spring-boot-starter-log4j2不要引入 logback
    • 不要使用 spring-boot-starter-logging,否则可能会启用 Logback。

    三、使用自定义 Layout 的正确方式

    如果你确实需要自定义日志格式,可以使用 PatternLayout + Style 来实现颜色和样式支持,而不是 CustomPatternLayout

    示例代码(Java 配置):

    @Configuration
    public class LogConfig {
    
        @Bean
        public PatternLayout patternLayout() {
            return PatternLayout.newBuilder()
                    .setPattern("%d{HH:mm:ss.SSS} [%t] %highlight{%-5level} %logger{36} - %msg%n%style{%throwable}{red}")
                    .setDisableAnsi(false)
                    .build();
        }
    }
    

    然后在 log4j2.xml 中引用它:

    <Console name="consoleAppender" target="SYSTEM_OUT">
        <Layout ref="customLayout"/>
    </Console>
    

    注意:

    • 使用 Layout ref="customLayout" 来引用 Java 配置的 PatternLayout
    • 这样可以避免 CustomPatternLayout 引起的冲突。

    ✅ 总结

    | 问题原因 | 解决方法 | |----------|-----------| | 多个 ConsoleAppender | 只保留一个 ConsoleAppender | | 使用 CustomPatternLayout | 改用标准 PatternLayout | | 依赖冲突 | 排除 spring-boot-starter-logging,仅保留 spring-boot-starter-log4j2 | | 自定义格式需求 | 使用 PatternLayout + Style 实现 |


    如有需要,我可以提供完整的 log4j2.xml 模板或 Java 日志配置类。希望这些解决方案能帮助你解决日志重复打印的问题!

    评论

报告相同问题?

问题事件

  • 修改了问题 11月20日
  • 创建了问题 11月20日