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

一. Log4j2簡(jiǎn)單工作原理分析
使用Log4j2打印日志時(shí),我們自己接觸最多的就是Logger對(duì)象了,Logger對(duì)象叫做日志打印器,負(fù)責(zé)打印日志,一個(gè)Logger對(duì)象,結(jié)構(gòu)簡(jiǎn)單示意如下。

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

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

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

2、調(diào)用LoggingSystem的initialize() 方法來完成日志框架初始化。這里就是實(shí)際完成Log4j2或Logback等框架的初始化;
3、在日志框架完成初始化后基于logging.level的配置來設(shè)置日志打印器組和日志打印器的級(jí)別。
上述第2點(diǎn)是Springboot如何完成具體的日志框架的初始化,這個(gè)在后面章節(jié)中會(huì)詳細(xì)分析。上述第3點(diǎn)是日志框架初始化完畢后,Springboot如何幫助我們完成日志打印器組或日志打印器的級(jí)別的設(shè)置,這里就扯出來一個(gè)概念:日志打印器組,也就是LoggerGroup。
我們?nèi)绻僮饕粋€(gè)Logger,那么實(shí)際就是要拿著這個(gè)Logger的名稱,去找到Logger,然后再進(jìn)行操作,這在Logger不多的時(shí)候是沒問題的,但是假如我有幾十上百個(gè)Logger呢,一個(gè)一個(gè)去找到Logger再操作無疑是很不現(xiàn)實(shí)的,一個(gè)實(shí)際的場(chǎng)景就是修改Logger的級(jí)別,如果是通過Logger的名字去找到Logger再修改級(jí)別,那么是很痛苦的一件事情,但是如果能夠把所有Logger按照功能進(jìn)行分組,我們一組一組的去修改,一下子就優(yōu)雅起來了,LoggerGroup就是干這個(gè)事情的。
一個(gè)LoggerGroup,有三個(gè)字段,說明如下。
name: 表示LoggerGroup的名字,要操作LoggerGroup時(shí),就通過name來唯一確定一個(gè)LoggerGroup,假如有一個(gè)LoggerGroup名字為login,那么我們可以通過logging.level.loggin=debug,將這個(gè)LoggerGroup下所有的Logger的級(jí)別設(shè)置為debug;
members: 是當(dāng)前LoggerGroup里所有Logger的名字的集合;
configuredLevel: 表示最近一次給LoggerGroup設(shè)置的級(jí)別。
在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
結(jié)合logging.level可以直接給一組Logger設(shè)置級(jí)別,示例如下。
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
那么此時(shí)名稱為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中預(yù)置的LoggerGroup,有兩個(gè),名字分別為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這兩個(gè)LoggerGroup的級(jí)別是什么,有兩種手段來指定,第一種是通過配置debug=true來將web和sql這兩個(gè)LoggerGroup的級(jí)別指定為DEBUG,第二種是通過logging.level.web和logging.level.sql來指定web和sql這兩個(gè)LoggerGroup的級(jí)別,其中第二種優(yōu)先級(jí)高于第一種。
上面最后講的這一點(diǎn),其實(shí)就是告訴我們?cè)趺磥砜刂芐pringboot自己的相關(guān)的日志的打印級(jí)別,如果配置debug=true,那么如下的Springboot自己的LoggerGroup和Logger級(jí)別會(huì)設(shè)置為debug。
sql
web
org.springframework.boot
如果配置trace=true,那么如下的Springboot自己的Logger級(jí)別會(huì)設(shè)置為trace。
org.springframework
org.apache.tomcat
org.apache.catalina
org.eclipse.jetty
org.hibernate.tool.hbm2ddl
現(xiàn)在小結(jié)一下,監(jiān)聽到ApplicationEnvironmentPreparedEvent事件后,Springboot主要完成三件事情。
- 把通過配置文件配置的日志相關(guān)屬性設(shè)置為系統(tǒng)屬性;
- 設(shè)置Springboot和用戶自定義的LoggerGroup與Logger級(jí)別。
3. 監(jiān)聽到ApplicationPreparedEvent
LoggingApplicationListener的onApplicationPreparedEvent() 方法如下所示。
private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
ConfigurableListableBeanFactory beanFactory = event.getApplicationContext().getBeanFactory();
if (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {
// 把實(shí)際加載的LoggingSystem注冊(cè)到容器中
beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME, this.loggingSystem);
}
if (this.logFile != null && !beanFactory.containsBean(LOG_FILE_BEAN_NAME)) {
// 把實(shí)際使用的LogFile注冊(cè)到容器中
beanFactory.registerSingleton(LOG_FILE_BEAN_NAME, this.logFile);
}
if (this.loggerGroups != null && !beanFactory.containsBean(LOGGER_GROUPS_BEAN_NAME)) {
// 把保存著所有LoggerGroup的LoggerGroups注冊(cè)到容器中
beanFactory.registerSingleton(LOGGER_GROUPS_BEAN_NAME, this.loggerGroups);
}
}
主要就是把之前加載的LoggingSystem,LogFile和LoggerGroups添加到Spring容器中,進(jìn)行到這里,其實(shí)整個(gè)日志框架已經(jīng)完成初始化了,這里只是把一些和日志密切相關(guān)的一些對(duì)象注冊(cè)為容器中的bean。
最后,本節(jié)以下圖對(duì)Springboot日志啟動(dòng)流程做一個(gè)總結(jié)。

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

