參考 知識星球 中 芋道源碼 星球的源碼解析,一個活躍度非常高的 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-logging
、log4j
、log4j2
、sl4j
和 logback
等等,眼花繚亂。經常會碰到各種依賴沖入的問題,非常煩惱,例如這幾個問題:
- Failed to load class org.slf4j.impl.StaticLoggerBinder,沒找到日志實現,如果你覺得你已經添加了對應的日志實現依賴了,那應該檢查一下版本是否兼容
- Multiple bindings,找到了多個日志實現,也可能是版本問題,
slf4j
會找其中一個作為日志實現
如果想要正確地使用它們,有必要先理清它們之間的關系,我們可以來看看 Log 的發展史,首先從 Java Log 的發展歷程開始說起:
log4j
(作者Ceki Gülcü)出來后,被開發者們廣泛的應用(注意,這里是直接使用),當初是 Java 日志事實上的標准,並成為了 Apache 的項目- Apache 要求把
log4j
並入到 jdk,SUN 表示拒絕,並在 jdk1.4 版本后增加了JUL
(java.util.logging
); - 畢竟是 JDK 自帶的,
JUL
也被很多人使用。同時還有其他的日志組件,如 SimpleLog 等。這個時候如果有人想換成其他日志組件,如log4j
換成JUL
,因為 API 完全不同,就需要改動代碼,當然很多人不願意呀; - Apache 見此,開發了
JCL
(Jakarta Commons Logging),即commons-logging-xx.jar
。它只提供一套通用的日志接口 API,並不提供日志的實現。很好的設計原則嘛,依賴抽象而非實現。這樣一來,我們的應用程序可以在運行時選擇自己想要的日志實現組件; - 這樣看上去也挺美好的,但是
log4j
的作者覺得JCL
不好用,自己開發出一套slf4j
,它跟JCL
類似,本身不替供日志的具體實現,只對外提供接口或門面。目的就是為了替代JCL
。同時,還開發出logback
,一個比log4j
擁有更高性能的組件,目的是為了替代log4j
; - 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();
}
過程如下:
- 創建 LoggingSystem 對象,指定了類型則使用指定的,沒有則嘗試創建對應的對象,ClassLoader 中有對應的 Class 對象則創建(
logback
>log4j2
>java
logging
) - 調用 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());
}
過程如下:
- 如果還未明確 LoggingSystem 類型,那么這里繼續創建 LoggingSystem 對象
- 調用
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);
}
初始過程如下:
-
根據 Environment 環境通過 LoggingSystemProperties 往 System 進行一些日志配置
-
根據 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; }
-
往 System 添加日志文件的名稱和路徑
-
創建一個日志分組對象
-
初始化早期的 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; } } }
-
初始化 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) { // 拋出異常 } } }
-
初始化最終的 Spring Boot 日志級別,逐個設置 Environment 配置的日志級別
-
向 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"));
}
過程如下:
- 從系統參數
org.springframework.boot.logging.LoggingSystem
獲得 LoggingSystem 類型 - 如果非空,說明配置了,那么創建一個該類型的 LoggingSystem 實例對象
- 否則,沒有配置,則通過順序依次嘗試創建對應類型的 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);
}
過程如下
-
調用
getSelfInitializationConfig()
方法,嘗試獲得約定配置文件,例如 log4j2 約定的是 log4j2.xmlprotected 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; }
-
如果找到了約定的配置文件,則調用
reinitialize(..)
抽象方法,自定義初始化,子類實現protected void reinitialize(LoggingInitializationContext initializationContext) { }
-
調用
getSpringInitializationConfig(..)
方法,嘗試獲取約定的配置文件(帶有-spring
),例如 log4j2 對應是 log4j2-spring.xmlprotected 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;}
-
獲取到了
-spring
配置文件,則調用loadConfiguration(..)
抽象方法,加載到日志系統中,子類實現protected abstract void loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile);
-
還沒有找到到指定的配置文件,那么調用
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. }}
過程如下:
-
判斷
JUL
是否橋接到slf4j
了protected final boolean isBridgeJulIntoSlf4j() { // 存在 SLF4JBridgeHandler 類,且 JUL 只有 ConsoleHandler 處理器被創建 return isBridgeHandlerAvailable() && isJulUsingASingleConsoleHandlerAtMost();}
-
移除 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 }}
-
重新安裝 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);}
過程如下:
-
調用
getLoggerContext()
方法,獲得 LoggerContext 日志上下文private LoggerContext getLoggerContext() { ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory(); // 這里會校驗 `factory` 是否為 LoggerContext 類型 return (LoggerContext) factory;}
-
如果 LoggerContext 已有 LoggingSystem,表示已經初始化,則直接返回
private boolean isAlreadyInitialized(LoggerContext loggerContext) { return loggerContext.getObject(LoggingSystem.class.getName()) != null;}
-
調用父方法
-
添加 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."); }}
過程如下:
-
調用
getLoggerContext()
方法,獲得 LoggerContext 日志上下文private LoggerContext getLoggerContext() { ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory(); // 這里會校驗 `factory` 是否為 LoggerContext 類型 return (LoggerContext) factory;}
-
如果 LoggerContext 已有 LoggingSystem,表示已經初始化,則直接返回
private boolean isAlreadyInitialized(LoggerContext loggerContext) { return loggerContext.getObject(LoggingSystem.class.getName()) != null;}
-
調用父方法
-
移除之前添加的 FILTER,可以開始打印日志了
-
調用
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)); }}
過程如下:
-
調用父方法
-
重置 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);}
-
讀取配置文件並解析,配置到 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); }}
-
判斷是否發生錯誤,有的話拋出 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);}
過程如下:
-
重置 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);}
-
如果開啟 debug 模式則添加一個 OnConsoleStatusListener 監聽器
-
往 LoggerContext 中添加默認的日志配置
-
創建 DefaultLogbackConfiguration 對象,設置到
configurator
中,設置轉換規則,例如顏色轉換,空格轉換 -
設置日志文件,按天切割
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 日志系統主要分為 logback
、log4j2
和 JUL
三種,本文主要對 logback
的初始化過程進行了分析,因為它是 Spring Boot 的默認日志框架嘛。整個的初始化過程稍微有點繞,嵌套的方法有點多,主要的小節都標注了序號。
大致流程就是先配置 JUL
到 slf4j
的橋接器,然后嘗試找到指定的配置文件對日志系統進行配置,可通過 logging.config
設置;沒有指定則獲取約定好的配置文件,例如 logback.xml
、log4j2.xml
;還沒有獲取到則 Spring 約定好的配置文件,例如 logback-spring.xml
、log4j2-spring.xml
;要是還沒有找到配置文件,那只能嘗試加載默認的配置了。