Spring Boot -日志配置加載流程

描述

Spring Boot在所有內(nèi)部日志中使用Apache Commons Logging,但是默認(rèn)配置也提供了對常用日志的支持,如:Java Util Logging,Log4J, Log4J2和Logback。每種Logger都可以通過配置使用控制臺或者文件輸出日志內(nèi)容

SpringBoot默認(rèn)使用的是SLF4J(日志門面)+logback日志實(shí)現(xiàn)框架。jul-to-slf4j表示使用slf4j替換掉java.util.logging,log4j-over-slf4j表示使用slf4j替換掉log4j,jcl-over-slf4j表示使用slf4j替換掉java.commongs.logging。這里使用“替換”或許比較難理解,這是slf4j提供的一種橋接模式,對外統(tǒng)一使用slf4j門面。

下面通過代碼跟蹤spring日志初始化的流程查看spring對日志配置的初始化以及加載

日志初始化入口

springboot 初始化加載日志通過ApplicationListener完成工作的,springboot通過讀取spring.factories擴(kuò)展配置文件,加載定義好的ApplicationListener,默認(rèn)的springboot包下配置如下:

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
#日志初始化加載,銷毀監(jiān)聽器,負(fù)責(zé)完成日志初始化以及銷毀工作
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

在配置中我們看到springboot默認(rèn)配置了LoggingApplicationListener,springboot就是通過此監(jiān)聽完成日志的加載,初始化,以及銷毀等工作的。

  • LoggingApplicationListener
public class LoggingApplicationListener implements GenericApplicationListener {

    /**
     * 日志配置文件在配置環(huán)境中的key
     * springboot 通過 environment根據(jù)此key 查找我們自定義的日志配置根據(jù)
     */
    public static final String CONFIG_PROPERTY = "logging.config";

    /**
     * The name of the {@link LoggingSystem} bean.
     */
    public static final String LOGGING_SYSTEM_BEAN_NAME = "springBootLoggingSystem";

    /**
     * springboot 對不同日志統(tǒng)一封裝接口,用于初始化日志接口
     * 
     */
    private LoggingSystem loggingSystem;

    private LogLevel springBootLogging = null;


