Springboot 源碼分析之log配置加載

在平時的項目中,會常常會遇到對項目日誌的配置問題,好比log日誌的存放位置、級別、單個 log 文件的大小,過時清理策略等等。java

那麼,這個是怎麼配置的呢?配置文件放在哪裏?springboot是怎麼加載配置文件的?web

這篇文章會從源碼出發對這些問題進行一一探討。spring

LoggingApplicationListener

這個監聽器類在監聽到環境中準備好事件發生後,會作出響應,對日誌系統進行配置。apache

仍是從監聽入口出發:tomcat

 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行判斷了是應用環境準備好事件發生,對此作出響應。springboot

 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中對於日誌的統一抽象,對外提供了日誌相關操做,封裝了底層日誌的實現細節。app

取到 LoggingSystem

咱們看下get方法。eclipse

 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}
複製代碼

能夠看到代碼總共分兩塊:ssh

  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 四種日誌系統。jvm

初始化

上一步取到了 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 配置的主幹邏輯進行了一些分析。這裏的有些細節很是值得琢磨。後面能夠專門針對這些細節在研究下。

相關文章
相關標籤/搜索