在平时的项目中,会经常会遇到对项目日志的配置问题,比如log日志的存放位置、级别、单个 log 文件的大小,过期清理策略等等。

那么,这个是怎么配置的呢?配置文件放在哪里?springboot是怎么加载配置文件的?

这篇文章会从源码出发对这些问题进行一一探讨。

LoggingApplicationListener

这个监听器类在监听到环境中准备好事件发生后,会做出响应,对日志系统进行配置。

还是从监听入口出发:

1@Override
 2public void onApplicationEvent(ApplicationEvent event) {
 3   if (event instanceof ApplicationStartedEvent) {
 4      onApplicationStartedEvent((ApplicationStartedEvent) event);
 5   }
 6   //监听到事件
 7   else if (event instanceof ApplicationEnvironmentPreparedEvent) {
 8      onApplicationEnvironmentPreparedEvent(
 9            (ApplicationEnvironmentPreparedEvent) event);
10   }
11   else if (event instanceof ContextClosedEvent && ((ContextClosedEvent) event)
12         .getApplicationContext().getParent() == null) {
13      onContextClosedEvent();
14   }
15}
复制代码

第7行判断了是应用环境准备好事件发生,对此做出响应。

1private void onApplicationEnvironmentPreparedEvent(
 2      ApplicationEnvironmentPreparedEvent event) {
 3   //生成日志系统
 4   if (this.loggingSystem == null) {
 5      this.loggingSystem = LoggingSystem
 6            .get(event.getSpringApplication().getClassLoader());
 7   }
 8   //初始化
 9   initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
10}
复制代码

LoggingSystem 是Springboot中对于日志的统一抽象,对外提供了日志相关操作,封装了底层日志的实现细节。

取到 LoggingSystem

我们看下get方法。

1public static LoggingSystem get(ClassLoader classLoader) {
 2    /*SYSTEM_PROPERTY = LoggingSystem.class.getName()*/
 3    //从系统变量中找到 loggingSystem 的类名,初始化
 4   String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
 5   if (StringUtils.hasLength(loggingSystem)) {
 6      return get(classLoader, loggingSystem);
 7   }
 8   //如果系统变量中没有,则从默认的日志系统中找一个存在的。
 9   for (Map.Entry<String, String> entry : SYSTEMS.entrySet()) {
10      if (ClassUtils.isPresent(entry.getKey(), classLoader)) {
11         return get(classLoader, entry.getValue());
12      }
13   }
14   throw new IllegalStateException("No suitable logging system located");
15}
复制代码

可以看到代码总共分两块:

  1. 如果系统变量中配置了loggingSystem的类,则找到利用反射初始化
  2. 反之,则从类路径中存在的日志系统类,加载。