    /**
     * 監(jiān)聽事件觸發(fā)執(zhí)行方法,用于根據(jù)不同事件完成不同的操作,此處包含日志初始化的前置操作,日志的初始化操作,日志的銷毀清除操作
     */
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        //系統(tǒng)開始啟動事件,LoggingSystem初始化前置操作
        if (event instanceof ApplicationStartingEvent) {
            onApplicationStartingEvent((ApplicationStartingEvent) event);
        }
        //環(huán)境資源加載完成事件,初始化LoggingSystem
        else if (event instanceof ApplicationEnvironmentPreparedEvent) {
            onApplicationEnvironmentPreparedEvent(
                    (ApplicationEnvironmentPreparedEvent) event);
        }
        //Application啟動完成事件,將LoggingSystem注冊到容器管理維護(hù)
        else if (event instanceof ApplicationPreparedEvent) {
            onApplicationPreparedEvent((ApplicationPreparedEvent) event);
        }
        //容器關(guān)閉事件,銷毀日志
        else if (event instanceof ContextClosedEvent && ((ContextClosedEvent) event)
                .getApplicationContext().getParent() == null) {
            onContextClosedEvent();
        }
        //Application啟動失敗事件,銷毀日志
        else if (event instanceof ApplicationFailedEvent) {
            onApplicationFailedEvent();
        }
    }

    /**
     * 執(zhí)行LoggingSystem初始化的前置操作
     */
    private void onApplicationStartingEvent(ApplicationStartingEvent event) {
        //獲取LoggingSystem的真實(shí)實(shí)現(xiàn),
        // 此處會根據(jù)不同的日志框架獲取不同的實(shí)現(xiàn),
        // logback : LogbackLoggingSystem
        // log4j2: Log4J2LoggingSystem
        // javalog: JavaLoggingSystem
        this.loggingSystem = LoggingSystem
            .get(event.getSpringApplication().getClassLoader());
        //執(zhí)行beforeInitialize方法完成初始化前置操作
        this.loggingSystem.beforeInitialize();
    }

    /**
     * 執(zhí)行LoggingSystem初始化操作
     */
    private void onApplicationEnvironmentPreparedEvent(
            ApplicationEnvironmentPreparedEvent event) {
        if (this.loggingSystem == null) {
            this.loggingSystem = LoggingSystem
                    .get(event.getSpringApplication().getClassLoader());
        }
        
        //調(diào)用initialize方法完成LoggingSystem初始化
        initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
    }

    /**
     * LoggingSystem 注冊到IOC容器托管
     */
    private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
        ConfigurableListableBeanFactory beanFactory = event.getApplicationContext()
                .getBeanFactory();
        if (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {
            beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME, this.loggingSystem);
        }
    }

    /**
     *  LoggingSystem 銷毀操作
     */
    private void onContextClosedEvent() {
        if (this.loggingSystem != null) {
            this.loggingSystem.cleanUp();
        }
    }
    
    /**
     *  LoggingSystem 銷毀操作
     */
    private void onApplicationFailedEvent() {
        if (this.loggingSystem != null) {
            this.loggingSystem.cleanUp();
        }
    }

    /**
     * 初始化 LoggingSystem
     */
    protected void initialize(ConfigurableEnvironment environment,
            ClassLoader classLoader) {
        new LoggingSystemProperties(environment).apply();
        //獲取logFile信息
        // logFile 分別在 environment中獲取key為:logging.file和logging.path的值創(chuàng)建LogFile對象,當(dāng)這兩個值任何一個不存在時LogFile對象為null
        LogFile logFile = LogFile.get(environment);
        if (logFile != null) {
            //logFile存在時將配置信息設(shè)置到System中
            logFile.applyToSystemProperties();
        }
        //初始化早期的日志等級
        initializeEarlyLoggingLevel(environment);
         //初始化LoggingSystem對象
        initializeSystem(environment, this.loggingSystem, logFile);
        //注冊最終的日志等級
        initializeFinalLoggingLevels(environment, this.loggingSystem);
        //注冊關(guān)閉鉤子用于系統(tǒng)關(guān)閉是銷毀
        registerShutdownHookIfNecessary(environment, this.loggingSystem);
    }

    /**
     * 初始化LoggingSystem對象
     */
    private void initializeSystem(ConfigurableEnvironment environment,
            LoggingSystem system, LogFile logFile) {
        //構(gòu)建日志初始化(LoggingSystem)的上下問環(huán)境
        LoggingInitializationContext initializationContext = new LoggingInitializationContext(
                environment);
        //獲取自定義配置的日志配置文件,在key(logging.config)中獲取
        String logConfig = environment.getProperty(CONFIG_PROPERTY);
        //判斷自定義配置是否有效
        if (ignoreLogConfig(logConfig)) {
            //無效時LoggingSystem的初始化,不根據(jù)logging.config初始化日志
            system.initialize(initializationContext, null, logFile);
        }
        else {
            try {
              //校驗logging.config的配置文件是否可讀
              ResourceUtils.getURL(logConfig).openStream().close();
              //根據(jù)logging.config的配置初始化日志
            system.initialize(initializationContext, logConfig, logFile);
            }
            catch (Exception ex) {
            }
        }
    }

    /**
     * 校驗logging.config獲取到的日志配置是否可以忽視
     */
    private boolean ignoreLogConfig(String logConfig) {
        return !StringUtils.hasLength(logConfig) || logConfig.startsWith("-D");
    }
}

LoggingApplicationListener 監(jiān)聽不同的事件,根據(jù)不同事件對日志做不同的操作:

  • ApplicationStartingEvents事件
  1. 完成LoggingSystem的創(chuàng)建,根據(jù)不同的日志框架獲取不同的實(shí)現(xiàn):(logback :LogbackLoggingSystem;log4j2: Log4J2LoggingSystem;javalog:JavaLoggingSystem)
  2. 執(zhí)行LoggingSystem的beforeInitialize方法完成初始化前置操作
  • ApplicationEnvironmentPreparedEvent事件
  1. 獲取LogFile對象: 根據(jù)配置logging.file和logging.path的值創(chuàng)建LogFile對象,當(dāng)這兩個值任何一個不存在時LogFile對象為null
  2. 初始化早期的日志等級:initializeEarlyLoggingLevel
  3. 初始化LoggingSystem對象: initializeSystem
  4. 獲取自定義日志配置:在key(logging.config)中獲取
  5. 根據(jù)義日志配置與LogFile對象調(diào)用LoggingSystem的initialize方法初始化日志
    6.初始化最終的日志等級給LoggingSystem對象
  6. 注冊銷毀鉤子,添加loggingSystem的銷毀
  • ApplicationPreparedEvent事件

