前言
但是了解日志框架怎么工作,以及學會Springboot怎么和Log4j2或Logback等日志框架集成,對我們擴展日志功能以及優雅打印日志大有好處,甚至在有些場景,還能通過調整日志的打印策略來提升我們的系統吞吐量。
所以本文將以Springboot集成Log4j2為例,詳細說明Springboot框架下Log4j2是如何工作的,你可能會擔心,如果是使用Logback日志框架該怎么辦呢,其實Log4j2和Logback極其相似,Springboot在啟動時處理Log4j2和處理Logback也幾乎是一樣的套路,所以學會Springboot框架下Log4j2如何工作,切換成Logback也是輕輕松松的。
本文遵循一個該深則深,該淺則淺的整體指導方針,全方位的闡述Springboot中日志怎么工作,思維導圖如下所示。

一. Log4j2簡單工作原理分析
使用Log4j2打印日志時,我們自己接觸最多的就是Logger對象了,Logger對象叫做日志打印器,負責打印日志,一個Logger對象,結構簡單示意如下。

實際打印日志的是Logger對象使用的Appender對象,至于Appender對象怎么打印日志,不在我們本文的關注范圍內。特別注意,在Log4j2中,Logger對象實際只是一個殼子,靈魂是其持有的LoggerConfig對象,LoggerConfig決定打印時使用哪些Appender對象,以及Logger的級別。
LoggerConfig和Appender通常是在Log4j2的配置文件中定義出來的,配置文件通常命名為Log4j2.xml,Log4j2框架在初始化時,會去加載這個配置文件并解析成一個配置對象Configuration,示意如下。

我們每在配置文件的<Appenders>
標簽下增加一項,解析得到的Configuration的appenders中就多一個Appender,每在<Loggers>
標簽下增加一項,解析得到的Configuration的loggerConfigs中就多一個LoggerConfig,并且LoggerConfig解析出來時,其和Appender的關系也就確認了。
在Log4j2中,還有一個LoggerContext對象,這個對象持有上述的Configuration對象,我們使用的每一個Logger,一開始都會先去LoggerContext的loggerRegistry中獲取,如果沒有,則會創建一個Logger出來再緩存到LoggerContext的loggerRegistry中,同時我們在創建Logger時其實核心就是要為這個創建的Logger找到它對應的LoggerConfig,那么去哪里找LoggerConfig呢,當然就是去Configuration中找,所以Logger,LoggerContext和Configuration的關系可以描述成下面這樣子。

