Springboot的Log日志系統(tǒng)詳解

Springboot的Log系統(tǒng)分為兩個啟動階段:LoggingApplicationListener啟動之前和LoggingApplicationListener成功加載。

1. LoggingApplicationListener啟動之前

此時,業(yè)務(wù)定義的log尚未加載,所以起作用的是Springboot系統(tǒng)內(nèi)部定義的log。
我們可以從main方法跟進去,發(fā)現(xiàn)在SpringApplication中定義了log

private static final Log logger = LogFactory.getLog(SpringApplication.class);

這就是系統(tǒng)內(nèi)部log的啟動。
我們看看這個Log和LogFactory

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

是不是很眼熟,怎么會是apache的package?!
有一個熟悉的commons-logging.jar出現(xiàn)在我們腦海里!
但是,當(dāng)我們再看看這個Log和LogFactory所在jar:

spring-jcl

竟然不是commons-logging.jar!
Spring夠雞賊的!用自己的jar包替換了apache的commons-logging.jar包!
這樣就導(dǎo)致了我們的項目中如果出現(xiàn)了spring-jcl.jar時,就不能再使用commons-logging.jar了!
既然Spring這樣做,那就有這么做的道理了,我們來看看這個jar主要做了什么工作吧。
我們先看看LogFactory.getLog(...)做了啥:

public abstract class LogFactory {

    /**
     * Convenience method to return a named logger.
     * @param clazz containing Class from which a log name will be derived
     */
    public static Log getLog(Class<?> clazz) {
        return getLog(clazz.getName());
    }

    /**
     * Convenience method to return a named logger.
     * @param name logical name of the <code>Log</code> instance to be returned
     */
    public static Log getLog(String name) {
        return LogAdapter.createLog(name);
    }

它就做了一件事,調(diào)用LogAdapter創(chuàng)建Log 。
我們接著往下看:

    public static Log createLog(String name) {
        switch (logApi) {
            case LOG4J:
                return Log4jAdapter.createLog(name);
            case SLF4J_LAL:
                return Slf4jAdapter.createLocationAwareLog(name);
            case SLF4J:
                return Slf4jAdapter.createLog(name);
            default:
                // Defensively use lazy-initializing adapter class here as well since the
                // java.logging module is not present by default on JDK 9. We are requiring
                // its presence if neither Log4j nor SLF4J is available; however, in the
                // case of Log4j or SLF4J, we are trying to prevent early initialization
                // of the JavaUtilLog adapter - e.g. by a JVM in debug mode - when eagerly
                // trying to parse the bytecode for all the cases of this switch clause.
                return JavaUtilAdapter.createLog(name);
        }
    }

而logApi又是怎么來的呢

    private static final LogApi logApi;

    static {
        if (isPresent(LOG4J_SPI)) {
            if (isPresent(LOG4J_SLF4J_PROVIDER) && isPresent(SLF4J_SPI)) {
                // log4j-to-slf4j bridge -> we'll rather go with the SLF4J SPI;
                // however, we still prefer Log4j over the plain SLF4J API since
                // the latter does not have location awareness support.
                logApi = LogApi.SLF4J_LAL;
            }
            else {
                // Use Log4j 2.x directly, including location awareness support
                logApi = LogApi.LOG4J;
            }
        }
        else if (isPresent(SLF4J_SPI)) {
            // Full SLF4J SPI including location awareness support
            logApi = LogApi.SLF4J_LAL;
        }
        else if (isPresent(SLF4J_API)) {
            // Minimal SLF4J API without location awareness support
            logApi = LogApi.SLF4J;
        }
        else {
            // java.util.logging as default
            logApi = LogApi.JUL;
        }
    }

這里又出現(xiàn)了4個常量:

    private static final String LOG4J_SPI = "org.apache.logging.log4j.spi.ExtendedLogger";

    private static final String LOG4J_SLF4J_PROVIDER = "org.apache.logging.slf4j.SLF4JProvider";

    private static final String SLF4J_SPI = "org.slf4j.spi.LocationAwareLogger";

    private static final String SLF4J_API = "org.slf4j.Logger";

其中,LOG4J_SPI 代表log4j日志系統(tǒng);其余3個都代表logback日志系統(tǒng)。
接著往下看:

    private static boolean isPresent(String className) {
        try {
            Class.forName(className, false, LogAdapter.class.getClassLoader());
            return true;
        }
        catch (ClassNotFoundException ex) {
            return false;
        }
    }

這個方法就是判定對應(yīng)的log實現(xiàn)是否存在于classpath中。
通過上面幾段代碼的分析,我們可以得出如下結(jié)論:
1.1. Spring先查找org.apache.logging.log4j.spi.ExtendedLogger;
1.2. 如果ExtendedLogger存在,那么繼續(xù)查找org.apache.logging.slf4j.SLF4JProvider和org.slf4j.spi.LocationAwareLogger;
1.3. 如果SLF4JProvider和LocationAwareLogger都存在,那么就啟用SLF4J_LAL日志系統(tǒng);
1.4. 如果SLF4JProvider和LocationAwareLogger有一個不存在,就啟用LOG4J日志系統(tǒng);
1.5. 如果ExtendedLogger不存在,就查找org.slf4j.spi.LocationAwareLogger;
1.6. 如果LocationAwareLogger存在,就啟用SLF4J_LAL日志系統(tǒng);
1.7. 如果LocationAwareLogger不存在,就繼續(xù)查找org.slf4j.Logger;
1.8. 如果org.slf4j.Logger存在,就啟用SLF4J日志系統(tǒng);
1.9. 如果以上都不存在,就啟用JUL日志系統(tǒng)。

接著往下看
LOG4J日志系統(tǒng)

    private static class Log4jAdapter {

        public static Log createLog(String name) {
            return new Log4jLog(name);
        }
    }

SLF4J_LAL和SLF4J日志系統(tǒng)

    private static class Slf4jAdapter {

        public static Log createLocationAwareLog(String name) {
            Logger logger = LoggerFactory.getLogger(name);
            return (logger instanceof LocationAwareLogger ?
                    new Slf4jLocationAwareLog((LocationAwareLogger) logger) : new Slf4jLog<>(logger));
        }

        public static Log createLog(String name) {
            return new Slf4jLog<>(LoggerFactory.getLogger(name));
        }
    }

JUL日志系統(tǒng)

    private static class JavaUtilAdapter {

        public static Log createLog(String name) {
            return new JavaUtilLog(name);
        }
    }

接下來我們不再一一分析了,我們主要看一下SLF4J_LAL日志系統(tǒng)。


SLF4J_LAL日志系統(tǒng)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.spi.LocationAwareLogger;

這里是啟用了原生態(tài)的logback的日志系統(tǒng),所以這里會啟用logback.xml配置文件,因此,我們最好把logback的配置文件命名為logback.xml,而非logback-spring.xml,或者其他,否者的話,這段啟動過程中的日志可能無法正常輸出。
這樣,Springboot的內(nèi)部日志系統(tǒng)就啟動起來了。

2. LoggingApplicationListener加載業(yè)務(wù)日志系統(tǒng)

這里的邏輯就比較簡單了,其核心代碼如下:

    private void onApplicationStartingEvent(ApplicationStartingEvent event) {
        this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
        this.loggingSystem.beforeInitialize();
    }

    private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
        if (this.loggingSystem == null) {
            this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
        }
        initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
    }

也就是從系統(tǒng)中查找LoggingSystem,然后用找到的loggingSystem初始化系統(tǒng)。
接下來的核心代碼如下


初始化業(yè)務(wù)日志系統(tǒng)

繼續(xù)


初始化業(yè)務(wù)日志系統(tǒng)

其中,
public static final String CONFIG_PROPERTY = "logging.config";

也就是我們定義的log系統(tǒng)的配置文件:

logging: 
  #config: classpath:logback-spring.xml
  config: classpath:logback.xml
  file.path: ${LOGGING_PATH}
  register-shutdown-hook: false

接下來我們看看LoggingSystem文件,其核心代碼如下


LoggingSystem核心代碼