將LoggingSystem對象注冊到IOC容器托管

  • ContextClosedEvent與ApplicationFailedEvent事件

清除銷毀LoggingSystem對象

通過代碼以及上面分析可以看到LoggingApplicationListener主要是完成了LoggingSystem的初始化以及銷毀等工作,根據(jù)不同的事件。那么日志的初始化主要流程是通過LoggingSystem完成的,下面我們通過源碼查看LoggingSystem的初始化工作。

日志系統(tǒng)的核心類初始化

spring實(shí)現(xiàn)了多個日志框架的的LoggingSystem默認(rèn)使用的是LogbackLoggingSystem,下面我們通過分析LogbackLoggingSystem源碼來看logbak在初始化前以及初始化是如何加載配置的。

LogbackLoggingSystem繼承Slf4JLoggingSystem
Slf4JLoggingSystem 繼承AbstractLoggingSystem

初始化前置操作 beforeInitialize
 // LogbackLoggingSystem類
    @Override
    public void beforeInitialize() {
       // 創(chuàng)建日志上下文環(huán)境
        LoggerContext loggerContext = getLoggerContext();
        //判斷日志是否初始化過,初始化過直接返回
        if (isAlreadyInitialized(loggerContext)) {
            return;
        }
        //調(diào)用父類的beforeInitialize
        super.beforeInitialize();
        //上下文環(huán)境中添加過濾器
        loggerContext.getTurboFilterList().add(FILTER);
    }
    
    //父類 Slf4JLoggingSystem
    
    @Override
    public void beforeInitialize() {
        //繼續(xù)調(diào)用父類的beforeInitialize
        super.beforeInitialize();
        //配置日志的橋接處理器
        configureJdkLoggingBridgeHandler();
    }
    
    // 父類 AbstractLoggingSystem 
    @Override
    public void beforeInitialize() {
     //空實(shí)現(xiàn)
    }
    

beforeInitialize 為日志的初始化前置操作,通過LoggingApplicationListener看到是事件ApplicationStartingEvents觸發(fā)
beforeInitialize主要完成以下功能:

  1. 創(chuàng)建log系統(tǒng)的上下文件環(huán)境LoggerContext
  2. 配置日志橋接處理器configureJdkLoggingBridgeHandler
  3. 上下文件環(huán)境LoggerContext添加過濾器 filter
初始化操作 initialize
    // LogbackLoggingSystem類
    @Override
    public void initialize(LoggingInitializationContext initializationContext,
            String configLocation, LogFile logFile) {
            
        //獲取日志上下文環(huán)境 
        LoggerContext loggerContext = getLoggerContext();
        //判斷當(dāng)前上下文環(huán)境是否已經(jīng)初始化過
        if (isAlreadyInitialized(loggerContext)) {
            return;
        }
        //調(diào)用父類的initialize執(zhí)行初始化
        super.initialize(initializationContext, configLocation, logFile);
        // 移除上下文中的過濾器FILTER
        loggerContext.getTurboFilterList().remove(FILTER);
        //設(shè)置當(dāng)前上下文環(huán)境的初始化標(biāo)識
        markAsInitialized(loggerContext);
    }
    
   // 父類 AbstractLoggingSystem 
    @Override
    public void initialize(LoggingInitializationContext initializationContext,
            String configLocation, LogFile logFile) {
        //判斷是否存在自定義的日志配置文件路徑    
        if (StringUtils.hasLength(configLocation)) {
            //根據(jù)自定義的日志配置文件初始化日志
            //此處是根據(jù)key(logging.config)的值初始化日志
            initializeWithSpecificConfig(initializationContext, configLocation, logFile);
            return;
        }
        //根據(jù)約定俗成的規(guī)則初始化日志
        
        initializeWithConventions(initializationContext, logFile);
    }

initialize 為日志的初始化操作,通過LoggingApplicationListener看到是事件ApplicationEnvironmentPreparedEvent觸發(fā)
initialize主要完成以下功能:

  1. 獲取日志上下文環(huán)境
  2. KEY(logging.config)值存在時根據(jù)此值初始化日志:initializeWithSpecificConfig
  3. KEY(logging.config)值不存在時根據(jù)約定俗成規(guī)則初始化日志:initializeWithConventions
  4. 移除上下文環(huán)境中的FILTER過濾器
  5. 設(shè)置當(dāng)前上下文環(huán)境的初始化標(biāo)識markAsInitialized

