Spring Boot 源碼分析 - 日志系統


參考 知識星球芋道源碼 星球的源碼解析,一個活躍度非常高的 Java 技術社群,感興趣的小伙伴可以加入 芋道源碼 星球,一起學習😄

該系列文章是筆者在學習 Spring Boot 過程中總結下來的,里面涉及到相關源碼,可能對讀者不太友好,請結合我的源碼注釋 Spring Boot 源碼分析 GitHub 地址 進行閱讀

Spring Boot 版本:2.2.x

最好對 Spring 源碼有一定的了解,可以先查看我的 《死磕 Spring 之 IoC 篇 - 文章導讀》 系列文章

如果該篇內容對您有幫助,麻煩點擊一下“推薦”,也可以關注博主,感激不盡~

該系列其他文章請查看:《精盡 Spring Boot 源碼分析 - 文章導讀》

概述

日志是一個系統必不可缺少的東西,記錄了系統運行時的點點滴滴,便於我們了解自己系統的運行狀態,在我們使用 Spring Boot 時,默認就已經提供了日志功能,使用 Logback 作為默認的日志框架。那么,接下來我們依賴來看看 Spring Boot 是如何初始化好日志系統的。

為什么 Spring Boot 默認的日志框架是 Logbasck 呢?

因為在 spring-boot-starter 模塊中引入 spring-boot-starter-logging 模塊,該 Starter 引入了 logback-classic 依賴。

Log 日志體系

在我們的日常工作中,可能看到項目中依賴的跟日志相關的 jar 包有很多,例如 commons-logginglog4jlog4j2sl4jlogback 等等,眼花繚亂。經常會碰到各種依賴沖入的問題,非常煩惱,例如這幾個問題:

  1. Failed to load class org.slf4j.impl.StaticLoggerBinder,沒找到日志實現,如果你覺得你已經添加了對應的日志實現依賴了,那應該檢查一下版本是否兼容
  2. Multiple bindings,找到了多個日志實現,也可能是版本問題,slf4j 會找其中一個作為日志實現

如果想要正確地使用它們,有必要先理清它們之間的關系,我們可以來看看 Log 的發展史,首先從 Java Log 的發展歷程開始說起:

  1. log4j(作者Ceki Gülcü)出來后,被開發者們廣泛的應用(注意,這里是直接使用),當初是 Java 日志事實上的標准,並成為了 Apache 的項目
  2. Apache 要求把 log4j 並入到 jdk,SUN 表示拒絕,並在 jdk1.4 版本后增加了 JULjava.util.logging);
  3. 畢竟是 JDK 自帶的,JUL 也被很多人使用。同時還有其他的日志組件,如 SimpleLog 等。這個時候如果有人想換成其他日志組件,如 log4j 換成 JUL,因為 API 完全不同,就需要改動代碼,當然很多人不願意呀;
  4. Apache 見此,開發了 JCL(Jakarta Commons Logging),即 commons-logging-xx.jar。它只提供一套通用的日志接口 API,並不提供日志的實現。很好的設計原則嘛,依賴抽象而非實現。這樣一來,我們的應用程序可以在運行時選擇自己想要的日志實現組件;
  5. 這樣看上去也挺美好的,但是 log4j 的作者覺得 JCL 不好用,自己開發出一套 slf4j,它跟 JCL 類似,本身不替供日志的具體實現,只對外提供接口或門面。目的就是為了替代 JCL。同時,還開發出 logback,一個比 log4j 擁有更高性能的組件,目的是為了替代 log4j
  6. Apache 參考了 logback,並做了一系列優化,推出了一套 log4j2 日志框架。

對於性能沒什么特別高要求的使用 Spring Boot 中默認的 logback 就可以了,如果想要使用 log4j2 可以參考我的 《MyBatis 使用手冊》 這篇文章,有提到過。

回顧

回到前面的 《SpringApplication 啟動類的啟動過程》 這篇文章,Spring Boot 啟動應用的入口和主流程都是在 SpringApplication#run(String.. args) 方法中。

在啟動 Spring 應用的整個過程中,到了不同的階段會發布不同類型的事件,例如最開始會發布一個 應用正在啟動 的事件,對於不同類型的事件都是通過 EventPublishingRunListener 事件發布器來發布,里面有一個事件廣播器,封裝了幾個 ApplicationListener 事件監聽器,如下:

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

