Java Util Log

Java Util Log

從需求的角度出發(fā), 漸進(jìn)的理解 Java Util Log 源碼
基于 JDK 1.8 源碼
Java Util tutorial: http://tutorials.jenkov.com/java-logging/index.html

在 JUL 中有以下主要對象

  • Logger 面向用戶獲取日志的接口
  • LoggerManager 負(fù)責(zé)管理所有 logger
  • Handler 負(fù)責(zé)最終日志的輸出, 例如 ConsoleHandler 輸出到終端, FileHandler 輸出到文件
  • Formatter 用于格式化日志內(nèi)容, 例如添加時間戳, 輸出包名等
  • Level 日志級別

最簡單的日志方式莫過于使用 System.out.println("debug log"); 的方式來輸出調(diào)試信息.

Logger logger = Logger.getLogger("TestLogger-" + random.nextInt());
logger.log(Level.INFO, "This is a test log.");

Logger

Logger 的創(chuàng)建

每個 Logger 對象在創(chuàng)建的時候都需要一個名稱, 這個名稱主要用在 logging.properties 中, 針對不同日志設(shè)置不同的輸出級別, 實現(xiàn)日志信息的過濾. 也可以指定 handler, 來輸出到不同的文件中去. 對于沒有上述需要的, 通常的做法是使用當(dāng)前類的名稱作為 logger name.

Logger logger = Logger.getLogger(JavaUtilLog.class.getName());

例如在 Web 服務(wù)中, HTTP 請求日志的級別設(shè)置為 Level.INFO, 應(yīng)用程序的日志級別設(shè)置為 Level.WARNING, 數(shù)據(jù)庫的日志級別設(shè)置為 Level.SEVERE. 將應(yīng)用程序的日志輸出到 app.log 文件中.

// In Java code
Logger rstLogger = Logger.getLogger("LOG_REQUEST");
Logger dbLogger = Logger.getLogger("LOG_DATABASE");
Logger appLogger = Logger.getLogger("LOG_APP");
############################################################
#   Custom Logging Configuration File
#   -Djava.util.logging.config.file=applogging.properties

LOG_REQUEST.level= INFO
LOG_DATABASE.level= SEVERE
LOG_APP.level= WARNING
LOG_APP.handlers = java.util.logging.FileHandler

java.util.logging.FileHandler.pattern = app.log
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter

額外提一點, 如果需要將 HTTP 請求的日志輸出到 request.log 文件中, 如何實現(xiàn)呢? JUL 其實是不支持這種需求的, 不過可以通過繼承 FileHandler 實現(xiàn)一個 RequestFileHandler 然后添加到配置文件中.

日志的輸出

Logger 中會構(gòu)建一個 LogRecord 對象來包裝日志內(nèi)容, 之后遍歷所有可用的 handler, 調(diào)用 Handler.publish(LogRecord record) 由 handler 負(fù)責(zé)寫入操作.

日志內(nèi)容的原子性

Logger 日志輸出的相關(guān)函數(shù)中都沒有使用 synchronized 或其他鎖, 在多個線程都獲得同一個 logger 對象時, 同時在寫日志, 它時如何保證線程安全的, 換句話說如何保證多條日志信息不會互相影響, 每一條日志都能完整的輸出?

public void log(Level level, String msg) {
  // 日志過濾
  if (!isLoggable(level)) {
    return;
  }
  LogRecord lr = new LogRecord(level, msg);
  doLog(lr);
}

private void doLog(LogRecord lr) {
  // 填充 LogRecord
  lr.setLoggerName(name);
  // 此處 final 是為了線程安全考慮
  final Logger.LoggerBundle lb = getEffectiveLoggerBundle();
  final ResourceBundle bundle = lb.userBundle;
  final String ebname = lb.resourceBundleName;
  if (ebname != null && bundle != null) {
    lr.setResourceBundleName(ebname);
    lr.setResourceBundle(bundle);
  }
  log(lr);
}

public void log(LogRecord record) {
  // ...
  // 查找可用的 handler
  // 如果當(dāng)前 logger 上沒有掛載 handler, 則去父logger上查找
  Logger logger = this;
  while (logger != null) {
    final Handler[] loggerHandlers = isSystemLogger
        ? logger.accessCheckedHandlers()
        : logger.getHandlers();

    for (Handler handler : loggerHandlers) {
      handler.publish(record);
    }

    final boolean useParentHdls = isSystemLogger
        ? logger.config.useParentHandlers
        : logger.getUseParentHandlers();

    if (!useParentHdls) {
      break;
    }

    logger = isSystemLogger ? logger.parent : logger.getParent();
  }
}