++備注:++ 初始化操作在initialize方法中出現(xiàn)了兩個分支,是根據(jù)配置logging.config判斷的,當(dāng)前springboot的environment存在logging.config值是按照配置的日志配置初始化日志系統(tǒng),否則按照約定俗成的規(guī)則初始化日志。

根據(jù)自定義配置初始化日志initializeWithSpecificConfig
  //  AbstractLoggingSystem 
    private void initializeWithSpecificConfig(
            LoggingInitializationContext initializationContext, String configLocation,
            LogFile logFile) {
        //轉(zhuǎn)換配置自定義配置文件資源路徑
        configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);
        //調(diào)用實(shí)現(xiàn)類的loadConfiguration完成配置加載
        loadConfiguration(initializationContext, configLocation, logFile);
    }
    
//  LogbackLoggingSystem 實(shí)現(xiàn)類
    protected void loadConfiguration(LoggingInitializationContext initializationContext,
            String location, LogFile logFile) {
        //獲取日志上下文件  
        LoggerContext loggerContext = getLoggerContext();
        //設(shè)置停止和重置上下文件環(huán)境
        stopAndReset(loggerContext);
        try {
           
           //調(diào)用configureByResourceUrl方法加載配置
            configureByResourceUrl(initializationContext, loggerContext,
                    ResourceUtils.getURL(location));
        }
        catch (Exception ex) {
        
        }
    }
    
    /**
     * 根據(jù)配置資源的URL加載日志配置
     */
    private void configureByResourceUrl(
            LoggingInitializationContext initializationContext,
            LoggerContext loggerContext, URL url) throws JoranException {
        //加載xml類型的日志配置文件    
        if (url.toString().endsWith("xml")) {
            JoranConfigurator configurator = new SpringBootJoranConfigurator(
                    initializationContext);
            configurator.setContext(loggerContext);
            configurator.doConfigure(url);
        }
        else {
          //加載非xml類型的日志配置文件
            new ContextInitializer(loggerContext).configureByResource(url);
        }
    }
    

initializeWithSpecificConfig 主要是根據(jù)配置logging.config加載日志配置信息,logging.config為系統(tǒng)啟動時我們執(zhí)行的配置信息如:

java  -jar xxx.jar --logging.config=/opt/config/log4j2-spring.xml
按照約定俗成的規(guī)則初始化日志initializeWithConventions
 //  AbstractLoggingSystem 
    private void initializeWithConventions(
            LoggingInitializationContext initializationContext, LogFile logFile) {
        //獲取約定俗成的配置資源路徑 
        String config = getSelfInitializationConfig();
        
        if (config != null && logFile == null) {
            //存在時加載資源,并返回
            reinitialize(initializationContext);
            return;
        }
        if (config == null) {
            //獲取spring規(guī)定的配置文件資源路徑
            config = getSpringInitializationConfig();
        }
        if (config != null) {
            //存在時加載并返回
            loadConfiguration(initializationContext, config, logFile);
            return;
        }
        //加載spring默認(rèn)的配置信息
        loadDefaults(initializationContext, logFile);
    }
    
    /**
     * 獲取約定俗成的日志配置資源路徑
     */
    protected String getSelfInitializationConfig() {
        return findConfig(getStandardConfigLocations());
    }
    
    /**
     * 獲取spring規(guī)定的配置文件資源路徑
     */
    protected String getSpringInitializationConfig() {
        return findConfig(getSpringConfigLocations());
    }
    
    /**
     * 返回spring規(guī)定的配置文件名集合
     * 約定俗成規(guī)則如下: 在標(biāo)準(zhǔn)的配置文件名中+ “-spring”
     * logback.xml spring約定如下:logback-spring.xml
     */
    protected String[] getSpringConfigLocations() {
        String[] locations = getStandardConfigLocations();
        for (int i = 0; i < locations.length; i++) {
            String extension = StringUtils.getFilenameExtension(locations[i]);
            locations[i] = locations[i].substring(0,
                    locations[i].length() - extension.length() - 1) + "-spring."
                    + extension;
        }
        return locations;
    }
    
    /**
     * 在系統(tǒng)classpath下查找是否存在給定的配置名,當(dāng)存在時直接返回,永遠(yuǎn)使用先查到的
     */ 
    private String findConfig(String[] locations) {
        for (String location : locations) {
            ClassPathResource resource = new ClassPathResource(location,
                    this.classLoader);
            if (resource.exists()) {
                return "classpath:" + location;
            }
        }
        return null;
    }
    
   // LogbackLoggingSystem
    /**
     * 返回logback標(biāo)準(zhǔn)的配置文件名稱
     */
    protected String[] getStandardConfigLocations() {
        //標(biāo)準(zhǔn)的配置文件名稱集合
        return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy",
                "logback.xml" };
    }
    
    /**
     * 加載默認(rèn)的配置信息
     */ 
    @Override
    protected void loadDefaults(LoggingInitializationContext initializationContext,
            LogFile logFile) {
        LoggerContext context = getLoggerContext();
        stopAndReset(context);
        //
        LogbackConfigurator configurator = new LogbackConfigurator(context);
        Environment environment = initializationContext.getEnvironment();
        context.putProperty(LoggingSystemProperties.LOG_LEVEL_PATTERN,
                environment.resolvePlaceholders(
                        "${logging.pattern.level:${LOG_LEVEL_PATTERN:%5p}}"));
        context.putProperty(LoggingSystemProperties.LOG_DATEFORMAT_PATTERN,
                environment.resolvePlaceholders(
                        "${logging.pattern.dateformat:${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}"));
        new DefaultLogbackConfiguration(initializationContext, logFile)
                .apply(configurator);
        context.setPackagingDataEnabled(true);
    }
    