其中有一個 LoggingApplicationListener 對象,監聽到不同事件后,會對日志系統進行一些相關的初始化工作

提示:Spring Boot 的 LoggingSystem 日志系統的初始化過程有點繞,嵌套的方法有點多,可參考序號耐心查看

LoggingApplicationListener

org.springframework.boot.context.logging.LoggingApplicationListener,Spring Boot 事件監聽器,用於初始化日志系統

onApplicationEvent 方法

onApplicationEvent(ApplicationEvent 方法,處理監聽到的事件

@Override
public void onApplicationEvent(ApplicationEvent event) {
    // 應用正在啟動的事件
    if (event instanceof ApplicationStartingEvent) {
        onApplicationStartingEvent((ApplicationStartingEvent) event);
    }
    // Environment 環境已准備事件
    else if (event instanceof ApplicationEnvironmentPreparedEvent) {
        onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
    }
    // 應用已准備事件
    else if (event instanceof ApplicationPreparedEvent) {
        onApplicationPreparedEvent((ApplicationPreparedEvent) event);
    }
    // Spring 上下文關閉事件
    else if (event instanceof ContextClosedEvent
            && ((ContextClosedEvent) event).getApplicationContext().getParent() == null) {
        onContextClosedEvent();
    }
    // 應用啟動失敗事件
    else if (event instanceof ApplicationFailedEvent) {
        onApplicationFailedEvent();
    }
}

對於不同的事件調用不同的方法,事件的發布順序也就是上面從上往下的順序

1. onApplicationStartingEvent 方法

處理應用正在啟動的事件

private void onApplicationStartingEvent(ApplicationStartingEvent event) {
    // <1> 創建 LoggingSystem 對象
    // 指定了類型則使用指定的,沒有則嘗試創建對應的對象,ClassLoader 中有對應的 Class 對象則創建(logback > log4j2 > java logging)
    this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
    // <2> LoggingSystem 的初始化前置處理
    this.loggingSystem.beforeInitialize();
}

過程如下:

  1. 創建 LoggingSystem 對象,指定了類型則使用指定的,沒有則嘗試創建對應的對象,ClassLoader 中有對應的 Class 對象則創建(logback > log4j2 > java logging
  2. 調用 LoggingSystem 的 beforeInitialize() 方法,初始化前置處理

2. onApplicationEnvironmentPreparedEvent 方法

處理環境已准備事件

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    // <1> 如果還未明確 LoggingSystem 類型,那么這里繼續創建 LoggingSystem 對象
    if (this.loggingSystem == null) {
        this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
    }
    // <2> 初始化 LoggingSystem 對象,創建日志文件,設置日志級別
    initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
}

過程如下:

  1. 如果還未明確 LoggingSystem 類型,那么這里繼續創建 LoggingSystem 對象
  2. 調用 initialize(..) 方法,初始化 LoggingSystem 對象,創建日志文件,設置日志級別

3. initialize 方法

protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
    // <1> 根據 Environment 環境通過 LoggingSystemProperties 往 System 進行一些日志配置
    new LoggingSystemProperties(environment).apply();
    // <2> 根據 Environment 環境配置的日志名稱和路徑創建一個日志文件
    // 默認情況沒有配置,這個對象也為 null,而是在打印第一個日志的時候會創建(如果不存在的話)
    this.logFile = LogFile.get(environment);
    if (this.logFile != null) {
        // <3> 往 System 添加日志文件的名稱和路徑
        this.logFile.applyToSystemProperties();
    }
    // <4> 創建一個日志分組對象
    this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
    // <5> 初始化早期的 Spring Boot 日志級別(Debug 或者 Trace)
    initializeEarlyLoggingLevel(environment);
    // <6> 初始化 LoggingSystem 對象
    initializeSystem(environment, this.loggingSystem, this.logFile);
    // <7> 初始化最終的 Spring Boot 日志級別,逐個設置 Environment 配置的日志級別
    initializeFinalLoggingLevels(environment, this.loggingSystem);
    // <8> 向 JVM 注冊一個鈎子,用於在 JVM 關閉時關閉日志系統
    registerShutdownHookIfNecessary(environment, this.loggingSystem);
}

