SLF4J2.0.x與Logback1.3.x的繫結變動還是很大的,不要亂點鴛鴦譜

青石路發表於2024-08-02

開心一刻

今天跟我姐聊天

我:我喜歡上了我們公司的一個女同事,她好漂亮,我心動了,怎麼辦

姐:喜歡一個女孩子不能只看她的外表

我:我知道,還要看她的內在嘛

姐:你想多了,還要看看自己的外表

還要看自己的外表

背景介紹

SpringBoot2.7 霸王硬上弓 Logback1.3 → 不甜但解渴 原理分析那部分,我對 Logback 的表述是很委婉的

委婉表述

後來想想,作為一個軟體開發人員,怎能如此不嚴謹,真是太不應該了,為表示最誠摯的歉意,請允許我自罰三耳光

羅永浩打臉

作為彌補,接下來我會帶你們盤一盤 Logback 1.3.14 的部分原始碼。參考 從原始碼來理解slf4j的繫結,以及logback對配置檔案的載入,同樣基於兩個問題

  1. SLF4J 與 Logback 是如何繫結的
  2. Logback 是如何載入配置檔案的

來展開分析。在分析之前,我先幫你們解決一個你們可能會有遇到的疑問點

Logback 1.3.14 依賴的 SLF4J 版本怎麼是 1.7.36?

假設我們的 pom.xml 內容如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.qsl</groupId>
    <artifactId>spring-boot-2_7_18</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <logback.version>1.3.14</logback.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>spring-boot-starter-logging</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
        </dependency>
    </dependencies>
</project>

但我們會發現 logback 的依賴樹如下

slf4j亂入

無論是 logback 官配

slf4j與logback官配

還是 logback 1.3.14 pom 檔案中的依賴

logback-parent_slf4j

slf4j-api 的版本都是 2.0.xlogback 1.3.14 依賴的是 slf4j-api 2.0.7) ,slf4j-api 1.7.36 是從哪亂入的?

這是因為引入了父依賴

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.7.18</version>
</parent>

spring-boot-starter-parent 的父依賴

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-dependencies</artifactId>
	<version>2.7.18</version>
</parent>

spring-boot-dependencies 指定了 slf4j 版本

spring-boot-dependencies_slf4j1.7.36

那為什麼不是 logback-parent-1.3.14.pom 中的 slf4j.version 生效,而是 spring-boot-dependencies-2.7.18.pom 中的 slf4j.version 生效呢?這就涉及 maven 依賴的優先順序了,感興趣的可以去查閱相關資料,本文就不展開了,因為偏離我們的最初的目標越來越遠了

那如何將 slf4j 改成 2.0.7,提供兩種方式

  1. 如果不需要 spring-boot,那就去掉父依賴 spring-boot-starter-parent

    這就相當於由 logback 帶入 slf4j,引入的就是 logback 所依賴的版本

  2. 在我們的 pom 檔案中指定 <slf4j.version>2.0.7</slf4j.version>

    這裡還是涉及 maven 依賴的優先順序,我們自己的 pom 檔案中的優先順序更高

不管採用哪種方式,反正要把版本搞正確

slf4j_2.0.7

SLF4J 繫結 Logback

準備測試程式碼

public class LogbackTest {

    private static Logger LOGGER = LoggerFactory.getLogger(LogbackTest.class);

    public static void main(String[] args)
    {
        LOGGER.info("......info");
    }
}

應該知道從哪開始跟原始碼吧,沒得選擇呀,只能選 getLogger 方法

bind前奏

推薦大家用 debug 的方式去跟,不然容易跟丟;來到 org.slf4j.LoggerFactory#bind 方法,這裡完成 slf4j 與具體實現的繫結。bind 方法中有 2 點需要我們自己分析下