initializeWithConventions按照約定俗成的規(guī)則初始化日志,在initializeWithConventions方法中我們可以看到以下幾步:

  1. 獲取標(biāo)準(zhǔn)的配置資源路徑,如果存在任何一個,通過此資源配置加載,標(biāo)準(zhǔn)的配置文件名集合如下:
 { "logback-test.groovy", "logback-test.xml", "logback.groovy",
                "logback.xml" }
  1. 獲取spring規(guī)定的配置資源路徑,如果存在任何一個,通過此資源配置加載,spring的規(guī)定規(guī)則如下: 在標(biāo)準(zhǔn)的配置文件名中+ “-spring”,如下:
{ "logback-test-spring.groovy", "logback-test-spring.xml", "logback-spring.groovy",
                "logback-spring.xml" }

3.加載spring默認(rèn)的配置信息,具體可看loadDefaults

備注:

  • initializeWithConventions 加載配置是分優(yōu)先級的,優(yōu)先級如上1,2,3一致,1的優(yōu)先級最高,當(dāng)存在多個條件并存的話,默認(rèn)使用優(yōu)先級最高的。
  • 同時initializeWithConventions 查找配置是否存在都是通過findConfig查找的,此方法只在classpath中查找,不在classpath中的是不會查找的

至此,springboot初始化日志組件的所有流程已根據(jù)源碼跟蹤完畢,在跟蹤源碼時默認(rèn)使用的是logback的實(shí)現(xiàn),Log4J2的實(shí)現(xiàn)與logback的流程近視一致。

順便描述下今天定位的一個測試問題,在測試環(huán)境啟動腳本中配置--logging.config ,而測試環(huán)境是好幾個服務(wù)同時啟動的,使用的是相同的打包流程,相同的腳本,有幾個服務(wù)是正常的,有幾個服務(wù)日志文件是不生成的。搞了好長時間,也因此查看了springboot的日志加載流程,最后發(fā)現(xiàn)原來是啟動主類的啟動方法調(diào)用的不同導(dǎo)致的正常生成日志的啟動類如下:

//將args參數(shù)傳入到SpringApplication,springboot負(fù)責(zé)解析成PropertySource存放到environment中供其它地方使用
SpringApplication.run(GatewayServerApplication.class, args);

不能生成日志文件的啟動類寫法如下:

//未將參數(shù)傳入到SpringApplication處理,導(dǎo)致 在腳本中 --logging.config沒有被解析,日志的初始化,不是預(yù)想的配置初始化的,所以也看不到定義的目錄存在日志。
SpringApplication.run(GatewayServerApplication.class);

不同服務(wù)不同人開發(fā)著,啟動類太簡單,導(dǎo)致問題出現(xiàn)從未想過是這里的問題,排查了好半天,最后發(fā)現(xiàn)竟然這么的尷尬,哎。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容