所以Log4j2在這種結構下,要修改日志打印器是十分方便的,我們通過LoggerContext就可以拿到Configuration,拿到Configuration之后,我們就可以方便的操作LoggerConfig了,例如最常用的日志打印器級別熱更新就是這么完成的。
在繼續閱讀后文之前,有一個很重要的概念需要闡述清楚,那就是對于Springboot來說,Springboot在操作Logger時,操作的對象就是一個Logger,比如要給一個名字為com.honey.Login
的Logger設置級別為DEBUG,那么在Springboot看來,它就是在設置名字為com.honey.Login
的Logger的級別為DEBUG,但是具體到Log4j2框架,其實底層是在設置名字為com.honey.Login
的LoggerConfig的級別為DEBUG,而具體到Logback框架,就是在設置名字為com.honey.Login
的Logger的級別為DEBUG。
二. Springboot日志簡單配置說明
我們在Springboot中使用Log4j2時,雖然大部分時候我們還是會提供一個Log4j2.xml文件來供Log4j2框架讀取,但是Springboot也提供了一些配置來供我們使用,在分析Springboot日志啟動機制前,先學習一下里面的若干配置項可以方便我們后續的機制理解。
1. logging.file.name
假如我們像下面這樣配置。
logging:
file:
name: test.log
那么Springboot會把日志內容輸出一份到當前項目根路徑下的test.log文件中。
2. logging.file.path
假如我們像下面這樣配置。
logging:
file:
path: /
那么Springboot會把日志內容輸出一份到指定目錄下的spring.log文件中。
3. logging.level
假如我們像下面這樣配置。
logging:
level:
com.pww.App: warn
那么我們可以指定名稱為com.pww.App
的日志打印器的級別為warn級別。
三. Springboot日志啟動機制分析
通常我們使用Springboot時,就算不提供Log4j2.xml配置文件,Springboot也能輸出很漂亮的日志,那么Springboot肯定在背后有幫我們完成Log4j2或Logback等框架的初始化,那么本節就刨析一下Springboot中的日志啟動機制。
Springboot中的日志啟動主要依賴于LoggingApplicationListener
,這個監聽器在Springboot啟動流程中主要會監聽如下三個事件。
- ApplicationStartingEvent: 在啟動
SpringApplication
之后就發布該事件,先于Environmen和ApplicationContext
可用之前發布; - ApplicationEnvironmentPreparedEvent: 在Environmen準備好之后立即發布;
- ApplicationPreparedEvent: 在
ApplicationContext
完全準備好之后但刷新容器之前發布。
下面依次分析下監聽到這些事件后,LoggingApplicationListener
會完成一些什么事情來幫助初始化日志框架。
1. 監聽到ApplicationStartingEvent
LoggingApplicationListener的onApplicationStartingEvent()
方法如下所示。
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
// 讀取org.springframework.boot.logging.LoggingSystem系統屬性來加載得到LoggingSystem
this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
// 調用LoggingSystem的beforeInitialize()方法提前做一些初始化準備工作
this.loggingSystem.beforeInitialize();
}
Springboot中操作日志的最關鍵的一個對象就是LoggingSystem,這個對象會在Springboot的整個生命周期中掌控著日志,在LoggingApplicationListener
監聽到ApplicationStartingEvent
事件后,第一件事情就是先讀取org.springframework.boot.logging.LoggingSystem
系統屬性,得到要加載的LoggingSystem的全限定名,然后完成加載。
如果是使用Log4j2框架,對應的LoggingSystem是Log4J2LoggingSystem
,如果是使用Logback框架,對應的LoggingSystem是LogbackLoggingSystem
,當然我們也可以在LoggingApplicationListener
監聽到ApplicationStartingEvent
事件之前,提前把org.springframework.boot.logging.LoggingSystem
設置為我們自己提供的LoggingSystem的全限定名,這樣我們就可以對Springboot中的日志初始化做一些定制修改。
拿到LoggingSystem后,就會調用其beforeInitialize()
方法來完成日志框架初始化前的一些準備,這里看一下Log4J2LoggingSystem
的beforeInitialize()
方法實現,如下所示。
@Override
public void beforeInitialize() {
LoggerContext loggerContext = getLoggerContext();
if (isAlreadyInitialized(loggerContext)) {
return;
}
super.beforeInitialize();
// 添加一個過濾器
// 這個過濾器會阻止所有日志的打印
loggerContext.getConfiguration().addFilter(FILTER);
}
上述方法最關鍵的就是添加了一個過濾器,雖然叫做過濾器,但是實則為阻斷器,因為這個FILTER
會阻止所有日志打印,Springboot這樣設計是為了防止日志系統在完全完成初始化前打印出不可控的日志。
所以小結一下,LoggingApplicationListener
監聽到ApplicationStartingEvent
之后,主要完成兩件事情。
- 從系統屬性中拿到LoggingSystem的全限定名并完成加載;
- 調用LoggingSystem的
beforeInitialize()
方法來添加會拒絕打印任何日志的過濾器以阻止日志打印。
2. 監聽到ApplicationEnvironmentPreparedEvent
LoggingApplicationListener
的onApplicationEnvironmentPreparedEvent()
方法如下所示。
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
SpringApplication springApplication = event.getSpringApplication();
if (this.loggingSystem == null) {
this.loggingSystem = LoggingSystem.get(springApplication.getClassLoader());
}
// 因為此時Environment已經完成了加載
// 獲取到Environment并繼續調用initialize()方法
initialize(event.getEnvironment(), springApplication.getClassLoader());
}
繼續跟進LoggingApplicationListener
的initialize()
方法。
protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
// 把通過logging.xxx配置的值設置到系統屬性中
getLoggingSystemProperties(environment).apply();
this.logFile = LogFile.get(environment);
if (this.logFile != null) {
// 把logging.file.name和logging.file.path的值設置到系統屬性中
this.logFile.applyToSystemProperties();
}
// 基于預置的web和sql日志打印器初始化LoggerGroups
this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
// 讀取配置中的debug和trace是否設置為true
// 哪個為true就把springBootLogging級別設置為什么
// 同時設置為true則trace優先級更高
initializeEarlyLoggingLevel(environment);
// 調用到具體的LoggingSystem實際初始化日志框架
initializeSystem(environment, this.loggingSystem, this.logFile);
// 完成日志打印器組和日志打印器的級別的設置
initializeFinalLoggingLevels(environment, this.loggingSystem);
registerShutdownHookIfNecessary(environment, this.loggingSystem);
}
上述方法概括下來就是做了三部分的事情。
1、把日志相關配置設置到系統屬性中。例如我們可以通過logging.pattern.console
來配置標準輸出日志格式,但是在XML文件里面沒辦法讀取到logging.pattern.console
配置的值,此時就需要設置一個系統屬性,屬性名是CONSOLE_LOG_PATTERN
,屬性值是logging.pattern.console
配置的值,后續在XML文件中就可以通過${sys:CONSOLE_LOG_PATTERN}
讀取到logging.pattern.console
配置的值。
下表是Springboot中日志配置和系統屬性名的對應關系:

