log4j2日志的使用

本文介绍了如何在Spring Boot项目中整合SLF4J与log4j2,避免直接依赖冲突,并详细讲解了log4j2配置,包括日志级别、输出器及自定义路径。重点在于如何动态设置日志路径和文件名,以及使用MDC实现应用环境参数传递。
摘要由CSDN通过智能技术生成

一、日志框架之间的关系

SLF4J,即简单日志门面(Simple Logging Facade for Java),不是具体的日志解决方案,而是通过Facade Pattern提供一些Java logging API,实际上,SLF4J所提供的核心API是一些接口以及一个LoggerFactory的工厂类。在使用SLF4J的时候,不需要在代码中或配置文件中指定你打算使用那个具体的日志系统。应该如下面代码这样写,好处是如果后面想要换具体的日志框架是,记日志代码的地方不用变动。

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

private static Logger logger = LogManager.getLogger(Xxx.class);

log4j2、logback都只是slf4j的实现框架。

二、log4j2的具体使用

集成log4j2日志框架需要注意的问题:

直接引入log4j2的依赖的话,会报如下的错。

Caused by: org.apache.logging.log4j.LoggingException: log4j-slf4j-impl cannot be present with log4j-to-slf4j
	at org.apache.logging.slf4j.Log4jLoggerFactory.validateContext(Log4jLoggerFactory.java:49)
	at org.apache.logging.slf4j.Log4jLoggerFactory.newLogger(Log4jLoggerFactory.java:39)
	at org.apache.logging.slf4j.Log4jLoggerFactory.newLogger(Log4jLoggerFactory.java:30)
	at org.apache.logging.log4j.spi.AbstractLoggerAdapter.getLogger(AbstractLoggerAdapter.java:54)
	at org.apache.logging.slf4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:30)
	at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:363)
	at org.apache.commons.logging.LogAdapter$Slf4jAdapter.createLocationAwareLog(LogAdapter.java:130)
	at org.apache.commons.logging.LogAdapter.createLog(LogAdapter.java:91)
	at org.apache.commons.logging.LogFactory.getLog(LogFactory.java:67)
	at org.apache.commons.logging.LogFactory.getLog(LogFactory.java:59)
	at org.springframework.boot.SpringApplication.<clinit>(SpringApplication.java:196)
	... 1 more

在创建Spring Boot工程时,我们引入了spring-boot-starter,其中包含了spring-boot-starter-logging,该依赖内容就是Spring Boot默认的日志框架Logback,所以我们在引入log4j2之前,需要先排除该包的依赖,再引入log4j2的依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</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>

log4j2的配置文件示例如下:

<?xml version="1.0" encoding="UTF-8"?>

<!-- status关闭log4j2自身的日志输出
monitorInterval log4j能够自动检测修改配置文件和重新配置本身,设置间隔秒数
 -->
<configuration status="OFF">
  <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->

  <!--变量配置-->
  <Properties>
    <!-- 格式化输出:
		%date表示日期,
		%thread表示线程名,
		%-5level:级别从左显示5个字符宽度
		%msg:日志消息,
		%n是换行符-->
    <!-- %logger{36} 表示 Logger 名字最长36个字符 -->
    <property name="LOG_PATTERN" value="%date{yy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" />
    <!-- 定义日志存储的路径,不要配置相对路径 -->
    <property name="FILE_PATH" value="./logs/${ctx:logPath}" />
	<!-- 定义日志文件的名称 -->
    <property name="FILE_NAME" value="${ctx:logName}" />
  </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}/${FILE_NAME}_test.log" append="false">
      <PatternLayout pattern="${LOG_PATTERN}"/>
    </File>

    <!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
    <RollingFile name="RollingFileDebug" fileName="${FILE_PATH}/${FILE_NAME}_debug.log" filePattern="${FILE_PATH}/${FILE_NAME}-DEBUG-%d{yyyy-MM-dd}_%i.log.gz">
      <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
      <ThresholdFilter level="debug" 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>

    <!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
    <RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/${FILE_NAME}_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}/${FILE_NAME}_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}/${FILE_NAME}_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>
    <root level="info">
      <appender-ref ref="Console"/>
      <appender-ref ref="Filelog"/>
      <appender-ref ref="RollingFileInfo"/>
      <appender-ref ref="RollingFileWarn"/>
      <appender-ref ref="RollingFileError"/>
    </root>

    <logger name="com.ken.data.mapper" level="info" additivity="false">
    </logger>

    <logger name="com.ken" level="debug" additivity="false">
      <appender-ref ref="Console"/>
    </logger>
  </loggers>