其中,SYSTEMS內(nèi)容如下

    private static final Map<String, String> SYSTEMS;

    static {
        Map<String, String> systems = new LinkedHashMap<>();
        systems.put("ch.qos.logback.core.Appender", "org.springframework.boot.logging.logback.LogbackLoggingSystem");
        systems.put("org.apache.logging.log4j.core.impl.Log4jContextFactory",
                "org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
        systems.put("java.util.logging.LogManager", "org.springframework.boot.logging.java.JavaLoggingSystem");
        SYSTEMS = Collections.unmodifiableMap(systems);
    }

public static final String SYSTEM_PROPERTY = LoggingSystem.class.getName();

也就是SYSTEM_PROPERTY是LoggingSystem具體實現(xiàn)類的名稱:

    private static LoggingSystem get(ClassLoader classLoader, String loggingSystemClass) {
        try {
            Class<?> systemClass = ClassUtils.forName(loggingSystemClass, classLoader);
            Constructor<?> constructor = systemClass.getDeclaredConstructor(ClassLoader.class);
            constructor.setAccessible(true);
            return (LoggingSystem) constructor.newInstance(classLoader);
        }
        catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }

或許沒有定義其他實現(xiàn)類,那么會從系統(tǒng)定義的SYSTEM中去查找第一個存在的LoggingSystem類

        return SYSTEMS.entrySet().stream().filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader))
                .map((entry) -> get(classLoader, entry.getValue())).findFirst()
                .orElseThrow(() -> new IllegalStateException("No suitable logging system located"));

我們?nèi)砸詌ogback為例說說具體實現(xiàn)。
系統(tǒng)定義了logback的LoggingSystem實現(xiàn)類(這也是Springboot的默認實現(xiàn))
LogbackLoggingSystem。
關(guān)于配置文件的加載核心代碼塊(AbstractLoggingSystem):

    @Override
    public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
        if (StringUtils.hasLength(configLocation)) {
            initializeWithSpecificConfig(initializationContext, configLocation, logFile);
            return;
        }
        initializeWithConventions(initializationContext, logFile);
    }

這里,configLocation就是logging.config定義的配置文件,如果該文件存在,那么就直接去初始化logback,這里的配置文件名沒有特定要求,只要是logback的.groovy或者.xml配置即可。但是為了與系統(tǒng)內(nèi)部日志配置保持一致,建議用logback.xml。
如果我們沒有定義logging.config配置文件,那么就去找系統(tǒng)默認配置文件,查找的核心代碼如下:

    private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
        String config = getSelfInitializationConfig();
        if (config != null && logFile == null) {
            // self initialization has occurred, reinitialize in case of property changes
            reinitialize(initializationContext);
            return;
        }
        if (config == null) {
            config = getSpringInitializationConfig();
        }
        if (config != null) {
            loadConfiguration(initializationContext, config, logFile);
            return;
        }
        loadDefaults(initializationContext, logFile);
    }

首先是通過String config = getSelfInitializationConfig()來加載:

    protected String getSelfInitializationConfig() {
        return findConfig(getStandardConfigLocations());
    }
    private String findConfig(String[] locations) {
        for (String location : locations) {
            ClassPathResource resource = new ClassPathResource(location, this.classLoader);
            if (resource.exists()) {
                return "classpath:" + location;
            }
        }
        return null;
    }
    protected abstract String[] getStandardConfigLocations();

在LogbackLoggingSystem的實現(xiàn)是

    @Override
    protected String[] getStandardConfigLocations() {
        return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml" };
    }

也就是說這些命名的配置文件都可以被加載。其中,logback.xml還可以與前面說的Springboot內(nèi)部log共用。
如果這些文件都不存在,那么會執(zhí)行config = getSpringInitializationConfig()去繼續(xù)查找配置文件:

    protected String getSpringInitializationConfig() {
        return findConfig(getSpringConfigLocations());
    }
    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;
    }

也就是說,對"logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml"這些文件名處理后再查找:"logback-test-spring.groovy", "logback-test-spring.xml", "logback-spring.groovy", "logback-spring.xml",這也是為什么網(wǎng)上很多教程要求大家配置logback-spring.xml文件名的原因。
如果這些處理后的文件還不存在,那就繼續(xù)執(zhí)行l(wèi)oadDefaults(initializationContext, logFile)去初始化logback配置:

protected abstract void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile);