五. Springboot日志打印器級(jí)別熱更新
在日志打印中,一條日志在發(fā)起打印時(shí),會(huì)根據(jù)我們的指定攜帶一個(gè)日志級(jí)別,同時(shí)打印日志的日志打印器,也有一個(gè)級(jí)別,日志打印器只能打印級(jí)別高于或等于自身的日志。
由于日志打印時(shí),日志級(jí)別是由代碼決定的,所以日志級(jí)別除非改代碼,否則無法改變,但是日志打印器的級(jí)別是可以隨時(shí)更改的,最簡(jiǎn)單的方式就是通過配置環(huán)境變量來更改logging.level,此時(shí)我們的應(yīng)用進(jìn)程所處的容器就會(huì)重啟,就可以讀取到我們更改后的logging.level,最終完成日志打印器級(jí)別的修改。
但是這種方式會(huì)使應(yīng)用重啟,導(dǎo)致流量受損,我們更希望的是通過一種熱更新的方式來修改日志打印器的級(jí)別,spring-boot-actuator包中提供了LoggersEndpoint來完成日志打印器級(jí)別熱更新,所以本節(jié)將結(jié)合LoggersEndpoint的簡(jiǎn)單使用和實(shí)現(xiàn)原理,說明一下Springboot中,如何熱更新日志打印器級(jí)別。
1. LoggersEndpoint簡(jiǎn)單使用
LoggersEndpoint由spring-boot-actuator提供,可以暴露一些端點(diǎn)用于獲取Springboot應(yīng)用中的所有日志打印器信息及其級(jí)別信息以及熱更新日志打印器級(jí)別,由于默認(rèn)情況下,LoggersEndpoint暴露的端點(diǎn)只能通過JMX的方式訪問,所以想要通過HTTP請(qǐng)求的方式訪問到LoggersEndpoint,需要做如下配置。
management:
server:
address: 127.0.0.1
port: 10999
endpoints:
web:
base-path: /actuator
exposure:
include: loggers # 設(shè)置LoggersEndpoint可以通過HTTP方式訪問
endpoint:
loggers:
enabled: true # 打開LoggersEndpoint
按照上述這么配置,我們可以通過GET調(diào)用如下接口拿到當(dāng)前所有的日志打印器的相關(guān)數(shù)據(jù)。
“http://localhost:10999/actuator/loggers
獲取數(shù)據(jù)如下所示。
{
'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'
]
}
}
}
上述內(nèi)容中,返回的levels表示當(dāng)前支持的日志級(jí)別,返回的loggers表示當(dāng)前所有日志打印器的級(jí)別信息,返回的groups表示當(dāng)前所有日志打印器組的級(jí)別信息,但是請(qǐng)注意,上述示例中的loggers其實(shí)做了大量的刪減,實(shí)際調(diào)用接口時(shí)得到的loggers里面的內(nèi)容會(huì)非常非常多,因?yàn)樗械娜罩敬蛴∑鞯男畔⒍紩?huì)被輸出出來。
此外,上述內(nèi)容中出現(xiàn)的configuredLevel字段表示當(dāng)前日志打印器或日志打印器組被設(shè)置過的級(jí)別,也就是只要通過LoggersEndpoint給某個(gè)日志打印器或日志打印器組設(shè)置過級(jí)別,那么對(duì)應(yīng)的configuredLevel字段就有值,最后上述內(nèi)容中出現(xiàn)的effectiveLevel字段表示當(dāng)前日志打印器正在生效的級(jí)別。
如果只想看某個(gè)日志打印器或日志打印器組的級(jí)別信息,可以調(diào)用如下的GET接口。
“http://localhost:10999/actuator/loggers/{日志打印器名或日志打印器組名}
如果pathVariable是日志打印器名,那么會(huì)得到如下結(jié)果。
{
'configuredLevel': null,
'effectiveLevel': 'INFO'
}
如果pathVariable是日志打印器組名,那么會(huì)得到如下結(jié)果。
{
'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'
]
}
除了查詢?nèi)罩敬蛴∑骰蛉罩敬蛴∑鹘M的級(jí)別信息,LoggersEndpoint更重要的功能是設(shè)置級(jí)別,比如可以通過如下POST接口來設(shè)置級(jí)別。
“http://localhost:10999/actuator/loggers/{日志打印器名或日志打印器組名}
{
'configuredLevel': 'DEBUG'
}
此時(shí)對(duì)應(yīng)的日志打印器或日志打印器組的級(jí)別就會(huì)更新為設(shè)置的級(jí)別,并且其configuredLevel也會(huì)更新為設(shè)置的級(jí)別。
2. LoggersEndpoint原理分析
這里主要關(guān)注LoggersEndpoint如何實(shí)現(xiàn)日志打印器級(jí)別的熱更新。LoggersEndpoint實(shí)現(xiàn)日志打印器級(jí)別的熱更新對(duì)應(yīng)的端點(diǎn)方法如下所示。
@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則對(duì)組下每個(gè)Logger熱更新級(jí)別
group.configureLogLevel(configuredLevel, this.loggingSystem::setLogLevel);
return;
}
// 獲取不到LoggerGroup則按照Logger來處理
this.loggingSystem.setLogLevel(name, configuredLevel);
}
上述方法的name即可以是Logger的名稱,也可以是LoggerGroup的名稱,如果是Logger的名稱,那么就基于LoggingSystem的setLogLevel() 方法來設(shè)置這個(gè)Logger的級(jí)別,如果是LoggerGroup的名稱,那么就遍歷這個(gè)組下所有的Logger,每個(gè)遍歷到的Logger都基于LoggingSystem的setLogLevel() 方法來設(shè)置級(jí)別。
所以實(shí)際上LoggersEndpoint熱更新日志打印器級(jí)別,還是依賴的對(duì)應(yīng)日志框架的LoggingSystem。
3. Log4J2LoggingSystem熱更新原理
由于本文是基于Log4j2日志框架進(jìn)行討論,所以這里選擇分析Log4J2LoggingSystem的setLogLevel() 方法,來探究Logger級(jí)別如何熱更新。
在開始分析前,有一點(diǎn)需要重申,那就是對(duì)于Log4j2來說,Logger只是殼子,靈魂是Logger持有的LoggerConfig,所以更新Log4j2里面的Logger的級(jí)別,其實(shí)就是要去更新其持有的LoggerConfig的級(jí)別。
Log4J2LoggingSystem的setLogLevel() 方法如下所示。
@Override
public void setLogLevel(String loggerName, LogLevel logLevel) {
// 將LogLevel轉(zhuǎn)換為L(zhǎng)evel
setLogLevel(loggerName, LEVELS.convertSystemToNative(logLevel));
}
LogLevel是Springboot中的日志級(jí)別對(duì)象,Level是Log4j2的日志級(jí)別對(duì)象,所以需要先將LogLevel轉(zhuǎn)換為L(zhǎng)evel,然后繼續(xù)調(diào)用如下方法。
private void setLogLevel(String loggerName, Level level) {
// 從Configuration中根據(jù)loggerName獲取到對(duì)應(yīng)的LoggerConfig
LoggerConfig logger = getLogger(loggerName);
if (level == null) {
// 2. 移除LoggerConfig或設(shè)置LoggerConfig級(jí)別為null
clearLogLevel(loggerName, logger);
} else {
// 1. 添加LoggerConfig或設(shè)置LoggerConfig級(jí)別
setLogLevel(loggerName, logger, level);
}
// 3. 更新Logger
getLoggerContext().updateLoggers();
}
通過第一節(jié)知道,Log4j2的Configuration對(duì)象有一個(gè)字段叫做loggerConfigs,所以上面首先就是通過loggerName去loggerConfigs中匹配對(duì)應(yīng)的LoggerConfig,那么這里就會(huì)存在一個(gè)問題,那就是配置文件里面每配一個(gè)Logger,loggerConfigs才會(huì)增加一個(gè)LoggerConfig,所以實(shí)際上loggerConfigs里面的LoggerConfig并不會(huì)很多,比如我們提供了如下一個(gè)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>
那么實(shí)際加載得到的Configuration的loggerConfigs只有下面這幾個(gè)名字的LoggerConfig。
''
com.honey
com.honey.auth.Login
其中空字符串是根日志打印器(rootLogger)的名字。此時(shí)如果在調(diào)用Log4J2LoggingSystem的setLogLevel() 方法時(shí)傳入的loggerName是com.honey.auth.Login,我們可以很順利的從Configuration的loggerConfigs中拿到名字是com.honey.auth.Login的LoggerConfig,可要是傳入的loggerName是com.honey.auth.Logout呢,那么獲取出來的LoggerConfig肯定是null,此時(shí)該怎么處理呢,難道就不設(shè)置日志打印器的級(jí)別了嗎?
當(dāng)然不是的,Springboot在這里做了一個(gè)巨巧妙的設(shè)計(jì),就是如果熱更新Log4j2時(shí)通過loggerName沒有獲取到LoggerConfig,那么Springboot就會(huì)創(chuàng)建一個(gè)LevelSetLoggerConfig(LoggerConfig的子類)然后添加到Configuration的loggerConfigs中。
下面先看一下LevelSetLoggerConfig長(zhǎng)什么樣。
private static class LevelSetLoggerConfig extends LoggerConfig {
LevelSetLoggerConfig(String name, Level level, boolean additive) {
super(name, level, additive);
}
}
既然我們往Configuration的loggerConfigs中添加了一個(gè)名字是com.honey.auth.Logout的LevelSetLoggerConfig,那么名字是com.honey.auth.Logout的Logger理所應(yīng)當(dāng)?shù)木蜁?huì)持有名字是com.honey.auth.Logout的LevelSetLoggerConfig,但是聰明的人就發(fā)現(xiàn)了,這個(gè)新創(chuàng)建出來的LevelSetLoggerConfig也是沒有靈魂的,為什么呢,因?yàn)?code>LevelSetLoggerConfig不引用任何的Appedner,沒有Appedner怎么打日志嘛,不過不用擔(dān)心,只要在創(chuàng)建LevelSetLoggerConfig時(shí),將additive指定為true,這個(gè)問題就解決了。
在Log4j2中,LoggerConfig之間是有父子關(guān)系的,假如Configuration的loggerConfigs有下面這幾個(gè)名字的LoggerConfig。
''
com.honey
com.honey.auth.Login
那么名字是com.honey.auth.Login的LoggerConfig會(huì)依次按照com.honey.auth,com.honey,com和 '' 去尋找自己的父LoggerConfig,所以每個(gè)LoggerConfig都有自己的父LoggerConfig,而additive參數(shù)的含義就是,當(dāng)前日志是否還需要由父LoggerConfig打印,如果某個(gè)LoggerConfig的additive是true,那么一條日志除了讓自己的所有Appedner打印,還會(huì)讓父LoggerConfig的所有Appender來打印。
所以只要在創(chuàng)建LevelSetLoggerConfig時(shí),將additive指定為true,就算LevelSetLoggerConfig自己沒有Appender,父親也是可以打印日志的。下面舉個(gè)例子來加深理解,還是假如Configuration的loggerConfigs有下面這幾個(gè)名字的LoggerConfig。
''
com.honey
com.honey.auth.Login
我們已經(jīng)有一個(gè)名字為com.honey.auth.Logout的Logger,并且按照Logger尋找LoggerConfig的規(guī)則,我們知道名字為com.honey.auth.Logout的Logger會(huì)持有名字為com.honey的LoggerConfig,那么現(xiàn)在我們要熱更新名字為com.honey.auth.Logout的Logger的級(jí)別,此時(shí)拿著com.honey.auth.Logout從Configuration的loggerConfigs中獲取出來的LoggerConfig肯定為null,所以我們會(huì)創(chuàng)建一個(gè)名字為com.honey.auth.Logout的LevelSetLoggerConfig,并且這個(gè)LevelSetLoggerConfig的additive為true,此時(shí)Configuration的loggerConfigs有下面這幾個(gè)名字的LoggerConfig。
''
com.honey
com.honey.auth.Login
com.honey.auth.Logout
此時(shí)我們重新讓名字為com.honey.auth.Logout的Logger去尋找自己應(yīng)該持有的LoggerConfig,那么肯定就會(huì)找到名字為com.honey.auth.Logout的LevelSetLoggerConfig,由于Log4j2中,Logger的級(jí)別跟著LoggerConfig走,所以名字為com.honey.auth.Logout的Logger的級(jí)別就更新了,現(xiàn)在使用名字為com.honey.auth.Logout的Logger打印日志,首先會(huì)讓其持有的LoggerConfig引用的Appedner來打印,由于沒有引用Appedner,所以不會(huì)打印日志,然后再讓其父LoggerConfig引用的Appedner來打印日志,而名字為com.honey.auth.Logout的LevelSetLoggerConfig的父親其實(shí)就是名字為com.honey的LoggerConfig,所以最終還是讓名字為com.honey的LoggerConfig引用的Appedner完成了日志打印。
到這里仿佛好像逐漸偏離了本小節(jié)的主題,其實(shí)不是的,我們現(xiàn)在再回看Log4J2LoggingSystem的setLogLevel() 方法,如下所示。
private void setLogLevel(String loggerName, Level level) {
// 從Configuration中根據(jù)loggerName獲取到對(duì)應(yīng)的LoggerConfig
LoggerConfig logger = getLogger(loggerName);
if (level == null) {
// 2. 移除LoggerConfig或設(shè)置LoggerConfig級(jí)別為null
clearLogLevel(loggerName, logger);
} else {
// 1. 添加LoggerConfig或設(shè)置LoggerConfig級(jí)別
setLogLevel(loggerName, logger, level);
}
// 3. 更新Logger
getLoggerContext().updateLoggers();
}
首先是第1點(diǎn),在傳入的level不為空時(shí),我們就會(huì)去設(shè)置對(duì)應(yīng)的LoggerConfig的級(jí)別,如果獲取到的LoggerConfig為空,那么就會(huì)創(chuàng)建一個(gè)名字為loggerName,級(jí)別為level的LevelSetLoggerConfig并加到Configuration的loggerConfigs中,如果獲取到的LoggerConfig不為空,則直接修改LoggerConfig的level字段。
其次是第2點(diǎn),傳入level為空時(shí),此時(shí)要求能通過loggerName找到LoggerConfig,否則拋空指針異常。如果通過loggerName找到的LoggerConfig不為空,此時(shí)需要判斷一下LoggerConfig的類型,如果LoggerConfig實(shí)際類型是LevelSetLoggerConfig,那么就從Configuration的loggerConfigs中將其移除,如果LoggerConfig實(shí)際類型就是LoggerConfig,那么就設(shè)置LoggerConfig的level字段為null。
最后是第3點(diǎn),在前面第1和第2點(diǎn),我們已經(jīng)讓目標(biāo)LoggerConfig的級(jí)別完成了更新,此時(shí)就需要讓LoggerContext里面所有的Logger重新去匹配一次自己的LoggerConfig,至此就完成了Logger的級(jí)別的更新。
相信到這里,Log4J2LoggingSystem熱更新原理就闡釋清楚了,小結(jié)一下就是通過loggerName找LoggerConfig,找到了就更新其level,找不到就創(chuàng)建一個(gè)名字為loggerName的LevelSetLoggerConfig,最后讓所有Logger去重新匹配一下自己的LoggerConfig,此時(shí)我們的目標(biāo)Logger就會(huì)持有更新過級(jí)別的LoggerConfig了。
最后給出基于LoggersEndpoint熱更新Log4j2日志打印器的流程圖,如下所示。