初始過程如下:

  1. 根據 Environment 環境通過 LoggingSystemProperties 往 System 進行一些日志配置

  2. 根據 Environment 環境配置的日志名稱和路徑創建一個日志文件,默認情況沒有配置,這個對象也為 null,而是在打印第一個日志的時候會創建(如果不存在的話)

    // LogFile.java
    public static LogFile get(PropertyResolver propertyResolver) {
        // 獲取 `logging.file.name` 指定的日志文件名稱,也可以通過 `logging.file` 指定
        String file = getLogFileProperty(propertyResolver, FILE_NAME_PROPERTY, FILE_PROPERTY);
        // 獲取 `logging.file.path` 指定的日志文件保存路徑,也可以通過 `logging.path` 指定
        String path = getLogFileProperty(propertyResolver, FILE_PATH_PROPERTY, PATH_PROPERTY);
        // 創建一個日志文件
        if (StringUtils.hasLength(file) || StringUtils.hasLength(path)) {
            return new LogFile(file, path);
        }
        return null;
    }
    
  3. 往 System 添加日志文件的名稱和路徑

  4. 創建一個日志分組對象

  5. 初始化早期的 Spring Boot 日志級別(Debug 或者 Trace)

    private void initializeEarlyLoggingLevel(ConfigurableEnvironment environment) {
        if (this.parseArgs && this.springBootLogging == null) {
            if (isSet(environment, "debug")) {
                this.springBootLogging = LogLevel.DEBUG;
            }
            if (isSet(environment, "trace")) {
                this.springBootLogging = LogLevel.TRACE;
            }
        }
    }
    
  6. 初始化 LoggingSystem 對象

    private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {
        LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);
        // <1> 找到 `logging.config` 指定的配置文件路徑
        String logConfig = environment.getProperty(CONFIG_PROPERTY);
        // <2> 如果沒配置文件,則不指定配置文件初始化 LoggingSystem 對象
        // 使用約定好的配置文件,或者使用默認配置
        if (ignoreLogConfig(logConfig)) {
            system.initialize(initializationContext, null, logFile);
        }
        // <3> 否則,指定配置文件初始化 LoggingSystem 對象
        else {
            try {
                system.initialize(initializationContext, logConfig, logFile);
            }
            catch (Exception ex) {
                // 拋出異常
            }
        }
    }
    
  7. 初始化最終的 Spring Boot 日志級別,逐個設置 Environment 配置的日志級別

  8. 向 JVM 注冊一個鈎子,用於在 JVM 關閉時關閉日志系統

可以看到需要通過 LoggingSystem 日志系統對象來初始化,后面會講到

4. onApplicationPreparedEvent 方法

處理應用已准備事件

private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
    // 往底層 IoC 容器注冊幾個 Bean:LoggingSystem、LogFile 和 LoggerGroups
    ConfigurableListableBeanFactory beanFactory = event.getApplicationContext().getBeanFactory();
    if (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {
        beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME, this.loggingSystem);
    }
    if (this.logFile != null && !beanFactory.containsBean(LOG_FILE_BEAN_NAME)) {
        beanFactory.registerSingleton(LOG_FILE_BEAN_NAME, this.logFile);
    }
    if (this.loggerGroups != null && !beanFactory.containsBean(LOGGER_GROUPS_BEAN_NAME)) {
        beanFactory.registerSingleton(LOGGER_GROUPS_BEAN_NAME, this.loggerGroups);
    }
}

LoggingSystem

org.springframework.boot.logging.LoggingSystem 抽象類,Spring Boot 的日志系統對象,每個日志框架,都會對應一個實現類。如下圖所示:

public abstract class LoggingSystem {

	private static final Map<String, String> SYSTEMS;