1private static final Map<String, String> SYSTEMS;
 2
 3static {
 4   Map<String, String> systems = new LinkedHashMap<String, String>();
 5   systems.put("ch.qos.logback.core.Appender",
 6         "org.springframework.boot.logging.logback.LogbackLoggingSystem");
 7   systems.put("org.apache.logging.log4j.LogManager",
 8         "org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
 9   systems.put("org.apache.log4j.PropertyConfigurator",
10         "org.springframework.boot.logging.log4j.Log4JLoggingSystem");
11   systems.put("java.util.logging.LogManager",
12         "org.springframework.boot.logging.java.JavaLoggingSystem");
13   SYSTEMS = Collections.unmodifiableMap(systems);
14}
复制代码

可以看到,自带了LogbackLoggingSystem、Log4J2LoggingSystem、Log4JLoggingSystem、JavaLoggingSystem 四种日志系统。

初始化

上一步取到了 LoggingSystem,接下来就是要对其进行初始化。

1protected void initialize(ConfigurableEnvironment environment,
 2      ClassLoader classLoader) {
 3   //PID_KEY = "PID"
 4   if (System.getProperty(PID_KEY) == null) {
 5      System.setProperty(PID_KEY, new ApplicationPid().toString());
 6   }
 7   initializeEarlyLoggingLevel(environment);
 8   initializeSystem(environment, this.loggingSystem);
 9   initializeFinalLoggingLevels(environment, this.loggingSystem);
10}
复制代码

这里的工作主要分为四步:

  1. 获取进程号
    代码 4~6 行为系统变量 PID_KEY 设置了进程号内容。我们可以简单看下其获取进程号的方式:
1private String getPid() {
2  try {
3     String jvmName = ManagementFactory.getRuntimeMXBean().getName();
4     return jvmName.split("@")[0];
5  }
6  catch (Throwable ex) {
7     return null;
8  }
9}
复制代码

ManagementFactory 是 jdk 包里面的一个工厂类。我试着自己打印了下这个 jvmName 是个什么东西:

3208@DFN0S9W18H6NNAS

可以看到“@”符号前面的正是进程号。

  1. 对日志级别进行早期初始化
1if (this.parseArgs && this.springBootLogging == null) {
2  if (environment.containsProperty("debug")) {
3     this.springBootLogging = LogLevel.DEBUG;
4  }
5  if (environment.containsProperty("trace")) {
6     this.springBootLogging = LogLevel.TRACE;
7  }
8}
复制代码

如果要设置了要解析命令行参数且没有指定日志级别,则从环境中找 debug 或者 trace 的属性,如果找得到,则设置成相应的级别。

  1. 开始实际初始化日志系统
1private void initializeSystem(ConfigurableEnvironment environment,
 2     LoggingSystem system) {
 3  LogFile logFile = LogFile.get(environment);
 4  //CONFIG_PROPERTY = "logging.config"
 5  String logConfig = environment.getProperty(CONFIG_PROPERTY);
 6  if (StringUtils.hasLength(logConfig)) {
 7     try {
 8        ResourceUtils.getURL(logConfig).openStream().close();
 9        system.initialize(logConfig, logFile);
10     }
11     catch (Exception ex) {
12        this.logger.warn("Logging environment value '" + logConfig
13              + "' cannot be opened and will be ignored "
14              + "(using default location instead)");
15        system.initialize(null, logFile);
16     }
17  }
18  else {
19     system.initialize(null, logFile);
20  }
21}
复制代码

LogFile 是一个对log日志文件的引用。

在第3行,初始化一个 logFile 对象。我们可以看下get方法:

1public static LogFile get(PropertyResolver propertyResolver) {
 2   //FILE_PROPERTY = "logging.file"
 3  String file = propertyResolver.getProperty(FILE_PROPERTY);
 4  //PATH_PROPERTY = "logging.path"
 5  String path = propertyResolver.getProperty(PATH_PROPERTY);
 6  if (StringUtils.hasLength(file) || StringUtils.hasLength(path)) {
 7     return new LogFile(file, path);
 8  }
 9  return null;
10}
复制代码

如果用户配置了 logging.file 或者 logging.path 属性,则将会返回一个LogFile对象。

第5行,如果配置了logging.config属性,则利用该属性的值,找到对应的配置文件进行解析加载。

我们可以看下 system.initialize 方法。

此方法在一个抽象类中提供了模板。

1@Override
 2public void initialize(String configLocation, LogFile logFile) {
 3  if (StringUtils.hasLength(configLocation)) {
 4     // Load a specific configuration
 5     configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);
 6     loadConfiguration(configLocation, logFile);
 7  }
 8  else {
 9     String selfInitializationConfig = getSelfInitializationConfig();
10     if (selfInitializationConfig == null) {
11        // No self initialization has occurred, use defaults
12        loadDefaults(logFile);
13     }
14     else if (logFile != null) {
15        // Self initialization has occurred but the file has changed, reload
16        loadConfiguration(selfInitializationConfig, logFile);
17     }
18     else {
19        reinitialize();
20     }
21  }
22}
复制代码

如果用户提供了配置,则加载配置路径中的日志文件配置;反之,则加载默认的配置。

先看加载用户自定义配置的逻辑。

  • 加载用户自定义配置
    对于 loadConfiguration 方法不同的实现类有不同的加载方式,我们试着看下 logback 的加载方法:
1protected void loadConfiguration(String location, LogFile logFile) {
 2  Assert.notNull(location, "Location must not be null");
 3  if (logFile != null) {
 4     logFile.applyToSystemProperties();
 5  }
 6  LoggerContext context = getLoggerContext();
 7  stopAndReset(context);
 8  try {
 9     URL url = ResourceUtils.getURL(location);
10     new ContextInitializer(context).configureByResource(url);
11  }
12  catch (Exception ex) {
13     throw new IllegalStateException(
14           "Could not initialize Logback logging from " + location, ex);
15  }
16}
复制代码

LoggerContext

重点看下第6行,获取一个 LoggerContext 的上下文环境对象,该类位于 ch.qos.logback.classic 包,不在 spring工程里。

