电竞比分网-中国电竞赛事及体育赛事平台

分享

史上最全,全方位闡述 SpringBoot 中的日志是怎么工作(珍藏版)

 昵稱34195792 2024-04-12 發(fā)布于重慶

前言

但是了解日志框架怎么工作,以及學(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)圖如下所示。

圖片
  • Springboot版本:2.7.2
  • Log4j2版本:2.17.2

一. 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)備,這里看一下Log4J2LoggingSystembeforeInitialize() 方法實(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

LoggingApplicationListeneronApplicationEnvironmentPreparedEvent() 方法如下所示。

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)LoggingApplicationListenerinitialize() 方法。

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.weblogging.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í)際完成日志框架的初始化;
  • 設(shè)置Springboot和用戶自定義的LoggerGroup與Logger級(jí)別。

3. 監(jiān)聽到ApplicationPreparedEvent

LoggingApplicationListeneronApplicationPreparedEvent() 方法如下所示。

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)用到LoggingApplicationListenerinitializeSystem() 方法來完成日志框架的初始化,所以我們先看一下這里的邏輯是什么,源碼實(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) {
        // 省略異常處理
    }
}

LoggingApplicationListenerinitializeSystem() 方法會(huì)讀取logging.config環(huán)境變量得到用戶提供的配置文件路徑,然后帶著配置文件路徑,調(diào)用到Log4J2LoggingSysteminitialize() 方法,所以后續(xù)分兩種情況討論,即沒配置logging.config和有配置logging.config。

1. 沒配置logging.config

Log4J2LoggingSysteminitialize() 方法如下所示。

@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)用到AbstractLoggingSysteminitialize() 方法,并且因?yàn)闆]有配置logging.config,所以傳遞過去的configLocation參數(shù)為null,下面看一下AbstractLoggingSysteminitialize() 方法的實(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)用到LoggerContextstart() 方法完成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)單了。還是從Log4J2LoggingSysteminitialize() 方法出發(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)AbstractLoggingSysteminitialize() 方法,如下所示。

@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)用到AbstractLoggingSysteminitializeWithSpecificConfig() 方法,該方法沒有什么額外邏輯,最終會(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的名稱,那么就基于LoggingSystemsetLogLevel() 方法來設(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)行討論,所以這里選擇分析Log4J2LoggingSystemsetLogLevel() 方法,來探究Logger級(jí)別如何熱更新。

在開始分析前,有一點(diǎn)需要重申,那就是對(duì)于Log4j2來說,Logger只是殼子,靈魂是Logger持有的LoggerConfig,所以更新Log4j2里面的Logger的級(jí)別,其實(shí)就是要去更新其持有的LoggerConfig的級(jí)別。

Log4J2LoggingSystemsetLogLevel() 方法如下所示。

@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.LogoutLevelSetLoggerConfig,那么名字是com.honey.auth.Logout的Logger理所應(yīng)當(dāng)?shù)木蜁?huì)持有名字是com.honey.auth.LogoutLevelSetLoggerConfig,但是聰明的人就發(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.authcom.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.LogoutLevelSetLoggerConfig,并且這個(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.LogoutLevelSetLoggerConfig,由于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)在再回看Log4J2LoggingSystemsetLogLevel() 方法,如下所示。

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,那么就從ConfigurationloggerConfigs中將其移除,如果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

后端專屬技術(shù)群

構(gòu)建高質(zhì)量的技術(shù)交流社群,歡迎從事編程開發(fā)、技術(shù)招聘HR進(jìn)群,也歡迎大家分享自己公司的內(nèi)推信息,相互幫助,一起進(jìn)步!

文明發(fā)言,以交流技術(shù)、職位內(nèi)推、行業(yè)探討為主

廣告人士勿入,切勿輕信私聊,防止被騙

加我好友,拉你進(jìn)群

圖片

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多