在LogbackLoggingSystem中實現(xiàn)如下:

    @Override
    protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {
        LoggerContext context = getLoggerContext();
        stopAndReset(context);
        boolean debug = Boolean.getBoolean("logback.debug");
        if (debug) {
            StatusListenerConfigHelper.addOnConsoleListenerInstance(context, new OnConsoleStatusListener());
        }
        LogbackConfigurator configurator = debug ? new DebugLogbackConfigurator(context)
                : 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}}"));
        context.putProperty(LoggingSystemProperties.ROLLING_FILE_NAME_PATTERN, environment
                .resolvePlaceholders("${logging.pattern.rolling-file-name:${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}"));
        new DefaultLogbackConfiguration(initializationContext, logFile).apply(configurator);
        context.setPackagingDataEnabled(true);
    }

這些執(zhí)行完畢之后,LoggingApplicationListener也起動起來了,同事還把前面定義的內(nèi)部log系統(tǒng)更新為剛加載的log日志系統(tǒng)。
經(jīng)過上面這些步驟,Springboot的日志系統(tǒng)算是正式啟動起來了。

3. 通過上面的分析,我們可以得出以下注意事項:

3.1 Springboot系統(tǒng)中不允許引入commons-logging.jar,但是我們在使用日志系統(tǒng)的時候,最好使用Log logger = LogFactory.getLog(SpringApplication.class)來定義,org.apache.commons.logging.Log和org.apache.commons.logging.LogFactory便于兼容多種日志系統(tǒng);
3.2 log4j-api.jar、log4j-to-slf4j.jar、slf4j-api.jar可以共存,但是優(yōu)先級是log4j > logback;
3.3 log4j只能啟用2.x;
3.4 Logback的配置文件命名最好使用logback.xml;
3.5 最好明確定義出logging.config: classpath:logback.xml;
3.6 可以通過繼承LoggingSystem來定義自己的LoggingSystem,通過設(shè)置System.setProperty(全類名)進行加載;
3.7 LoggingSystem加載完畢后,系統(tǒng)注冊了3個單例bean:springBootLoggingSystem=LoggingSystem實例,springBootLogFile=LogFile實例,對應(yīng)于logging.file.path配置,springBootLoggerGroups=LoggerGroups日志分組實例。
3.8 由于我們修訂的logback配置文件的名稱為logback.xml,這樣會導(dǎo)致系統(tǒng)未使用spring的方式加載logback,所以在logback.xml中的屬性配置就不能再使用springProperty,而直接使用logback的標(biāo)簽property即可。

4. 如何用log4j替換logback?

有時候,我們需要使用log4j而不是logback,那我們該如何做呢?
4.1 根據(jù)上面的分析,我們清楚地知道,在classpath中存在log4j-api.jar并且不存在slf4j-api.jar時,第一階段就會啟用log4j日志系統(tǒng),但是只能是啟用2.x版;
4.2 在第二階段,加載LoggingSystem時有如下代碼

    public static LoggingSystem get(ClassLoader classLoader) {
        String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
        if (StringUtils.hasLength(loggingSystem)) {
            if (NONE.equals(loggingSystem)) {
                return new NoOpLoggingSystem();
            }
            return get(classLoader, loggingSystem);
        }
        return SYSTEMS.entrySet().stream().filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader))
                .map((entry) -> get(classLoader, entry.getValue())).findFirst()
                .orElseThrow(() -> new IllegalStateException("No suitable logging system located"));
    }

其中,SYSTEM_PROPERTY=LoggingSystem.class.getName(),只要loggingSystem存在,系統(tǒng)就會加載該LoggingSystem,而不會再去classpath去查找其他信息了。而log4j對應(yīng)的LoggingSystem是org.springframework.boot.logging.log4j2.Log4J2LoggingSystem,因此,我們只需要在main啟動的時候,添加對應(yīng)的屬性值即可:

    public static void main(String[] args) throws Exception {
        System.setProperty("org.springframework.boot.logging.LoggingSystem", 
                "org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
        
        configureApplication(new SpringApplicationBuilder()).run(args);
    }

4.3 注意配置log的配置文件:logging.config: classpath:log4j.properties,另外注意引入log4j的依賴jar。
這樣,Springboot的默認logback日志系統(tǒng)就會被log4j日志系統(tǒng)所替代。

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

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

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