Springboot 源码分析之log配置加载

3,429 阅读11分钟

在平时的项目中,会经常会遇到对项目日志的配置问题,比如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<StringString> 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<StringString> SYSTEMS;
2
3static {
4   Map<StringString> systems = new LinkedHashMap<StringString>();
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 的属性,如果找得到,则设置成相应的级别。

  2. 开始实际初始化日志系统

     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.classfactory,
    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 配置的主干逻辑进行了一些分析。这里的有些细节非常值得琢磨。后面可以专门针对这些细节在研究下。