六. 自定義Springboot下日志打印器級(jí)別熱更新
有些時(shí)候,使用spring-boot-actuator包提供的LoggersEndpoint來熱更新日志打印器級(jí)別,是有點(diǎn)不方便的,因?yàn)橄胍獰岣氯罩炯?jí)別而引入spring-boot-actuator包,大部分時(shí)候這個(gè)操作都有點(diǎn)重,而通過上面的分析,我們發(fā)現(xiàn)其實(shí)熱更新日志打印器級(jí)別的原理特別簡(jiǎn)單,就是通過LoggingSystem來操作Logger,所以我們可以自己提供一個(gè)接口,通過這個(gè)接口來操作Logger的級(jí)別。
@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
}
}
通過調(diào)用上述接口使用LoggingSystem就能夠完成指定日志打印器的級(jí)別熱更新。
總結(jié)
對(duì)于Log4j2日志框架,我們需要知道Logger只是一個(gè)殼子,靈魂是Logger持有的LoggerConfig。
Springboot框架啟動(dòng)時(shí),日志的初始化的發(fā)起點(diǎn)是LoggingApplicationListener,但是實(shí)際去尋找日志框架的配置文件并完成日志框架初始化是LoggingSystem。
在Springboot中提供日志框架的配置文件時(shí),我們可以將配置文件命名為約定的名字然后放在classpath下,也可以通過logging.config顯示的指定要使用的配置文件的路徑,甚至可以完全不自己提供配置文件而使用Springboot預(yù)置的配置文件,因此使用Springboot框架,想打印日志是十分容易的。
Springboot框架中,為了統(tǒng)一的管理一組Logger,定義了一個(gè)日志打印器組LoggerGroup,通過操作LoggerGroup,可以方便的操作一組Logger,我們可以使用logging.group.xxx來定義LoggerGroup,而xxx就是組名,后續(xù)拿著組名就可以找到LoggerGroup并操作。
所謂日志打印器級(jí)別熱更新,其實(shí)就是不重啟應(yīng)用的情況下修改日志打印器的級(jí)別,核心思路就是通過LoggingSystem去操作底層的日志框架,因?yàn)長(zhǎng)oggingSystem可以為我們屏蔽底層的日志框架的細(xì)節(jié),所以通過LoggingSystem修改日志打印器級(jí)別,是十分容易的。
轉(zhuǎn)載自:https:///post/7348309454700183561
構(gòu)建高質(zhì)量的技術(shù)交流社群,歡迎從事編程開發(fā)、技術(shù)招聘HR進(jìn)群,也歡迎大家分享自己公司的內(nèi)推信息,相互幫助,一起進(jìn)步!