https://segmentfault.com/a/1190000005797595

Netty是一個簡化Java NIO編程的網絡框架。就像人要吃飯一樣,框架也要打日志。
Netty不像大多數框架,默認支持某一種日志實現。相反,Netty本身實現了一套日志機制,但這套日志機制並不會真正去打日志。相反,Netty自身的日志機制更像一個日志包裝層。
日志框架檢測順序
Netty在啟動的時候,會自動去檢測當前Java進程的classpath下是否已經有其它的日志框架。
檢查的順序是:
先檢查是否有slf4j,如果沒有則檢查是否有Log4j,如果上面兩個都沒有,則默認使用JDK自帶的日志框架JDK Logging。
JDK的Logging就不用費事去檢測了,直接拿來用了,因為它是JDK自帶的。
注意到雖然Netty支持Common Logging,但在Netty本文所用的4.10.Final版本的代碼里,沒有去檢測Common Logging,即使有支持Common Logging的代碼存在。
日志框架檢測細節
在Netty自身的代碼里面,如果需要打日志,會通過以下代碼來獲得一個logger,以io.netty.bootstrap.Bootstrap這個類為例,讀者可以翻開這個類瞧一瞧。
private static final InternalLogger logger = InternalLoggerFactory.getInstance(Bootstrap.class);
要知道Netty是怎么得到logger的,關鍵就在於這個InternalLoggerFactory類了,可以看出來,所有的logger都是通過這個工廠類產生的。
翻開InternalLoggerFactory類的代碼,可以看到類中有一個靜態初始化塊
private static volatile InternalLoggerFactory defaultFactory; static { final String name = InternalLoggerFactory.class.getName(); InternalLoggerFactory f; try { f = new Slf4JLoggerFactory(true); f.newInstance(name).debug("Using SLF4J as the default logging framework"); defaultFactory = f; } catch (Throwable t1) { try { f = new Log4JLoggerFactory(); f.newInstance(name).debug("Using Log4J as the default logging framework"); } catch (Throwable t2) { f = new JdkLoggerFactory(); f.newInstance(name).debug("Using java.util.logging as the default logging framework"); } } defaultFactory = f; }
Javaer們都知道,類的初始化塊會在類第一次被使用的時候執行。那么什么時候稱之為第一次被使用呢?比如說,靜態方法被調用,靜態變量被訪問,或者調用構造函數。
當調用InternalLoggerFactory.getInstance(Bootstrap.class)之前,上面的靜態塊會被調用,而Netty對於當前應用所使用的日志框架的檢測,就是在這短短的20幾行代碼里面實現。
首先從代碼整體上可以看到,一個try-catch,在catch里面又嵌套了一個try-catch,這正好體現了日志框架的檢測順序:先檢測SLF4J,后檢測Log4J,都沒有的話,就直接使用JDK Logging
檢測SLF4J
在f = new Slf4JLoggerFactory(true);這里開始檢測SLF4J是否存在。
public class Slf4JLoggerFactory extends InternalLoggerFactory { public Slf4JLoggerFactory() { } Slf4JLoggerFactory(boolean failIfNOP) { assert failIfNOP; // Should be always called with true. // SFL4J writes it error messages to System.err. Capture them so that the user does not see such a message on // the console during automatic detection. final StringBuffer buf = new StringBuffer(); final PrintStream err = System.err; try { System.setErr(new PrintStream(new OutputStream() { @Override public void write(int b) { buf.append((char) b); } }, true, "US-ASCII")); } catch (UnsupportedEncodingException e) { throw new Error(e); } try { if (LoggerFactory.getILoggerFactory() instanceof NOPLoggerFactory) { throw new NoClassDefFoundError(buf.toString()); } else { err.print(buf.toString()); err.flush(); } } finally { System.setErr(err); } } @Override public InternalLogger newInstance(String name) { return new Slf4JLogger(LoggerFactory.getLogger(name)); } }
在這里可以看到Slf4JLoggerFactory是InternalLoggerFactory的一個子類實現。
如果應用的classpath下存在slf4j相關的jar包,那么當slf4j的日志框架初始化的時候,如果產生了什么錯誤,將會通過System.err輸出;
對於Netty來講,即使slf4j初始化失敗,它也不願讓用戶看到錯誤輸出,因為對netty來說,slf4j初始化失敗並不代表netty不能選擇其它日志框架;
所以可以從上面代碼中看到,一開始先把System.err給替換掉,讓err輸出被重定向到一個StringBuffer,如下代碼所示:
// SFL4J writes it error messages to System.err. Capture them so that the user does not see such a message on // the console during automatic detection. final StringBuffer buf = new StringBuffer(); final PrintStream err = System.err; try { System.setErr(new PrintStream(new OutputStream() { @Override public void write(int b) { buf.append((char) b); } }, true, "US-ASCII")); } catch (UnsupportedEncodingException e) { throw new Error(e); }
我們已經明白上面這段代碼,就是為了重定向err輸出,不讓用戶輕易看到。接下來看這些代碼:
try { if (LoggerFactory.getILoggerFactory() instanceof NOPLoggerFactory) { throw new NoClassDefFoundError(buf.toString()); } else { err.print(buf.toString()); err.flush(); } } finally { System.setErr(err); }
首先可以看到一個try-finally結構,finally塊里把System.err復位了,也就是說在初始化SLF4J之后,無論發生什么事,都應該把System.err復位。
接下來看try塊里面的代碼:
if (LoggerFactory.getILoggerFactory() instanceof NOPLoggerFactory) { throw new NoClassDefFoundError(buf.toString()); } else { err.print(buf.toString()); err.flush(); }
解釋這些代碼之前,我們先要認識到,SLF4J其實是一個日志門面(facade),它可以充當Log4j, Logback等日志框架的包裝器。因此你的應用除了要有slf4j的依賴包,還要有其它具體的日志實現框架的依賴。例如下面是我的maven依賴,依賴了slf4j還有Logback。
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>${logback.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> <scope>runtime</scope> </dependency>
如果只有slf4j,而沒有logback,那么LoggerFactory.getILoggerFactory() instanceof NOPLoggerFactory就會為true,然后代碼就會拋出NoClassDefFoundError。
如果連slf4j本身都沒有呢?那么運行到LoggerFactory.getLoggerFactory()就已經拋出異常了,因為找不到這個LoggerFactory類。
以上便是檢測SLF4J的整個過程。
檢測Log4J
如果需要檢測Log4J,則說明檢測不到SLF4J的存在,或者是SLF4J不可以使用。
回到InternalLoggerFactory代碼里:
try { f = new Slf4JLoggerFactory(true); f.newInstance(name).debug("Using SLF4J as the default logging framework"); defaultFactory = f; } catch (Throwable t1) { try { f = new Log4JLoggerFactory(); f.newInstance(name).debug("Using Log4J as the default logging framework"); } catch (Throwable t2) { f = new JdkLoggerFactory(); f.newInstance(name).debug("Using java.util.logging as the default logging framework"); } }
Log4J的檢測很簡單,很直接,直接在newInstance()方法里加載org.apache.log4j.Logger;類,如果加載不到,直接拋異常,然后轉而直接使用JDK Logging。
public class Log4JLoggerFactory extends InternalLoggerFactory { @Override public InternalLogger newInstance(String name) { return new Log4JLogger(Logger.getLogger(name)); } }
兼容性
日志級別
Netty的內部日志機制也自定義了日志打印級別,像日志的layout或者appender,則沒有自己定義,完全交給底層的日志框架去做。
public enum InternalLogLevel { /** * 'TRACE' log level. */ TRACE, /** * 'DEBUG' log level. */ DEBUG, /** * 'INFO' log level. */ INFO, /** * 'WARN' log level. */ WARN, /** * 'ERROR' log level. */ ERROR }
這里會面臨日志打印級別的兼容性問題,因為SLF4J,Log4J,以及JDK Logging,都有自己的日志打印級別,比如說JDK Logging,它的日志打印級別是這樣的:
-
SEVERE (highest value)
-
WARNING
-
INFO
-
CONFIG
-
FINE
-
FINER
-
FINEST (lowest value)
不僅數目對不上,而且名稱也沒對上。Netty采用的方式是,按級別的高低來匹配,比如Netty的DEBUG將會對應到JDK的FINE,以此做到級別的對應關系和兼容性。
消息格式化
SLF4J的Logger會有這樣一種打日志的方式,采用占位的方式,舉個例子:、
logger.info("Hello, I m {}, I m the president of {}","Obama","America");
上面這行代碼的日志輸出結果是:
Hello, I m Obama, I m the president of America
可以看到,{}大括號是一個占位符,其內容將會被后面的參數所代替。
但Log4J和JDK Logging並不支持這種占位的日志打印方式,因此Netty又自己搞了一下,讓它的InternalLogger可以以占位的方式格式化日志輸出信息。
詳情可以參考io.netty.util.internal.logging.MessageFormatter這個類,到這里就不再展開了。