bind方法
  1. findServiceProviders

    static List<SLF4JServiceProvider> findServiceProviders() {
    	// retain behaviour similar to that of 1.7 series and earlier. More specifically, use the class loader that
    	// loaded the present class to search for services
    	final ClassLoader classLoaderOfLoggerFactory = LoggerFactory.class.getClassLoader();
    	ServiceLoader<SLF4JServiceProvider> serviceLoader = getServiceLoader(classLoaderOfLoggerFactory);
    	List<SLF4JServiceProvider> providerList = new ArrayList<>();
    	Iterator<SLF4JServiceProvider> iterator = serviceLoader.iterator();
    	while (iterator.hasNext()) {
    		safelyInstantiate(providerList, iterator);
    	}
    	return providerList;
    }
    

    有沒有一點熟悉的感覺?大家回顧下 JDK SPI,是不是恍然大悟了?會去 classpath 下的 META-INF/services 目錄下尋找 org.slf4j.spi.SLF4JServiceProvider 檔案

    spi

    然後讀取其中的內容,並例項化

    LogbackServiceProvider

    這裡拿到的是 Provider,並非 Logger

  2. initialize

    initialize

    大家注意看下 defaultLoggerContext 的型別 LoggerContext

    public class LoggerContext extends ContextBase implements ILoggerFactory, LifeCycle

    第 2 點與 Logback 載入配置檔案有關,後續再細看,暫且先只看第 1 點

    LoggerContext

    注意看下 Logger 的型別

    public final class Logger
    implements org.slf4j.Logger, LocationAwareLogger, LoggingEventAware, AppenderAttachable, Serializable

    實現了 org.slf4j.Logger,這就跟 slf4j 關聯起來了

    接下來出棧,回到

    public static ILoggerFactory getILoggerFactory() {
    	return getProvider().getLoggerFactory();
    }
    

    getProvider() 已經分析過了,接下來就看 getLoggerFactory()

        public ILoggerFactory getLoggerFactory() {
            return defaultLoggerContext;
    
    //        if (!initialized) {
    //            return defaultLoggerContext;
    //        
    //
    //        if (contextSelectorBinder.getContextSelector() == null) {
    //            throw new IllegalStateException("contextSelector cannot be null. See also " + NULL_CS_URL);
    //        }
    //        return contextSelectorBinder.getContextSelector().getLoggerContext();
        }
    

    非常簡單,直接返回 defaultLoggerContext,defaultLoggerContext 在前面的 initialize 已經講過,忘記了的小夥伴回到上面看看

    getILoggerFactory() 繼續出棧,來到

    public static Logger getLogger(String name) {
    	ILoggerFactory iLoggerFactory = getILoggerFactory();
    	return iLoggerFactory.getLogger(name);
    }
    

    這裡的 iLoggerFactory 是不是就是 defaultLoggerContext?接下來就看 iLoggerFactory.getLogger(name)

    這個方法雖然略微有點長,但不難,只是有個快取設計,我就不展開了,你們自行去看

總結下

  1. 透過 SPI 的方式,實現 SLF4JServiceProvider 的繫結(ch.qos.logback.classic.spi.LogbackServiceProvider)
  2. LogbackServiceProvider 的 initialize 方法會例項化 defaultLoggerContext(ch.qos.logback.classic.LoggerContext implement org.slf4j.ILoggerFactory)
  3. 透過 defaultLoggerContext 獲取 logger(ch.qos.logback.classic.Logger implements org.slf4j.Logger)
  4. org.slf4j.Logger 繫結 ch.qos.logback.classic.Logger 完成

Logback 載入配置檔案

前面已經提到過,ch.qos.logback.classic.spi.LogbackServiceProvider#initializeLoggerContext 完成對配置檔案的載入

private void initializeLoggerContext() {
	try {
		try {
			new ContextInitializer(defaultLoggerContext).autoConfig();
		} catch (JoranException je) {
			Util.report("Failed to auto configure default logger context", je);
		}
		// LOGBACK-292
		if (!StatusUtil.contextHasStatusListener(defaultLoggerContext)) {
			StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext);
		}
		// contextSelectorBinder.init(defaultLoggerContext, KEY);

	} catch (Exception t) { // see LOGBACK-1159
		Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t);
	}
}

一眼就能看出,下一步直接看 autoConfig,跟進去 2 步,會來到如下方法