1private LoggerContext getLoggerContext() {
 2  ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory();
 3  Assert.isInstanceOf(LoggerContext.class, factory,
 4        String.format(
 5              "LoggerFactory is not a Logback LoggerContext but Logback is on "
 6                    + "the classpath. Either remove Logback or the competing "
 7                    + "implementation (%s loaded from %s). If you are using "
 8                    + "Weblogic you will need to add 'org.slf4j' to "
 9                    + "prefer-application-packages in WEB-INF/weblogic.xml",
10              factory.getClass(), getLocation(factory)));
11  return (LoggerContext) factory;
12}
复制代码

在代码的第2行,获取到了一个 LoggerContext 对象。

第3行判断LoggerContext是否对ILoggerFactory进行了实现,如果不是,则报错。

LoggerFactory is not a Logback LoggerContext but Logback is on the classpath.

logback 在类路径下,但是加载出来的 LoggerContext 却不是 logback 的实现。

在错误提示的第 10 行中给出了加载的 LoggerContext 的类路径。

config

在获取到了 LoggerContext 对象之后,通过下面两行的核心代码对日志系统进行配置。

URL url = ResourceUtils.getURL(location); new ContextInitializer(context).configureByResource(url);

  • 加载默认日志配置
1protected String getSelfInitializationConfig() {
 2  for (String location : getStandardConfigLocations()) {
 3     ClassPathResource resource = new ClassPathResource(location,
 4           this.classLoader);
 5     if (resource.exists()) {
 6        return "classpath:" + location;
 7     }
 8  }
 9  return null;
10}
复制代码

加载默认配置的方式,首先找到默认的日志 location。

getStandardConfigLocations 也是一个模板方法,不同日志系统有不同的位置,还是以 logback 为例:

1protected String[] getStandardConfigLocations() {
2  return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy","logback.xml" };
3}
复制代码

可以看到,默认的日志配置文件名总共有这四种。

如果没有找到这四种配置文件的任意一种,则执行下面的逻辑:

1public void apply(LogbackConfigurator config) {
 2  synchronized (config.getConfigurationLock()) {
 3     base(config);
 4     Appender<ILoggingEvent> consoleAppender = consoleAppender(config);
 5     if (this.logFile != null) {
 6        Appender<ILoggingEvent> fileAppender = fileAppender(config,
 7              this.logFile.toString());
 8        config.root(Level.INFO, consoleAppender, fileAppender);
 9     }
10     else {
11        config.root(Level.INFO, consoleAppender);
12     }
13  }
14}
复制代码

可以看到默认的日志级别是 info,如果 logFile 为 null 的话,默认打印在控制台上。

第3行的base方法给出了 spring 工程包里面的默认配置的日志级别:

1private void base(LogbackConfigurator config) {
 2  config.conversionRule("clr", ColorConverter.class);
 3  config.conversionRule("wex", WhitespaceThrowableProxyConverter.class);
 4  LevelRemappingAppender debugRemapAppender = new LevelRemappingAppender(
 5        "org.springframework.boot");
 6  config.start(debugRemapAppender);
 7  config.appender("DEBUG_LEVEL_REMAPPER", debugRemapAppender);
 8  config.logger("", Level.ERROR);
 9  config.logger("org.apache.catalina.startup.DigesterFactory", Level.ERROR);
10  config.logger("org.apache.catalina.util.LifecycleBase", Level.ERROR);
11  config.logger("org.apache.coyote.http11.Http11NioProtocol", Level.WARN);
12  config.logger("org.apache.sshd.common.util.SecurityUtils", Level.WARN);
13  config.logger("org.apache.tomcat.util.net.NioSelectorPool", Level.WARN);
14  config.logger("org.crsh.plugin", Level.WARN);
15  config.logger("org.crsh.ssh", Level.WARN);
16  config.logger("org.eclipse.jetty.util.component.AbstractLifeCycle", Level.ERROR);
17  config.logger("org.hibernate.validator.internal.util.Version", Level.WARN);
18  config.logger("org.springframework.boot.actuate.autoconfigure."
19        + "CrshAutoConfiguration", Level.WARN);
20  config.logger("org.springframework.boot.actuate.endpoint.jmx", null, false,
21        debugRemapAppender);
22  config.logger("org.thymeleaf", null, false, debugRemapAppender);
23}
复制代码
  1. 对日志级别进行最终初始化
1private void initializeFinalLoggingLevels(ConfigurableEnvironment environment,
2      LoggingSystem system) {
3   if (this.springBootLogging != null) {
4      initializeLogLevel(system, this.springBootLogging);
5   }
6   setLogLevels(system, environment);
7}
复制代码

这里执行一些最终的 log级别的设置。

总结

本文对于 log 配置的主干逻辑进行了一些分析。这里的有些细节非常值得琢磨。后面可以专门针对这些细节在研究下。