<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          SpringBoot 中的日志是怎么工作的?(萬(wàn)字長(zhǎng)文)

          共 48152字,需瀏覽 97分鐘

           ·

          2024-04-13 14:20

          來(lái)源:https://juejin.cn/post/7348309454700183561

          ?? 歡迎加入小哈的星球 ,你將獲得: 專屬的項(xiàng)目實(shí)戰(zhàn) / Java 學(xué)習(xí)路線 / 一對(duì)一提問(wèn) / 學(xué)習(xí)打卡 /  贈(zèng)書福利


          全棧前后端分離博客項(xiàng)目 2.0 版本完結(jié)啦, 演示鏈接http://116.62.199.48/ ,新項(xiàng)目正在醞釀中。全程手摸手,后端 + 前端全棧開(kāi)發(fā),從 0 到 1 講解每個(gè)功能點(diǎn)開(kāi)發(fā)步驟,1v1 答疑,直到項(xiàng)目上線。目前已更新了239小節(jié),累計(jì)38w+字,講解圖:1645張,還在持續(xù)爆肝中.. 后續(xù)還會(huì)上新更多項(xiàng)目,目標(biāo)是將Java領(lǐng)域典型的項(xiàng)目都整一波,如秒殺系統(tǒng), 在線商城, IM即時(shí)通訊,Spring Cloud Alibaba 等等,戳我加入學(xué)習(xí),已有1200+小伙伴加入(早鳥價(jià)超低)


          前言

          日志,是開(kāi)發(fā)中熟悉又陌生的伙伴,熟悉是因?yàn)槲覀兘?jīng)常會(huì)在各種場(chǎng)合打印日志,陌生是因?yàn)榇蟛糠謺r(shí)候我們都不太關(guān)心日志是怎么打印出來(lái)的,因?yàn)榇蛴∫粭l日志,在我們看來(lái)是一件太平常不過(guò)的事情了,特別是在宇宙第一框架Springboot的加持下,日志打印是怎么工作的就更沒(méi)人關(guān)注了。

          但是了解日志框架怎么工作,以及學(xué)會(huì)Springboot怎么和Log4j2或Logback等日志框架集成,對(duì)我們擴(kuò)展日志功能以及優(yōu)雅打印日志大有好處,甚至在有些場(chǎng)景,還能通過(guò)調(diào)整日志的打印策略來(lái)提升我們的系統(tǒng)吞吐量。

          所以本文將以Springboot集成Log4j2為例,詳細(xì)說(shuō)明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ái)的,配置文件通常命名為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解析出來(lái)時(shí),其和Appender的關(guān)系也就確認(rèn)了。

          在Log4j2中,還有一個(gè)LoggerContext對(duì)象,這個(gè)對(duì)象持有上述的Configuration對(duì)象,我們使用的每一個(gè)Logger,一開(kāi)始都會(huì)先去LoggerContext的loggerRegistry中獲取,如果沒(méi)有,則會(huì)創(chuàng)建一個(gè)Logger出來(lái)再緩存到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)下,要修改日志打印器是十分方便的,我們通過(guò)LoggerContext就可以拿到Configuration,拿到Configuration之后,我們就可以方便的操作LoggerConfig了,例如最常用的日志打印器級(jí)別熱更新就是這么完成的。

          在繼續(xù)閱讀后文之前,有一個(gè)很重要的概念需要闡述清楚,那就是對(duì)于Springboot來(lái)說(shuō),Springboot在操作Logger時(shí),操作的對(duì)象就是一個(gè)Logger,比如要給一個(gè)名字為com.honey.Login的Logger設(shè)置級(jí)別為DEBUG,那么在Springboot看來(lái),它就是在設(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)單配置說(shuō)明

          我們?cè)赟pringboot中使用Log4j2時(shí),雖然大部分時(shí)候我們還是會(huì)提供一個(gè)Log4j2.xml文件來(lái)供Log4j2框架讀取,但是Springboot也提供了一些配置來(lái)供我們使用,在分析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)聽(tīng)器在Springboot啟動(dòng)流程中主要會(huì)監(jiān)聽(tīng)如下三個(gè)事件。

          • ApplicationStartingEvent: 在啟動(dòng)SpringApplication之后就發(fā)布該事件,先于Environmen和ApplicationContext可用之前發(fā)布;
          • ApplicationEnvironmentPreparedEvent: 在Environmen準(zhǔn)備好之后立即發(fā)布;
          • ApplicationPreparedEvent:ApplicationContext完全準(zhǔn)備好之后但刷新容器之前發(fā)布。

          下面依次分析下監(jiān)聽(tīng)到這些事件后,LoggingApplicationListener會(huì)完成一些什么事情來(lái)幫助初始化日志框架。

          1. 監(jiān)聽(tīng)到ApplicationStartingEvent

          LoggingApplicationListener的onApplicationStartingEvent() 方法如下所示。

          private void onApplicationStartingEvent(ApplicationStartingEvent event) {
              // 讀取org.springframework.boot.logging.LoggingSystem系統(tǒng)屬性來(lái)加載得到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)聽(tīng)到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)聽(tīng)到ApplicationStartingEvent事件之前,提前把org.springframework.boot.logging.LoggingSystem設(shè)置為我們自己提供的LoggingSystem的全限定名,這樣我們就可以對(duì)Springboot中的日志初始化做一些定制修改。

          拿到LoggingSystem后,就會(huì)調(diào)用其beforeInitialize() 方法來(lái)完成日志框架初始化前的一些準(zhǔn)備,這里看一下Log4J2LoggingSystembeforeInitialize() 方法實(shí)現(xiàn),如下所示。

          @Override
          public void beforeInitialize() {
              LoggerContext loggerContext = getLoggerContext();
              if (isAlreadyInitialized(loggerContext)) {
                  return;
              }
              super.beforeInitialize();
              // 添加一個(gè)過(guò)濾器
              // 這個(gè)過(guò)濾器會(huì)阻止所有日志的打印
              loggerContext.getConfiguration().addFilter(FILTER);
          }

          上述方法最關(guān)鍵的就是添加了一個(gè)過(guò)濾器,雖然叫做過(guò)濾器,但是實(shí)則為阻斷器,因?yàn)檫@個(gè)FILTER會(huì)阻止所有日志打印,Springboot這樣設(shè)計(jì)是為了防止日志系統(tǒng)在完全完成初始化前打印出不可控的日志。

          所以小結(jié)一下,LoggingApplicationListener監(jiān)聽(tīng)到ApplicationStartingEvent之后,主要完成兩件事情。

          • 從系統(tǒng)屬性中拿到LoggingSystem的全限定名并完成加載;
          • 調(diào)用LoggingSystem的beforeInitialize() 方法來(lái)添加會(huì)拒絕打印任何日志的過(guò)濾器以阻止日志打印。

          2. 監(jiān)聽(tīng)到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) {
              // 把通過(guò)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);
          }

          上述方法概括下來(lái)就是做了三部分的事情。

          1、把日志相關(guān)配置設(shè)置到系統(tǒng)屬性中。例如我們可以通過(guò)logging.pattern.console來(lái)配置標(biāo)準(zhǔn)輸出日志格式,但是在XML文件里面沒(méi)辦法讀取到logging.pattern.console配置的值,此時(shí)就需要設(shè)置一個(gè)系統(tǒng)屬性,屬性名是CONSOLE_LOG_PATTERN,屬性值是logging.pattern.console配置的值,后續(xù)在XML文件中就可以通過(guò)${sys:CONSOLE_LOG_PATTERN}讀取到logging.pattern.console配置的值。

          下表是Springboot中日志配置和系統(tǒng)屬性名的對(duì)應(yīng)關(guān)系:

          圖片

          2、調(diào)用LoggingSystem的initialize() 方法來(lái)完成日志框架初始化。這里就是實(shí)際完成Log4j2或Logback等框架的初始化;

          3、在日志框架完成初始化后基于logging.level的配置來(lái)設(shè)置日志打印器組和日志打印器的級(jí)別。

          上述第2點(diǎn)是Springboot如何完成具體的日志框架的初始化,這個(gè)在后面章節(jié)中會(huì)詳細(xì)分析。上述第3點(diǎn)是日志框架初始化完畢后,Springboot如何幫助我們完成日志打印器組或日志打印器的級(jí)別的設(shè)置,這里就扯出來(lái)一個(gè)概念:日志打印器組,也就是LoggerGroup。

          我們?nèi)绻僮饕粋€(gè)Logger,那么實(shí)際就是要拿著這個(gè)Logger的名稱,去找到Logger,然后再進(jìn)行操作,這在Logger不多的時(shí)候是沒(méi)問(wèn)題的,但是假如我有幾十上百個(gè)Logger呢,一個(gè)一個(gè)去找到Logger再操作無(wú)疑是很不現(xiàn)實(shí)的,一個(gè)實(shí)際的場(chǎng)景就是修改Logger的級(jí)別,如果是通過(guò)Logger的名字去找到Logger再修改級(jí)別,那么是很痛苦的一件事情,但是如果能夠把所有Logger按照功能進(jìn)行分組,我們一組一組的去修改,一下子就優(yōu)雅起來(lái)了,LoggerGroup就是干這個(gè)事情的。

          一個(gè)LoggerGroup,有三個(gè)字段,說(shuō)明如下。

          • name: 表示LoggerGroup的名字,要操作LoggerGroup時(shí),就通過(guò)name來(lái)唯一確定一個(gè)LoggerGroup,假如有一個(gè)LoggerGroup名字為login,那么我們可以通過(guò)logging.level.loggin=debug,將這個(gè)LoggerGroup下所有的Logger的級(jí)別設(shè)置為debug;
          • members: 是當(dāng)前LoggerGroup里所有Logger的名字的集合;
          • configuredLevel: 表示最近一次給LoggerGroup設(shè)置的級(jí)別。

          在Springboot中,通過(guò)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í)別是什么,有兩種手段來(lái)指定,第一種是通過(guò)配置debug=true來(lái)將web和sql這兩個(gè)LoggerGroup的級(jí)別指定為DEBUG,第二種是通過(guò)logging.level.weblogging.level.sql來(lái)指定web和sql這兩個(gè)LoggerGroup的級(jí)別,其中第二種優(yōu)先級(jí)高于第一種。

          上面最后講的這一點(diǎn),其實(shí)就是告訴我們?cè)趺磥?lái)控制Springboot自己的相關(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)聽(tīng)到ApplicationEnvironmentPreparedEvent事件后,Springboot主要完成三件事情。

          • 把通過(guò)配置文件配置的日志相關(guān)屬性設(shè)置為系統(tǒng)屬性;
          • 實(shí)際完成日志框架的初始化;
          • 設(shè)置Springboot和用戶自定義的LoggerGroup與Logger級(jí)別。

          3. 監(jiān)聽(tīng)到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原理說(shuō)明

          在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ì)盡可能的去搜索我們提供的配置文件,無(wú)論我們?cè)赾lasspath下提供的配置文件名字是Log4j2.xml還是Log4j2-spring.xml,都是能夠被Springboot搜索到并加載的。

          上述的Springboot集成Log4j2的行為,全部發(fā)生在Log4J2LoggingSystem中,本節(jié)將對(duì)這里面的流程和原理進(jìn)行說(shuō)明。

          在第三節(jié)中已經(jīng)知道,Springboot啟動(dòng)時(shí),當(dāng)LoggingApplicationListener監(jiān)聽(tīng)到ApplicationEnvironmentPreparedEvent事件后,最終會(huì)調(diào)用到LoggingApplicationListenerinitializeSystem() 方法來(lái)完成日志框架的初始化,所以我們先看一下這里的邏輯是什么,源碼實(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. 沒(méi)有配置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ù)分兩種情況討論,即沒(méi)配置logging.config和有配置logging.config。

          1. 沒(méi)配置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í)行過(guò)初始化邏輯
              if (isAlreadyInitialized(loggerContext)) {
                  return;
              }
              // 移除之前添加的防噪過(guò)濾器
              loggerContext.getConfiguration().removeFilter(FILTER);
              // 調(diào)用到父類AbstractLoggingSystem的initialize()方法
              // 注意因?yàn)闆](méi)有配置logging.config所以這里configLocation為null
              super.initialize(initializationContext, configLocation, logFile);
              // 將當(dāng)前LoggingSystem的全限定名設(shè)置給LoggerContext的ExternalContext
              // 表明當(dāng)前LoggingSystem已經(jīng)對(duì)LoggerContext執(zhí)行過(guò)初始化邏輯
              markAsInitialized(loggerContext);
          }

          上述方法會(huì)繼續(xù)調(diào)用到AbstractLoggingSysteminitialize() 方法,并且因?yàn)闆](méi)有配置logging.config,所以傳遞過(guò)去的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(無(wú)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ì)拿到所有通過(guò)logging.log4j2.config.override配置的路徑,去加載得到Configuration,最終如果得到大于1個(gè)的Configuration,則將這些Configuration創(chuàng)建為CompositeConfiguration。

          這里可能會(huì)有疑問(wèn),logging.log4j2.config.override到底是一個(gè)什么東西,其實(shí)不難發(fā)現(xiàn),無(wú)論是通過(guò)logging.config指定了配置文件路徑,還是按照Springboot約定提供了配置文件,亦或者使用了Springboot預(yù)置的配置文件,其實(shí)最終都只能得到一個(gè)配置文件路徑然后得到一個(gè)Configuration,那么怎么才能加載多份配置文件呢,那就要通過(guò)logging.log4j2.config.override來(lái)指定多個(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)建出來(lái)并完成啟動(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。

          至此,沒(méi)配置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() 方法,該方法沒(méi)有什么額外邏輯,最終會(huì)執(zhí)行到和沒(méi)配置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í),主要分為兩種情況:

          • 沒(méi)配置logging.config。 這種情況下,Springboot會(huì)基于約定努力去尋找符合的配置文件,如果找不到則會(huì)使用預(yù)置的配置文件且預(yù)置的配置文件需要在LoggingSystem的同目錄下,拿到配置文件后就會(huì)加載為Configuration然后替換掉LoggerContext里的舊的Configuration,此時(shí)就完成日志框架初始化;
          • 有配置logging.config。 這種情況下,會(huì)將logging.config指定的配置文件加載為Configuration,然后替換掉LoggerContext里的舊的Configuration,此時(shí)就完成日志框架初始化。

          無(wú)論有沒(méi)有配置logging.config,都只能加載一個(gè)配置文件為Configuration,如果想加載多個(gè)Configuration,那么需要通過(guò)logging.log4j2.config.override配置多個(gè)配置文件路徑,此時(shí)就能加載多個(gè)Configuration來(lái)初始化Log4j2日志框架了。

          Springboot集成Log4j2日志框架的流程圖如下所示。

          圖片

          五. Springboot日志打印器級(jí)別熱更新

          在日志打印中,一條日志在發(fā)起打印時(shí),會(huì)根據(jù)我們的指定攜帶一個(gè)日志級(jí)別,同時(shí)打印日志的日志打印器,也有一個(gè)級(jí)別,日志打印器只能打印級(jí)別高于或等于自身的日志。

          由于日志打印時(shí),日志級(jí)別是由代碼決定的,所以日志級(jí)別除非改代碼,否則無(wú)法改變,但是日志打印器的級(jí)別是可以隨時(shí)更改的,最簡(jiǎn)單的方式就是通過(guò)配置環(huán)境變量來(lái)更改logging.level,此時(shí)我們的應(yīng)用進(jìn)程所處的容器就會(huì)重啟,就可以讀取到我們更改后的logging.level,最終完成日志打印器級(jí)別的修改。

          但是這種方式會(huì)使應(yīng)用重啟,導(dǎo)致流量受損,我們更希望的是通過(guò)一種熱更新的方式來(lái)修改日志打印器的級(jí)別,spring-boot-actuator包中提供了LoggersEndpoint來(lái)完成日志打印器級(jí)別熱更新,所以本節(jié)將結(jié)合LoggersEndpoint的簡(jiǎn)單使用和實(shí)現(xiàn)原理,說(shuō)明一下Springboot中,如何熱更新日志打印器級(jí)別。

          1. LoggersEndpoint簡(jiǎn)單使用

          LoggersEndpoint由spring-boot-actuator提供,可以暴露一些端點(diǎn)用于獲取Springboot應(yīng)用中的所有日志打印器信息及其級(jí)別信息以及熱更新日志打印器級(jí)別,由于默認(rèn)情況下,LoggersEndpoint暴露的端點(diǎn)只能通過(guò)JMX的方式訪問(wèn),所以想要通過(guò)HTTP請(qǐng)求的方式訪問(wèn)到LoggersEndpoint,需要做如下配置。

          management:
            server:
              address: 127.0.0.1
              port: 10999
            endpoints:
              web:
                base-path: /actuator
                exposure:
                  include: loggers    # 設(shè)置LoggersEndpoint可以通過(guò)HTTP方式訪問(wèn)
            endpoint:
              loggers:
                enabled: true     # 打開(kāi)LoggersEndpoint

          按照上述這么配置,我們可以通過(guò)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ì)被輸出出來(lái)。

          此外,上述內(nèi)容中出現(xiàn)的configuredLevel字段表示當(dāng)前日志打印器或日志打印器組被設(shè)置過(guò)的級(jí)別,也就是只要通過(guò)LoggersEndpoint給某個(gè)日志打印器或日志打印器組設(shè)置過(guò)級(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í)別,比如可以通過(guò)如下POST接口來(lái)設(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來(lái)處理
              this.loggingSystem.setLogLevel(name, configuredLevel);
          }

          上述方法的name即可以是Logger的名稱,也可以是LoggerGroup的名稱,如果是Logger的名稱,那么就基于LoggingSystemsetLogLevel() 方法來(lái)設(shè)置這個(gè)Logger的級(jí)別,如果是LoggerGroup的名稱,那么就遍歷這個(gè)組下所有的Logger,每個(gè)遍歷到的Logger都基于LoggingSystem的setLogLevel() 方法來(lái)設(shè)置級(jí)別。

          所以實(shí)際上LoggersEndpoint熱更新日志打印器級(jí)別,還是依賴的對(duì)應(yīng)日志框架的LoggingSystem。

          3. Log4J2LoggingSystem熱更新原理

          由于本文是基于Log4j2日志框架進(jìn)行討論,所以這里選擇分析Log4J2LoggingSystemsetLogLevel() 方法,來(lái)探究Logger級(jí)別如何熱更新。

          在開(kāi)始分析前,有一點(diǎn)需要重申,那就是對(duì)于Log4j2來(lái)說(shuō),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();
          }

          通過(guò)第一節(jié)知道,Log4j2的Configuration對(duì)象有一個(gè)字段叫做loggerConfigs,所以上面首先就是通過(guò)loggerName去loggerConfigs中匹配對(duì)應(yīng)的LoggerConfig,那么這里就會(huì)存在一個(gè)問(wèn)題,那就是配置文件里面每配一個(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呢,那么獲取出來(lái)的LoggerConfig肯定是null,此時(shí)該怎么處理呢,難道就不設(shè)置日志打印器的級(jí)別了嗎?

          當(dāng)然不是的,Springboot在這里做了一個(gè)巨巧妙的設(shè)計(jì),就是如果熱更新Log4j2時(shí)通過(guò)loggerName沒(méi)有獲取到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)建出來(lái)的LevelSetLoggerConfig也是沒(méi)有靈魂的,為什么呢,因?yàn)?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);">LevelSetLoggerConfig不引用任何的Appedner,沒(méi)有Appedner怎么打日志嘛,不過(guò)不用擔(dān)心,只要在創(chuàng)建LevelSetLoggerConfig時(shí),將additive指定為true,這個(gè)問(wèn)題就解決了。

          在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來(lái)打印。

          所以只要在創(chuàng)建LevelSetLoggerConfig時(shí),將additive指定為true,就算LevelSetLoggerConfig自己沒(méi)有Appender,父親也是可以打印日志的。下面舉個(gè)例子來(lái)加深理解,還是假如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中獲取出來(lái)的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來(lái)打印,由于沒(méi)有引用Appedner,所以不會(huì)打印日志,然后再讓其父LoggerConfig引用的Appedner來(lái)打印日志,而名字為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í)要求能通過(guò)loggerName找到LoggerConfig,否則拋空指針異常。如果通過(guò)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é)一下就是通過(guò)loggerName找LoggerConfig,找到了就更新其level,找不到就創(chuàng)建一個(gè)名字為loggerName的LevelSetLoggerConfig,最后讓所有Logger去重新匹配一下自己的LoggerConfig,此時(shí)我們的目標(biāo)Logger就會(huì)持有更新過(guò)級(jí)別的LoggerConfig了。

          最后給出基于LoggersEndpoint熱更新Log4j2日志打印器的流程圖,如下所示。

          圖片

          六. 自定義Springboot下日志打印器級(jí)別熱更新

          有些時(shí)候,使用spring-boot-actuator包提供的LoggersEndpoint來(lái)熱更新日志打印器級(jí)別,是有點(diǎn)不方便的,因?yàn)橄胍獰岣氯罩炯?jí)別而引入spring-boot-actuator包,大部分時(shí)候這個(gè)操作都有點(diǎn)重,而通過(guò)上面的分析,我們發(fā)現(xiàn)其實(shí)熱更新日志打印器級(jí)別的原理特別簡(jiǎn)單,就是通過(guò)LoggingSystem來(lái)操作Logger,所以我們可以自己提供一個(gè)接口,通過(guò)這個(gè)接口來(lái)操作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
              }

          }

          通過(guò)調(diào)用上述接口使用LoggingSystem就能夠完成指定日志打印器的級(jí)別熱更新。

          總結(jié)

          對(duì)于Log4j2日志框架,我們需要知道Logger只是一個(gè)殼子,靈魂是Logger持有的LoggerConfig。

          Springboot框架啟動(dòng)時(shí),日志的初始化的發(fā)起點(diǎn)是LoggingApplicationListener,但是實(shí)際去尋找日志框架的配置文件并完成日志框架初始化是LoggingSystem。

          在Springboot中提供日志框架的配置文件時(shí),我們可以將配置文件命名為約定的名字然后放在classpath下,也可以通過(guò)logging.config顯示的指定要使用的配置文件的路徑,甚至可以完全不自己提供配置文件而使用Springboot預(yù)置的配置文件,因此使用Springboot框架,想打印日志是十分容易的。

          Springboot框架中,為了統(tǒng)一的管理一組Logger,定義了一個(gè)日志打印器組LoggerGroup,通過(guò)操作LoggerGroup,可以方便的操作一組Logger,我們可以使用logging.group.xxx來(lái)定義LoggerGroup,而xxx就是組名,后續(xù)拿著組名就可以找到LoggerGroup并操作。

          所謂日志打印器級(jí)別熱更新,其實(shí)就是不重啟應(yīng)用的情況下修改日志打印器的級(jí)別,核心思路就是通過(guò)LoggingSystem去操作底層的日志框架,因?yàn)長(zhǎng)oggingSystem可以為我們屏蔽底層的日志框架的細(xì)節(jié),所以通過(guò)LoggingSystem修改日志打印器級(jí)別,是十分容易的。

          ?? 歡迎加入小哈的星球 ,你將獲得: 專屬的項(xiàng)目實(shí)戰(zhàn) / Java 學(xué)習(xí)路線 / 一對(duì)一提問(wèn) / 學(xué)習(xí)打卡 /  贈(zèng)書福利


          全棧前后端分離博客項(xiàng)目 2.0 版本完結(jié)啦, 演示鏈接http://116.62.199.48/ ,新項(xiàng)目正在醞釀中。全程手摸手,后端 + 前端全棧開(kāi)發(fā),從 0 到 1 講解每個(gè)功能點(diǎn)開(kāi)發(fā)步驟,1v1 答疑,直到項(xiàng)目上線。目前已更新了239小節(jié),累計(jì)38w+字,講解圖:1645張,還在持續(xù)爆肝中.. 后續(xù)還會(huì)上新更多項(xiàng)目,目標(biāo)是將Java領(lǐng)域典型的項(xiàng)目都整一波,如秒殺系統(tǒng), 在線商城, IM即時(shí)通訊,Spring Cloud Alibaba 等等,戳我加入學(xué)習(xí),已有1200+小伙伴加入(早鳥價(jià)超低)



              
                 

          1. 我的私密學(xué)習(xí)小圈子~

          2. 騰訊二面:@Bean 與 @Component 用在同一個(gè)類上,會(huì)怎么樣?

          3. 提高系統(tǒng)吞吐量的一把利器:DeferredResult 到底有多強(qiáng)?

          4. 面試官:?jiǎn)魏?CPU 支持 Java 多線程嗎?為什么?被問(wèn)懵了!

          最近面試BAT,整理一份面試資料Java面試BATJ通關(guān)手冊(cè),覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫(kù)、數(shù)據(jù)結(jié)構(gòu)等等。

          獲取方式:點(diǎn)“在看”,關(guān)注公眾號(hào)并回復(fù) Java 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。

          PS:因公眾號(hào)平臺(tái)更改了推送規(guī)則,如果不想錯(cuò)過(guò)內(nèi)容,記得讀完點(diǎn)一下在看,加個(gè)星標(biāo),這樣每次新文章推送才會(huì)第一時(shí)間出現(xiàn)在你的訂閱列表里。

          點(diǎn)“在看”支持小哈呀,謝謝啦

          瀏覽 159
          10點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          10點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  熟女系列-x88AV | 日日撸夜夜爱 | 精品伊人大香蕉 | 大香蕉这里只有精品视频 | 天天草天天日天天干天天舔 |