剖析 Spring Boot 日志框架

837 阅读13分钟

对于程序而言,日志是其中非常重要的一个部分,可以说,没有日志的程序是不完整的。市面上日志解决方案很多,那 Spring Boot 提供的怎样的日志解决方案呢?我们是否可以借鉴 Spring Boot 的方案呢?本文在分析 Spring Boot 日志方案的同时,探讨以下几点内容。

  1. Java 日志体系梳理。
  2. 日志框架如何加载日志实现的。
  3. Spring Boot 日志加载流程。
  4. Spring Boot 日志配置是如何生效的?

Spring Boot 日志集成

Spring Boot 日志体系依赖于 spring-boot-starter-logging。如下所示,我们在开发的时候,并不需要主动引用这个 jar 包。因为它默认包含在了 spring-boot-starter 中。

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-logging</artifactId>
</dependency>

使用起来也很简单,在成员变量中申明一个 logger,将当前类作为参数插入工厂方法中,就可以使用 logger.info("xxx") 进行日志输出了。

private final Logger logger = LoggerFactory.getLogger(DemoInitializer.class);

我们看下 spring-boot-starter-logging 中包含了什么?

<dependencies>
  <dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
    <scope>compile</scope>
  </dependency>
  <dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-to-slf4j</artifactId>
    <version>2.13.3</version>
    <scope>compile</scope>
  </dependency>
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jul-to-slf4j</artifactId>
    <version>1.7.30</version>
    <scope>compile</scope>
  </dependency>
</dependencies>
  1. logback-classic 实现了slf4j。说明 Spring Boot 采用的是 slf4j + logback 的日志方式

  2. log4j-to-slf4j 是一个 log4j 到 slf4j 适配器,用于将使用了 log4j API 的程序重定向到 slf4j。

  3. jul-to-slf4j 和 log4j-to-slf4j 类似,实现 jul 日志重定向到 slf4j。

slf4j 全称是 Simple Logging Facade for Java。是一个 Java 日志门面框架,通俗的讲就是一个日志的抽象接口,它本身不负责日志的实现。

logback 是一个遵循 slf4j 规范的日志实现,我们调用 slf4j 接口打印的时候,slf4j 底层调用的其实是 logback。

Java 日志体系

slf4j + logback 是当前 java 日志体系中比较推荐的方式。我们按照时间顺序梳理下java 日志体系。

log4j 是最早出现的日志系统,再此之前用户只能用 java 自带的 system.out.print() 进行日志打印。log4j 出现后,作为唯一的日志系统,很快被大量的使用,成为 java 日志事实上的标准,直到现在,仍然有大量的项目使用 log4j。

jdk 不甘寂寞,在 1.4 中增加了 JUL(Java util logging) 日志实现,由于 JUL 内置于 jdk 中,无需引用任何 jar 包,加上 sun 公司的光环,在当时吸引了很多用户。但是 JUL 的使用存在一些缺陷,现在应该很少有人使用 JUL 了。

鉴于市面上并存 log4j 和 JUL 两种日志系统,Apache 提供了 Commons Logging 解决这种混乱。Commons Logging 被称为 JCL,它可以挂接不同的日志系统,并通过配置文件指定挂接的日志系统。默认情况下,JCL 自动搜索并使用 Log4j,如果没有找到Log4j,再使用 JUL。这样看来,JCL 确定能够满足大部分的场景,基本实现了一统江湖,就连 spring 也是依赖了 JCL。

事情还没完,log4j 的作者觉得 JCL 提供的接口不够优秀,他自己要做一套更优雅的出来,于是有了 slf4j。slf4j 和 JCL 的定位一致,也确实很更加好用。不仅如此,作者亲自为 slf4j 做了一套实现,也就是 logback。性能要高于 log4j。slf4j 还提供了一些桥接的方式将 JCL 转变过来。Spring Boot 目前采用的就是这种方式。

目前已经不建议使用 log4j 了,因为 log4j 在 2015 年停止维护,作者去维护 log4j2 了。一般框架层使用 slf4j,实现层使用 log4j2 或者是 logback。得益于 log4j2 和 logback 是一个作者,目前主流的日志使用方式趋近于一致。

