前言
當我們在使用日誌框架的時候,每個類都要通過工廠方法獲取一個日誌對象來打印日誌,感覺太麻煩了。所以想着去封裝一個日誌靜態類。但是問題是封裝的那個靜態日誌類打印出的類信息都是日誌類自己,這肯定不是我們想要的啊,我們需要的是當前調用打印日誌的這個類的信息啊。
開始思考
我們發現平時使用的Logback等日誌框架能準確捕獲源代碼的所在的類、方法、行。但java並沒有提供相應的方法,這似乎很神奇。其實Logback是通過java錯誤堆棧來實現的,也就是說通過new一個異常Throwable,然後再捕獲,從而得到堆棧信息,在進行分析就可以得到行號等信息了。
我們可以借鑑這種思想,在靜態日誌類中,獲取當前線程的堆棧信息,然後找到實際調用者。JDK的Thread類提供了一個getStackTrace()可以獲取線程的堆棧跟蹤鏈。下面來看一下這個方法。
/**
* 返回一個表示該線程堆棧轉儲的堆棧跟蹤元素數組。如果該線程尚未啓動或已經終止,則該方法將返回一個零長度數組。
* 如果返回的數組不是零長度的,則其第一個元素代表堆棧頂,它是該序列中最新的方法調用。最後一個元素代表堆棧底,是該序列中最舊的方法調用。
*/
public StackTraceElement[] getStackTrace() {
// 如果該線程不是當前線程
if (this != Thread.currentThread()) {
// 獲取安全管理器
SecurityManager security = System.getSecurityManager();
// 如果有安全管理器
if (security != null) {
// 檢查是否有權限獲取堆棧跟蹤
security.checkPermission(SecurityConstants.GET_STACK_TRACE_PERMISSION);
}
// 如果線程未啓動或已終止,則不會獲取堆棧跟蹤信息,直接返回一個空堆棧
if (!isAlive()) {
return EMPTY_STACK_TRACE;
}
StackTraceElement[][] stackTraceArray = dumpThreads(new Thread[] {this});
StackTraceElement[] stackTrace = stackTraceArray[0];
// 可能線程剛剛終止,沒有獲取到堆棧跟蹤,也返回一個空堆棧
if (stackTrace == null) {
stackTrace = EMPTY_STACK_TRACE;
}
return stackTrace;
} else {
// new一個Exception,然後捕獲當前線程的堆棧信息並返回
return (new Exception()).getStackTrace();
}
}
那麼堆棧信息時啥樣的呢,下面打印一個看看。
java.lang.Thread.getStackTrace(Thread.java:1559)
util.Logger.getCaller(Logger.java:20)
util.Logger.log(Logger.java:60)
util.Logger.info(Logger.java:99)
byteBuf.test1.main(test1.java:36)
這是調用靜態日誌的方法得到的堆棧信息。可以看到日誌類的堆棧信息的後面緊跟着的就是調用日誌方法的類。要拿到的就是這個調用類,然後通過這個類名構造一個日誌對象,真正的日誌打印是通過這個對象來執行的。
靜態日誌類
public class Logger {
// 當前日誌類名
private final static String logClassName = Logger.class.getName();
/**
* 獲取最原始被調用的堆棧信息
*/
private static StackTraceElement getCaller() {
// 獲取堆棧信息
StackTraceElement[] traceElements = Thread.currentThread()
.getStackTrace();
if (null == traceElements) {
return null;
}
// 最原始被調用的堆棧信息
StackTraceElement caller = null;
// 循環遍歷到日誌類標識
boolean isEachLogFlag = false;
// 遍歷堆棧信息,獲取出最原始被調用的方法信息
// 當前日誌類的堆棧信息完了就是調用該日誌類對象信息
for (StackTraceElement element : traceElements) {
// 遍歷到日誌類
if (element.getClassName().equals(logClassName)) {
isEachLogFlag = true;
}
// 下一個非日誌類的堆棧,就是最原始被調用的方法
if (isEachLogFlag) {
if (!element.getClassName().equals(logClassName)) {
caller = element;
break;
}
}
}
return caller;
}
/**
* 自動匹配請求類名,生成logger對象
*/
private static org.slf4j.Logger log() {
// 最原始被調用的堆棧對象
StackTraceElement caller = getCaller();
// 空堆棧處理
if (caller == null) {
return LoggerFactory.getLogger(Logger.class);
}
// 取出被調用對象的類名,並構造一個Logger對象返回
return LoggerFactory.getLogger(caller.getClassName());
}
// 下列是封裝後的方法
// 省略其它方法的封裝
public static void info(String message) {
log().info(message);
}
}