	static {
		Map<String, String> systems = new LinkedHashMap<>();
		systems.put("ch.qos.logback.core.Appender", "org.springframework.boot.logging.logback.LogbackLoggingSystem");
		systems.put("org.apache.logging.log4j.core.impl.Log4jContextFactory",
				"org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
		systems.put("java.util.logging.LogManager", "org.springframework.boot.logging.java.JavaLoggingSystem");
		SYSTEMS = Collections.unmodifiableMap(systems);
	}
}

1.1 get 方法

創建一個 LoggingSystem 日志系統對象,如下:

public static LoggingSystem get(ClassLoader classLoader) {
    // <1> 從系統參數 `org.springframework.boot.logging.LoggingSystem` 獲得 LoggingSystem 類型
    String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
    // <2> 如果非空,說明配置了,那么創建一個該類型的 LoggingSystem 實例對象
    if (StringUtils.hasLength(loggingSystem)) {
        if (NONE.equals(loggingSystem)) {
            return new NoOpLoggingSystem();
        }
        return get(classLoader, loggingSystem);
    }
    // <3> 否則,沒有配置,則通過順序依次嘗試創建對應類型的 LoggingSystem 實例對象
    // logback > log4j2 > java logging
    return SYSTEMS.entrySet().stream().filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader))
            .map((entry) -> get(classLoader, entry.getValue())).findFirst()
            .orElseThrow(() -> new IllegalStateException("No suitable logging system located"));
}

過程如下:

  1. 從系統參數 org.springframework.boot.logging.LoggingSystem 獲得 LoggingSystem 類型
  2. 如果非空,說明配置了,那么創建一個該類型的 LoggingSystem 實例對象
  3. 否則,沒有配置,則通過順序依次嘗試創建對應類型的 LoggingSystem 實例對象,也就是在 static 代碼塊中初始化好的集合,logback > log4j2 > java logging

1.2 beforeInitialize 方法

初始化的前置操作,抽象方法,交由子類實現

/**
 * Reset the logging system to be limit output. This method may be called before
 * {@link #initialize(LoggingInitializationContext, String, LogFile)} to reduce
 * logging noise until the system has been fully initialized.
 */
public abstract void beforeInitialize();

2. initialize 方法

初始化操作,空方法,由子類來重寫

/**
 * Fully initialize the logging system.
 * @param initializationContext the logging initialization context
 * @param configLocation a log configuration location or {@code null} if default
 * initialization is required
 * @param logFile the log output file that should be written or {@code null} for
 * console only output
 */
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
}

AbstractLoggingSystem

org.springframework.boot.logging.AbstractLoggingSystem 抽象類,繼承 LoggingSystem 抽象類,作為一個基類

2.1 initialize 方法

重寫父類的 initialize(..) 方法,提供模板化的初始化邏輯,如下:

@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
    // <1> 有自定義的配置文件,則使用指定配置文件進行初始化
    if (StringUtils.hasLength(configLocation)) {
        initializeWithSpecificConfig(initializationContext, configLocation, logFile);
        return;
    }
    // <2> 無自定義的配置文件,則使用約定配置文件進行初始化
    initializeWithConventions(initializationContext, logFile);
}

有指定的配置文件,則調用 initializeWithSpecificConfig(..) 方法, 使用指定配置文件進行初始化

沒有自定義的配置文件,則調用 initializeWithConventions(..) 方法,使用約定配置文件進行初始化

2.1.1 initializeWithSpecificConfig 方法

initializeWithSpecificConfig(LoggingInitializationContext, String, LogFile) 方法,使用指定配置文件進行初始化

private void initializeWithSpecificConfig(LoggingInitializationContext initializationContext, String configLocation,
        LogFile logFile) {
    // <1> 獲得配置文件的路徑(可能有占位符)
    configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);
    // <2> 加載配置文件到日志系統中,抽象方法,子類實現
    loadConfiguration(initializationContext, configLocation, logFile);
}

先獲取配置文件的路徑(可能有占位符),然后調用 loadConfiguration(..) 抽象方法,加載配置文件到日志系統中

/**
 * Load a specific configuration.
 * @param initializationContext the logging initialization context
 * @param location the location of the configuration to load (never {@code null})
 * @param logFile the file to load or {@code null} if no log file is to be written
 */
protected abstract void loadConfiguration(LoggingInitializationContext initializationContext, String location,
        LogFile logFile);

2.1.2 initializeWithConventions 方法

initializeWithConventions(LoggingInitializationContext, LogFile) 方法,使用約定配置文件進行初始化