slf4j 是如何加载 logback 的?

slf4j 开始调用日志的入口是 getLogger 方法。

public static Logger getLogger(String name) {
  ILoggerFactory iLoggerFactory = getILoggerFactory();
  return iLoggerFactory.getLogger(name);
}

getILoggerFactory 中有一系列的状态判断,只要是判断当前环境中 ILoggerFactory 的初始化状态。首次调用的时候,肯定还没有加载,执行 performInitialization 方法进行初始化。performInitialization 中执行 bind 方法。

public static ILoggerFactory getILoggerFactory() {
  if (INITIALIZATION_STATE == UNINITIALIZED) {
      synchronized (LoggerFactory.class) {
          if (INITIALIZATION_STATE == UNINITIALIZED) {
              INITIALIZATION_STATE = ONGOING_INITIALIZATION;
              performInitialization();
          }
      }
  }
  switch (INITIALIZATION_STATE) {
    case SUCCESSFUL_INITIALIZATION:
      return StaticLoggerBinder.getSingleton().getLoggerFactory();
    ...
  }
}

slf4j 本身不提供日志的实现,所以在 bind 阶段它会去找可能存在的 slf4j 的实现。具体是如何约定的呢?我们需要关注下 findPossibleStaticLoggerBinderPathSet 方法。

private final static void bind() {
  try {
      Set<URL> staticLoggerBinderPathSet = null;
      if (!isAndroid()) {
          // 找出可能存在的 slf4j 的实现
          staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
      }
      ...
  } 
  ...
}

findPossibleStaticLoggerBinderPathSet 的核心思路是,查找 org/slf4j/impl/StaticLoggerBinder.class 这个类,如果有多个,那就返回多个的路径

// 约定的包路径
private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";

static Set<URL> findPossibleStaticLoggerBinderPathSet() {
  Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
  try {
      // 获得 classLoader
      ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
      Enumeration<URL> paths;
      if (loggerFactoryClassLoader == null) {
          paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
      } else {
          // 调用 classLoader 的 getResources 方法
          paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
      }
      while (paths.hasMoreElements()) {
          URL path = paths.nextElement();
          staticLoggerBinderPathSet.add(path);
      }
  } catch (IOException ioe) {
      Util.report("Error getting resources from path", ioe);
  }
  return staticLoggerBinderPathSet;
}

我们在 logback 的包路径下找到 org/slf4j/impl/StaticLoggerBinder.class 这个类。

log.jpg

按照我们一贯的想法,这个时候应该使用反射将这个类反射出来。slf4j 不是这么做的,它直接将 org.slf4j.impl.StaticLoggerBinder 引入,然后调用其相应的方法创建了 ILoggerFactory。

package org.slf4j;

import org.slf4j.impl.StaticLoggerBinder;

public static ILoggerFactory getILoggerFactory() {
  ...
  switch (INITIALIZATION_STATE) {
    case SUCCESSFUL_INITIALIZATION:
      return StaticLoggerBinder.getSingleton().getLoggerFactory();
    ...
  }
}

slf4j 采用约定优先的思想,约定好实现类的路径,直接在代码中引入实现类,生成日志工厂实例。这种方式简单且高效。

Spring Boot 加载日志流程

我们上面讲了 Spring Boot 项目推荐使用的日志方式是 slf4j + logback。那 Spring Boot 项目本身使用的日志框架是什么呢?

Spring Boot 和 Spring 保持一致,使用 JCL 作为日志框架,这样可以在不引入任何额外的 jar 包 的情况下打印日志。

我们启动 Spring Boot 程序,会 new 一个 SpringApplication 实例。该实例有一个成员变量,就是 logger。这个 logger 的类型是 org.apache.commons.logging.Log。

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

private static final Log logger = LogFactory.getLog(SpringApplication.class);

那 Spring Boot 打印的日志为什么会和我们使用 slf4j 时候一致呢?我们具体看下后续的流程。getLog 方法中使用 LogAdapter 获得真正得 Log 实现。

public static Log getLog(String name) {
  return LogAdapter.createLog(name);
}