通過代碼可以看出來, LogRecord 是作為一個原子集, 在 Logger 中填充好日志相關(guān)的信息, 然后將它對象傳遞給 handler, 由 handler 來保證一個 LogRecord 能夠完整的輸出.

handler 線程安全

Logger 上 handler 的添加和刪除操作都沒有加鎖, 如何保證在遍歷 handler 時, 當(dāng)前 handler 的數(shù)量保持不變? 例如線程A正在遍歷 handler 來輸出日志, 線程B此時刪除了中間某個 handler, 如何避免線程A拋出 java.util.ConcurrentModificationException 的異常

handler 存儲在 Logger.ConfigurationData 類中, 它是一個 CopyOnWriteArrayList 的 List 對象, 所以 Logger.addHandler() 和 Logger.removeHandler() 可以保持線程安全. 在遍歷 handler 輸出日志時, 會返回一個 Handler[], 這個數(shù)組不是 CopyOnWriteArrayList 中實際存儲的數(shù)組的包裝引用, 而是重新拷貝了一份. 所以線程B的刪除操作不會影響到線程A.

LogManager

LogManager 主要負(fù)責(zé)所有 Logger 的創(chuàng)建, 管理和銷毀. LogManager 的構(gòu)造函數(shù)是 private, LogManager 中定了一個全局的對象, 后續(xù)所有的操作其實都是在操作這一個對象.

Logger 的創(chuàng)建

Logger.getLogger() 實際調(diào)用的是 LogManager.demandLogger() 方法. 首先查找是否已經(jīng)存在名稱是 name 的 logger, 如果不存在則創(chuàng)建一個新的 logger 加入到 logger 列表.

Logger demandLogger(String name, String resourceBundleName, Class<?> caller) {
  Logger result = getLogger(name);
  if (result == null) {
    Logger newLogger = new Logger(name, resourceBundleName, caller, this, false);
    do {
      if (addLogger(newLogger)) {
        return newLogger;
      }

      // 因為方法中并沒有任何的加鎖操作, 所以在判斷完 result == null 之后, addLogger 之前
      // 可能有其他的線程已經(jīng)添加了同名的 logger, 我們這里的 addLogger 將會返回 false,
      // 所以在 addLogger 返回 false 之后我們需要調(diào)用 getLogger 嘗試獲取那個同名的 logger.
      //
      // 既然已經(jīng)有其他線程已經(jīng)創(chuàng)建并添加了同名的 logger, 為什么 getLogger 會有可能返回 null呢?
      // 因為 LogManager 在管理 logger 時使用的是弱引用(WeakReference), 如果那個已經(jīng)添加的 logger
      // 沒有被強(qiáng)引用, 在 addLogger 返回 false 之后 JVM 恰好執(zhí)行了一個 GC 操作, 那個沒有強(qiáng)引用的
      // logger 會被系統(tǒng)回收掉, 所以 getLogger 得到的是一個 null. 這就是這里 do-while 存在的意義.
      result = getLogger(name);
    } while (result == null);
  }
  return result;
}

Logger 的生命周期

LogManager 上有一個 userContext 的成員變量, 所有的 logger 實際都在保存在這個變量上. namedLoggers 的 value 實際是一個 logger 的弱引用.

// class LogManager {
private final LoggerContext userContext = new LoggerContext();
class LoggerContext {
    // Table of named Loggers that maps names to Loggers.
    private final ConcurrentHashMap<String,LoggerWeakRef> namedLoggers =
            new ConcurrentHashMap<>();
}

final class LoggerWeakRef extends WeakReference<Logger> {
  private String name;  // for namedLoggers cleanup
  private LogManager.LogNode node;       // for loggerRef cleanup
  private WeakReference<Logger> parentRef;  // for kids cleanup
  private boolean disposed = false;         // avoid calling dispose twice

  LoggerWeakRef(Logger logger) {
    super(logger, loggerRefQueue);
    name = logger.getName();
  }

  void dispose() {
    // ...
  }
}

對于弱引用通常會有這幾個疑問:

  1. 如何知道一個 logger 已經(jīng)被回收了?
  2. logger 被回收之后, 清理工作什么時候進(jìn)行?
  3. logger 上綁定的 handler 什么時候關(guān)閉?

如果一個 logger 對象已經(jīng)被回收了, LogManager 并不能第一時間知道, 而是在每次調(diào)用 getLogger()addLogger() 的時候才知道的, 而這兩個方法是在 demandLogger() 被調(diào)用.

LogManager.demandLogger() 實際調(diào)用的是 LoggerContext.findLogger(String) 方法. LogManager.addLogger() 實際調(diào)用的是 LoggerContext.addLocalLogger(Logger, boolean) 方法.