private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
    // <1> 嘗試獲得約定配置文件,例如 log4j2 約定的是 log4j2.xml
    String config = getSelfInitializationConfig();
    // <2> 如果找到了約定的配置文件
    if (config != null && logFile == null) {
        // self initialization has occurred, reinitialize in case of property changes
        // <2.1> 自定義初始化,子類實現
        reinitialize(initializationContext);
        return;
    }
    // <3> 嘗試獲取約定的配置文件(帶有 `-spring` ),例如 log4j2 對應是 log4j2-spring.xml
    if (config == null) {
        config = getSpringInitializationConfig();
    }
    // <4> 獲取到了 `-spring` 配置文件,則加載到日志系統中,抽象方法,子類實現
    if (config != null) {
        loadConfiguration(initializationContext, config, logFile);
        return;
    }
    // <5> 加載默認配置,抽象方法,子類實現
    loadDefaults(initializationContext, logFile);
}

過程如下

  1. 調用 getSelfInitializationConfig() 方法,嘗試獲得約定配置文件,例如 log4j2 約定的是 log4j2.xml

    protected String getSelfInitializationConfig() {
        return findConfig(getStandardConfigLocations());
    }
    
    protected abstract String[] getStandardConfigLocations();
    
    private String findConfig(String[] locations) {
        for (String location : locations) {
            ClassPathResource resource = new ClassPathResource(location, this.classLoader);
            if (resource.exists()) {
                return "classpath:" + location;
            }
        }
        return null;
    }
    
  2. 如果找到了約定的配置文件,則調用 reinitialize(..) 抽象方法,自定義初始化,子類實現

    protected void reinitialize(LoggingInitializationContext initializationContext) { }
    
  3. 調用 getSpringInitializationConfig(..) 方法,嘗試獲取約定的配置文件(帶有 -spring ),例如 log4j2 對應是 log4j2-spring.xml

    protected String getSpringInitializationConfig() {    return findConfig(getSpringConfigLocations());}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;}private String findConfig(String[] locations) {    for (String location : locations) {        ClassPathResource resource = new ClassPathResource(location, this.classLoader);        if (resource.exists()) {            return "classpath:" + location;        }    }    return null;}
    
  4. 獲取到了 -spring 配置文件,則調用 loadConfiguration(..) 抽象方法,加載到日志系統中,子類實現

    protected abstract void loadConfiguration(LoggingInitializationContext initializationContext, String location,        LogFile logFile);
    
  5. 還沒有找到到指定的配置文件,那么調用 loadDefaults(..) 抽象方法,加載默認配置,子類實現

    protected abstract void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile);
    

整個過程就是嘗試獲取到各個日志框架約定好的配置文件名稱,如果存在這個配置文件,則加載到日志系統中,否則使用默認的配置

Slf4JLoggingSystem

org.springframework.boot.logging.Slf4JLoggingSystem,繼承 AbstractLoggingSystem 抽象類,基於 Slf4J 的 LoggingSystem 的抽象基類

1.2.1 beforeInitialize 方法

初始化的前置操作