2、調用LoggingSystem的initialize()
方法來完成日志框架初始化。這里就是實際完成Log4j2或Logback等框架的初始化;
3、在日志框架完成初始化后基于logging.level
的配置來設置日志打印器組和日志打印器的級別。
上述第2點是Springboot如何完成具體的日志框架的初始化,這個在后面章節中會詳細分析。上述第3點是日志框架初始化完畢后,Springboot如何幫助我們完成日志打印器組或日志打印器的級別的設置,這里就扯出來一個概念:日志打印器組,也就是LoggerGroup。
我們如果要操作一個Logger,那么實際就是要拿著這個Logger的名稱,去找到Logger,然后再進行操作,這在Logger不多的時候是沒問題的,但是假如我有幾十上百個Logger呢,一個一個去找到Logger再操作無疑是很不現實的,一個實際的場景就是修改Logger的級別,如果是通過Logger的名字去找到Logger再修改級別,那么是很痛苦的一件事情,但是如果能夠把所有Logger按照功能進行分組,我們一組一組的去修改,一下子就優雅起來了,LoggerGroup就是干這個事情的。
一個LoggerGroup,有三個字段,說明如下。
name: 表示LoggerGroup的名字,要操作LoggerGroup時,就通過name來唯一確定一個LoggerGroup,假如有一個LoggerGroup名字為login,那么我們可以通過logging.level.loggin=debug
,將這個LoggerGroup下所有的Logger的級別設置為debug;
members: 是當前LoggerGroup里所有Logger的名字的集合;
configuredLevel: 表示最近一次給LoggerGroup設置的級別。
在Springboot中,通過logging.group
可以配置LoggerGroup,示例如下。
logging:
group:
login:
- com.lee.controller.LoginController
- com.lee.service.LoginService
- com.lee.dao.LoginDao
common:
- com.lee.util
- com.lee.config
結合logging.level可以直接給一組Logger設置級別,示例如下。
logging:
level:
login: info
common: debug
group:
login:
- com.lee.controller.LoginController
- com.lee.service.LoginService
- com.lee.dao.LoginDao
common:
- com.lee.util
- com.lee.config
那么此時名稱為login的LoggerGroup表示如下。
{
'name': 'login',
'members': [
'com.lee.controller.LoginController',
'com.lee.service.LoginService',
'com.lee.dao.LoginDao'
],
'configuredLevel': 'INFO'
}
名稱為common的LoggerGroup表示如下。
{
'name': 'common',
'members': [
'com.lee.util',
'com.lee.config'
],
'configuredLevel': 'DEBUG'
}
最后再看一下Springboot中預置的LoggerGroup,有兩個,名字分別為web和sql,如下所示。
{
'name': 'web',
'members': [
'org.springframework.core.codec',
'org.springframework.http',
'org.springframework.web',
'org.springframework.boot.actuate.endpoint.web',
'org.springframework.boot.web.servlet.ServletContextInitializerBeans'
],
'configuredLevel': ''
}
{
'name': 'sql',
'members': [
'org.springframework.jdbc.core',
'org.hibernate.SQL',
'org.jooq.tools.LoggerListener'
],
'configuredLevel': ''
}
至于web和sql這兩個LoggerGroup的級別是什么,有兩種手段來指定,第一種是通過配置debug=true
來將web和sql這兩個LoggerGroup的級別指定為DEBUG,第二種是通過logging.level.web
和logging.level.sql
來指定web和sql這兩個LoggerGroup的級別,其中第二種優先級高于第一種。
上面最后講的這一點,其實就是告訴我們怎么來控制Springboot自己的相關的日志的打印級別,如果配置debug=true
,那么如下的Springboot自己的LoggerGroup和Logger級別會設置為debug。
sql
web
org.springframework.boot
如果配置trace=true
,那么如下的Springboot自己的Logger級別會設置為trace。
org.springframework
org.apache.tomcat
org.apache.catalina
org.eclipse.jetty
org.hibernate.tool.hbm2ddl
現在小結一下,監聽到ApplicationEnvironmentPreparedEvent
事件后,Springboot主要完成三件事情。
- 設置Springboot和用戶自定義的LoggerGroup與Logger級別。
3. 監聽到ApplicationPreparedEvent
LoggingApplicationListener
的onApplicationPreparedEvent()
方法如下所示。
private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
ConfigurableListableBeanFactory beanFactory = event.getApplicationContext().getBeanFactory();
if (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {
// 把實際加載的LoggingSystem注冊到容器中
beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME, this.loggingSystem);
}
if (this.logFile != null && !beanFactory.containsBean(LOG_FILE_BEAN_NAME)) {
// 把實際使用的LogFile注冊到容器中
beanFactory.registerSingleton(LOG_FILE_BEAN_NAME, this.logFile);
}
if (this.loggerGroups != null && !beanFactory.containsBean(LOGGER_GROUPS_BEAN_NAME)) {
// 把保存著所有LoggerGroup的LoggerGroups注冊到容器中
beanFactory.registerSingleton(LOGGER_GROUPS_BEAN_NAME, this.loggerGroups);
}
}
主要就是把之前加載的LoggingSystem,LogFile和LoggerGroups添加到Spring容器中,進行到這里,其實整個日志框架已經完成初始化了,這里只是把一些和日志密切相關的一些對象注冊為容器中的bean。
最后,本節以下圖對Springboot日志啟動流程做一個總結。