LogAdapter 会根据当前项目是否存在执行的类来判断使用的是哪种日志系统。LogAdapter 中定义了这么几种情况:

  1. 有 log4j2 的依赖,且没有桥接到 slf4j,使用 Log4j 2.x。
  2. 有 log4j2 的依赖,且桥接到 slf4j,使用 slf4j。
  3. 没有 log4j2 的依赖,但是有 slf4j(忽略 slf4j 的不同用法),使用 slf4j。
  4. 不满足以上情况,使用 JUL。
final class LogAdapter {

    // log4j2 提供
	private static final String LOG4J_SPI = "org.apache.logging.log4j.spi.ExtendedLogger";

  // log4j-to-slf4j 提供
	private static final String LOG4J_SLF4J_PROVIDER = "org.apache.logging.slf4j.SLF4JProvider";

  // slf4j-api 提供
	private static final String SLF4J_SPI = "org.slf4j.spi.LocationAwareLogger";

  // slf4j-api 提供
	private static final String SLF4J_API = "org.slf4j.Logger";


	private static final LogApi logApi;

	static {
		if (isPresent(LOG4J_SPI)) {
			if (isPresent(LOG4J_SLF4J_PROVIDER) && isPresent(SLF4J_SPI)) {
				// log4j-to-slf4j bridge -> we'll rather go with the SLF4J SPI;
				// however, we still prefer Log4j over the plain SLF4J API since
				// the latter does not have location awareness support.
				logApi = LogApi.SLF4J_LAL;
			}
			else {
				// Use Log4j 2.x directly, including location awareness support
				logApi = LogApi.LOG4J;
			}
		}
		else if (isPresent(SLF4J_SPI)) {
			// Full SLF4J SPI including location awareness support
			logApi = LogApi.SLF4J_LAL;
		}
		else if (isPresent(SLF4J_API)) {
			// Minimal SLF4J API without location awareness support
			logApi = LogApi.SLF4J;
		}
		else {
			// java.util.logging as default
			logApi = LogApi.JUL;
		}
	}

  private static boolean isPresent(String className) {
		try {
			Class.forName(className, false, LogAdapter.class.getClassLoader());
			return true;
		}
		catch (ClassNotFoundException ex) {
			return false;
		}
	}
  ...
}

为什么 jcl 中会有 slf4j 的内容,进一步我们发现,Spring 5 中对于日志系统进行了更新。spring4 所依赖的是 jcl,而 spring5 则依赖 spring-jcl。spring-jcl 是 spring 内部对于 jcl 的升级,在维持原先 jcl 接口基础上,增加了对于 log4j2 和 slf4j 的支持。这样 spring 不需要改代码,只通过修改依赖,就将日志系统切换到了 log4j2 或者是 slf4j 上。

返回之前的 createLog 方法看,会根据 LogAdapter 静态方法中判断出的当前依赖 jar 包的情况,使用对应的适配器来创建真正的日志系统。

public static Log createLog(String name) {
  switch (logApi) {
    case LOG4J:
      return Log4jAdapter.createLog(name);
    case SLF4J_LAL:
      return Slf4jAdapter.createLocationAwareLog(name);
    case SLF4J:
      return Slf4jAdapter.createLog(name);
    default:
      return JavaUtilAdapter.createLog(name);
  }
}

由于 spring-boot-starter-logging 中包含了 slf4j 的依赖,所以这里就调用了 Slf4jAdapter。后续就是委托 slf4j 的 LoggerFactory 进行日志系统创建。完美的完成了从 jcl 到 slf4j 的转换。

private static class Slf4jAdapter {
  public static Log createLocationAwareLog(String name) {
    Logger logger = LoggerFactory.getLogger(name);
    return (logger instanceof LocationAwareLogger ?
        new Slf4jLocationAwareLog((LocationAwareLogger) logger) : new Slf4jLog<>(logger));
  }
}

Spring Boot 中日志配置是如何生效的

本小节较为琐碎,如果对内部原理不感兴趣,小节末尾有结论,可以直接看。

这是一份 logback 的配置,放在 application.properties 的同级目录下。配置中每一项的含义我们不介绍,我们了解下配置文件的加载时机和策略。

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
    <!-- 文件路径 -->
    <property name="logPath" value="./logs" />
    <!-- 日志级别 -->
    <property name="logLevel" value="INFO" />
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--输出格式-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>
    <!-- 日志文件 -->
    <appender name="FILE"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--文件名-->
            <FileNamePattern>${logPath}/log.%d{yyyy-MM-dd}.log</FileNamePattern>
            <!--保留天数-->
            <MaxHistory>30</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--输出格式-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <!--日志文件切割临界值-->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

    <root level="${logLevel}">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="FILE" />
    </root>
</configuration>

我们之前谈到,slf4j 在使用 getILoggerFactory 获得日志工厂类的时候,会按照约定通过 ClassLoader 加载指定位置的实现类。如果有且仅有一个话,说明存在实现类。接下来就是获得实现类(以 logback 为例)的配置了。

logback 中负责获得并解析配置的是 StaticLoggerBinder。该类有一个静态代码块,会在静态代码块中加载指定的配置文件。

  1. 首先是看系统变量中有没有指定 logback.configurationFile,有的话,加载这个文件。

  2. 查找文件的顺序依次是 logback-test.xml -> logback.groovy -> logback.configurationFile -> logback.xml。

  3. 如果一个配置文件都没有,logback 提供了 BasicConfigurator,这是默认配置,会将日志输出到控制台。

final public static String GROOVY_AUTOCONFIG_FILE = "logback.groovy";
final public static String AUTOCONFIG_FILE = "logback.xml";
final public static String TEST_AUTOCONFIG_FILE = "logback-test.xml";
final public static String CONFIG_FILE_PROPERTY = "logback.configurationFile";

public URL findURLOfDefaultConfigurationFile(boolean updateStatus) {
    ClassLoader myClassLoader = Loader.getClassLoaderOfObject(this);
    URL url = findConfigFileURLFromSystemProperties(myClassLoader, updateStatus);
    if (url != null) {
        return url;
    }

    url = getResource(TEST_AUTOCONFIG_FILE, myClassLoader, updateStatus);
    if (url != null) {
        return url;
    }

    url = getResource(GROOVY_AUTOCONFIG_FILE, myClassLoader, updateStatus);
    if (url != null) {
        return url;
    }

    return getResource(AUTOCONFIG_FILE, myClassLoader, updateStatus);
}

以上是 logback 加载日志配置的方式,但是在 Spring Boot 中,我们通常会在 application.properties 配置一些以 logging 开头的配置,比如 logging.file.path,logging.file.name 等。这些配置项需要依赖 Spring Boot 的能力才能被读取到。这个时间点是晚于 logback 加载日志配置的。所以如果我们配置了 application.properties,但是使用的配置文件是 logback.xml,那 application.properties 中的配置是不生效的。

Spring Boot 推荐的配置文件的方式是使用 logback-spring.xml + application.properties 的方式

那么 Spring Boot 是如何将日志本身的配置和 Spring Boot 提供的配置相结合的呢?

我们在 Spring Boot 是如何监听启动事件的 一文中谈到过,Spring Boot 在启动的不同阶段会发出不同的事件。我们可以通过实现 ApplicationListener 来监听这些事件。

在 spring-boot 的 jar 包下的 META-INF 中找到 spring.factories,ApplicationListener 的实现类中有一个是 LoggingApplicationListener。这个监听器是实现 Spring Boot 日志自定义配置的关键。

# Application Listeners
org.springframework.context.ApplicationListener=
org.springframework.boot.context.logging.LoggingApplicationListener,
...

从 onApplicationEvent 方法中可以看出,LoggingApplicationListener 监听了多个事件,包括 ApplicationStartingEvent,ApplicationEnvironmentPreparedEvent,ApplicationPreparedEvent,ContextClosedEvent 以及 ApplicationFailedEvent。这些事件中最需要关注的是 ApplicationEnvironmentPreparedEvent。因为该事件表示应用的配置信息已经解析完毕了。