@Overridepublic void beforeInitialize() {    super.beforeInitialize();    // <1> 配置 JUL 的橋接處理器,橋接到 slf4j    configureJdkLoggingBridgeHandler();}

先調用父類的 beforeInitialize() 方法,然后調用 configureJdkLoggingBridgeHandler() 方法,配置 JUL 的橋接處理器,橋接到 slf4j

private void configureJdkLoggingBridgeHandler() {    try {        // <1> 判斷 JUL 是否橋接到 SLF4J 了        if (isBridgeJulIntoSlf4j()) {            // <2> 移除 JUL 橋接處理器            removeJdkLoggingBridgeHandler();            // <3> 重新安裝 SLF4JBridgeHandler            SLF4JBridgeHandler.install();        }    }    catch (Throwable ex) {        // Ignore. No java.util.logging bridge is installed.    }}

過程如下:

  1. 判斷 JUL 是否橋接到 slf4j

    protected final boolean isBridgeJulIntoSlf4j() {    // 存在 SLF4JBridgeHandler 類,且 JUL 只有 ConsoleHandler 處理器被創建    return isBridgeHandlerAvailable() && isJulUsingASingleConsoleHandlerAtMost();}
    
  2. 移除 JUL 橋接處理器

    private void removeJdkLoggingBridgeHandler() {    try {        // 移除 JUL 的 ConsoleHandler        removeDefaultRootHandler();        // 卸載 SLF4JBridgeHandler        SLF4JBridgeHandler.uninstall();    }    catch (Throwable ex) {        // Ignore and continue    }}private void removeDefaultRootHandler() {    try {        Logger rootLogger = LogManager.getLogManager().getLogger("");        Handler[] handlers = rootLogger.getHandlers();        if (handlers.length == 1 && handlers[0] instanceof ConsoleHandler) {            rootLogger.removeHandler(handlers[0]);        }    }    catch (Throwable ex) {        // Ignore and continue    }}
    
  3. 重新安裝 SLF4JBridgeHandler

2.3 loadConfiguration 方法

重寫 AbstractLoggingSystem 父類的方法,加載指定的日志配置文件到日志系統中

@Overrideprotected void loadConfiguration(LoggingInitializationContext initializationContext, String location,        LogFile logFile) {    Assert.notNull(location, "Location must not be null");    if (initializationContext != null) {        // 將 Environment 中的日志配置往 System 中配置        applySystemProperties(initializationContext.getEnvironment(), logFile);    }}

實際上就是將 Environment 中的日志配置往 System 中配置

LogbackLoggingSystem

org.springframework.boot.logging.logback.LogbackLoggingSystem,繼承 Slf4JLoggingSystem 抽象類,基於 logback 的 LoggingSystem 實現類

1.2.2 beforeInitialize 方法

重寫 LoggingSystem 的方法,初始化前置操作

@Overridepublic void beforeInitialize() {    // <1> 獲得 LoggerContext 日志上下文    LoggerContext loggerContext = getLoggerContext();    // <2> 如果 LoggerContext 已有 LoggingSystem,表示已經初始化,則直接返回    if (isAlreadyInitialized(loggerContext)) {        return;    }    // <3> 調用父方法    super.beforeInitialize();    // <4> 添加 FILTER 到其中,因為還未初始化,不打印日志    loggerContext.getTurboFilterList().add(FILTER);}

過程如下:

  1. 調用 getLoggerContext() 方法,獲得 LoggerContext 日志上下文

    private LoggerContext getLoggerContext() {    ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory();    // 這里會校驗 `factory` 是否為 LoggerContext 類型    return (LoggerContext) factory;}
    
  2. 如果 LoggerContext 已有 LoggingSystem,表示已經初始化,則直接返回

    private boolean isAlreadyInitialized(LoggerContext loggerContext) {    return loggerContext.getObject(LoggingSystem.class.getName()) != null;}
    
  3. 調用父方法

  4. 添加 FILTER 到其中,因為還未初始化,不打印日志

    private static final TurboFilter FILTER = new TurboFilter() {    @Override    public FilterReply decide(Marker marker, ch.qos.logback.classic.Logger logger, Level level, String format,            Object[] params, Throwable t) {        // 一律拒絕        return FilterReply.DENY;    }};
    

getStandardConfigLocations 方法

重寫 AbstractLoggingSystem 的方法,獲取 logback 標准的配置文件名稱

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

2.2 initialize 方法

重寫 LoggingSystem 的方法,初始化操作

@Overridepublic void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {    // <1> 獲得 LoggerContext 日志上下文    LoggerContext loggerContext = getLoggerContext();    // <2> 如果 LoggerContext 已有 LoggingSystem,表示已經初始化,則直接返回    if (isAlreadyInitialized(loggerContext)) {        return;    }    // <3> 調用父方法    super.initialize(initializationContext, configLocation, logFile);    // <4> 移除之前添加的 FILTER,可以開始打印日志了    loggerContext.getTurboFilterList().remove(FILTER);    // <5> 標記為已初始化,往 LoggerContext 中添加一個 LoggingSystem 對象    markAsInitialized(loggerContext);    if (StringUtils.hasText(System.getProperty(CONFIGURATION_FILE_PROPERTY))) {        getLogger(LogbackLoggingSystem.class.getName()).warn("Ignoring '" + CONFIGURATION_FILE_PROPERTY                + "' system property. Please use 'logging.config' instead.");    }}

過程如下:

  1. 調用 getLoggerContext() 方法,獲得 LoggerContext 日志上下文

    private LoggerContext getLoggerContext() {    ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory();    // 這里會校驗 `factory` 是否為 LoggerContext 類型    return (LoggerContext) factory;}
    
  2. 如果 LoggerContext 已有 LoggingSystem,表示已經初始化,則直接返回

    private boolean isAlreadyInitialized(LoggerContext loggerContext) {    return loggerContext.getObject(LoggingSystem.class.getName()) != null;}
    
  3. 調用父方法

  4. 移除之前添加的 FILTER,可以開始打印日志了

  5. 調用 markAsInitialized(..) 方法,標記為已初始化,往 LoggerContext 中添加一個 LoggingSystem 對象

2.4 loadConfiguration 方法

重寫 AbstractLoggingSystem 的方法,加載指定的日志配置文件到日志系統中

@Overrideprotected void loadConfiguration(LoggingInitializationContext initializationContext, String location,        LogFile logFile) {    // <1> 調用父方法    super.loadConfiguration(initializationContext, location, logFile);    LoggerContext loggerContext = getLoggerContext();    // <2> 重置 LoggerContext 對象    // 這里會添加一個 LevelChangePropagator 監聽器,當日志級別被修改時會立即生效,而不用重啟應用    stopAndReset(loggerContext);    try {        // <3> 讀取配置文件並解析,配置到 LoggerContext 中        configureByResourceUrl(initializationContext, loggerContext, ResourceUtils.getURL(location));    }    catch (Exception ex) {        throw new IllegalStateException("Could not initialize Logback logging from " + location, ex);    }    // <4> 判斷是否發生錯誤,有的話拋出 IllegalStateException 異常    List<Status> statuses = loggerContext.getStatusManager().getCopyOfStatusList();    StringBuilder errors = new StringBuilder();    for (Status status : statuses) {        if (status.getLevel() == Status.ERROR) {            errors.append((errors.length() > 0) ? String.format("%n") : "");            errors.append(status.toString());        }    }    if (errors.length() > 0) {        throw new IllegalStateException(String.format("Logback configuration error detected: %n%s", errors));    }}

過程如下:

  1. 調用父方法

  2. 重置 LoggerContext 對象,這里會添加一個 LevelChangePropagator 監聽器,當日志級別被修改時會立即生效,而不用重啟應用

    private void stopAndReset(LoggerContext loggerContext) {    // 停止    loggerContext.stop();    // 重置    loggerContext.reset();    // 如果有橋接器    if (isBridgeHandlerInstalled()) {        // 添加一個日志級別的監聽器,能夠及時更新日志級別        addLevelChangePropagator(loggerContext);    }}private void addLevelChangePropagator(LoggerContext loggerContext) {    LevelChangePropagator levelChangePropagator = new LevelChangePropagator();    levelChangePropagator.setResetJUL(true);    levelChangePropagator.setContext(loggerContext);    loggerContext.addListener(levelChangePropagator);}
    
  3. 讀取配置文件並解析,配置到 LoggerContext 中

    private void configureByResourceUrl(LoggingInitializationContext initializationContext, LoggerContext loggerContext,        URL url) throws JoranException {    if (url.toString().endsWith("xml")) {        JoranConfigurator configurator = new SpringBootJoranConfigurator(initializationContext);        configurator.setContext(loggerContext);        configurator.doConfigure(url);    }    else {        new ContextInitializer(loggerContext).configureByResource(url);    }}
    
  4. 判斷是否發生錯誤,有的話拋出 IllegalStateException 異常

reinitialize 方法

實現類 AbstractLoggingSystem 的方法,重新初始化

@Overrideprotected void reinitialize(LoggingInitializationContext initializationContext) {    // 重置    getLoggerContext().reset();    // 清空資源    getLoggerContext().getStatusManager().clear();    // 加載指定的配置文件,此時使用約定的配置文件    loadConfiguration(initializationContext, getSelfInitializationConfig(), null);}

loadDefaults 方法

實現類 AbstractLoggingSystem 的方法,沒有指定的配置文件,也沒有約定的配置文件,那么加載默認的配置到日志系統

@Overrideprotected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {    LoggerContext context = getLoggerContext();    // <1> 重置 LoggerContext 對象    // 這里會添加一個 LevelChangePropagator 監聽器,當日志級別被修改時會立即生效,而不用重啟應用    stopAndReset(context);    // <2> 如果開啟 debug 模式則添加一個 OnConsoleStatusListener 監聽器    boolean debug = Boolean.getBoolean("logback.debug");    if (debug) {        StatusListenerConfigHelper.addOnConsoleListenerInstance(context, new OnConsoleStatusListener());    }    // <3> 往 LoggerContext 中添加默認的日志配置    LogbackConfigurator configurator = debug ? new DebugLogbackConfigurator(context)            : new LogbackConfigurator(context);    Environment environment = initializationContext.getEnvironment();    context.putProperty(LoggingSystemProperties.LOG_LEVEL_PATTERN,            environment.resolvePlaceholders("${logging.pattern.level:${LOG_LEVEL_PATTERN:%5p}}"));    context.putProperty(LoggingSystemProperties.LOG_DATEFORMAT_PATTERN, environment.resolvePlaceholders(            "${logging.pattern.dateformat:${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}"));    context.putProperty(LoggingSystemProperties.ROLLING_FILE_NAME_PATTERN, environment            .resolvePlaceholders("${logging.pattern.rolling-file-name:${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}"));    // <4> 創建 DefaultLogbackConfiguration 對象,設置到 `configurator` 中    // 設置轉換規則,例如顏色轉換,空格轉換    new DefaultLogbackConfiguration(initializationContext, logFile).apply(configurator);    // <5> 設置日志文件,按天切割    context.setPackagingDataEnabled(true);}

過程如下:

  1. 重置 LoggerContext 對象,這里會添加一個 LevelChangePropagator 監聽器,當日志級別被修改時會立即生效,而不用重啟應用

    private void stopAndReset(LoggerContext loggerContext) {    // 停止    loggerContext.stop();    // 重置    loggerContext.reset();    // 如果有橋接器    if (isBridgeHandlerInstalled()) {        // 添加一個日志級別的監聽器,能夠及時更新日志級別        addLevelChangePropagator(loggerContext);    }}private void addLevelChangePropagator(LoggerContext loggerContext) {    LevelChangePropagator levelChangePropagator = new LevelChangePropagator();    levelChangePropagator.setResetJUL(true);    levelChangePropagator.setContext(loggerContext);    loggerContext.addListener(levelChangePropagator);}
    
  2. 如果開啟 debug 模式則添加一個 OnConsoleStatusListener 監聽器

  3. 往 LoggerContext 中添加默認的日志配置

  4. 創建 DefaultLogbackConfiguration 對象,設置到 configurator 中,設置轉換規則,例如顏色轉換,空格轉換

  5. 設置日志文件,按天切割

Log4J2LoggingSystem

org.springframework.boot.logging.log4j2.Log4J2LoggingSystem,繼承 Slf4JLoggingSystem 抽象類,基於 log4j2 的 LoggingSystem 實現類

LogbackLoggingSystem 基本類似,感興趣的小伙伴可以自己去瞧一瞧

JavaLoggingSystem

org.springframework.boot.logging.java.JavaLoggingSystem,繼承 AbstractLoggingSystem 抽象類,基於 jul 的 LoggingSystem 實現類

邏輯比較簡單,感興趣的小伙伴可以自己去瞧一瞧

總結

本文分析了 Sping Boot 初始化不同 LoggingSystem 日志系統的一個過程,同樣是借助於 Spring 的 ApplicationListener 事件監聽器機制,在啟動 Spring 應用的過程中,例如會廣播 應用正在啟動的事件應用環境已准備好,然后 LoggingApplicationListener 監聽到不同的事件會進行不同的初始化操作。

LoggingSystem 日志系統主要分為 logbacklog4j2JUL 三種,本文主要對 logback 的初始化過程進行了分析,因為它是 Spring Boot 的默認日志框架嘛。整個的初始化過程稍微有點繞,嵌套的方法有點多,主要的小節都標注了序號。

大致流程就是先配置 JULslf4j 的橋接器,然后嘗試找到指定的配置文件對日志系統進行配置,可通過 logging.config 設置;沒有指定則獲取約定好的配置文件,例如 logback.xmllog4j2.xml;還沒有獲取到則 Spring 約定好的配置文件,例如 logback-spring.xmllog4j2-spring.xml;要是還沒有找到配置文件,那只能嘗試加載默認的配置了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM