史上最全,全方位闡述 SpringBoot 中的日志是怎么工作(珍藏版)
共 48506字,需瀏覽 98分鐘
·
2024-05-25 10:30
來源:juejin.cn/post/7348309454700183561
?? 歡迎加入小哈的星球 ,你將獲得: 專屬的項目實戰(zhàn) / Java 學(xué)習(xí)路線 / 一對一提問 / 學(xué)習(xí)打卡 / 每月贈書
新項目:仿小紅書(微服務(wù)架構(gòu))正在更新中... , 全棧前后端分離博客項目 2.0 版本完結(jié)啦, 演示鏈接:http://116.62.199.48/ 。全程手摸手,后端 + 前端全棧開發(fā),從 0 到 1 講解每個功能點開發(fā)步驟,1v1 答疑,直到項目上線。目前已更新了261小節(jié),累計43w+字,講解圖:1806張,還在持續(xù)爆肝中.. 后續(xù)還會上新更多項目,目標(biāo)是將Java領(lǐng)域典型的項目都整一波,如秒殺系統(tǒng), 在線商城, IM即時通訊,Spring Cloud Alibaba 等等,戳我加入學(xué)習(xí),已有1400+小伙伴加入(早鳥價超低)
-
前言 -
一. Log4j2簡單工作原理分析 -
二. Springboot日志簡單配置說明 -
三. Springboot日志啟動機(jī)制分析 -
四. Springboot集成Log4j2原理說明 -
五. Springboot日志打印器級別熱更新 -
六. 自定義Springboot下日志打印器級別熱更新 -
總結(jié)
前言
日志,是開發(fā)中熟悉又陌生的伙伴,熟悉是因為我們經(jīng)常會在各種場合打印日志,陌生是因為大部分時候我們都不太關(guān)心日志是怎么打印出來的,因為打印一條日志,在我們看來是一件太平常不過的事情了,特別是在宇宙第一框架Springboot的加持下,日志打印是怎么工作的就更沒人關(guān)注了。
但是了解日志框架怎么工作,以及學(xué)會Springboot怎么和Log4j2或Logback等日志框架集成,對我們擴(kuò)展日志功能以及優(yōu)雅打印日志大有好處,甚至在有些場景,還能通過調(diào)整日志的打印策略來提升我們的系統(tǒng)吞吐量。
所以本文將以Springboot集成Log4j2為例,詳細(xì)說明Springboot框架下Log4j2是如何工作的,你可能會擔(dān)心,如果是使用Logback日志框架該怎么辦呢,其實Log4j2和Logback極其相似,Springboot在啟動時處理Log4j2和處理Logback也幾乎是一樣的套路,所以學(xué)會Springboot框架下Log4j2如何工作,切換成Logback也是輕輕松松的。
本文遵循一個該深則深,該淺則淺的整體指導(dǎo)方針,全方位的闡述Springboot中日志怎么工作,思維導(dǎo)圖如下所示。
-
Springboot版本:2.7.2 -
Log4j2版本:2.17.2
一. Log4j2簡單工作原理分析
使用Log4j2打印日志時,我們自己接觸最多的就是Logger對象了,Logger對象叫做日志打印器,負(fù)責(zé)打印日志,一個Logger對象,結(jié)構(gòu)簡單示意如下。
實際打印日志的是Logger對象使用的Appender對象,至于Appender對象怎么打印日志,不在我們本文的關(guān)注范圍內(nèi)。特別注意,在Log4j2中,Logger對象實際只是一個殼子,靈魂是其持有的LoggerConfig對象,LoggerConfig決定打印時使用哪些Appender對象,以及Logger的級別。
LoggerConfig和Appender通常是在Log4j2的配置文件中定義出來的,配置文件通常命名為Log4j2.xml,Log4j2框架在初始化時,會去加載這個配置文件并解析成一個配置對象Configuration,示意如下。
我們每在配置文件的<Appenders>標(biāo)簽下增加一項,解析得到的Configuration的appenders中就多一個Appender,每在<Loggers>標(biāo)簽下增加一項,解析得到的Configuration的loggerConfigs中就多一個LoggerConfig,并且LoggerConfig解析出來時,其和Appender的關(guān)系也就確認(rèn)了。
在Log4j2中,還有一個LoggerContext對象,這個對象持有上述的Configuration對象,我們使用的每一個Logger,一開始都會先去LoggerContext的loggerRegistry中獲取,如果沒有,則會創(chuàng)建一個Logger出來再緩存到LoggerContext的loggerRegistry中,同時我們在創(chuàng)建Logger時其實核心就是要為這個創(chuàng)建的Logger找到它對應(yīng)的LoggerConfig,那么去哪里找LoggerConfig呢,當(dāng)然就是去Configuration中找,所以Logger,LoggerContext和Configuration的關(guān)系可以描述成下面這樣子。
所以Log4j2在這種結(jié)構(gòu)下,要修改日志打印器是十分方便的,我們通過LoggerContext就可以拿到Configuration,拿到Configuration之后,我們就可以方便的操作LoggerConfig了,例如最常用的日志打印器級別熱更新就是這么完成的。
在繼續(xù)閱讀后文之前,有一個很重要的概念需要闡述清楚,那就是對于Springboot來說,Springboot在操作Logger時,操作的對象就是一個Logger,比如要給一個名字為com.honey.Login的Logger設(shè)置級別為DEBUG,那么在Springboot看來,它就是在設(shè)置名字為com.honey.Login的Logger的級別為DEBUG,但是具體到Log4j2框架,其實底層是在設(shè)置名字為com.honey.Login的LoggerConfig的級別為DEBUG,而具體到Logback框架,就是在設(shè)置名字為com.honey.Login的Logger的級別為DEBUG。
二. Springboot日志簡單配置說明
我們在Springboot中使用Log4j2時,雖然大部分時候我們還是會提供一個Log4j2.xml文件來供Log4j2框架讀取,但是Springboot也提供了一些配置來供我們使用,在分析Springboot日志啟動機(jī)制前,先學(xué)習(xí)一下里面的若干配置項可以方便我們后續(xù)的機(jī)制理解。
1. logging.file.name
假如我們像下面這樣配置。
logging:
file:
name: test.log
那么Springboot會把日志內(nèi)容輸出一份到當(dāng)前項目根路徑下的test.log文件中。
2. logging.file.path
假如我們像下面這樣配置。
logging:
file:
path: /
那么Springboot會把日志內(nèi)容輸出一份到指定目錄下的spring.log文件中。
3. logging.level
假如我們像下面這樣配置。
logging:
level:
com.pww.App: warn
那么我們可以指定名稱為com.pww.App的日志打印器的級別為warn級別。
三. Springboot日志啟動機(jī)制分析
通常我們使用Springboot時,就算不提供Log4j2.xml配置文件,Springboot也能輸出很漂亮的日志,那么Springboot肯定在背后有幫我們完成Log4j2或Logback等框架的初始化,那么本節(jié)就刨析一下Springboot中的日志啟動機(jī)制。
Springboot中的日志啟動主要依賴于LoggingApplicationListener,這個監(jiān)聽器在Springboot啟動流程中主要會監(jiān)聽如下三個事件。
-
ApplicationStartingEvent: 在啟動 SpringApplication之后就發(fā)布該事件,先于Environmen和ApplicationContext可用之前發(fā)布; -
ApplicationEnvironmentPreparedEvent: 在Environmen準(zhǔn)備好之后立即發(fā)布; -
ApplicationPreparedEvent: 在 ApplicationContext完全準(zhǔn)備好之后但刷新容器之前發(fā)布。
下面依次分析下監(jiān)聽到這些事件后,LoggingApplicationListener會完成一些什么事情來幫助初始化日志框架。
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)鍵的一個對象就是LoggingSystem,這個對象會在Springboot的整個生命周期中掌控著日志,在LoggingApplicationListener監(jiān)聽到ApplicationStartingEvent事件后,第一件事情就是先讀取org.springframework.boot.logging.LoggingSystem系統(tǒng)屬性,得到要加載的LoggingSystem的全限定名,然后完成加載。
如果是使用Log4j2框架,對應(yīng)的LoggingSystem是Log4J2LoggingSystem,如果是使用Logback框架,對應(yīng)的LoggingSystem是LogbackLoggingSystem,當(dāng)然我們也可以在LoggingApplicationListener監(jiān)聽到ApplicationStartingEvent事件之前,提前把org.springframework.boot.logging.LoggingSystem設(shè)置為我們自己提供的LoggingSystem的全限定名,這樣我們就可以對Springboot中的日志初始化做一些定制修改。
拿到LoggingSystem后,就會調(diào)用其beforeInitialize() 方法來完成日志框架初始化前的一些準(zhǔn)備,這里看一下Log4J2LoggingSystem的beforeInitialize() 方法實現(xiàn),如下所示。
@Override
public void beforeInitialize() {
LoggerContext loggerContext = getLoggerContext();
if (isAlreadyInitialized(loggerContext)) {
return;
}
super.beforeInitialize();
// 添加一個過濾器
// 這個過濾器會阻止所有日志的打印
loggerContext.getConfiguration().addFilter(FILTER);
}
上述方法最關(guān)鍵的就是添加了一個過濾器,雖然叫做過濾器,但是實則為阻斷器,因為這個FILTER會阻止所有日志打印,Springboot這樣設(shè)計是為了防止日志系統(tǒng)在完全完成初始化前打印出不可控的日志。
所以小結(jié)一下,LoggingApplicationListener監(jiān)聽到ApplicationStartingEvent之后,主要完成兩件事情。
-
從系統(tǒng)屬性中拿到LoggingSystem的全限定名并完成加載; -
調(diào)用LoggingSystem的 beforeInitialize()方法來添加會拒絕打印任何日志的過濾器以阻止日志打印。
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());
}
// 因為此時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
// 哪個為true就把springBootLogging級別設(shè)置為什么
// 同時設(shè)置為true則trace優(yōu)先級更高
initializeEarlyLoggingLevel(environment);
// 調(diào)用到具體的LoggingSystem實際初始化日志框架
initializeSystem(environment, this.loggingSystem, this.logFile);
// 完成日志打印器組和日志打印器的級別的設(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è)置一個系統(tǒng)屬性,屬性名是CONSOLE_LOG_PATTERN,屬性值是logging.pattern.console配置的值,后續(xù)在XML文件中就可以通過${sys:CONSOLE_LOG_PATTERN}讀取到logging.pattern.console配置的值。
下表是Springboot中日志配置和系統(tǒng)屬性名的對應(yīng)關(guān)系:
2、調(diào)用LoggingSystem的initialize() 方法來完成日志框架初始化。這里就是實際完成Log4j2或Logback等框架的初始化;
3、在日志框架完成初始化后基于logging.level的配置來設(shè)置日志打印器組和日志打印器的級別。
上述第2點是Springboot如何完成具體的日志框架的初始化,這個在后面章節(jié)中會詳細(xì)分析。上述第3點是日志框架初始化完畢后,Springboot如何幫助我們完成日志打印器組或日志打印器的級別的設(shè)置,這里就扯出來一個概念:日志打印器組,也就是LoggerGroup。
我們?nèi)绻僮饕粋€Logger,那么實際就是要拿著這個Logger的名稱,去找到Logger,然后再進(jìn)行操作,這在Logger不多的時候是沒問題的,但是假如我有幾十上百個Logger呢,一個一個去找到Logger再操作無疑是很不現(xiàn)實的,一個實際的場景就是修改Logger的級別,如果是通過Logger的名字去找到Logger再修改級別,那么是很痛苦的一件事情,但是如果能夠把所有Logger按照功能進(jìn)行分組,我們一組一組的去修改,一下子就優(yōu)雅起來了,LoggerGroup就是干這個事情的。
一個LoggerGroup,有三個字段,說明如下。
-
name: 表示LoggerGroup的名字,要操作LoggerGroup時,就通過name來唯一確定一個LoggerGroup,假如有一個LoggerGroup名字為login,那么我們可以通過 logging.level.loggin=debug,將這個LoggerGroup下所有的Logger的級別設(shè)置為debug; -
members: 是當(dāng)前LoggerGroup里所有Logger的名字的集合; -
configuredLevel: 表示最近一次給LoggerGroup設(shè)置的級別。
在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è)置級別,示例如下。
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中預(yù)置的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的級別,其中第二種優(yōu)先級高于第一種。
上面最后講的這一點,其實就是告訴我們怎么來控制Springboot自己的相關(guān)的日志的打印級別,如果配置debug=true,那么如下的Springboot自己的LoggerGroup和Logger級別會設(shè)置為debug。
sql
web
org.springframework.boot
如果配置trace=true,那么如下的Springboot自己的Logger級別會設(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級別。
3. 監(jiān)聽到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容器中,進(jìn)行到這里,其實整個日志框架已經(jīng)完成初始化了,這里只是把一些和日志密切相關(guān)的一些對象注冊為容器中的bean。
最后,本節(jié)以下圖對Springboot日志啟動流程做一個總結(jié)。
四. Springboot集成Log4j2原理說明
在Springboot中使用Log4j2時,我們不提供Log4j2的配置文件也能打印日志,而我們提供了Log4j2的配置文件后日志打印行為又會以我們提供的配置文件為準(zhǔn),這里面其實Springboot為我們做了很多事情,當(dāng)我們不提供Log4j2配置文件時,Springboot會加載其預(yù)置的配置文件,并且會根據(jù)我們是否配置了logging.file.xxx自動決定是加載預(yù)置的log4j2.xml還是log4j2-file.xml,而與此同時Springboot也會盡可能的去搜索我們提供的配置文件,無論我們在classpath下提供的配置文件名字是Log4j2.xml還是Log4j2-spring.xml,都是能夠被Springboot搜索到并加載的。
上述的Springboot集成Log4j2的行為,全部發(fā)生在Log4J2LoggingSystem中,本節(jié)將對這里面的流程和原理進(jìn)行說明。
在第三節(jié)中已經(jīng)知道,Springboot啟動時,當(dāng)LoggingApplicationListener監(jiān)聽到ApplicationEnvironmentPreparedEvent事件后,最終會調(diào)用到LoggingApplicationListener的initializeSystem() 方法來完成日志框架的初始化,所以我們先看一下這里的邏輯是什么,源碼實現(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對象
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環(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()方法
// 注意因為沒有配置logging.config所以這里configLocation為null
super.initialize(initializationContext, configLocation, logFile);
// 將當(dāng)前LoggingSystem的全限定名設(shè)置給LoggerContext的ExternalContext
// 表明當(dāng)前LoggingSystem已經(jīng)對LoggerContext執(zhí)行過初始化邏輯
markAsInitialized(loggerContext);
}
上述方法會繼續(xù)調(diào)用到AbstractLoggingSystem的initialize() 方法,并且因為沒有配置logging.config,所以傳遞過去的configLocation參數(shù)為null,下面看一下AbstractLoggingSystem的initialize() 方法的實現(xiàn),如下所示。
@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
if (StringUtils.hasLength(configLocation)) {
initializeWithSpecificConfig(initializationContext, configLocation, logFile);
return;
}
// 基于約定尋找配置文件并完成初始化
initializeWithConventions(initializationContext, logFile);
}
因為configLocation為null,所以會繼續(xù)調(diào)用到initializeWithConventions() 方法完成初始化,并且初始化使用到的配置文件,Springboot會按照約定的名字去classpath尋找,下面看一下initializeWithConventions() 方法的實現(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);
}
上述方法中,首先會去搜索標(biāo)準(zhǔn)日志配置文件路徑,其實就是判斷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)作為日志配置文件,所以不用擔(dān)心找不到配置文件,有Springboot為我們進(jìn)行兜底。
在獲取到配置文件路徑后,最終會調(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個的Configuration則基于所有Configuration創(chuàng)建CompositeConfiguration
Configuration configuration = (configurations.size() > 1) ? createComposite(configurations)
: configurations.iterator().next();
// 將加載得到的Configuration啟動并設(shè)置給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創(chuàng)建為CompositeConfiguration。
這里可能會有疑問,logging.log4j2.config.override到底是一個什么東西,其實不難發(fā)現(xiàn),無論是通過logging.config指定了配置文件路徑,還是按照Springboot約定提供了配置文件,亦或者使用了Springboot預(yù)置的配置文件,其實最終都只能得到一個配置文件路徑然后得到一個Configuration,那么怎么才能加載多份配置文件呢,那就要通過logging.log4j2.config.override來指定多個配置文件路徑,使用示例如下。
logging:
config: classpath:Log4j2.xml
log4j2:
config:
override:
- classpath:Log4j2-custom1.xml
- classpath:Log4j2-custom2.xml
如果按照上面這樣配置,那么最終就會加載得到三個Configuration,然后再基于這三個Configuration創(chuàng)建得到一個CompositeConfiguration。
在加載得到Configuration之后,就會調(diào)用到LoggerContext的start() 方法完成Log4j2框架的初始化,那么這里其實會做如下三件事情。
-
調(diào)用Configuration的start() 方法完成配置對象的初始化。 這里其實就是將我們在配置文件中定義的各種Appedner和LoggerConfig等都創(chuàng)建出來并完成啟動; -
將啟動完畢的Configuration設(shè)置給LoggerContext。 這里會把LoggerContext持有的老的Configuration覆蓋掉,所以如果LoggerContext之前持有其它的Configuration,那么其實在Springboot日志初始化完畢后老的Configuration會被丟棄掉; -
更新Logger。 如果之前有已經(jīng)創(chuàng)建好的Logger,那么就基于新的Configuration替換掉這些Logger持有的LoggerConfig。
至此,沒配置logging.config時的初始化邏輯就分析完畢。
2. 有配置logging.config
有配置logging.config時,情況就變得簡單了。還是從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()方法
// 注意因為配置了logging.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);
}
由于指定了配置文件,所以會調(diào)用到AbstractLoggingSystem的initializeWithSpecificConfig() 方法,該方法沒有什么額外邏輯,最終會執(zhí)行到和沒配置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();
// 根據(jù)配置文件路徑加載得到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創(chuàng)建CompositeConfiguration
Configuration configuration = (configurations.size() > 1) ? createComposite(configurations)
: configurations.iterator().next();
// 將加載得到的Configuration啟動并設(shè)置給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同目錄下預(yù)置的配置文件。
小結(jié)一下,Springboot集成Log4j2日志框架時,主要分為兩種情況:
-
沒配置logging.config。 這種情況下,Springboot會基于約定努力去尋找符合的配置文件,如果找不到則會使用預(yù)置的配置文件且預(yù)置的配置文件需要在 LoggingSystem的同目錄下,拿到配置文件后就會加載為Configuration然后替換掉LoggerContext里的舊的Configuration,此時就完成日志框架初始化; -
有配置logging.config。 這種情況下,會將 logging.config指定的配置文件加載為Configuration,然后替換掉LoggerContext里的舊的Configuration,此時就完成日志框架初始化。
無論有沒有配置logging.config,都只能加載一個配置文件為Configuration,如果想加載多個Configuration,那么需要通過logging.log4j2.config.override配置多個配置文件路徑,此時就能加載多個Configuration來初始化Log4j2日志框架了。
Springboot集成Log4j2日志框架的流程圖如下所示。
五. Springboot日志打印器級別熱更新
在日志打印中,一條日志在發(fā)起打印時,會根據(jù)我們的指定攜帶一個日志級別,同時打印日志的日志打印器,也有一個級別,日志打印器只能打印級別高于或等于自身的日志。
由于日志打印時,日志級別是由代碼決定的,所以日志級別除非改代碼,否則無法改變,但是日志打印器的級別是可以隨時更改的,最簡單的方式就是通過配置環(huán)境變量來更改logging.level,此時我們的應(yīng)用進(jìn)程所處的容器就會重啟,就可以讀取到我們更改后的logging.level,最終完成日志打印器級別的修改。
但是這種方式會使應(yīng)用重啟,導(dǎo)致流量受損,我們更希望的是通過一種熱更新的方式來修改日志打印器的級別,spring-boot-actuator包中提供了LoggersEndpoint來完成日志打印器級別熱更新,所以本節(jié)將結(jié)合LoggersEndpoint的簡單使用和實現(xiàn)原理,說明一下Springboot中,如何熱更新日志打印器級別。
1. LoggersEndpoint簡單使用
LoggersEndpoint由spring-boot-actuator提供,可以暴露一些端點用于獲取Springboot應(yīng)用中的所有日志打印器信息及其級別信息以及熱更新日志打印器級別,由于默認(rèn)情況下,LoggersEndpoint暴露的端點只能通過JMX的方式訪問,所以想要通過HTTP請求的方式訪問到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)前支持的日志級別,返回的loggers表示當(dāng)前所有日志打印器的級別信息,返回的groups表示當(dāng)前所有日志打印器組的級別信息,但是請注意,上述示例中的loggers其實做了大量的刪減,實際調(diào)用接口時得到的loggers里面的內(nèi)容會非常非常多,因為所有的日志打印器的信息都會被輸出出來。
此外,上述內(nèi)容中出現(xiàn)的configuredLevel字段表示當(dāng)前日志打印器或日志打印器組被設(shè)置過的級別,也就是只要通過LoggersEndpoint給某個日志打印器或日志打印器組設(shè)置過級別,那么對應(yīng)的configuredLevel字段就有值,最后上述內(nèi)容中出現(xiàn)的effectiveLevel字段表示當(dāng)前日志打印器正在生效的級別。
如果只想看某個日志打印器或日志打印器組的級別信息,可以調(diào)用如下的GET接口。
“
http://localhost:10999/actuator/loggers/{日志打印器名或日志打印器組名}
如果pathVariable是日志打印器名,那么會得到如下結(jié)果。
{
"configuredLevel": null,
"effectiveLevel": "INFO"
}
如果pathVariable是日志打印器組名,那么會得到如下結(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的級別信息,LoggersEndpoint更重要的功能是設(shè)置級別,比如可以通過如下POST接口來設(shè)置級別。
“
http://localhost:10999/actuator/loggers/{日志打印器名或日志打印器組名}
{
"configuredLevel": "DEBUG"
}
此時對應(yīng)的日志打印器或日志打印器組的級別就會更新為設(shè)置的級別,并且其configuredLevel也會更新為設(shè)置的級別。
2. LoggersEndpoint原理分析
這里主要關(guān)注LoggersEndpoint如何實現(xiàn)日志打印器級別的熱更新。LoggersEndpoint實現(xiàn)日志打印器級別的熱更新對應(yīng)的端點方法如下所示。
@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() 方法來設(shè)置這個Logger的級別,如果是LoggerGroup的名稱,那么就遍歷這個組下所有的Logger,每個遍歷到的Logger都基于LoggingSystem的setLogLevel() 方法來設(shè)置級別。
所以實際上LoggersEndpoint熱更新日志打印器級別,還是依賴的對應(yīng)日志框架的LoggingSystem。
3. Log4J2LoggingSystem熱更新原理
由于本文是基于Log4j2日志框架進(jìn)行討論,所以這里選擇分析Log4J2LoggingSystem的setLogLevel() 方法,來探究Logger級別如何熱更新。
在開始分析前,有一點需要重申,那就是對于Log4j2來說,Logger只是殼子,靈魂是Logger持有的LoggerConfig,所以更新Log4j2里面的Logger的級別,其實就是要去更新其持有的LoggerConfig的級別。
Log4J2LoggingSystem的setLogLevel() 方法如下所示。
@Override
public void setLogLevel(String loggerName, LogLevel logLevel) {
// 將LogLevel轉(zhuǎn)換為Level
setLogLevel(loggerName, LEVELS.convertSystemToNative(logLevel));
}
LogLevel是Springboot中的日志級別對象,Level是Log4j2的日志級別對象,所以需要先將LogLevel轉(zhuǎn)換為Level,然后繼續(xù)調(diào)用如下方法。
private void setLogLevel(String loggerName, Level level) {
// 從Configuration中根據(jù)loggerName獲取到對應(yīng)的LoggerConfig
LoggerConfig logger = getLogger(loggerName);
if (level == null) {
// 2. 移除LoggerConfig或設(shè)置LoggerConfig級別為null
clearLogLevel(loggerName, logger);
} else {
// 1. 添加LoggerConfig或設(shè)置LoggerConfig級別
setLogLevel(loggerName, logger, level);
}
// 3. 更新Logger
getLoggerContext().updateLoggers();
}
通過第一節(jié)知道,Log4j2的Configuration對象有一個字段叫做loggerConfigs,所以上面首先就是通過loggerName去loggerConfigs中匹配對應(yīng)的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)的名字。此時如果在調(diào)用Log4J2LoggingSystem的setLogLevel() 方法時傳入的loggerName是com.honey.auth.Login,我們可以很順利的從Configuration的loggerConfigs中拿到名字是com.honey.auth.Login的LoggerConfig,可要是傳入的loggerName是com.honey.auth.Logout呢,那么獲取出來的LoggerConfig肯定是null,此時該怎么處理呢,難道就不設(shè)置日志打印器的級別了嗎?
當(dāng)然不是的,Springboot在這里做了一個巨巧妙的設(shè)計,就是如果熱更新Log4j2時通過loggerName沒有獲取到LoggerConfig,那么Springboot就會創(chuàng)建一個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理所應(yīng)當(dāng)?shù)木蜁钟忻质?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(40, 202, 113);">com.honey.auth.Logout的LevelSetLoggerConfig,但是聰明的人就發(fā)現(xiàn)了,這個新創(chuàng)建出來的LevelSetLoggerConfig也是沒有靈魂的,為什么呢,因為LevelSetLoggerConfig不引用任何的Appedner,沒有Appedner怎么打日志嘛,不過不用擔(dān)心,只要在創(chuàng)建LevelSetLoggerConfig時,將additive指定為true,這個問題就解決了。
在Log4j2中,LoggerConfig之間是有父子關(guān)系的,假如Configuration的loggerConfigs有下面這幾個名字的LoggerConfig。
""
com.honey
com.honey.auth.Login
那么名字是com.honey.auth.Login的LoggerConfig會依次按照com.honey.auth,com.honey,com和 "" 去尋找自己的父LoggerConfig,所以每個LoggerConfig都有自己的父LoggerConfig,而additive參數(shù)的含義就是,當(dāng)前日志是否還需要由父LoggerConfig打印,如果某個LoggerConfig的additive是true,那么一條日志除了讓自己的所有Appedner打印,還會讓父LoggerConfig的所有Appender來打印。
所以只要在創(chuàng)建LevelSetLoggerConfig時,將additive指定為true,就算LevelSetLoggerConfig自己沒有Appender,父親也是可以打印日志的。下面舉個例子來加深理解,還是假如Configuration的loggerConfigs有下面這幾個名字的LoggerConfig。
""
com.honey
com.honey.auth.Login
我們已經(jīng)有一個名字為com.honey.auth.Logout的Logger,并且按照Logger尋找LoggerConfig的規(guī)則,我們知道名字為com.honey.auth.Logout的Logger會持有名字為com.honey的LoggerConfig,那么現(xiàn)在我們要熱更新名字為com.honey.auth.Logout的Logger的級別,此時拿著com.honey.auth.Logout從Configuration的loggerConfigs中獲取出來的LoggerConfig肯定為null,所以我們會創(chuàng)建一個名字為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去尋找自己應(yīng)該持有的LoggerConfig,那么肯定就會找到名字為com.honey.auth.Logout的LevelSetLoggerConfig,由于Log4j2中,Logger的級別跟著LoggerConfig走,所以名字為com.honey.auth.Logout的Logger的級別就更新了,現(xiàn)在使用名字為com.honey.auth.Logout的Logger打印日志,首先會讓其持有的LoggerConfig引用的Appedner來打印,由于沒有引用Appedner,所以不會打印日志,然后再讓其父LoggerConfig引用的Appedner來打印日志,而名字為com.honey.auth.Logout的LevelSetLoggerConfig的父親其實就是名字為com.honey的LoggerConfig,所以最終還是讓名字為com.honey的LoggerConfig引用的Appedner完成了日志打印。
到這里仿佛好像逐漸偏離了本小節(jié)的主題,其實不是的,我們現(xiàn)在再回看Log4J2LoggingSystem的setLogLevel() 方法,如下所示。
private void setLogLevel(String loggerName, Level level) {
// 從Configuration中根據(jù)loggerName獲取到對應(yīng)的LoggerConfig
LoggerConfig logger = getLogger(loggerName);
if (level == null) {
// 2. 移除LoggerConfig或設(shè)置LoggerConfig級別為null
clearLogLevel(loggerName, logger);
} else {
// 1. 添加LoggerConfig或設(shè)置LoggerConfig級別
setLogLevel(loggerName, logger, level);
}
// 3. 更新Logger
getLoggerContext().updateLoggers();
}
首先是第1點,在傳入的level不為空時,我們就會去設(shè)置對應(yīng)的LoggerConfig的級別,如果獲取到的LoggerConfig為空,那么就會創(chuàng)建一個名字為loggerName,級別為level的LevelSetLoggerConfig并加到Configuration的loggerConfigs中,如果獲取到的LoggerConfig不為空,則直接修改LoggerConfig的level字段。
其次是第2點,傳入level為空時,此時要求能通過loggerName找到LoggerConfig,否則拋空指針異常。如果通過loggerName找到的LoggerConfig不為空,此時需要判斷一下LoggerConfig的類型,如果LoggerConfig實際類型是LevelSetLoggerConfig,那么就從Configuration的loggerConfigs中將其移除,如果LoggerConfig實際類型就是LoggerConfig,那么就設(shè)置LoggerConfig的level字段為null。
最后是第3點,在前面第1和第2點,我們已經(jīng)讓目標(biāo)LoggerConfig的級別完成了更新,此時就需要讓LoggerContext里面所有的Logger重新去匹配一次自己的LoggerConfig,至此就完成了Logger的級別的更新。
相信到這里,Log4J2LoggingSystem熱更新原理就闡釋清楚了,小結(jié)一下就是通過loggerName找LoggerConfig,找到了就更新其level,找不到就創(chuàng)建一個名字為loggerName的LevelSetLoggerConfig,最后讓所有Logger去重新匹配一下自己的LoggerConfig,此時我們的目標(biāo)Logger就會持有更新過級別的LoggerConfig了。
最后給出基于LoggersEndpoint熱更新Log4j2日志打印器的流程圖,如下所示。
六. 自定義Springboot下日志打印器級別熱更新
有些時候,使用spring-boot-actuator包提供的LoggersEndpoint來熱更新日志打印器級別,是有點不方便的,因為想要熱更新日志級別而引入spring-boot-actuator包,大部分時候這個操作都有點重,而通過上面的分析,我們發(fā)現(xiàn)其實熱更新日志打印器級別的原理特別簡單,就是通過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
}
}
通過調(diào)用上述接口使用LoggingSystem就能夠完成指定日志打印器的級別熱更新。
總結(jié)
對于Log4j2日志框架,我們需要知道Logger只是一個殼子,靈魂是Logger持有的LoggerConfig。
Springboot框架啟動時,日志的初始化的發(fā)起點是LoggingApplicationListener,但是實際去尋找日志框架的配置文件并完成日志框架初始化是LoggingSystem。
在Springboot中提供日志框架的配置文件時,我們可以將配置文件命名為約定的名字然后放在classpath下,也可以通過logging.config顯示的指定要使用的配置文件的路徑,甚至可以完全不自己提供配置文件而使用Springboot預(yù)置的配置文件,因此使用Springboot框架,想打印日志是十分容易的。
Springboot框架中,為了統(tǒng)一的管理一組Logger,定義了一個日志打印器組LoggerGroup,通過操作LoggerGroup,可以方便的操作一組Logger,我們可以使用logging.group.xxx來定義LoggerGroup,而xxx就是組名,后續(xù)拿著組名就可以找到LoggerGroup并操作。
所謂日志打印器級別熱更新,其實就是不重啟應(yīng)用的情況下修改日志打印器的級別,核心思路就是通過LoggingSystem去操作底層的日志框架,因為LoggingSystem可以為我們屏蔽底層的日志框架的細(xì)節(jié),所以通過LoggingSystem修改日志打印器級別,是十分容易的。
?? 歡迎加入小哈的星球 ,你將獲得: 專屬的項目實戰(zhàn) / Java 學(xué)習(xí)路線 / 一對一提問 / 學(xué)習(xí)打卡 / 每月贈書
新項目:仿小紅書(微服務(wù)架構(gòu))正在更新中... , 全棧前后端分離博客項目 2.0 版本完結(jié)啦, 演示鏈接:http://116.62.199.48/ 。全程手摸手,后端 + 前端全棧開發(fā),從 0 到 1 講解每個功能點開發(fā)步驟,1v1 答疑,直到項目上線。目前已更新了261小節(jié),累計43w+字,講解圖:1806張,還在持續(xù)爆肝中.. 后續(xù)還會上新更多項目,目標(biāo)是將Java領(lǐng)域典型的項目都整一波,如秒殺系統(tǒng), 在線商城, IM即時通訊,Spring Cloud Alibaba 等等,戳我加入學(xué)習(xí),已有1400+小伙伴加入(早鳥價超低)
2. 面試官:Spring 為什么不支持 static 字段的注入?
最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點“在看”,關(guān)注公眾號并回復(fù) Java 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
PS:因公眾號平臺更改了推送規(guī)則,如果不想錯過內(nèi)容,記得讀完點一下“在看”,加個“星標(biāo)”,這樣每次新文章推送才會第一時間出現(xiàn)在你的訂閱列表里。
點“在看”支持小哈呀,謝謝啦