四. Springboot集成Log4j2原理說明
在Springboot中使用Log4j2時,我們不提供Log4j2的配置文件也能打印日志,而我們提供了Log4j2的配置文件后日志打印行為又會以我們提供的配置文件為準,這里面其實Springboot為我們做了很多事情,當我們不提供Log4j2配置文件時,Springboot會加載其預置的配置文件,并且會根據我們是否配置了logging.file.xxx
自動決定是加載預置的log4j2.xml還是log4j2-file.xml,而與此同時Springboot也會盡可能的去搜索我們提供的配置文件,無論我們在classpath下提供的配置文件名字是Log4j2.xml還是Log4j2-spring.xml,都是能夠被Springboot搜索到并加載的。
上述的Springboot集成Log4j2的行為,全部發生在Log4J2LoggingSystem
中,本節將對這里面的流程和原理進行說明。
在第三節中已經知道,Springboot啟動時,當LoggingApplicationListener
監聽到ApplicationEnvironmentPreparedEvent
事件后,最終會調用到LoggingApplicationListener
的initializeSystem()
方法來完成日志框架的初始化,所以我們先看一下這里的邏輯是什么,源碼實現如下。
private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {
// 讀取環境變量中的logging.config作為用戶提供的配置文件路徑
String logConfig = StringUtils.trimWhitespace(environment.getProperty(CONFIG_PROPERTY));
try {
// 創建LoggingInitializationContext用于傳遞Environment對象
LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);
if (ignoreLogConfig(logConfig)) {
// 1. 沒有配置logging.config
system.initialize(initializationContext, null, logFile);
} else {
// 2. 配置了logging.config
system.initialize(initializationContext, logConfig, logFile);
}
} catch (Exception ex) {
// 省略異常處理
}
}
LoggingApplicationListener
的initializeSystem()
方法會讀取logging.config
環境變量得到用戶提供的配置文件路徑,然后帶著配置文件路徑,調用到Log4J2LoggingSystem
的initialize()
方法,所以后續分兩種情況討論,即沒配置logging.config
和有配置logging.config
。
1. 沒配置logging.config
Log4J2LoggingSystem
的initialize()
方法如下所示。
@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
LoggerContext loggerContext = getLoggerContext();
// 判斷LoggerContext的ExternalContext是不是當前LoggingSystem的全限定名
// 如果是則表明當前LoggingSystem已經執行過初始化邏輯
if (isAlreadyInitialized(loggerContext)) {
return;
}
// 移除之前添加的防噪過濾器
loggerContext.getConfiguration().removeFilter(FILTER);
// 調用到父類AbstractLoggingSystem的initialize()方法
// 注意因為沒有配置logging.config所以這里configLocation為null
super.initialize(initializationContext, configLocation, logFile);
// 將當前LoggingSystem的全限定名設置給LoggerContext的ExternalContext
// 表明當前LoggingSystem已經對LoggerContext執行過初始化邏輯
markAsInitialized(loggerContext);
}
上述方法會繼續調用到AbstractLoggingSystem
的initialize()
方法,并且因為沒有配置logging.config
,所以傳遞過去的configLocation
參數為null,下面看一下AbstractLoggingSystem
的initialize()
方法的實現,如下所示。
@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
if (StringUtils.hasLength(configLocation)) {
initializeWithSpecificConfig(initializationContext, configLocation, logFile);
return;
}
// 基于約定尋找配置文件并完成初始化
initializeWithConventions(initializationContext, logFile);
}
因為configLocation為null,所以會繼續調用到initializeWithConventions()
方法完成初始化,并且初始化使用到的配置文件,Springboot會按照約定的名字去classpath尋找,下面看一下initializeWithConventions()
方法的實現。
private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
// 搜索標準日志配置文件路徑
String config = getSelfInitializationConfig();
if (config != null && logFile == null) {
reinitialize(initializationContext);
return;
}
if (config == null) {
// 搜索Spring日志配置文件路徑
config = getSpringInitializationConfig();
}
if (config != null) {
// 如果搜索到約定的配置文件則進行配置文件加載
loadConfiguration(initializationContext, config, logFile);
return;
}
// 如果搜索不到則使用LoggingSystem同目錄下的配置文件
loadDefaults(initializationContext, logFile);
}
上述方法中,首先會去搜索標準日志配置文件路徑,其實就是判斷classpath下是否存在如下名字的配置文件。
log4j2-test.properties
log4j2-test.json
log4j2-test.jsn
log4j2-test.xml
log4j2.properties
log4j2.json
log4j2.jsn
log4j2.xml
如果不存在,則再去搜索Spring日志配置文件路徑,也就是判斷classpath下是否存在如下名字的配置文件。
log4j2-test-spring.properties
log4j2-test-spring.json
log4j2-test-spring.jsn
log4j2-test-spring.xml
log4j2-spring.properties
log4j2-spring.json
log4j2-spring.jsn
log4j2-spring.xml
如果都找不到,此時Springboot就會將Log4J2LoggingSystem
同目錄下的log4j2.xml(無LogFile)或log4j2-file.xml(有LogFile)作為日志配置文件,所以不用擔心找不到配置文件,有Springboot為我們進行兜底。
在獲取到配置文件路徑后,最終會調用到Log4J2LoggingSystem
如下的加載配置的方法。
protected void loadConfiguration(String location, LogFile logFile, List<String> overrides) {
Assert.notNull(location, 'Location must not be null');
try {
List<Configuration> configurations = new ArrayList<>();
LoggerContext context = getLoggerContext();
// 根據配置文件路徑加載得到Configuration并添加到集合中
configurations.add(load(location, context));
// 加載logging.log4j2.config.override配置的配置文件為Configuration
// 所有加載的Configuration都要添加到configurations集合中
for (String override : overrides) {
configurations.add(load(override, context));
}
// 如果得到了大于1個的Configuration則基于所有Configuration創建CompositeConfiguration
Configuration configuration = (configurations.size() > 1) ? createComposite(configurations)
: configurations.iterator().next();
// 將加載得到的Configuration啟動并設置給LoggerContext
// 這里會將加載得到的Configuration覆蓋LoggerContext持有的老的Configuration
context.start(configuration);
} catch (Exception ex) {
throw new IllegalStateException('Could not initialize Log4J2 logging from ' + location, ex);
}
}
上述方法中實際就會拿著配置文件的路徑去加載得到Configuration,與此同時還會拿到所有通過logging.log4j2.config.override
配置的路徑,去加載得到Configuration,最終如果得到大于1個的Configuration,則將這些Configuration創建為CompositeConfiguration
。
這里可能會有疑問,logging.log4j2.config.override
到底是一個什么東西,其實不難發現,無論是通過logging.config
指定了配置文件路徑,還是按照Springboot約定提供了配置文件,亦或者使用了Springboot預置的配置文件,其實最終都只能得到一個配置文件路徑然后得到一個Configuration,那么怎么才能加載多份配置文件呢,那就要通過logging.log4j2.config.override
來指定多個配置文件路徑,使用示例如下。
logging:
config: classpath:Log4j2.xml
log4j2:
config:
override:
- classpath:Log4j2-custom1.xml
- classpath:Log4j2-custom2.xml
如果按照上面這樣配置,那么最終就會加載得到三個Configuration,然后再基于這三個Configuration創建得到一個CompositeConfiguration
。
在加載得到Configuration之后,就會調用到LoggerContext
的start()
方法完成Log4j2框架的初始化,那么這里其實會做如下三件事情。
調用Configuration的start() 方法完成配置對象的初始化。 這里其實就是將我們在配置文件中定義的各種Appedner和LoggerConfig等都創建出來并完成啟動;
將啟動完畢的Configuration設置給LoggerContext。 這里會把LoggerContext持有的老的Configuration覆蓋掉,所以如果LoggerContext之前持有其它的Configuration,那么其實在Springboot日志初始化完畢后老的Configuration會被丟棄掉;
更新Logger。 如果之前有已經創建好的Logger,那么就基于新的Configuration替換掉這些Logger持有的LoggerConfig。
至此,沒配置logging.config
時的初始化邏輯就分析完畢。
2. 有配置logging.config
有配置logging.config時,情況就變得簡單了。還是從Log4J2LoggingSystem
的initialize()
方法出發,跟一下源碼。
@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
LoggerContext loggerContext = getLoggerContext();
if (isAlreadyInitialized(loggerContext)) {
return;
}
loggerContext.getConfiguration().removeFilter(FILTER);
// 調用到父類AbstractLoggingSystem的initialize()方法
// 注意因為配置了logging.config所以這里configLocation不為null
super.initialize(initializationContext, configLocation, logFile);
markAsInitialized(loggerContext);
}
繼續跟進AbstractLoggingSystem
的initialize()
方法,如下所示。
@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
if (StringUtils.hasLength(configLocation)) {
// 基于指定的配置文件完成初始化
initializeWithSpecificConfig(initializationContext, configLocation, logFile);
return;
}
initializeWithConventions(initializationContext, logFile);
}
由于指定了配置文件,所以會調用到AbstractLoggingSystem
的initializeWithSpecificConfig()
方法,該方法沒有什么額外邏輯,最終會執行到和沒配置logging.config
時一樣的Log4J2LoggingSystem
的加載配置的方法,如下所示。
protected void loadConfiguration(String location, LogFile logFile, List<String> overrides) {
Assert.notNull(location, 'Location must not be null');
try {
List<Configuration> configurations = new ArrayList<>();
LoggerContext context = getLoggerContext();
// 根據配置文件路徑加載得到Configuration并添加到集合中
configurations.add(load(location, context));
// 加載logging.log4j2.config.override配置的配置文件為Configuration
// 所有加載的Configuration都要添加到configurations集合中
for (String override : overrides) {
configurations.add(load(override, context));
}
// 如果得到了大于1個的Configuration則基于所有Configuration創建CompositeConfiguration
Configuration configuration = (configurations.size() > 1) ? createComposite(configurations)
: configurations.iterator().next();
// 將加載得到的Configuration啟動并設置給LoggerContext
// 這里會將加載得到的Configuration覆蓋LoggerContext持有的老的Configuration
context.start(configuration);
} catch (Exception ex) {
throw new IllegalStateException('Could not initialize Log4J2 logging from ' + location, ex);
}
}
所以配置了logging.config
時,就會以logging.config
指定的配置文件作為最終使用的配置文件,而不會去基于約定搜索配置文件,同時也不會去使用LoggingSystem同目錄下預置的配置文件。
小結一下,Springboot集成Log4j2日志框架時,主要分為兩種情況:
沒配置logging.config。 這種情況下,Springboot會基于約定努力去尋找符合的配置文件,如果找不到則會使用預置的配置文件且預置的配置文件需要在LoggingSystem
的同目錄下,拿到配置文件后就會加載為Configuration
然后替換掉LoggerContext
里的舊的Configuration
,此時就完成日志框架初始化;
有配置logging.config。 這種情況下,會將logging.config
指定的配置文件加載為Configuration
,然后替換掉LoggerContext
里的舊的Configuration
,此時就完成日志框架初始化。
無論有沒有配置logging.config
,都只能加載一個配置文件為Configuration,如果想加載多個Configuration,那么需要通過logging.log4j2.config.override
配置多個配置文件路徑,此時就能加載多個Configuration來初始化Log4j2日志框架了。
Springboot集成Log4j2日志框架的流程圖如下所示。

五. Springboot日志打印器級別熱更新
在日志打印中,一條日志在發起打印時,會根據我們的指定攜帶一個日志級別,同時打印日志的日志打印器,也有一個級別,日志打印器只能打印級別高于或等于自身的日志。
由于日志打印時,日志級別是由代碼決定的,所以日志級別除非改代碼,否則無法改變,但是日志打印器的級別是可以隨時更改的,最簡單的方式就是通過配置環境變量來更改logging.level
,此時我們的應用進程所處的容器就會重啟,就可以讀取到我們更改后的logging.level
,最終完成日志打印器級別的修改。
但是這種方式會使應用重啟,導致流量受損,我們更希望的是通過一種熱更新的方式來修改日志打印器的級別,spring-boot-actuator包中提供了LoggersEndpoint來完成日志打印器級別熱更新,所以本節將結合LoggersEndpoint的簡單使用和實現原理,說明一下Springboot中,如何熱更新日志打印器級別。
1. LoggersEndpoint簡單使用
LoggersEndpoint由spring-boot-actuator
提供,可以暴露一些端點用于獲取Springboot應用中的所有日志打印器信息及其級別信息以及熱更新日志打印器級別,由于默認情況下,LoggersEndpoint暴露的端點只能通過JMX的方式訪問,所以想要通過HTTP請求的方式訪問到LoggersEndpoint,需要做如下配置。
management:
server:
address: 127.0.0.1
port: 10999
endpoints:
web:
base-path: /actuator
exposure:
include: loggers # 設置LoggersEndpoint可以通過HTTP方式訪問
endpoint:
loggers:
enabled: true # 打開LoggersEndpoint
按照上述這么配置,我們可以通過GET調用如下接口拿到當前所有的日志打印器的相關數據。
“http://localhost:10999/actuator/loggers
獲取數據如下所示。
{
'levels': [
'OFF',
'FATAL',
'ERROR',
'WARN',
'INFO',
'DEBUG',
'TRACE'
],
'loggers': {
'ROOT': {
'configuredLevel': null,
'effectiveLevel': 'INFO'
},
'org.springframework.boot.actuate.autoconfigure.web.server': {
'configuredLevel': null,
'effectiveLevel': 'DEBUG'
},
'org.springframework.http.converter.ResourceRegionHttpMessageConverter': {
'configuredLevel': null,
'effectiveLevel': 'ERROR'
}
},
'groups': {
'web': {
'configuredLevel': null,
'members': [
'org.springframework.core.codec',
'org.springframework.http',
'org.springframework.web',
'org.springframework.boot.actuate.endpoint.web',
'org.springframework.boot.web.servlet.ServletContextInitializerBeans'
]
},
'login': {
'configuredLevel': 'INFO',
'members': [
'com.lee.controller.LoginController',
'com.lee.service.LoginService',
'com.lee.dao.LoginDao'
]
},
'common': {
'configuredLevel': 'DEBUG',
'members': [
'com.lee.util',
'com.lee.config'
]
},
'sql': {
'configuredLevel': null,
'members': [
'org.springframework.jdbc.core',
'org.hibernate.SQL',
'org.jooq.tools.LoggerListener'
]
}
}
}
上述內容中,返回的levels表示當前支持的日志級別,返回的loggers表示當前所有日志打印器的級別信息,返回的groups表示當前所有日志打印器組的級別信息,但是請注意,上述示例中的loggers其實做了大量的刪減,實際調用接口時得到的loggers里面的內容會非常非常多,因為所有的日志打印器的信息都會被輸出出來。
此外,上述內容中出現的configuredLevel字段表示當前日志打印器或日志打印器組被設置過的級別,也就是只要通過LoggersEndpoint給某個日志打印器或日志打印器組設置過級別,那么對應的configuredLevel字段就有值,最后上述內容中出現的effectiveLevel字段表示當前日志打印器正在生效的級別。
如果只想看某個日志打印器或日志打印器組的級別信息,可以調用如下的GET接口。
“http://localhost:10999/actuator/loggers/{日志打印器名或日志打印器組名}
如果pathVariable是日志打印器名,那么會得到如下結果。
{
'configuredLevel': null,
'effectiveLevel': 'INFO'
}
如果pathVariable是日志打印器組名,那么會得到如下結果。
{
'configuredLevel': null,
'members': [
'org.springframework.core.codec',
'org.springframework.http',
'org.springframework.web',
'org.springframework.boot.actuate.endpoint.web',
'org.springframework.boot.web.servlet.ServletContextInitializerBeans'
]
}
除了查詢日志打印器或日志打印器組的級別信息,LoggersEndpoint更重要的功能是設置級別,比如可以通過如下POST接口來設置級別。
“http://localhost:10999/actuator/loggers/{日志打印器名或日志打印器組名}
{
'configuredLevel': 'DEBUG'
}
此時對應的日志打印器或日志打印器組的級別就會更新為設置的級別,并且其configuredLevel也會更新為設置的級別。
2. LoggersEndpoint原理分析
這里主要關注LoggersEndpoint如何實現日志打印器級別的熱更新。LoggersEndpoint實現日志打印器級別的熱更新對應的端點方法如下所示。
@WriteOperation
public void configureLogLevel(@Selector String name, @Nullable LogLevel configuredLevel) {
Assert.notNull(name, 'Name must not be empty');
// 先嘗試獲取到LoggerGroup
LoggerGroup group = this.loggerGroups.get(name);
if (group != null && group.hasMembers()) {
// 如果能獲取到LoggerGroup則對組下每個Logger熱更新級別
group.configureLogLevel(configuredLevel, this.loggingSystem::setLogLevel);
return;
}
// 獲取不到LoggerGroup則按照Logger來處理
this.loggingSystem.setLogLevel(name, configuredLevel);
}
上述方法的name即可以是Logger的名稱,也可以是LoggerGroup的名稱,如果是Logger的名稱,那么就基于LoggingSystem
的setLogLevel()
方法來設置這個Logger的級別,如果是LoggerGroup的名稱,那么就遍歷這個組下所有的Logger,每個遍歷到的Logger都基于LoggingSystem的setLogLevel() 方法來設置級別。
所以實際上LoggersEndpoint
熱更新日志打印器級別,還是依賴的對應日志框架的LoggingSystem
。
3. Log4J2LoggingSystem熱更新原理
由于本文是基于Log4j2日志框架進行討論,所以這里選擇分析Log4J2LoggingSystem
的setLogLevel()
方法,來探究Logger級別如何熱更新。
在開始分析前,有一點需要重申,那就是對于Log4j2來說,Logger只是殼子,靈魂是Logger持有的LoggerConfig,所以更新Log4j2里面的Logger的級別,其實就是要去更新其持有的LoggerConfig的級別。
Log4J2LoggingSystem
的setLogLevel()
方法如下所示。
@Override
public void setLogLevel(String loggerName, LogLevel logLevel) {
// 將LogLevel轉換為Level
setLogLevel(loggerName, LEVELS.convertSystemToNative(logLevel));
}
LogLevel是Springboot中的日志級別對象,Level是Log4j2的日志級別對象,所以需要先將LogLevel轉換為Level,然后繼續調用如下方法。
private void setLogLevel(String loggerName, Level level) {
// 從Configuration中根據loggerName獲取到對應的LoggerConfig
LoggerConfig logger = getLogger(loggerName);
if (level == null) {
// 2. 移除LoggerConfig或設置LoggerConfig級別為null
clearLogLevel(loggerName, logger);
} else {
// 1. 添加LoggerConfig或設置LoggerConfig級別
setLogLevel(loggerName, logger, level);
}
// 3. 更新Logger
getLoggerContext().updateLoggers();
}
通過第一節知道,Log4j2的Configuration對象有一個字段叫做loggerConfigs,所以上面首先就是通過loggerName去loggerConfigs中匹配對應的LoggerConfig,那么這里就會存在一個問題,那就是配置文件里面每配一個Logger,loggerConfigs才會增加一個LoggerConfig,所以實際上loggerConfigs里面的LoggerConfig并不會很多,比如我們提供了如下一個Log4j2.xml文件。
<?xml version='1.0' encoding='UTF-8'?>
<Configuration status='INFO'>
<Appenders>
<Console name='MyConsole'/>
</Appenders>
<Loggers>
<Root level='INFO'>
<Appender-ref ref='MyConsole'/>
</Root>
<Logger name='com.honey' level='WARN'>
<Appender-ref ref='MyConsole'/>
</Logger>
<Logger name='com.honey.auth.Login' level='DEBUG'>
<Appender-ref ref='MyConsole'/>
</Logger>
</Loggers>
</Configuration>
那么實際加載得到的Configuration的loggerConfigs只有下面這幾個名字的LoggerConfig。
''
com.honey
com.honey.auth.Login
其中空字符串是根日志打印器(rootLogger)的名字。此時如果在調用Log4J2LoggingSystem的setLogLevel()
方法時傳入的loggerName是com.honey.auth.Login
,我們可以很順利的從Configuration的loggerConfigs中拿到名字是com.honey.auth.Login
的LoggerConfig,可要是傳入的loggerName是com.honey.auth.Logout
呢,那么獲取出來的LoggerConfig肯定是null,此時該怎么處理呢,難道就不設置日志打印器的級別了嗎?
當然不是的,Springboot在這里做了一個巨巧妙的設計,就是如果熱更新Log4j2時通過loggerName沒有獲取到LoggerConfig,那么Springboot就會創建一個LevelSetLoggerConfig
(LoggerConfig的子類)然后添加到Configuration的loggerConfigs中。
下面先看一下LevelSetLoggerConfig
長什么樣。
private static class LevelSetLoggerConfig extends LoggerConfig {
LevelSetLoggerConfig(String name, Level level, boolean additive) {
super(name, level, additive);
}
}
既然我們往Configuration的loggerConfigs中添加了一個名字是com.honey.auth.Logout
的LevelSetLoggerConfig
,那么名字是com.honey.auth.Logout
的Logger理所應當的就會持有名字是com.honey.auth.Logout
的LevelSetLoggerConfig
,但是聰明的人就發現了,這個新創建出來的LevelSetLoggerConfig
也是沒有靈魂的,為什么呢,因為LevelSetLoggerConfig
不引用任何的Appedner,沒有Appedner怎么打日志嘛,不過不用擔心,只要在創建LevelSetLoggerConfig時,將additive指定為true,這個問題就解決了。
在Log4j2中,LoggerConfig之間是有父子關系的,假如Configuration的loggerConfigs有下面這幾個名字的LoggerConfig。
''
com.honey
com.honey.auth.Login
那么名字是com.honey.auth.Login
的LoggerConfig會依次按照com.honey.auth
,com.honey,com
和 ''
去尋找自己的父LoggerConfig,所以每個LoggerConfig都有自己的父LoggerConfig,而additive參數的含義就是,當前日志是否還需要由父LoggerConfig打印,如果某個LoggerConfig的additive是true,那么一條日志除了讓自己的所有Appedner打印,還會讓父LoggerConfig的所有Appender來打印。
所以只要在創建LevelSetLoggerConfig時,將additive指定為true,就算LevelSetLoggerConfig自己沒有Appender,父親也是可以打印日志的。下面舉個例子來加深理解,還是假如Configuration的loggerConfigs有下面這幾個名字的LoggerConfig。
''
com.honey
com.honey.auth.Login
我們已經有一個名字為com.honey.auth.Logout
的Logger,并且按照Logger尋找LoggerConfig的規則,我們知道名字為com.honey.auth.Logout
的Logger會持有名字為com.honey的LoggerConfig,那么現在我們要熱更新名字為com.honey.auth.Logout
的Logger的級別,此時拿著com.honey.auth.Logout
從Configuration的loggerConfigs中獲取出來的LoggerConfig肯定為null,所以我們會創建一個名字為com.honey.auth.Logout
的LevelSetLoggerConfig
,并且這個LevelSetLoggerConfig
的additive為true,此時Configuration的loggerConfigs有下面這幾個名字的LoggerConfig。
''
com.honey
com.honey.auth.Login
com.honey.auth.Logout
此時我們重新讓名字為com.honey.auth.Logout
的Logger去尋找自己應該持有的LoggerConfig,那么肯定就會找到名字為com.honey.auth.Logout
的LevelSetLoggerConfig
,由于Log4j2中,Logger的級別跟著LoggerConfig走,所以名字為com.honey.auth.Logout
的Logger的級別就更新了,現在使用名字為com.honey.auth.Logout
的Logger打印日志,首先會讓其持有的LoggerConfig引用的Appedner來打印,由于沒有引用Appedner,所以不會打印日志,然后再讓其父LoggerConfig引用的Appedner來打印日志,而名字為com.honey.auth.Logou
t的LevelSetLoggerConfig的父親其實就是名字為com.honey
的LoggerConfig,所以最終還是讓名字為com.honey
的LoggerConfig引用的Appedner完成了日志打印。
到這里仿佛好像逐漸偏離了本小節的主題,其實不是的,我們現在再回看Log4J2LoggingSystem
的setLogLevel()
方法,如下所示。
private void setLogLevel(String loggerName, Level level) {
// 從Configuration中根據loggerName獲取到對應的LoggerConfig
LoggerConfig logger = getLogger(loggerName);
if (level == null) {
// 2. 移除LoggerConfig或設置LoggerConfig級別為null
clearLogLevel(loggerName, logger);
} else {
// 1. 添加LoggerConfig或設置LoggerConfig級別
setLogLevel(loggerName, logger, level);
}
// 3. 更新Logger
getLoggerContext().updateLoggers();
}
首先是第1點,在傳入的level不為空時,我們就會去設置對應的LoggerConfig的級別,如果獲取到的LoggerConfig為空,那么就會創建一個名字為loggerName,級別為level的LevelSetLoggerConfig
并加到Configuration的loggerConfigs中,如果獲取到的LoggerConfig不為空,則直接修改LoggerConfig的level字段。
其次是第2點,傳入level為空時,此時要求能通過loggerName找到LoggerConfig,否則拋空指針異常。如果通過loggerName找到的LoggerConfig不為空,此時需要判斷一下LoggerConfig的類型,如果LoggerConfig實際類型是LevelSetLoggerConfig
,那么就從Configuration
的loggerConfigs
中將其移除,如果LoggerConfig實際類型就是LoggerConfig,那么就設置LoggerConfig的level字段為null。
最后是第3點,在前面第1和第2點,我們已經讓目標LoggerConfig
的級別完成了更新,此時就需要讓LoggerContext
里面所有的Logger重新去匹配一次自己的LoggerConfig,至此就完成了Logger的級別的更新。
相信到這里,Log4J2LoggingSystem
熱更新原理就闡釋清楚了,小結一下就是通過loggerName找LoggerConfig,找到了就更新其level,找不到就創建一個名字為loggerName的LevelSetLoggerConfig
,最后讓所有Logger去重新匹配一下自己的LoggerConfig,此時我們的目標Logger就會持有更新過級別的LoggerConfig了。
最后給出基于LoggersEndpoint
熱更新Log4j2日志打印器的流程圖,如下所示。

六. 自定義Springboot下日志打印器級別熱更新
有些時候,使用spring-boot-actuator
包提供的LoggersEndpoint來熱更新日志打印器級別,是有點不方便的,因為想要熱更新日志級別而引入spring-boot-actuator
包,大部分時候這個操作都有點重,而通過上面的分析,我們發現其實熱更新日志打印器級別的原理特別簡單,就是通過LoggingSystem來操作Logger,所以我們可以自己提供一個接口,通過這個接口來操作Logger的級別。
@RestController
public class HotModificationLevel {
private final LoggingSystem loggingSystem;
public HotModificationLevel(LoggingSystem loggingSystem) {
this.loggingSystem = loggingSystem;
}
@PostMapping('/logger/level')
public void setLoggerLevel(@RequestBody SetLoggerLevelParam levelParam) {
loggingSystem.setLogLevel(levelParam.getLoggerName(), levelParam.getLoggerLevel());
}
public static class SetLoggerLevelParam {
private String loggerName;
private LogLevel loggerLevel;
// 省略getter和setter
}
}
通過調用上述接口使用LoggingSystem就能夠完成指定日志打印器的級別熱更新。
總結
對于Log4j2日志框架,我們需要知道Logger只是一個殼子,靈魂是Logger持有的LoggerConfig。
Springboot框架啟動時,日志的初始化的發起點是LoggingApplicationListener
,但是實際去尋找日志框架的配置文件并完成日志框架初始化是LoggingSystem。
在Springboot中提供日志框架的配置文件時,我們可以將配置文件命名為約定的名字然后放在classpath下,也可以通過logging.config
顯示的指定要使用的配置文件的路徑,甚至可以完全不自己提供配置文件而使用Springboot預置的配置文件,因此使用Springboot框架,想打印日志是十分容易的。
Springboot框架中,為了統一的管理一組Logger,定義了一個日志打印器組LoggerGroup,通過操作LoggerGroup,可以方便的操作一組Logger,我們可以使用logging.group.xxx
來定義LoggerGroup,而xxx就是組名,后續拿著組名就可以找到LoggerGroup并操作。
所謂日志打印器級別熱更新,其實就是不重啟應用的情況下修改日志打印器的級別,核心思路就是通過LoggingSystem去操作底層的日志框架,因為LoggingSystem可以為我們屏蔽底層的日志框架的細節,所以通過LoggingSystem修改日志打印器級別,是十分容易的。
轉載自:https:///post/7348309454700183561