【SpringBoot實踐】切換日誌系統

0 修訂

時間 描述
2019-04-12 建檔

1 簡介

神使鬼差地問了自己一個問題:如何在SpringBoot中使用另一個日誌實現?於是開始了一個上午的崩潰研究。

Log4j 1.x於2015年8月5日結束其生命週期後,Logback作爲Log4j項目的後續產品替代了它留下的空白。而Log4j 2.x是對Log4j 1.x的一次重大升級,不僅整合了Logback中的改進,還修復了其架構中的一些固有問題。

本文着重講述如何在SpringBoot中使用Log4j 2.x,具體特性不做介紹,請參閱相關文檔。

2 分析

SpringBoot說明文檔的Logging章節給出了切換日誌系統的官方方式:

可以使用org.springframework.boot.logging.LoggingSystem系統參數來強制SpringBoot使用指定的日誌系統。該參數是LoggingSystem實現的全限定類名,或者設置成none來完全禁用SpringBoot的日誌配置。

以下是其決定日誌系統的邏輯:

// org.springframework.boot.logging.LoggingSystem.java
public static LoggingSystem get(ClassLoader classLoader) {
    // SYSTEM_PROPERTY = LoggingSystem.class.getName();
	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"));
}

即:若程序未指定系統參數org.springframework.boot.logging.LoggingSystem,則從日誌系統列表SYSTEMS中遍歷可用的。而該列表的順序爲LogbackLoggingSystemLog4J2LoggingSystemJavaLoggingSystem

SpringBoot有兩個日誌啓動器:spring-boot-starter-loggingspring-boot-starter-log4j。默認使用前者,下面是其內部依賴:

org.springframework.boot:spring-boot-starter-logging:2.0.4.RELEASE
+ ch.qos.logback:logback-classic:1.2.3
  + ch.qos.logback:logback-core:1.2.3
  + org.slf4j:slf4j-api:1.7.25
+ org.apache.logging.log4j:log4j-to-slf4j:2.10.0
  + org.slf4j:slf4j-api:1.7.25
  + org.apache.logging.log4j:log4j-api:2.10.0
+ org.slf4j:jul-to-slf4j:1.7.25
  + org.slf4j:slf4j-api:1.7.25

2.1 指定日誌系統

既然可以指定日誌系統,那就先將其指定爲Log4j 2:

// Application.java
@SpringBootApplication
public class Application extends SpringBootServletInitializer {

	@Override
	protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
		System.setProperty("org.springframework.boot.logging.LoggingSystem", "org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
		return builder.sources(Application.class);
	}

}

運行程序,報錯:

java.lang.ClassNotFoundException: org.apache.logging.log4j.core.Filter

2.2 添加核心JAR包

既然缺少核心包下的類,那就補上:

<dependency>
	<groupId>org.apache.logging.log4j</groupId>
	<artifactId>log4j-core</artifactId>
	<version>2.10.0</version>
</dependency>

運行程序,報錯:

Caused by: java.lang.ClassCastException: org.apache.logging.slf4j.SLF4JLoggerContext cannot be cast to org.apache.logging.log4j.core.LoggerContext
	at org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.getLoggerContext(Log4J2LoggingSystem.java:264)
	at org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.beforeInitialize(Log4J2LoggingSystem.java:131)

2.2.1 查找原因

查看org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.getLoggerContext()的源碼:

// org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.java
private LoggerContext getLoggerContext() {
	return (LoggerContext) LogManager.getContext(false);
}

其中的LoggerContext來自2.2節裏面添加的log4j-core,路徑爲org.apache.logging.log4j.core.LoggerContext

方法其內部返回的上下文則來自log4j-api中的LogManager

// org.apache.logging.log4j.LogManager.java
public static LoggerContext getContext(final boolean currentContext) {
    // TODO: would it be a terrible idea to try and find the caller ClassLoader here?
    try {
        return factory.getContext(FQCN, null, null, currentContext, null, null);
    } catch (final IllegalStateException ex) {
        LOGGER.warn(ex.getMessage() + " Using SimpleLogger");
        return new SimpleLoggerContextFactory().getContext(FQCN, null, null, currentContext, null, null);
    }
}

異常的原因就是,LogManager內的日誌上下文工廠是SLF4JLoggerContextFactory實例,其返回的上下文對象實現的是org.apache.logging.log4j.spi.LoggerContext,與org.apache.logging.log4j.core.LoggerContext之間沒有任何關係,因此強制轉化失敗。

利用開發工具可以查看到,factory.getContext()有三個實現:Log4JContextFactorySLF4JLoggerContextFactorySimpleLoggerContextFactory。那LogManager是如何選擇上下文工廠的呢?

2.2.2 定位問題

LogManager有一個靜態代碼塊:

// org.apache.logging.log4j.LogManager.java
static {
    // 1. 從log4j2.component.properties文件中讀取配置
    // 2. 若配置了log4j2.loggerContextFactory,則以此創建上下文工廠
    // 3. 若未配置或工廠創建失敗,則獲取所有的上下文工廠實現,取權重最大的那個創建上下文工廠
}

在沒有添加配置的前提下,由於log4j-core中上下文工廠實現的權重是10log4j-to-slf4j中的權重是15。所以,LogManager選擇了後者創建上下文工廠,從而造成無法強制轉換。

// org.apache.logging.log4j.core.impl.Log4jProvider.java
public class Log4jProvider extends Provider {
    public Log4jProvider() {
        super(Integer.valueOf(10), "2.6.0", Log4jContextFactory.class);
    }
}

// org.apache.logging.log4j.spi.Provider.SLF4JProvider.java
public class SLF4JProvider extends Provider {
    public SLF4JProvider() {
        super(15, "2.6.0", SLF4JLoggerContextFactory.class, MDCContextMap.class);
    }
}

3 方案

3.1 指定上下文工廠實現

既然從2.2.2節中得知,LogManager在存在多個上下文工廠實現的情況下會選擇權重大的,無法修改權重,那就指定具體的實現。

// resources/log4j2.component.properties
log4j2.loggerContextFactory=org.apache.logging.log4j.core.impl.Log4jContextFactory

上述配置強制SpringBoot使用log4j-core中的上下文工廠實現。

重啓程序,成功切換到了Log4j 2。

3.2 排除相關依賴

既然知道衝突是由log4j-to-slf4j造成的,那麼排除掉該依賴即可。事實上,網上絕大部分的解決方案都是基於或間接基於該點的。比如,排除掉整個spring-boot-starter-logging

但問題是,SpringBoot許多模塊都內部依賴了該啓動器。因此要想完全排除,就必須一個一個的檢查並排除。很顯然,這是反人類的。

因此,既然spring-boot-starter-logging的其他代碼對程序不會造成影響,那麼只需要排除掉log4j-to-slf4j就行了:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-logging</artifactId>
	<exclusions>
		<exclusion>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-to-slf4j</artifactId>
		</exclusion>
	</exclusions>
</dependency>

這裏用到了Maven依賴傳遞中的優先級。詳細介紹參閱Maven依賴機制介紹

4 關於spring-boot-starter-log4j

待補充

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章