1、背景
有個舊專案之前使用的是log4j2來列印日誌的,因為某些原因,同事想換成logback。
換成logback改動也很簡單,大致就一下2步:
-
刪除log4j2.xml配置,新增logback.xml配置。剔除掉log4j相關的jar
-
引入slf4j (其實之前使用log4j2的時候就已經引入了,只是有些地方寫法不規範),
程式碼【import org.apache.log4j.Logger】改成【import org.slf4j.Logger】(以及其他類似修改)
2、現象
全部改了之後,按道理說,應該就可以正常列印了。
但是啟動發現,日誌報錯:
ERROR {org.springframework.web.context.ContextLoader:356} - Context initialization failed
java.lang.NoClassDefFoundError: org/apache/log4j/Logger
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
at java.lang.Class.getDeclaredMethods(Class.java:1975)
at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:612)
at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:524)
at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:510)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors(AutowiredAnnotationBeanPostProcessor.java:241)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineConstructorsFromBeanPostProcessors(AbstractAutowireCapableBeanFactory.java:1069)
★小技巧:
報錯的日誌比較多,甚至有些報錯看起來有點莫名其妙。
不要慌,先找到最早的案發現場日誌,或者搜一下關鍵字。
因為我們改的是日誌,所以可以在報錯資訊中搜一下log/log4j 等關鍵字
3、問題排查
看到這裡第一反應應該是有程式碼沒改全了。全域性搜一下log4j關鍵字,果然發現還有一處:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
...
<property name="filters" value="config,stat,log4j,wall" /> <!-- 這裡的log4j 需要改成 slf4j -->
...
</bean>
DruidDatasource中定義了com.alibaba.druid.filter.logging.LogFilter,他有3個子類,分別對應不同的日誌列印實現方式
- com.alibaba.druid.filter.logging.Slf4jLogFilter
- com.alibaba.druid.filter.logging.Log4jFilter
- com.alibaba.druid.filter.logging.CommonsLogFilter
那我們怎麼知道是配置成slf4j、Slf4j、還是Slf4jLogger呢?可以看這裡druid原始碼檔案【META-INF/druid-filter.properties】
druid.filters.default=com.alibaba.druid.filter.stat.StatFilter
druid.filters.stat=com.alibaba.druid.filter.stat.StatFilter
druid.filters.mergeStat=com.alibaba.druid.filter.stat.MergeStatFilter
druid.filters.counter=com.alibaba.druid.filter.stat.StatFilter
druid.filters.encoding=com.alibaba.druid.filter.encoding.EncodingConvertFilter
druid.filters.log4j=com.alibaba.druid.filter.logging.Log4jFilter
druid.filters.slf4j=com.alibaba.druid.filter.logging.Slf4jLogFilter
druid.filters.commonlogging=com.alibaba.druid.filter.logging.CommonsLogFilter
druid.filters.commonLogging=com.alibaba.druid.filter.logging.CommonsLogFilter
druid.filters.wall=com.alibaba.druid.wall.WallFilter
druid.filters.config=com.alibaba.druid.filter.config.ConfigFilter
無意中發現commonlogging這一行寫重複了,哈哈,這個不是我複製重的,原始碼druid-1.0.18就是搞重複了!
改完之後,再啟動專案,發現問題依舊啊!
4、問題分析
估計還是有別的地方寫明瞭需要使用log4j,為了驗證猜想,我設定了NoClassDefFoundError異常斷點,再次debug啟動。
進入斷點的時候,就發現還有個 HttpSessionManager 程式碼中寫死了【import org.apache.log4j.Logger;】
這個是個第三方的jar,程式碼是改不了的。就只能另尋他法了。
其實像這種情況,程式碼寫死了使用log4j,想統一成slf4j,slf4j已經提供瞭解決方法。那就是引入log4j-over-slf4j。
使用log4j-over-slf4j取代log4j,這樣log4j介面輸出的日誌就會透過log4j-over-slf4j路由到SLF4J上,這樣即使系統(包含使用的第三方jar庫,比如dubbo)都可以將日誌最終路由到SLF4J上,進而集中輸出
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.1</version>
</dependency>
引入log4j-over-slf4j之後,再啟動,就ok了~
5、問題延伸
再回過頭看一下,log4j-over-slf4j到底給我們做了什麼?
- 定義了【org.apache.log4j.Logger】物件,確保使用了log4j的老專案程式碼不至於編譯不透過
package org.apache.log4j;
import org.slf4j.Marker;
public class Logger extends Category {
...
}
- 將【org.apache.log4j.Logger】的列印動作偷偷轉移到slf4j上。
Logger繼承自Category,並且實現了info、warn、error等列印日誌的方法
package org.apache.log4j;
import java.util.Enumeration;
import org.apache.log4j.helpers.NullEnumeration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import org.slf4j.spi.LocationAwareLogger;
public class Category {
private static final String CATEGORY_FQCN = Category.class.getName();
private String name;
protected Logger slf4jLogger;
private LocationAwareLogger locationAwareLogger;
private static Marker FATAL_MARKER = MarkerFactory.getMarker("FATAL");
Category(String name) {
this.name = name;
this.slf4jLogger = LoggerFactory.getLogger(name);
if (this.slf4jLogger instanceof LocationAwareLogger) {
this.locationAwareLogger = (LocationAwareLogger)this.slf4jLogger;
}
}
public Level getEffectiveLevel() {
if (this.slf4jLogger.isTraceEnabled()) {
return Level.TRACE;
} else if (this.slf4jLogger.isDebugEnabled()) {
return Level.DEBUG;
} else if (this.slf4jLogger.isInfoEnabled()) {
return Level.INFO;
} else {
return this.slf4jLogger.isWarnEnabled() ? Level.WARN : Level.ERROR;
}
}
public void info(Object message) {
this.differentiatedLog((Marker)null, CATEGORY_FQCN, 20, message, (Throwable)null);
}
void differentiatedLog(Marker marker, String fqcn, int level, Object message, Throwable t) {
String m = this.convertToString(message);
if (this.locationAwareLogger != null) {
this.locationAwareLogger.log(marker, fqcn, level, m, (Object[])null, t);
} else {
switch(level) {
case 0:
this.slf4jLogger.trace(marker, m);
break;
case 10:
this.slf4jLogger.debug(marker, m);
break;
case 20:
this.slf4jLogger.info(marker, m);
break;
case 30:
this.slf4jLogger.warn(marker, m);
break;
case 40:
this.slf4jLogger.error(marker, m);
}
}
}
}
其實,類似的還有【jcl-over-slf4j】也是起到相同的作用。
例如:把Commons logging,log4j和java.util.logging橋接到SLF4J,底層使用logback的case。其他示例