文章目錄
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
中遍歷可用的。而該列表的順序爲LogbackLoggingSystem
、Log4J2LoggingSystem
和JavaLoggingSystem
。
SpringBoot有兩個日誌啓動器:spring-boot-starter-logging
和spring-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()
有三個實現:Log4JContextFactory
、SLF4JLoggerContextFactory
和SimpleLoggerContextFactory
。那LogManager
是如何選擇上下文工廠的呢?
2.2.2 定位問題
LogManager
有一個靜態代碼塊:
// org.apache.logging.log4j.LogManager.java
static {
// 1. 從log4j2.component.properties文件中讀取配置
// 2. 若配置了log4j2.loggerContextFactory,則以此創建上下文工廠
// 3. 若未配置或工廠創建失敗,則獲取所有的上下文工廠實現,取權重最大的那個創建上下文工廠
}
在沒有添加配置的前提下,由於log4j-core
中上下文工廠實現的權重是10
,log4j-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
待補充