@Override
public void onApplicationEvent(ApplicationEvent event) {
  // 获得当前的日志系统是 Logback,log4j2 或是 java
  // 本质是判断是否存在约定的类
  if (event instanceof ApplicationStartingEvent) {
    onApplicationStartingEvent((ApplicationStartingEvent) event);
  }
  // 该方法重点关注
  else if (event instanceof ApplicationEnvironmentPreparedEvent) {
    onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
  }
  else if (event instanceof ApplicationPreparedEvent) {
    onApplicationPreparedEvent((ApplicationPreparedEvent) event);
  }
  else if (event instanceof ContextClosedEvent
      && ((ContextClosedEvent) event).getApplicationContext().getParent() == null) {
    onContextClosedEvent();
  }
  else if (event instanceof ApplicationFailedEvent) {
    onApplicationFailedEvent();
  }
}

onApplicationEnvironmentPreparedEvent 方法会调用 initialize 来重新初始化日志配置。

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
  if (this.loggingSystem == null) {
    this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
  }
  initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
}

initialize 方法的调用栈比较长,就不列出代码了,简单的讲下每个方法做了些什么。

protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
  // 从所有的配置中读取日志相关的配置
  getLoggingSystemProperties(environment).apply();
  // 通过 logging.file.name 以及 logging.file.path 生成 logFile 对象
  // 该对象单独处理的原因是这两个对象在纯配置模式下决定是否生成日志文件
  this.logFile = LogFile.get(environment);
  // 如果 logFile 非空,将上述两个配置也放到 LoggingSystemProperties 中
  if (this.logFile != null) {
    this.logFile.applyToSystemProperties();
  }
  this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
  initializeEarlyLoggingLevel(environment);
  // 读取日志系统配置
  initializeSystem(environment, this.loggingSystem, this.logFile);
  initializeFinalLoggingLevels(environment, this.loggingSystem);
  registerShutdownHookIfNecessary(environment, this.loggingSystem);
}

Spring Boot 定义了一个 LoggingSystemProperties,这个类是专门用于存储和解析有关于日志的配置的,它存储了一些日志系统共用的配置,比如 logging.pattern.file,logging.pattern.level 等等。它的子类和日志系统有关,比如 logback 就是 LogbackLoggingSystemProperties,它存储了该日志系统相关的配置,比如 logging.logback.rollingpolicy.max-file-size 等等。所以在应用的配置信息都准备好之后,会根据这些配置生成 LoggingSystemProperties 对象。

有了这个配置对象后,Spring Boot 会重新进行日志配置的解析。这一次解析配置的主体由 logback 变为了 Spring Boot。解析文件的顺序和 logback 解析文件的顺序是一致的。

@Override
protected String[] getStandardConfigLocations() {
  return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml" };
}

如果这几个文件都不存在,那么需要获得另外的配置文件,规则是在原先的配置文件名称后加上 -spring,文件类型不变。

protected String[] getSpringConfigLocations() {
  String[] locations = getStandardConfigLocations();
  for (int i = 0; i < locations.length; i++) {
    String extension = StringUtils.getFilenameExtension(locations[i]);
    locations[i] = locations[i].substring(0, locations[i].length() - extension.length() - 1) + "-spring."
        + extension;
  }
  return locations;
}

Spring Boot 解析日志配置文件和 logback 解析配置文件是不一样的,Spring Boot 允许日志的配置文件中存在环境变量(不是所有,是特定的)。因为 Spring Boot 能很方便的解析这些变量。而 logback 不行。这也是 Spring Boot 推荐我们使用 logback-spring.xml 进行配置的原因,毕竟在 logback.xml 是没办法读取 Spirng Boot 配置的变量的。

还有一个很重要的原因就是,Spring Boot 解析日志文件的时候,增加了一个解析 springProfile 节点的规则,这个节点为我们多环境配置提供了基础。有了它,我们可以很方便的为不同的环境定制不同的日志配置。

@Override
public void addInstanceRules(RuleStore rs) {
  super.addInstanceRules(rs);
  Environment environment = this.initializationContext.getEnvironment();
  rs.addRule(new ElementSelector("configuration/springProperty"), new SpringPropertyAction(environment));
  rs.addRule(new ElementSelector("*/springProfile"), new SpringProfileAction(environment));
  rs.addRule(new ElementSelector("*/springProfile/*"), new NOPAction());
}