public void autoConfig(ClassLoader classLoader) throws JoranException {

	// see https://github.com/qos-ch/logback/issues/715
	classLoader = Loader.systemClassloaderIfNull(classLoader);

	String versionStr = EnvUtil.logbackVersion();
	if (versionStr == null) {
		versionStr = CoreConstants.NA;
	}
	loggerContext.getStatusManager().add(new InfoStatus(CoreConstants.LOGBACK_CLASSIC_VERSION_MESSAGE + versionStr, loggerContext));
	StatusListenerConfigHelper.installIfAsked(loggerContext);


	// invoke custom configurators
	List<Configurator> configuratorList = ClassicEnvUtil.loadFromServiceLoader(Configurator.class, classLoader);
	configuratorList.sort(rankComparator);
	if (configuratorList.isEmpty()) {
		contextAware.addInfo("No custom configurators were discovered as a service.");
	} else {
		printConfiguratorOrder(configuratorList);
	}

	for (Configurator c : configuratorList) {
		if (invokeConfigure(c) == Configurator.ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY)
			return;
	}

	// invoke internal configurators
	for (String configuratorClassName : INTERNAL_CONFIGURATOR_CLASSNAME_LIST) {
		contextAware.addInfo("Trying to configure with "+configuratorClassName);
		Configurator c = instantiateConfiguratorByClassName(configuratorClassName, classLoader);
		if(c == null)
			continue;
		if (invokeConfigure(c) == Configurator.ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY)
			return;
	}
}

前部分讀自定義配置,因為我們沒有自定義配置,所以可以忽略,直接看

// invoke internal configurators
for (String configuratorClassName : INTERNAL_CONFIGURATOR_CLASSNAME_LIST) {
	contextAware.addInfo("Trying to configure with "+configuratorClassName);
	Configurator c = instantiateConfiguratorByClassName(configuratorClassName, classLoader);
	if(c == null)
		continue;
	if (invokeConfigure(c) == Configurator.ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY)
		return;
}

INTERNAL_CONFIGURATOR_CLASSNAME_LIST 內容如下

String[] INTERNAL_CONFIGURATOR_CLASSNAME_LIST = {"ch.qos.logback.classic.joran.SerializedModelConfigurator",
            "ch.qos.logback.classic.util.DefaultJoranConfigurator", "ch.qos.logback.classic.BasicConfigurator"}

這個 for 迴圈是一旦 invoke 上,則直接返回,所以是 INTERNAL_CONFIGURATOR_CLASSNAME_LIST 元素從前往後,逐個 invoke,一旦成功則直接結束;透過 debug 我們會發現 DefaultJoranConfigurator invoke 上了,其 performMultiStepConfigurationFileSearch 方法尋找配置檔案

performMultiStepConfigurationFileSearch

優先順序從高到低,會從 classpath 下尋找三個檔案

  1. 尋找 logback.configurationFile
  2. 尋找 logback-test.xml
  3. 尋找 logback.xml

一旦找到,直接返回,不會繼續尋找;我們用的是 logback.xml

logback.xml

而沒有使用其它兩個檔案,所以生效的是 logback.xml

再回過頭去看 背景介紹 中的不嚴謹處,我們發現 Logback 1.3.14 對配置檔案的載入與 Logback 1.1.7 基本一致,只是少了 logback.groovy 的讀取;但話說回來,SLF4JLogback 的繫結過程還是有非常大的變動,大家可以和 從原始碼來理解slf4j的繫結,以及logback對配置檔案的載入 仔細對比

愣著幹啥,鼓掌

總結

  1. SLF4J 2.0.x 與 Logback 1.3.x 的繫結,採用了 SPI 機制

  2. Logback 1.3.x 預設配置檔案優先順序

    logback.configurationFile > logback-test.xml > logback.xml

    優先順序從高到低一旦讀取一個,則直接採用這個,不會繼續往下讀

    所以 SpringBoot2.7 霸王硬上弓 Logback1.3 → 不甜但解渴 中提到的

    配置檔案必須是logback.xml

    配置檔案必須是 logback.xml 不夠嚴謹,還可以是哪些,你們應該知道了吧?

    頭給你敲破
  3. 儘量選擇 官配 依賴版本,不要頭鐵,不要頭鐵,不要頭鐵!

相關文章