// class LoggerContext {
Logger findLogger(String name) {

  // ...

  synchronized (this) {
    // 確保這個 context 已經(jīng)初始化過了.
    ensureInitialized();
    LogManager.LoggerWeakRef ref = namedLoggers.get(name);
    if (ref == null) {
      // 在 LoggerWeakRef.dispose() 方法中清理 namedLoggers
      // 此處不需要清理
      return null;
    }
    logger = ref.get();
    if (logger == null) {
      // namedLoggers 中保存的弱引用 logger 已經(jīng)被 GC 回收了
      // 此時調(diào)用 LoggerWeakRef.dispose() 執(zhí)行清理操作.
      ref.dispose();
    }
    return logger;
  }
}

synchronized boolean addLocalLogger(Logger logger, boolean addDefaultLoggersIfNeeded) {

  // ...

  LogManager.LoggerWeakRef ref = namedLoggers.get(name);
  if (ref != null) {
    if (ref.get() == null) {
      // It's possible that the Logger was GC'ed after a
      // drainLoggerRefQueueBounded() call above so allow
      // a new one to be registered.
      ref.dispose();
    } else {
      // We already have a registered logger with the given name.
      return false;
    }
  }

  // ...

}

handler 只由 Logger 對象持有, 當(dāng) logger 對象銷毀后 handler 并未關(guān)閉. LogManager 的構(gòu)造函數(shù)中注冊了一個 JVM 退出的鉤子, 最終調(diào)用 LogManager.resetLogger(Logger) 來執(zhí)行 handler 的清理操作. 如果在代碼中給某個 logger 添加了自定義的 handler, 記得保持這個 handler 的強(qiáng)應(yīng)用, 并在合適的時候手動調(diào)用 Handler.close() 關(guān)閉它.

// class LogManager {
private LogManager(Void checked) {
  // Add a shutdown hook to close the global handlers.
  try {
    Runtime.getRuntime().addShutdownHook(new Cleaner());
  } catch (IllegalStateException e) {
    // If the VM is already shutting down,
    // We do not need to register shutdownHook.
  }
}

private void resetLogger(java.util.logging.Logger logger) {
  // Close all the Logger's handlers.
  Handler[] targets = logger.getHandlers();
  for (int i = 0; i < targets.length; i++) {
    Handler h = targets[i];
    logger.removeHandler(h);
    try {
      h.close();
    } catch (Exception ex) {
      // Problems closing a handler?  Keep going...
    }
  }
  
  // ...

}

handler

handler 負(fù)責(zé)將 logger 傳遞過來的 LogRecord 輸出到具體的介質(zhì)中. JDK 默認(rèn)提供了 ConsoleHandler , FileHandlerSocketHandler 等. handler 需要保證一條 LogRecord 在多線程的情況下能夠完整輸出, 不會被其他線程擾亂.

handler 是由 logger 調(diào)用過來的, 所以不能在 handler 中調(diào)用 logger, 這樣會造成調(diào)用死循環(huán), 導(dǎo)致棧溢出.

handler 會持有一個類型為 ErrorManager 的成員變量, handler 中任何有需要寫日志的地方, 通過它來輸出.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锐墙,一起剝皮案震驚了整個濱河市垃瞧,隨后出現(xiàn)的幾起案子则披,更是在濱河造成了極大的恐慌芹壕,老刑警劉巖具钥,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異称开,居然都是意外死亡皮获,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門曹步,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宪彩,“玉大人,你說我怎么就攤上這事讲婚∧蚩祝” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長活合。 經(jīng)常有香客問我雏婶,道長,這世上最難降的妖魔是什么白指? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任留晚,我火速辦了婚禮,結(jié)果婚禮上告嘲,老公的妹妹穿的比我還像新娘错维。我一直安慰自己,他們只是感情好橄唬,可當(dāng)我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布赋焕。 她就那樣靜靜地躺著,像睡著了一般仰楚。 火紅的嫁衣襯著肌膚如雪隆判。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天僧界,我揣著相機(jī)與錄音侨嘀,去河邊找鬼。 笑死捂襟,一個胖子當(dāng)著我的面吹牛咬腕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播葬荷,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼郎汪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了闯狱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤抛计,失蹤者是張志新(化名)和其女友劉穎哄孤,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吹截,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡瘦陈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了波俄。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晨逝。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖懦铺,靈堂內(nèi)的尸體忽然破棺而出捉貌,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布趁窃,位于F島的核電站牧挣,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏醒陆。R本人自食惡果不足惜瀑构,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望刨摩。 院中可真熱鬧寺晌,春花似錦、人聲如沸澡刹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽像屋。三九已至怕犁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間己莺,已是汗流浹背奏甫。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留凌受,地道東北人阵子。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像胜蛉,于是被迫代替她去往敵國和親挠进。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,044評論 2 355