假设我们在 application.properties 进行了如下配置,全量的配置请参考官方文档Common Application properties

logging.file.path=./logs
logging.file.name=log
logging.pattern.level=ERROR

我们就可以在 logback-spring.xml 中使用 LOG_PATH ,LOG_FILE 和 LOG_LEVEL_PATTERN 这些变量。springProfile 节点用于判断当前的环境是否满足条件。在实际项目中,可以根据需求灵活使用这些环境变量。

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--输出格式-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>
    <!-- 日志文件 -->
    <appender name="FILE"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--文件名-->
            <FileNamePattern>${LOG_PATH}/${LOG_FILE}.%d{yyyy-MM-dd}.log</FileNamePattern>
            <!--保留天数-->
            <MaxHistory>30</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--输出格式-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <!--日志文件切割临界值-->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

    <springProfile name="dev">
        <root level="${LOG_LEVEL_PATTERN}">
            <appender-ref ref="STDOUT" />
            <appender-ref ref="FILE" />
        </root>
    </springProfile>
    <springProfile name="test,prod">
        <root level="${LOG_LEVEL_PATTERN}">
            <appender-ref ref="FILE" />
        </root>
    </springProfile>

</configuration>

还有一种情况是,我们项目没有任何的配置文件,所有的配置都写在 application.properties 中,Sping Boot 也支持这种写法。但是这样配置限制较大,配置项只能满足较为简单的场景。我们总结下 Spring Boot 对于 logback 配置方面做的增强。

  1. 首先 logback 在初始化的时候会自动加载 logback.xml 配置文件(为了简化描述,忽略其它的文件名)。此时 Spring 环境未初始化完毕,所有的配置都不会生效,logback.xml 的编写也需遵循 logback 标准写法。如果没有配置文件,logback 默认有一个配置方案,也就是打印到控制台。

  2. Spring Boot 内置了一个监听器,监听到环境变量初始化完毕的时间后后,会重新配置 logback。此时 application.properties 中的配置解析完毕。

  3. 判断是否存在原生配置文件,也就是 logback.xml。如果存在,且 application.properties 中没有配置 logging.file.path 或 logging.file.name。就以 logback.xml 中的配置为准。流程停止。其实就算是配置了 logging.file.path 或 logging.file.name 也不会生效,只是多了一步将这个配置写入日志配置类的流程。

  4. 判断是否存在 logback-sping.xml。如果存在,Sping Boot 会解析这个文件,logback-sping.xml 不仅可以使用 application.properties 中的配置(特定的几个),还能使用 springProfile 节点做环境判断。

  5. 不存在任何配置文件,以 application.properties 中的配置参数构造一个默认的配置。构造的规则看官方文档Common Application properties中每个配置的含义就可以了。

总结

  1. Java 日志体系中,JUL 已经成为过去时,log4j 停止更新。jcl 在 spring 5 后也名存实亡。推荐使用 slf4j 作为日志门面框架,logback 或者 log4j2 作为日志系统。新系统如果要加日志,最简单的做法是引入 spring-boot-starter-logging 包,一次性引入 slf4j 和 logback。

  2. slf4j 采用约定优先的思想,约定好实现类的路径,直接在代码中引入实现类,生成日志工厂实例。这就是 slf4j 实现日志门面的原理。

  3. Spring Boot 和 Spring 的日志方案是一致的,使用 jcl 作为系统日志,Spring 5 之后,将 jcl 更新为 spring-jcl,spring-jcl 保留了原先的接口,但是实现确是根据当前项目中存在的类,将日志系统桥接到 log4j2 或 slf4j,至此,jcl 在 Spring 中名存实亡。

  4. Spring Boot 默认引入 spring-boot-starter-logging 包,采用 slf4j + logback 的方式记录日志。由于 logback 本身会读取配置文件,且时间优先于 Spring 环境变量的初始化。Spring Boot 使用事件监听的方式,重新设计了一套配置解析方式,增加了日志配置对于 application.properties 的支持,以及对于多环境配置的支持。

如果您觉得有所收获,就请点个赞吧!