</configuration>

application.yml中引入该配置文件, classpath相当于resources文件夹:

logging:
  config: classpath:log/log4j2.xml  #配置日志文件路径
  file:  
    name: demo  #设置日志的文件名称

其中重要的配置片段:

<!-- 定义日志存储的路径,不要配置相对路径 -->
<property name="FILE_PATH" value="./logs/${ctx:logPath}" />
<!-- 定义日志文件的名称 -->
<property name="FILE_NAME" value="${ctx:logName}" />

也就是日志存储的路径和日志文件名称不能写死了,需要根据微服务不同的工程来动态的指定。

如果想要读取微服务工程的spring.application.name作为logPath(日志存储的路径),logging.file.name作为logName(日志文件的名称)。

问题是如何在加载application.yml文件后,在加载日志配置文件log4j2.xml之前读取并设置进去。

使用MDC存储上面说的k-v键值对,配合监听sprign的事件,微服务工程启动后,加载好application.yml时,会自动广播事件。

MDC:

public class MDC {

    static MDCAdapter mdcAdapter;

MDCAdapter实现类:

image-20220116135716806

import org.slf4j.MDC;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.GenericApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.Map;

/**
 * SpringBoot事件的监听器
 *
 * ApplicationEnvironmentPreparedEvent - 应用环境准备事件
 */
public class LogMDCListener implements GenericApplicationListener {

    private static final String APPLICATION_CONFIG_NAME = "configurationProperties";

    private static final String LOG_NAME = "logging.file.name";

    private static final String LOG_PATH = "spring.application.name";

    private static final Map<String, String> cache = new HashMap<>();

    /**
     * 设置当前要监听什么类型的事件
     * @param eventType
     * @return
     */
    @Override
    public boolean supportsEventType(ResolvableType eventType) {
        // ApplicationEnvironmentPreparedEvent来源于springboot
        //A.class.isAssignableFrom(B.class)
        //如果A类型是B类型的父类(接口、同一个类型),则返回true
        return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType.getRawClass());
    }

    /**
     * 监听到具体事件的处理方法
     * @param event
     */
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        String logName = cache.get("logName");
        String logPath = cache.get("logPath");
        //第一次会去读取环境变量中的属性
        if (StringUtils.isEmpty(logName) || StringUtils.isEmpty(logPath)) {
            //获得当前微服务的名字
            ApplicationEnvironmentPreparedEvent environmentPreparedEvent = (ApplicationEnvironmentPreparedEvent) event;
            //获得配置环境对象
            ConfigurableEnvironment environment = environmentPreparedEvent.getEnvironment();
            //获取属性源集合 - SpringBoot的各种属性来源(application.yml...)
            MutablePropertySources propertySources = environment.getPropertySources();

            PropertySource<?> propertySource = propertySources.get(APPLICATION_CONFIG_NAME);
            logName = (String) propertySource.getProperty(LOG_NAME);
            logPath = (String) propertySource.getProperty(LOG_PATH);
            //如果没有设置日志文件名称,则默认采用微服务的名称作为日志文件名
            if (StringUtils.isEmpty(logName)) {
                logName = logPath;
            }

            //有值之后进行缓存
            cache.put("logName", logName);
            cache.put("logPath", logPath);
        }

        //设置MDC
        MDC.put("logPath", logPath);
        MDC.put("logName", logName);
    }

    /**
     * 控制监听器的顺序 值越小,优先级越高
     * @return
     */
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 19;
    }
}

LogMDCListener配置给spring的事件监听:

resources/META-INF/spring.factories配置如下。

org.springframework.context.ApplicationListener=\
com.ken.common.web.log.LogMDCListener

给LogMDCListener 配置合适的order:

LoggingApplicationListener部分代码如下。

public class LoggingApplicationListener implements GenericApplicationListener {
    /**
	 * The default order for the LoggingApplicationListener.
	 */
	public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 20;
    
    private int order = DEFAULT_ORDER;
    
	@Override
	public int getOrder() {
		return this.order;
	}

LoggingApplicationListener的order为Ordered.HIGHEST_PRECEDENCE + 20,所以想要LogMDCListener在LoggingApplicationListener之前执行,就得配置合适的order,order值越小,优先级越高,所以配置Ordered.HIGHEST_PRECEDENCE + 19。

参考的工程:https://gitee.com/wlkken/ken_framework_pom

参考课程:https://www.bilibili.com/video/BV1hQ4y1z78B?p=20

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值