開心一刻
今天跟我姐聊天
我:我喜歡上了我們公司的一個女同事,她好漂亮,我心動了,怎麼辦
姐:喜歡一個女孩子不能只看她的外表
我:我知道,還要看她的內在嘛
姐:你想多了,還要看看自己的外表
背景介紹
在 SpringBoot2.7 霸王硬上弓 Logback1.3 → 不甜但解渴 原理分析那部分,我對 Logback
的表述是很委婉的
後來想想,作為一個軟體開發人員,怎能如此不嚴謹,真是太不應該了,為表示最誠摯的歉意,請允許我自罰三耳光
作為彌補,接下來我會帶你們盤一盤 Logback 1.3.14
的部分原始碼。參考 從原始碼來理解slf4j的繫結,以及logback對配置檔案的載入,同樣基於兩個問題
- SLF4J 與 Logback 是如何繫結的
- 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 的依賴樹如下
無論是 logback 官配
還是 logback 1.3.14 pom 檔案中的依賴
slf4j-api
的版本都是 2.0.x
(logback 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 版本
那為什麼不是 logback-parent-1.3.14.pom
中的 slf4j.version
生效,而是 spring-boot-dependencies-2.7.18.pom
中的 slf4j.version
生效呢?這就涉及 maven
依賴的優先順序了,感興趣的可以去查閱相關資料,本文就不展開了,因為偏離我們的最初的目標越來越遠了
那如何將 slf4j
改成 2.0.7
,提供兩種方式
-
如果不需要
spring-boot
,那就去掉父依賴spring-boot-starter-parent
這就相當於由 logback 帶入 slf4j,引入的就是 logback 所依賴的版本
-
在我們的 pom 檔案中指定
<slf4j.version>2.0.7</slf4j.version>
這裡還是涉及 maven 依賴的優先順序,我們自己的 pom 檔案中的優先順序更高
不管採用哪種方式,反正要把版本搞正確
SLF4J 繫結 Logback
準備測試程式碼
public class LogbackTest {
private static Logger LOGGER = LoggerFactory.getLogger(LogbackTest.class);
public static void main(String[] args)
{
LOGGER.info("......info");
}
}
應該知道從哪開始跟原始碼吧,沒得選擇呀,只能選 getLogger
方法
推薦大家用 debug
的方式去跟,不然容易跟丟;來到 org.slf4j.LoggerFactory#bind
方法,這裡完成 slf4j
與具體實現的繫結。bind 方法中有 2 點需要我們自己分析下
-
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
檔案然後讀取其中的內容,並例項化
這裡拿到的是
Provider
,並非Logger
-
initialize
大家注意看下
defaultLoggerContext
的型別LoggerContext
public class LoggerContext extends ContextBase implements ILoggerFactory, LifeCycle
第 2 點與 Logback 載入配置檔案有關,後續再細看,暫且先只看第 1 點
注意看下
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)
這個方法雖然略微有點長,但不難,只是有個快取設計,我就不展開了,你們自行去看
總結下
- 透過 SPI 的方式,實現 SLF4JServiceProvider 的繫結(ch.qos.logback.classic.spi.LogbackServiceProvider)
- LogbackServiceProvider 的 initialize 方法會例項化 defaultLoggerContext(ch.qos.logback.classic.LoggerContext implement org.slf4j.ILoggerFactory)
- 透過 defaultLoggerContext 獲取 logger(ch.qos.logback.classic.Logger implements org.slf4j.Logger)
- 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
方法尋找配置檔案
優先順序從高到低,會從 classpath
下尋找三個檔案
- 尋找
logback.configurationFile
- 尋找
logback-test.xml
- 尋找
logback.xml
一旦找到,直接返回,不會繼續尋找;我們用的是 logback.xml
而沒有使用其它兩個檔案,所以生效的是 logback.xml
再回過頭去看 背景介紹
中的不嚴謹處,我們發現 Logback 1.3.14
對配置檔案的載入與 Logback 1.1.7
基本一致,只是少了 logback.groovy
的讀取;但話說回來,SLF4J
與 Logback
的繫結過程還是有非常大的變動,大家可以和 從原始碼來理解slf4j的繫結,以及logback對配置檔案的載入 仔細對比
總結
-
SLF4J 2.0.x 與 Logback 1.3.x 的繫結,採用了 SPI 機制
-
Logback 1.3.x 預設配置檔案優先順序
logback.configurationFile > logback-test.xml > logback.xml
優先順序從高到低一旦讀取一個,則直接採用這個,不會繼續往下讀
所以 SpringBoot2.7 霸王硬上弓 Logback1.3 → 不甜但解渴 中提到的
配置檔案必須是 logback.xml
不夠嚴謹,還可以是哪些,你們應該知道了吧? -
儘量選擇
官配
依賴版本,不要頭鐵,不要頭鐵,不要頭鐵!