日志是進行軟件開發必不可少的一項功能,目前流行着很多開源日志庫,比如log4j、log4j2、logback、JDK Logging、commons-logging、slf4j等。
幾種日志產品的介紹
JDK Logging:Java標准庫內置的日志包 java.util.logging,以下簡稱jul。
log4j:一種非常流行的日志框架,最新版本是2.x。
commons-logging:簡稱jcl,它是一個第三方的日志庫,由Apache創建的日志模塊。特點是可以掛接不同的日志系統,可以根據配置文件指定掛接的日志系統。默認情況下,jcl自動搜索並使用log4j,如果過沒找到log4j,再使用JDK Logging。
前面介紹了Commons Logging和Log4j,它們一個負責充當日志API,一個負責實現日志底層,搭配使用非常便於開發。還有SLF4J和Logback,其實SLF4J類似於Commons Logging,也是一個日志接口,而Logback類似於Log4j,是一個日志的實現。
為什么有了Commons Logging和Log4j,又會蹦出來SLF4J和Logback?這是因為Java有着非常悠久的開源歷史,不但OpenJDK本身是開源的,而且我們用到的第三方庫,幾乎全部都是開源的。開源生態豐富的一個特定就是,同一個功能,可以找到若干種互相競爭的開源庫。因為對Commons Logging的接口不滿意,有人就搞了SLF4J。因為對Log4j的性能不滿意,有人就搞了Logback。
總結起來,幾個日志產品的關系如圖所示
spring日志測試
spring 4.x 及以前版本基本采用jcl,擴展機制根據用戶手動依賴的日志產品進行掛接,改變spring默認日志框架,有興趣的童鞋可以看下spring 4.x版本的源碼。本文基於spring 5.x 版本。
書寫了一個小demo進行spring的默認日志測試,pom文件中只添加了spring上下文環境依賴
1 <dependency> 2 <groupId>org.springframework</groupId> 3 <artifactId>spring-context</artifactId> 4 <version>5.0.4.RELEASE</version> 5 </dependency>
運行以下spring上下文初始化代碼,
1 public class SpringLogTest { 2 public static void main(String[] args) { 3 AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(BeanConfig.class); 4 } 5 }
console打印以下日志
十一月 27, 2019 4:38:09 下午 org.springframework.context.support.AbstractApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@51081592: startup date [Wed Nov 27 16:37:30 CST 2019]; root of context hierarchy
spring日志源碼分析
當然,以上測試中,日志的輸出內容並不重要,重要的是spring是如何選擇logging類型,並進行打印的。
打印上述日志的地方,是在AbstractApplicationContext類(AnnotationConfigApplicationContext繼承自它)的prepareRefresh方法,該方法在AnnotationConfigApplicationContext初始化的時候執行。
1 protected void prepareRefresh() { 2 this.startupDate = System.currentTimeMillis(); 3 this.closed.set(false); 4 this.active.set(true); 5 6 if (logger.isInfoEnabled()) { 7 logger.info("Refreshing " + this); 8 }
這段代碼里面有個logger是關鍵,該logger是在AbstractApplicationContext中的定義的一個屬性:
1 protected final Log logger = LogFactory.getLog(getClass());
那我們繼續看LogFactory類,會發現該類是在org.apache.commons.logging包下面,但是仔細查看spring包結構,發現該包位於spring-jcl目錄下(這是與4.x不同的地方,5.x系列spring開發團隊改寫了commons-logging的內部源碼)
在LogFactory類中,getLog方法是一個門面方法,調用了重載的getFactory方法。
1 public abstract class LogFactory { 2 private static LogFactory.LogApi logApi; //默認的 3 4 public LogFactory() { 5 } 6 7 public static Log getLog(Class<?> clazz) { 8 return getLog(clazz.getName()); 9 } 10 //根據logApi的值選擇logger類型 11 public static Log getLog(String name) { 12 switch(logApi) { 13 case LOG4J: 14 return LogFactory.Log4jDelegate.createLog(name); 15 case SLF4J_LAL: 16 return LogFactory.Slf4jDelegate.createLocationAwareLog(name); 17 case SLF4J: 18 return LogFactory.Slf4jDelegate.createLog(name); 19 default: 20 return LogFactory.JavaUtilDelegate.createLog(name); 21 } 22 } 23 ......部分不太重要的代碼,此處忽略42 43 static {
//logApi的默認值 44 logApi = LogFactory.LogApi.JUL; 45 ClassLoader cl = LogFactory.class.getClassLoader(); 46 //根據load的情況,動態更改logApi的值 47 try { 48 cl.loadClass("org.apache.logging.log4j.spi.ExtendedLogger"); 49 logApi = LogFactory.LogApi.LOG4J; 50 } catch (ClassNotFoundException var6) { 51 try { 52 cl.loadClass("org.slf4j.spi.LocationAwareLogger"); 53 logApi = LogFactory.LogApi.SLF4J_LAL; 54 } catch (ClassNotFoundException var5) { 55 try { 56 cl.loadClass("org.slf4j.Logger"); 57 logApi = LogFactory.LogApi.SLF4J; 58 } catch (ClassNotFoundException var4) { 59 ; 60 } 61 } 62 } 63 64 }
在重載的getLog方法中,會根據一個變量logApi的值返回不同的日志對象,logApi的值初始化為jul;
在目前的場景下,代碼會返回一個JDK Logging logger進行打印信息,也就是以上的getLog方法中的default情況。
那么,現在問題來了,如果想改變spring的輸出日志類型,如何修改?
從上述的代碼看,返回logger對象是根據logApi的值的,但是spring在此處並未提供可供用戶修改logApi值的方法或者接口。細看LogFactory中有一個static的靜態代碼塊,在該代碼塊執行的時候會去load相關的日志包產品,根據加載到的情況去動態的修改logApi的值,從而可以達到用戶自選擇logger類型的目的。
那么我如果要改變spring的輸出是log4j打印的消息,那么可以考慮采用slf4j的橋接方式,在pom中加入如下依賴
1 <dependency> 2 <groupId>org.slf4j</groupId> 3 <artifactId>slf4j-api</artifactId> 4 <version>1.7.25</version> 5 </dependency> 6 <dependency> 7 <groupId>org.slf4j</groupId> 8 <artifactId>slf4j-log4j12</artifactId> 9 <version>1.7.28</version> 10 </dependency>
運行代碼會發現此處打印的日志和之前默認的不同
INFO - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@3e57cd70: startup date [Wed Nov 27 17:26:47 CST 2019]; root of context hierarchy
spring 5.x 日志的基本功能和核心源碼基本說明完了,可能有不太准確的地方,希望各位大神給我留言討論交流。