久久精品精选,精品九九视频,www久久只有这里有精品,亚洲熟女乱色综合一区
    分享

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

     昵稱34195792 2024-04-12 發布于重慶

    前言

    但是了解日志框架怎么工作,以及學會Springboot怎么和Log4j2或Logback等日志框架集成,對我們擴展日志功能以及優雅打印日志大有好處,甚至在有些場景,還能通過調整日志的打印策略來提升我們的系統吞吐量。

    所以本文將以Springboot集成Log4j2為例,詳細說明Springboot框架下Log4j2是如何工作的,你可能會擔心,如果是使用Logback日志框架該怎么辦呢,其實Log4j2和Logback極其相似,Springboot在啟動時處理Log4j2和處理Logback也幾乎是一樣的套路,所以學會Springboot框架下Log4j2如何工作,切換成Logback也是輕輕松松的。

    本文遵循一個該深則深,該淺則淺的整體指導方針,全方位的闡述Springboot中日志怎么工作,思維導圖如下所示。

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

    一. Log4j2簡單工作原理分析

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

    圖片

    實際打印日志的是Logger對象使用的Appender對象,至于Appender對象怎么打印日志,不在我們本文的關注范圍內。特別注意,在Log4j2中,Logger對象實際只是一個殼子,靈魂是其持有的LoggerConfig對象,LoggerConfig決定打印時使用哪些Appender對象,以及Logger的級別。

    LoggerConfig和Appender通常是在Log4j2的配置文件中定義出來的,配置文件通常命名為Log4j2.xml,Log4j2框架在初始化時,會去加載這個配置文件并解析成一個配置對象Configuration,示意如下。

    圖片

    我們每在配置文件的<Appenders>標簽下增加一項,解析得到的Configuration的appenders中就多一個Appender,每在<Loggers>標簽下增加一項,解析得到的Configuration的loggerConfigs中就多一個LoggerConfig,并且LoggerConfig解析出來時,其和Appender的關系也就確認了。

    在Log4j2中,還有一個LoggerContext對象,這個對象持有上述的Configuration對象,我們使用的每一個Logger,一開始都會先去LoggerContext的loggerRegistry中獲取,如果沒有,則會創建一個Logger出來再緩存到LoggerContext的loggerRegistry中,同時我們在創建Logger時其實核心就是要為這個創建的Logger找到它對應的LoggerConfig,那么去哪里找LoggerConfig呢,當然就是去Configuration中找,所以Logger,LoggerContext和Configuration的關系可以描述成下面這樣子。

    圖片

    所以Log4j2在這種結構下,要修改日志打印器是十分方便的,我們通過LoggerContext就可以拿到Configuration,拿到Configuration之后,我們就可以方便的操作LoggerConfig了,例如最常用的日志打印器級別熱更新就是這么完成的。

    在繼續閱讀后文之前,有一個很重要的概念需要闡述清楚,那就是對于Springboot來說,Springboot在操作Logger時,操作的對象就是一個Logger,比如要給一個名字為com.honey.Login的Logger設置級別為DEBUG,那么在Springboot看來,它就是在設置名字為com.honey.Login的Logger的級別為DEBUG,但是具體到Log4j2框架,其實底層是在設置名字為com.honey.Login的LoggerConfig的級別為DEBUG,而具體到Logback框架,就是在設置名字為com.honey.Login的Logger的級別為DEBUG。

    二. Springboot日志簡單配置說明

    我們在Springboot中使用Log4j2時,雖然大部分時候我們還是會提供一個Log4j2.xml文件來供Log4j2框架讀取,但是Springboot也提供了一些配置來供我們使用,在分析Springboot日志啟動機制前,先學習一下里面的若干配置項可以方便我們后續的機制理解。

    1. logging.file.name

    假如我們像下面這樣配置。

    logging:
      file:
        name: test.log

    那么Springboot會把日志內容輸出一份到當前項目根路徑下的test.log文件中。

    2. logging.file.path

    假如我們像下面這樣配置。

    logging:
      file:
        path: /

    那么Springboot會把日志內容輸出一份到指定目錄下的spring.log文件中。

    3. logging.level

    假如我們像下面這樣配置。

    logging:
      level:
        com.pww.App: warn

    那么我們可以指定名稱為com.pww.App的日志打印器的級別為warn級別。

    三. Springboot日志啟動機制分析

    通常我們使用Springboot時,就算不提供Log4j2.xml配置文件,Springboot也能輸出很漂亮的日志,那么Springboot肯定在背后有幫我們完成Log4j2或Logback等框架的初始化,那么本節就刨析一下Springboot中的日志啟動機制。

    Springboot中的日志啟動主要依賴于LoggingApplicationListener,這個監聽器在Springboot啟動流程中主要會監聽如下三個事件。

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

    下面依次分析下監聽到這些事件后,LoggingApplicationListener會完成一些什么事情來幫助初始化日志框架。

    1. 監聽到ApplicationStartingEvent

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

    private void onApplicationStartingEvent(ApplicationStartingEvent event) {
        // 讀取org.springframework.boot.logging.LoggingSystem系統屬性來加載得到LoggingSystem
        this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
        // 調用LoggingSystem的beforeInitialize()方法提前做一些初始化準備工作
        this.loggingSystem.beforeInitialize();
    }

    Springboot中操作日志的最關鍵的一個對象就是LoggingSystem,這個對象會在Springboot的整個生命周期中掌控著日志,在LoggingApplicationListener監聽到ApplicationStartingEvent事件后,第一件事情就是先讀取org.springframework.boot.logging.LoggingSystem系統屬性,得到要加載的LoggingSystem的全限定名,然后完成加載。

    如果是使用Log4j2框架,對應的LoggingSystem是Log4J2LoggingSystem,如果是使用Logback框架,對應的LoggingSystem是LogbackLoggingSystem,當然我們也可以在LoggingApplicationListener監聽到ApplicationStartingEvent事件之前,提前把org.springframework.boot.logging.LoggingSystem設置為我們自己提供的LoggingSystem的全限定名,這樣我們就可以對Springboot中的日志初始化做一些定制修改。

    拿到LoggingSystem后,就會調用其beforeInitialize() 方法來完成日志框架初始化前的一些準備,這里看一下Log4J2LoggingSystembeforeInitialize() 方法實現,如下所示。

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

    上述方法最關鍵的就是添加了一個過濾器,雖然叫做過濾器,但是實則為阻斷器,因為這個FILTER會阻止所有日志打印,Springboot這樣設計是為了防止日志系統在完全完成初始化前打印出不可控的日志。

    所以小結一下,LoggingApplicationListener監聽到ApplicationStartingEvent之后,主要完成兩件事情。

    • 從系統屬性中拿到LoggingSystem的全限定名并完成加載;
    • 調用LoggingSystem的beforeInitialize() 方法來添加會拒絕打印任何日志的過濾器以阻止日志打印。

    2. 監聽到ApplicationEnvironmentPreparedEvent

    LoggingApplicationListeneronApplicationEnvironmentPreparedEvent() 方法如下所示。

    private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
        SpringApplication springApplication = event.getSpringApplication();
        if (this.loggingSystem == null) {
            this.loggingSystem = LoggingSystem.get(springApplication.getClassLoader());
        }
        // 因為此時Environment已經完成了加載
        // 獲取到Environment并繼續調用initialize()方法
        initialize(event.getEnvironment(), springApplication.getClassLoader());
    }

    繼續跟進LoggingApplicationListenerinitialize() 方法。

    protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
        // 把通過logging.xxx配置的值設置到系統屬性中
        getLoggingSystemProperties(environment).apply();
        this.logFile = LogFile.get(environment);
        if (this.logFile != null) {
            // 把logging.file.name和logging.file.path的值設置到系統屬性中
            this.logFile.applyToSystemProperties();
        }
        // 基于預置的web和sql日志打印器初始化LoggerGroups
        this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
        // 讀取配置中的debug和trace是否設置為true
        // 哪個為true就把springBootLogging級別設置為什么
        // 同時設置為true則trace優先級更高
        initializeEarlyLoggingLevel(environment);
        // 調用到具體的LoggingSystem實際初始化日志框架
        initializeSystem(environment, this.loggingSystem, this.logFile);
        // 完成日志打印器組和日志打印器的級別的設置
        initializeFinalLoggingLevels(environment, this.loggingSystem);
        registerShutdownHookIfNecessary(environment, this.loggingSystem);
    }

    上述方法概括下來就是做了三部分的事情。

    1、把日志相關配置設置到系統屬性中。例如我們可以通過logging.pattern.console來配置標準輸出日志格式,但是在XML文件里面沒辦法讀取到logging.pattern.console配置的值,此時就需要設置一個系統屬性,屬性名是CONSOLE_LOG_PATTERN,屬性值是logging.pattern.console配置的值,后續在XML文件中就可以通過${sys:CONSOLE_LOG_PATTERN}讀取到logging.pattern.console配置的值。

    下表是Springboot中日志配置和系統屬性名的對應關系:

    圖片

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

    3、在日志框架完成初始化后基于logging.level的配置來設置日志打印器組和日志打印器的級別。

    上述第2點是Springboot如何完成具體的日志框架的初始化,這個在后面章節中會詳細分析。上述第3點是日志框架初始化完畢后,Springboot如何幫助我們完成日志打印器組或日志打印器的級別的設置,這里就扯出來一個概念:日志打印器組,也就是LoggerGroup。

    我們如果要操作一個Logger,那么實際就是要拿著這個Logger的名稱,去找到Logger,然后再進行操作,這在Logger不多的時候是沒問題的,但是假如我有幾十上百個Logger呢,一個一個去找到Logger再操作無疑是很不現實的,一個實際的場景就是修改Logger的級別,如果是通過Logger的名字去找到Logger再修改級別,那么是很痛苦的一件事情,但是如果能夠把所有Logger按照功能進行分組,我們一組一組的去修改,一下子就優雅起來了,LoggerGroup就是干這個事情的。

    一個LoggerGroup,有三個字段,說明如下。

    • name: 表示LoggerGroup的名字,要操作LoggerGroup時,就通過name來唯一確定一個LoggerGroup,假如有一個LoggerGroup名字為login,那么我們可以通過logging.level.loggin=debug,將這個LoggerGroup下所有的Logger的級別設置為debug;

    • members: 是當前LoggerGroup里所有Logger的名字的集合;

    • configuredLevel: 表示最近一次給LoggerGroup設置的級別。

    在Springboot中,通過logging.group可以配置LoggerGroup,示例如下。

    logging:
      group:
        login:
          - com.lee.controller.LoginController
          - com.lee.service.LoginService
          - com.lee.dao.LoginDao
        common:
          - com.lee.util
          - com.lee.config

    結合logging.level可以直接給一組Logger設置級別,示例如下。

    logging:
      level:
        login: info
        common: debug
      group:
        login:
          - com.lee.controller.LoginController
          - com.lee.service.LoginService
          - com.lee.dao.LoginDao
        common:
          - com.lee.util
          - com.lee.config

    那么此時名稱為login的LoggerGroup表示如下。

    {
        'name''login',
        'members': [
            'com.lee.controller.LoginController',
            'com.lee.service.LoginService',
            'com.lee.dao.LoginDao'
        ],
        'configuredLevel''INFO'
    }

    名稱為common的LoggerGroup表示如下。

    {
        'name''common',
        'members': [
            'com.lee.util',
            'com.lee.config'
        ],
        'configuredLevel''DEBUG'
    }

    最后再看一下Springboot中預置的LoggerGroup,有兩個,名字分別為web和sql,如下所示。

    {
        'name''web',
        'members': [
            'org.springframework.core.codec',
            'org.springframework.http',
            'org.springframework.web',
            'org.springframework.boot.actuate.endpoint.web',
            'org.springframework.boot.web.servlet.ServletContextInitializerBeans'
        ],
        'configuredLevel'''
    }
    {
        'name''sql',
        'members': [
            'org.springframework.jdbc.core',
            'org.hibernate.SQL',
            'org.jooq.tools.LoggerListener'
        ],
        'configuredLevel'''
    }

    至于web和sql這兩個LoggerGroup的級別是什么,有兩種手段來指定,第一種是通過配置debug=true來將web和sql這兩個LoggerGroup的級別指定為DEBUG,第二種是通過logging.level.weblogging.level.sql來指定web和sql這兩個LoggerGroup的級別,其中第二種優先級高于第一種。

    上面最后講的這一點,其實就是告訴我們怎么來控制Springboot自己的相關的日志的打印級別,如果配置debug=true,那么如下的Springboot自己的LoggerGroup和Logger級別會設置為debug。

    sql
    web
    org.springframework.boot

    如果配置trace=true,那么如下的Springboot自己的Logger級別會設置為trace。

    org.springframework
    org.apache.tomcat
    org.apache.catalina
    org.eclipse.jetty
    org.hibernate.tool.hbm2ddl

    現在小結一下,監聽到ApplicationEnvironmentPreparedEvent事件后,Springboot主要完成三件事情。

    • 把通過配置文件配置的日志相關屬性設置為系統屬性;
    • 實際完成日志框架的初始化;
    • 設置Springboot和用戶自定義的LoggerGroup與Logger級別。

    3. 監聽到ApplicationPreparedEvent

    LoggingApplicationListeneronApplicationPreparedEvent() 方法如下所示。

    private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
        ConfigurableListableBeanFactory beanFactory = event.getApplicationContext().getBeanFactory();
        if (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {
            // 把實際加載的LoggingSystem注冊到容器中
            beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME, this.loggingSystem);
        }
        if (this.logFile != null && !beanFactory.containsBean(LOG_FILE_BEAN_NAME)) {
            // 把實際使用的LogFile注冊到容器中
            beanFactory.registerSingleton(LOG_FILE_BEAN_NAME, this.logFile);
        }
        if (this.loggerGroups != null && !beanFactory.containsBean(LOGGER_GROUPS_BEAN_NAME)) {
            // 把保存著所有LoggerGroup的LoggerGroups注冊到容器中
            beanFactory.registerSingleton(LOGGER_GROUPS_BEAN_NAME, this.loggerGroups);
        }
    }

    主要就是把之前加載的LoggingSystem,LogFile和LoggerGroups添加到Spring容器中,進行到這里,其實整個日志框架已經完成初始化了,這里只是把一些和日志密切相關的一些對象注冊為容器中的bean。

    最后,本節以下圖對Springboot日志啟動流程做一個總結。

    圖片

    四. Springboot集成Log4j2原理說明

    在Springboot中使用Log4j2時,我們不提供Log4j2的配置文件也能打印日志,而我們提供了Log4j2的配置文件后日志打印行為又會以我們提供的配置文件為準,這里面其實Springboot為我們做了很多事情,當我們不提供Log4j2配置文件時,Springboot會加載其預置的配置文件,并且會根據我們是否配置了logging.file.xxx自動決定是加載預置的log4j2.xml還是log4j2-file.xml,而與此同時Springboot也會盡可能的去搜索我們提供的配置文件,無論我們在classpath下提供的配置文件名字是Log4j2.xml還是Log4j2-spring.xml,都是能夠被Springboot搜索到并加載的。

    上述的Springboot集成Log4j2的行為,全部發生在Log4J2LoggingSystem中,本節將對這里面的流程和原理進行說明。

    在第三節中已經知道,Springboot啟動時,當LoggingApplicationListener監聽到ApplicationEnvironmentPreparedEvent事件后,最終會調用到LoggingApplicationListenerinitializeSystem() 方法來完成日志框架的初始化,所以我們先看一下這里的邏輯是什么,源碼實現如下。

    private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {
        // 讀取環境變量中的logging.config作為用戶提供的配置文件路徑
        String logConfig = StringUtils.trimWhitespace(environment.getProperty(CONFIG_PROPERTY));
        try {
            // 創建LoggingInitializationContext用于傳遞Environment對象
            LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);
            if (ignoreLogConfig(logConfig)) {
                // 1. 沒有配置logging.config
                system.initialize(initializationContext, null, logFile);
            } else {
                // 2. 配置了logging.config
                system.initialize(initializationContext, logConfig, logFile);
            }
        } catch (Exception ex) {
            // 省略異常處理
        }
    }

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

    1. 沒配置logging.config

    Log4J2LoggingSysteminitialize() 方法如下所示。

    @Override
    public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
        LoggerContext loggerContext = getLoggerContext();
        // 判斷LoggerContext的ExternalContext是不是當前LoggingSystem的全限定名
        // 如果是則表明當前LoggingSystem已經執行過初始化邏輯
        if (isAlreadyInitialized(loggerContext)) {
            return;
        }
        // 移除之前添加的防噪過濾器
        loggerContext.getConfiguration().removeFilter(FILTER);
        // 調用到父類AbstractLoggingSystem的initialize()方法
        // 注意因為沒有配置logging.config所以這里configLocation為null
        super.initialize(initializationContext, configLocation, logFile);
        // 將當前LoggingSystem的全限定名設置給LoggerContext的ExternalContext
        // 表明當前LoggingSystem已經對LoggerContext執行過初始化邏輯
        markAsInitialized(loggerContext);
    }

    上述方法會繼續調用到AbstractLoggingSysteminitialize() 方法,并且因為沒有配置logging.config,所以傳遞過去的configLocation參數為null,下面看一下AbstractLoggingSysteminitialize() 方法的實現,如下所示。

    @Override
    public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
        if (StringUtils.hasLength(configLocation)) {
            initializeWithSpecificConfig(initializationContext, configLocation, logFile);
            return;
        }
        // 基于約定尋找配置文件并完成初始化
        initializeWithConventions(initializationContext, logFile);
    }

    因為configLocation為null,所以會繼續調用到initializeWithConventions() 方法完成初始化,并且初始化使用到的配置文件,Springboot會按照約定的名字去classpath尋找,下面看一下initializeWithConventions() 方法的實現。

    private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
        // 搜索標準日志配置文件路徑
        String config = getSelfInitializationConfig();
        if (config != null && logFile == null) {
            reinitialize(initializationContext);
            return;
        }
        if (config == null) {
            // 搜索Spring日志配置文件路徑
            config = getSpringInitializationConfig();
        }
        if (config != null) {
            // 如果搜索到約定的配置文件則進行配置文件加載
            loadConfiguration(initializationContext, config, logFile);
            return;
        }
        // 如果搜索不到則使用LoggingSystem同目錄下的配置文件
        loadDefaults(initializationContext, logFile);
    }

    上述方法中,首先會去搜索標準日志配置文件路徑,其實就是判斷classpath下是否存在如下名字的配置文件。

    log4j2-test.properties
    log4j2-test.json
    log4j2-test.jsn
    log4j2-test.xml
    log4j2.properties
    log4j2.json
    log4j2.jsn
    log4j2.xml

    如果不存在,則再去搜索Spring日志配置文件路徑,也就是判斷classpath下是否存在如下名字的配置文件。

    log4j2-test-spring.properties
    log4j2-test-spring.json
    log4j2-test-spring.jsn
    log4j2-test-spring.xml
    log4j2-spring.properties
    log4j2-spring.json
    log4j2-spring.jsn
    log4j2-spring.xml

    如果都找不到,此時Springboot就會將Log4J2LoggingSystem同目錄下的log4j2.xml(無LogFile)或log4j2-file.xml(有LogFile)作為日志配置文件,所以不用擔心找不到配置文件,有Springboot為我們進行兜底。

    在獲取到配置文件路徑后,最終會調用到Log4J2LoggingSystem如下的加載配置的方法。

    protected void loadConfiguration(String location, LogFile logFile, List<String> overrides) {
        Assert.notNull(location, 'Location must not be null');
        try {
            List<Configuration> configurations = new ArrayList<>();
            LoggerContext context = getLoggerContext();
            // 根據配置文件路徑加載得到Configuration并添加到集合中
            configurations.add(load(location, context));
            // 加載logging.log4j2.config.override配置的配置文件為Configuration
            // 所有加載的Configuration都要添加到configurations集合中
            for (String override : overrides) {
                configurations.add(load(override, context));
            }
            // 如果得到了大于1個的Configuration則基于所有Configuration創建CompositeConfiguration
            Configuration configuration = (configurations.size() > 1) ? createComposite(configurations)
                    : configurations.iterator().next();
            // 將加載得到的Configuration啟動并設置給LoggerContext
            // 這里會將加載得到的Configuration覆蓋LoggerContext持有的老的Configuration
            context.start(configuration);
        } catch (Exception ex) {
            throw new IllegalStateException('Could not initialize Log4J2 logging from ' + location, ex);
        }
    }

    上述方法中實際就會拿著配置文件的路徑去加載得到Configuration,與此同時還會拿到所有通過logging.log4j2.config.override配置的路徑,去加載得到Configuration,最終如果得到大于1個的Configuration,則將這些Configuration創建為CompositeConfiguration

    這里可能會有疑問,logging.log4j2.config.override到底是一個什么東西,其實不難發現,無論是通過logging.config指定了配置文件路徑,還是按照Springboot約定提供了配置文件,亦或者使用了Springboot預置的配置文件,其實最終都只能得到一個配置文件路徑然后得到一個Configuration,那么怎么才能加載多份配置文件呢,那就要通過logging.log4j2.config.override來指定多個配置文件路徑,使用示例如下。

    logging:
      config: classpath:Log4j2.xml
      log4j2:
        config:
          override:
            - classpath:Log4j2-custom1.xml
            - classpath:Log4j2-custom2.xml

    如果按照上面這樣配置,那么最終就會加載得到三個Configuration,然后再基于這三個Configuration創建得到一個CompositeConfiguration

    在加載得到Configuration之后,就會調用到LoggerContextstart() 方法完成Log4j2框架的初始化,那么這里其實會做如下三件事情。

    • 調用Configuration的start() 方法完成配置對象的初始化。 這里其實就是將我們在配置文件中定義的各種Appedner和LoggerConfig等都創建出來并完成啟動;

    • 將啟動完畢的Configuration設置給LoggerContext。 這里會把LoggerContext持有的老的Configuration覆蓋掉,所以如果LoggerContext之前持有其它的Configuration,那么其實在Springboot日志初始化完畢后老的Configuration會被丟棄掉;

    • 更新Logger。 如果之前有已經創建好的Logger,那么就基于新的Configuration替換掉這些Logger持有的LoggerConfig。

    至此,沒配置logging.config時的初始化邏輯就分析完畢。

    2. 有配置logging.config

    有配置logging.config時,情況就變得簡單了。還是從Log4J2LoggingSysteminitialize() 方法出發,跟一下源碼。

    @Override
    public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
        LoggerContext loggerContext = getLoggerContext();
        if (isAlreadyInitialized(loggerContext)) {
            return;
        }
        loggerContext.getConfiguration().removeFilter(FILTER);
        // 調用到父類AbstractLoggingSystem的initialize()方法
        // 注意因為配置了logging.config所以這里configLocation不為null
        super.initialize(initializationContext, configLocation, logFile);
        markAsInitialized(loggerContext);
    }

    繼續跟進AbstractLoggingSysteminitialize() 方法,如下所示。

    @Override
    public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
        if (StringUtils.hasLength(configLocation)) {
            // 基于指定的配置文件完成初始化
            initializeWithSpecificConfig(initializationContext, configLocation, logFile);
            return;
        }
        initializeWithConventions(initializationContext, logFile);
    }

    由于指定了配置文件,所以會調用到AbstractLoggingSysteminitializeWithSpecificConfig() 方法,該方法沒有什么額外邏輯,最終會執行到和沒配置logging.config時一樣的Log4J2LoggingSystem的加載配置的方法,如下所示。

    protected void loadConfiguration(String location, LogFile logFile, List<String> overrides) {
        Assert.notNull(location, 'Location must not be null');
        try {
            List<Configuration> configurations = new ArrayList<>();
            LoggerContext context = getLoggerContext();
            // 根據配置文件路徑加載得到Configuration并添加到集合中
            configurations.add(load(location, context));
            // 加載logging.log4j2.config.override配置的配置文件為Configuration
            // 所有加載的Configuration都要添加到configurations集合中
            for (String override : overrides) {
                configurations.add(load(override, context));
            }
            // 如果得到了大于1個的Configuration則基于所有Configuration創建CompositeConfiguration
            Configuration configuration = (configurations.size() > 1) ? createComposite(configurations)
                    : configurations.iterator().next();
            // 將加載得到的Configuration啟動并設置給LoggerContext
            // 這里會將加載得到的Configuration覆蓋LoggerContext持有的老的Configuration
            context.start(configuration);
        } catch (Exception ex) {
            throw new IllegalStateException('Could not initialize Log4J2 logging from ' + location, ex);
        }
    }

    所以配置了logging.config時,就會以logging.config指定的配置文件作為最終使用的配置文件,而不會去基于約定搜索配置文件,同時也不會去使用LoggingSystem同目錄下預置的配置文件。

    小結一下,Springboot集成Log4j2日志框架時,主要分為兩種情況:

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

    • 有配置logging.config。 這種情況下,會將logging.config指定的配置文件加載為Configuration,然后替換掉LoggerContext里的舊的Configuration,此時就完成日志框架初始化。

    無論有沒有配置logging.config,都只能加載一個配置文件為Configuration,如果想加載多個Configuration,那么需要通過logging.log4j2.config.override配置多個配置文件路徑,此時就能加載多個Configuration來初始化Log4j2日志框架了。

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

    圖片

    五. Springboot日志打印器級別熱更新

    在日志打印中,一條日志在發起打印時,會根據我們的指定攜帶一個日志級別,同時打印日志的日志打印器,也有一個級別,日志打印器只能打印級別高于或等于自身的日志。

    由于日志打印時,日志級別是由代碼決定的,所以日志級別除非改代碼,否則無法改變,但是日志打印器的級別是可以隨時更改的,最簡單的方式就是通過配置環境變量來更改logging.level,此時我們的應用進程所處的容器就會重啟,就可以讀取到我們更改后的logging.level,最終完成日志打印器級別的修改。

    但是這種方式會使應用重啟,導致流量受損,我們更希望的是通過一種熱更新的方式來修改日志打印器的級別,spring-boot-actuator包中提供了LoggersEndpoint來完成日志打印器級別熱更新,所以本節將結合LoggersEndpoint的簡單使用和實現原理,說明一下Springboot中,如何熱更新日志打印器級別。

    1. LoggersEndpoint簡單使用

    LoggersEndpoint由spring-boot-actuator提供,可以暴露一些端點用于獲取Springboot應用中的所有日志打印器信息及其級別信息以及熱更新日志打印器級別,由于默認情況下,LoggersEndpoint暴露的端點只能通過JMX的方式訪問,所以想要通過HTTP請求的方式訪問到LoggersEndpoint,需要做如下配置。

    management:
      server:
        address: 127.0.0.1
        port: 10999
      endpoints:
        web:
          base-path: /actuator
          exposure:
            include: loggers    # 設置LoggersEndpoint可以通過HTTP方式訪問
      endpoint:
        loggers:
          enabled: true     # 打開LoggersEndpoint

    按照上述這么配置,我們可以通過GET調用如下接口拿到當前所有的日志打印器的相關數據。

    http://localhost:10999/actuator/loggers

    獲取數據如下所示。

    {
        'levels': [
            'OFF',
            'FATAL',
            'ERROR',
            'WARN',
            'INFO',
            'DEBUG',
            'TRACE'
        ],
        'loggers': {
            'ROOT': {
                'configuredLevel'null,
                'effectiveLevel''INFO'
            },
            'org.springframework.boot.actuate.autoconfigure.web.server': {
                'configuredLevel'null,
                'effectiveLevel''DEBUG'
            },
            'org.springframework.http.converter.ResourceRegionHttpMessageConverter': {
                'configuredLevel'null,
                'effectiveLevel''ERROR'
            }
        },
        'groups': {
            'web': {
                'configuredLevel'null,
                'members': [
                    'org.springframework.core.codec',
                    'org.springframework.http',
                    'org.springframework.web',
                    'org.springframework.boot.actuate.endpoint.web',
                    'org.springframework.boot.web.servlet.ServletContextInitializerBeans'
                ]
            },
            'login': {
                'configuredLevel''INFO',
                'members': [
                    'com.lee.controller.LoginController',
                    'com.lee.service.LoginService',
                    'com.lee.dao.LoginDao'
                ]
            },
            'common': {
                'configuredLevel''DEBUG',
                'members': [
                    'com.lee.util',
                    'com.lee.config'
                ]
            },
            'sql': {
                'configuredLevel'null,
                'members': [
                    'org.springframework.jdbc.core',
                    'org.hibernate.SQL',
                    'org.jooq.tools.LoggerListener'
                ]
            }
        }
    }

    上述內容中,返回的levels表示當前支持的日志級別,返回的loggers表示當前所有日志打印器的級別信息,返回的groups表示當前所有日志打印器組的級別信息,但是請注意,上述示例中的loggers其實做了大量的刪減,實際調用接口時得到的loggers里面的內容會非常非常多,因為所有的日志打印器的信息都會被輸出出來。

    此外,上述內容中出現的configuredLevel字段表示當前日志打印器或日志打印器組被設置過的級別,也就是只要通過LoggersEndpoint給某個日志打印器或日志打印器組設置過級別,那么對應的configuredLevel字段就有值,最后上述內容中出現的effectiveLevel字段表示當前日志打印器正在生效的級別。

    如果只想看某個日志打印器或日志打印器組的級別信息,可以調用如下的GET接口。

    http://localhost:10999/actuator/loggers/{日志打印器名或日志打印器組名}

    如果pathVariable是日志打印器名,那么會得到如下結果。

    {
        'configuredLevel'null,
        'effectiveLevel''INFO'
    }

    如果pathVariable是日志打印器組名,那么會得到如下結果。

    {
        'configuredLevel'null,
        'members': [
            'org.springframework.core.codec',
            'org.springframework.http',
            'org.springframework.web',
            'org.springframework.boot.actuate.endpoint.web',
            'org.springframework.boot.web.servlet.ServletContextInitializerBeans'
        ]
    }

    除了查詢日志打印器或日志打印器組的級別信息,LoggersEndpoint更重要的功能是設置級別,比如可以通過如下POST接口來設置級別。

    http://localhost:10999/actuator/loggers/{日志打印器名或日志打印器組名}

    {
     'configuredLevel''DEBUG'
    }

    此時對應的日志打印器或日志打印器組的級別就會更新為設置的級別,并且其configuredLevel也會更新為設置的級別。

    2. LoggersEndpoint原理分析

    這里主要關注LoggersEndpoint如何實現日志打印器級別的熱更新。LoggersEndpoint實現日志打印器級別的熱更新對應的端點方法如下所示。

    @WriteOperation
    public void configureLogLevel(@Selector String name, @Nullable LogLevel configuredLevel) {
        Assert.notNull(name, 'Name must not be empty');
        // 先嘗試獲取到LoggerGroup
        LoggerGroup group = this.loggerGroups.get(name);
        if (group != null && group.hasMembers()) {
            // 如果能獲取到LoggerGroup則對組下每個Logger熱更新級別
            group.configureLogLevel(configuredLevel, this.loggingSystem::setLogLevel);
            return;
        }
        // 獲取不到LoggerGroup則按照Logger來處理
        this.loggingSystem.setLogLevel(name, configuredLevel);
    }

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

    所以實際上LoggersEndpoint熱更新日志打印器級別,還是依賴的對應日志框架的LoggingSystem

    3. Log4J2LoggingSystem熱更新原理

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

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

    Log4J2LoggingSystemsetLogLevel() 方法如下所示。

    @Override
    public void setLogLevel(String loggerName, LogLevel logLevel) {
        // 將LogLevel轉換為Level
        setLogLevel(loggerName, LEVELS.convertSystemToNative(logLevel));
    }

    LogLevel是Springboot中的日志級別對象,Level是Log4j2的日志級別對象,所以需要先將LogLevel轉換為Level,然后繼續調用如下方法。

    private void setLogLevel(String loggerName, Level level) {
        // 從Configuration中根據loggerName獲取到對應的LoggerConfig
        LoggerConfig logger = getLogger(loggerName);
        if (level == null) {
            // 2. 移除LoggerConfig或設置LoggerConfig級別為null
            clearLogLevel(loggerName, logger);
        } else {
            // 1. 添加LoggerConfig或設置LoggerConfig級別
            setLogLevel(loggerName, logger, level);
        }
        // 3. 更新Logger
        getLoggerContext().updateLoggers();
    }

    通過第一節知道,Log4j2的Configuration對象有一個字段叫做loggerConfigs,所以上面首先就是通過loggerName去loggerConfigs中匹配對應的LoggerConfig,那么這里就會存在一個問題,那就是配置文件里面每配一個Logger,loggerConfigs才會增加一個LoggerConfig,所以實際上loggerConfigs里面的LoggerConfig并不會很多,比如我們提供了如下一個Log4j2.xml文件。

    <?xml version='1.0' encoding='UTF-8'?>
    <Configuration status='INFO'>
        <Appenders>
            <Console name='MyConsole'/>
        </Appenders>

        <Loggers>
            <Root level='INFO'>
                <Appender-ref ref='MyConsole'/>
            </Root>
            <Logger name='com.honey' level='WARN'>
                <Appender-ref ref='MyConsole'/>
            </Logger>
            <Logger name='com.honey.auth.Login' level='DEBUG'>
                <Appender-ref ref='MyConsole'/>
            </Logger>
        </Loggers>
    </Configuration>

    那么實際加載得到的Configuration的loggerConfigs只有下面這幾個名字的LoggerConfig。

    ''
    com.honey
    com.honey.auth.Login

    其中空字符串是根日志打印器(rootLogger)的名字。此時如果在調用Log4J2LoggingSystem的setLogLevel() 方法時傳入的loggerName是com.honey.auth.Login,我們可以很順利的從Configuration的loggerConfigs中拿到名字是com.honey.auth.Login的LoggerConfig,可要是傳入的loggerName是com.honey.auth.Logout呢,那么獲取出來的LoggerConfig肯定是null,此時該怎么處理呢,難道就不設置日志打印器的級別了嗎?

    當然不是的,Springboot在這里做了一個巨巧妙的設計,就是如果熱更新Log4j2時通過loggerName沒有獲取到LoggerConfig,那么Springboot就會創建一個LevelSetLoggerConfig(LoggerConfig的子類)然后添加到Configuration的loggerConfigs中。

    下面先看一下LevelSetLoggerConfig長什么樣。

    private static class LevelSetLoggerConfig extends LoggerConfig {

        LevelSetLoggerConfig(String name, Level level, boolean additive) {
            super(name, level, additive);
        }

    }

    既然我們往Configuration的loggerConfigs中添加了一個名字是com.honey.auth.LogoutLevelSetLoggerConfig,那么名字是com.honey.auth.Logout的Logger理所應當的就會持有名字是com.honey.auth.LogoutLevelSetLoggerConfig,但是聰明的人就發現了,這個新創建出來的LevelSetLoggerConfig也是沒有靈魂的,為什么呢,因為LevelSetLoggerConfig不引用任何的Appedner,沒有Appedner怎么打日志嘛,不過不用擔心,只要在創建LevelSetLoggerConfig時,將additive指定為true,這個問題就解決了。

    在Log4j2中,LoggerConfig之間是有父子關系的,假如Configuration的loggerConfigs有下面這幾個名字的LoggerConfig。

    ''
    com.honey
    com.honey.auth.Login

    那么名字是com.honey.auth.Login的LoggerConfig會依次按照com.honey.authcom.honey,com'' 去尋找自己的父LoggerConfig,所以每個LoggerConfig都有自己的父LoggerConfig,而additive參數的含義就是,當前日志是否還需要由父LoggerConfig打印,如果某個LoggerConfig的additive是true,那么一條日志除了讓自己的所有Appedner打印,還會讓父LoggerConfig的所有Appender來打印。

    所以只要在創建LevelSetLoggerConfig時,將additive指定為true,就算LevelSetLoggerConfig自己沒有Appender,父親也是可以打印日志的。下面舉個例子來加深理解,還是假如Configuration的loggerConfigs有下面這幾個名字的LoggerConfig。

    ''
    com.honey
    com.honey.auth.Login

    我們已經有一個名字為com.honey.auth.Logout的Logger,并且按照Logger尋找LoggerConfig的規則,我們知道名字為com.honey.auth.Logout的Logger會持有名字為com.honey的LoggerConfig,那么現在我們要熱更新名字為com.honey.auth.Logout的Logger的級別,此時拿著com.honey.auth.Logout從Configuration的loggerConfigs中獲取出來的LoggerConfig肯定為null,所以我們會創建一個名字為com.honey.auth.LogoutLevelSetLoggerConfig,并且這個LevelSetLoggerConfig的additive為true,此時Configuration的loggerConfigs有下面這幾個名字的LoggerConfig。

    ''
    com.honey
    com.honey.auth.Login
    com.honey.auth.Logout

    此時我們重新讓名字為com.honey.auth.Logout的Logger去尋找自己應該持有的LoggerConfig,那么肯定就會找到名字為com.honey.auth.LogoutLevelSetLoggerConfig,由于Log4j2中,Logger的級別跟著LoggerConfig走,所以名字為com.honey.auth.Logout的Logger的級別就更新了,現在使用名字為com.honey.auth.Logout的Logger打印日志,首先會讓其持有的LoggerConfig引用的Appedner來打印,由于沒有引用Appedner,所以不會打印日志,然后再讓其父LoggerConfig引用的Appedner來打印日志,而名字為com.honey.auth.Logout的LevelSetLoggerConfig的父親其實就是名字為com.honey的LoggerConfig,所以最終還是讓名字為com.honey的LoggerConfig引用的Appedner完成了日志打印。

    到這里仿佛好像逐漸偏離了本小節的主題,其實不是的,我們現在再回看Log4J2LoggingSystemsetLogLevel() 方法,如下所示。

    private void setLogLevel(String loggerName, Level level) {
        // 從Configuration中根據loggerName獲取到對應的LoggerConfig
        LoggerConfig logger = getLogger(loggerName);
        if (level == null) {
            // 2. 移除LoggerConfig或設置LoggerConfig級別為null
            clearLogLevel(loggerName, logger);
        } else {
            // 1. 添加LoggerConfig或設置LoggerConfig級別
            setLogLevel(loggerName, logger, level);
        }
        // 3. 更新Logger
        getLoggerContext().updateLoggers();
    }

    首先是第1點,在傳入的level不為空時,我們就會去設置對應的LoggerConfig的級別,如果獲取到的LoggerConfig為空,那么就會創建一個名字為loggerName,級別為level的LevelSetLoggerConfig并加到Configuration的loggerConfigs中,如果獲取到的LoggerConfig不為空,則直接修改LoggerConfig的level字段。

    其次是第2點,傳入level為空時,此時要求能通過loggerName找到LoggerConfig,否則拋空指針異常。如果通過loggerName找到的LoggerConfig不為空,此時需要判斷一下LoggerConfig的類型,如果LoggerConfig實際類型是LevelSetLoggerConfig,那么就從ConfigurationloggerConfigs中將其移除,如果LoggerConfig實際類型就是LoggerConfig,那么就設置LoggerConfig的level字段為null。

    最后是第3點,在前面第1和第2點,我們已經讓目標LoggerConfig的級別完成了更新,此時就需要讓LoggerContext里面所有的Logger重新去匹配一次自己的LoggerConfig,至此就完成了Logger的級別的更新。

    相信到這里,Log4J2LoggingSystem熱更新原理就闡釋清楚了,小結一下就是通過loggerName找LoggerConfig,找到了就更新其level,找不到就創建一個名字為loggerName的LevelSetLoggerConfig,最后讓所有Logger去重新匹配一下自己的LoggerConfig,此時我們的目標Logger就會持有更新過級別的LoggerConfig了。

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

    圖片

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

    有些時候,使用spring-boot-actuator包提供的LoggersEndpoint來熱更新日志打印器級別,是有點不方便的,因為想要熱更新日志級別而引入spring-boot-actuator包,大部分時候這個操作都有點重,而通過上面的分析,我們發現其實熱更新日志打印器級別的原理特別簡單,就是通過LoggingSystem來操作Logger,所以我們可以自己提供一個接口,通過這個接口來操作Logger的級別。

    @RestController
    public class HotModificationLevel {

        private final LoggingSystem loggingSystem;

        public HotModificationLevel(LoggingSystem loggingSystem) {
            this.loggingSystem = loggingSystem;
        }

        @PostMapping('/logger/level')
        public void setLoggerLevel(@RequestBody SetLoggerLevelParam levelParam) {
            loggingSystem.setLogLevel(levelParam.getLoggerName(), levelParam.getLoggerLevel());
        }

        public static class SetLoggerLevelParam {
            private String loggerName;
            private LogLevel loggerLevel;

            // 省略getter和setter
        }

    }

    通過調用上述接口使用LoggingSystem就能夠完成指定日志打印器的級別熱更新。

    總結

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

    Springboot框架啟動時,日志的初始化的發起點是LoggingApplicationListener,但是實際去尋找日志框架的配置文件并完成日志框架初始化是LoggingSystem。

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

    Springboot框架中,為了統一的管理一組Logger,定義了一個日志打印器組LoggerGroup,通過操作LoggerGroup,可以方便的操作一組Logger,我們可以使用logging.group.xxx來定義LoggerGroup,而xxx就是組名,后續拿著組名就可以找到LoggerGroup并操作。

    所謂日志打印器級別熱更新,其實就是不重啟應用的情況下修改日志打印器的級別,核心思路就是通過LoggingSystem去操作底層的日志框架,因為LoggingSystem可以為我們屏蔽底層的日志框架的細節,所以通過LoggingSystem修改日志打印器級別,是十分容易的。

    轉載自:https:///post/7348309454700183561

    后端專屬技術群

    構建高質量的技術交流社群,歡迎從事編程開發、技術招聘HR進群,也歡迎大家分享自己公司的內推信息,相互幫助,一起進步!

    文明發言,以交流技術職位內推行業探討為主

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

    加我好友,拉你進群

    圖片

      本站是提供個人知識管理的網絡存儲空間,所有內容均由用戶發布,不代表本站觀點。請注意甄別內容中的聯系方式、誘導購買等信息,謹防詐騙。如發現有害或侵權內容,請點擊一鍵舉報。
      轉藏 分享 獻花(0

      0條評論

      發表

      請遵守用戶 評論公約

      類似文章 更多

      主站蜘蛛池模板: 亚洲理论在线A中文字幕| 日本怡春院一区二区三区| 国产午夜A理论毛片| 国产精品午夜剧场免费观看| 国产线观看免费观看| 男人把女人桶到喷白浆的软件免费| 亚洲熟妇无码AV在线播放| 免费看国产美女裸体视频| 男女动态无遮挡动态图| 久9视频这里只有精品试看| 中文国产成人精品久久不卡| 中文字幕在线精品人妻| 强奷乱码中文字幕| 一本精品99久久精品77| 免费人成网站视频在线观看| 精品亚洲国产成人av| 四虎国产精品免费久久久| 又大又硬又爽18禁免费看| 久久综合九色综合97欧美| 国产精品一区二区中文| 久久精品蜜芽亚洲国产AV| 国产草草影院ccyycom| 亚洲最大成人网色| 人妻中文字幕精品一页| 人妻久久久一区二区三区| 成在人线AV无码免观看| 国产精品中文字幕一区| 国产精品免费久久久久影院| 国产美女MM131爽爽爽| 国产一区二区日韩在线| 亚洲AV综合色区无码二区偷拍| 亚洲丰满熟女一区二区蜜桃| 无码激情亚洲一区| 中文字幕永久精品国产| 亚洲国产精品一区二区WWW| 中文精品一卡2卡3卡4卡| 午夜性爽视频男人的天堂| 亚洲 日本 欧洲 欧美 视频| 亚洲av日韩在线资源| 国产精品天天看天天狠| 国产偷国产偷亚洲高清人|