天天看點

一文詳解Java日志架構JUL

摘要:JUL(Java util logging),Java原生日志架構,不需要引入第三方依賴包,使用簡單友善。

本文分享自華為雲社群《​​Java 日志架構 JUL 詳解大全​​》,作者: 陳皮的JavaLib 。

JUL 簡介

JUL(Java util logging),Java 原生日志架構,不需要引入第三方依賴包,使用簡單友善,一般在小型應用中使用,主流項目中現在很少使用了。

JUL 架構

一文詳解Java日志架構JUL
  • Application:Java 應用程式。
  • Logger:記錄器,Java 應用程式通過調用記錄器的方法來釋出日志記錄。
  • Handler:處理器,每一個 Logger 都可以關聯一個或多個 Handler,Handler 從 Logger 擷取日志并将日志輸出到某個目的地,目的地可以是控制台,本地檔案,或網絡日志服務,或将它們轉發到作業系統日志等等。通過Handler.setLevel(level.off)方法可以禁用一個 Handler,也可以設定其他級别來開啟此 Handler。
  • Filter:過濾器,根據條件過濾哪些日志記錄。每一個 Logger 和 Handler 都可以關聯一個 Filter。
  • Formatter :格式化器,負責對日志事件中的日志記錄進行轉換和格式化。
  • Level:每一條日志記錄都有一個關聯的日志級别,表示此條日志的重要性和緊急程度。也可以對 Logger 和 Handler 設定關聯的日志級别。

入門示例

package com.chenpi;

import java.util.logging.Level;
import java.util.logging.Logger;
import org.junit.Test;

/**
 * @author 陳皮
 * @version 1.0
 * @description
 * @date 2022/3/2
 */
public class ChenPiJULMain {

  // JUL 日志架構示範
  @Test
  public void testLog() {

    // 擷取日志記錄器對象
    Logger logger = Logger.getLogger("com.chenpi.ChenPiJULMain");

    // 日志記錄輸出
    logger.severe(">>>>> Hello ChenPi!!");
    logger.warning(">>>>> Hello ChenPi!!");
    logger.info(">>>>> Hello ChenPi!!"); // 預設日志級别
    logger.config(">>>>> Hello ChenPi!!");
    logger.fine(">>>>> Hello ChenPi!!");
    logger.finer(">>>>> Hello ChenPi!!");
    logger.finest(">>>>> Hello ChenPi!!");

    // 輸出指定日志級别的日志記錄
    logger.log(Level.WARNING, ">>>>> Hello Warning ChenPi!!");

    // 占位符形式
    String name = "ChenPi";
    int age = 18;
    logger.log(Level.INFO, ">>>>> Hello {0},{1} years old!", new Object[]{name, age});

    // 異常堆棧資訊
    logger.log(Level.SEVERE, ">>>>> Hello NPE!", new NullPointerException());

  }
}      

JUL 預設将日志資訊輸出到控制台,預設日志級别是 info。控制台輸出結果如下:

一文詳解Java日志架構JUL

Logger 父子繼承關系

在 JUL 中,Logger 有父子繼承關系概念,會根據 Logger 對象的名稱的包含關系,劃分父子繼承關系。對于某個 Logger 對象,如果找不到顯性建立的父 Logger 對象,那麼它的父 Logger 是根 Logger,即 RootLogger。

子代 Logger 對象會繼承父 Logger 的配置,例如日志級别,關聯的 Handler,日志格式等等。對于任何 Logger 對象,如果沒對其做特殊配置,那麼它最終都會繼承 RootLogger 的配置。

package com.chenpi;

import java.util.logging.Logger;
import org.junit.Test;

/**
 * @author 陳皮
 * @version 1.0
 * @description
 * @date 2022/3/2
 */
public class ChenPiJULMain {

  @Test
  public void testLog() {

    // logger1的父Logger是RootLogger
    Logger logger1 = Logger.getLogger("com.chenpi.a");
    // logger2的父Logger是logger1  
    Logger logger2 = Logger.getLogger("com.chenpi.a.b.c");

    Logger logger1Parent = logger1.getParent();
    System.out.println("logger1Parent:" + logger1Parent + ",name:" + logger1Parent.getName());

    Logger logger2Parent = logger2.getParent();
    System.out.println("logger1:" + logger1 + ",name:" + logger1.getName());
    System.out.println("logger2Parent:" + logger2Parent + ",name:" + logger2Parent.getName());

  }
}

// 輸出結果如下
logger1Parent:java.util.logging.LogManager$RootLogger@61e717c2,name:
logger1:java.util.logging.Logger@66cd51c3,name:com.chenpi.a
logger2Parent:java.util.logging.Logger@66cd51c3,name:com.chenpi.a      

日志配置

我們可以通過2種方式調整 JUL 預設的日志行為(設定日志級别,日志輸出目的地,日志格式等等),一種是通過在程式中寫死形式(不推薦),另一種是通過單獨的配置檔案形式。

寫死日志配置

在 JUL 中,Logger 是有父子繼承關系的,是以當我們需要對某一個 Logger 對象進行單獨的配置時,需要将它設定為不繼承使用父 Logger 的配置。

以下示範名稱為com.chenpi.ChenPiJULMain的 Logger 對象單獨進行配置,并且關聯兩個 Handler。

package com.chenpi;

import java.io.IOException;
import java.util.logging.*;
import org.junit.Test;

/**
 * @author 陳皮
 * @version 1.0
 * @description
 * @date 2022/3/2
 */
public class ChenPiJULMain {

  @Test
  public void testLog() throws IOException {

    // 擷取日志記錄器對象
    Logger logger = Logger.getLogger("com.chenpi.ChenPiJULMain");

    // 關閉預設配置,即不使用父Logger的Handlers
    logger.setUseParentHandlers(false);

    // 設定記錄器的日志級别為ALL
    logger.setLevel(Level.ALL);

    // 日志記錄格式,使用簡單格式轉換對象
    SimpleFormatter simpleFormatter = new SimpleFormatter();

    // 控制台輸出Handler,并且設定日志級别為INFO,日志記錄格式
    ConsoleHandler consoleHandler = new ConsoleHandler();
    consoleHandler.setLevel(Level.INFO);
    consoleHandler.setFormatter(simpleFormatter);

    // 檔案輸出Handler,并且設定日志級别為FINE,日志記錄格式
    FileHandler fileHandler = new FileHandler("./jul.log");
    fileHandler.setLevel(Level.FINE);
    fileHandler.setFormatter(simpleFormatter);

    // 記錄器關聯處理器,即此logger對象的日志資訊輸出到這兩個Handler進行處理
    logger.addHandler(consoleHandler);
    logger.addHandler(fileHandler);

    // 日志記錄輸出
    logger.severe(">>>>> Hello ChenPi!!");
    logger.warning(">>>>> Hello ChenPi!!");
    logger.info(">>>>> Hello ChenPi!!");
    logger.config(">>>>> Hello ChenPi!!");
    logger.fine(">>>>> Hello ChenPi!!");
    logger.finer(">>>>> Hello ChenPi!!");
    logger.finest(">>>>> Hello ChenPi!!");
  }
}      

因為 Logger 設定的日志級别是 ALL,即所有級别的日志記錄都可以通過。但是 ConsoleHandler 設定的日志級别是 INFO,是以控制台隻輸出 INFO 級别以上的日志記錄。而 FileHandler 設定的日志級别是 FINE,是以日志檔案中輸出的是 FINE 級别以上的日志記錄。

以下是控制台輸出的日志結果:

一文詳解Java日志架構JUL

以下是日志檔案jul.log中輸出的日志結果:

一文詳解Java日志架構JUL

日志配置檔案

通過 debug 調試,按以下方法順序,可以發現,如果我們沒有配置 JUL 的配置檔案,系統預設從 JDK 的安裝目錄下的 lib 目錄下讀取預設的配置檔案logging.properties。

getLogger()  -> demandLogger() -> LogManager.getLogManager() -> ensureLogManagerInitialized() -> owner.readPrimordialConfiguration() -> readConfiguration()      
一文詳解Java日志架構JUL
一文詳解Java日志架構JUL

以下是 JDK 自帶的 JUL 預設日志配置檔案内容:

一文詳解Java日志架構JUL

對于配置選項有哪些,其實可以通過相應類的源碼找出,例如 Logger 類的源碼如下:

一文詳解Java日志架構JUL

FileHandler 類的源碼如下:

一文詳解Java日志架構JUL

是以我們可以拷貝預設的配置檔案到我們工程的 resources 目錄下,自定義修改配置資訊。

############################################################
# 全局屬性
############################################################

# 頂級RootLogger關聯的Handler,多個Handler使用逗号隔開
# 對于其他Logger,如果沒有指定自己的Handler,則預設繼承此
handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler

# 預設全局日志級别,Logger和Handler都可以設定自己的日志級别來覆寫此級别
.level= ALL
############################################################
# Handler 配置
############################################################

# FileHandler定義
# 日志檔案存儲位置
java.util.logging.FileHandler.pattern = ./jul%u.log
# 單個檔案的最大位元組數,0代表不限制
java.util.logging.FileHandler.limit = 50000
# 檔案數量上限,多個檔案為jul0.log.0,jul0.log.1 ...
java.util.logging.FileHandler.count = 5
# 日志級别
java.util.logging.FileHandler.level = SEVERE
# 日志追加方式
java.util.logging.FileHandler.append = true
# Handler對象采用的字元集
java.util.logging.FileHandler.encoding = UTF-8
# 日志格式,使用系統預設的簡單格式
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter

# ConsoleHandler定義
# 日志級别
java.util.logging.ConsoleHandler.level = INFO
# Handler對象采用的字元集
java.util.logging.ConsoleHandler.encoding = UTF-8
# 日志格式,使用系統預設的簡單格式
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
############################################################
# Logger 配置
############################################################

# 設定名稱為com.chenpi.person的Logger對象的日志級别為WARNING
com.chenpi.person.level = WARNING      

然後我們應用中加載我們類路徑中自定義的配置檔案。

package com.chenpi;

import java.io.IOException;
import java.io.InputStream;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import org.junit.Test;

/**
 * @author 陳皮
 * @version 1.0
 * @description
 * @date 2022/3/2
 */
public class ChenPiJULMain {

  // JUL 日志架構示範
  @Test
  public void testLog() throws IOException {

    // 讀取配置檔案
    // 也可以通過使用java.util.logging.config.file系統屬性指定檔案名
    // 例如 java -Djava.util.logging.config.file=myfile
    InputStream resourceAsStream = ChenPiJULMain.class.getClassLoader()
        .getResourceAsStream("logging.properties");
    // 擷取LogManager
    LogManager logManager = LogManager.getLogManager();
    // 記載配置檔案
    logManager.readConfiguration(resourceAsStream);

    // 擷取日志記錄器對象
    Logger logger = Logger.getLogger("com.chenpi.ChenPiJULMain");

    // 日志記錄輸出
    logger.severe(">>>>> Hello ChenPi!!");
    logger.warning(">>>>> Hello ChenPi!!");
    logger.info(">>>>> Hello ChenPi!!");
    logger.config(">>>>> Hello ChenPi!!");
    logger.fine(">>>>> Hello ChenPi!!");
    logger.finer(">>>>> Hello ChenPi!!");
    logger.finest(">>>>> Hello ChenPi!!");

    // 擷取日志記錄器對象
    Logger personLogger = Logger.getLogger("com.chenpi.person");

    // 日志記錄輸出
    personLogger.severe(">>>>> Hello Person!!");
    personLogger.warning(">>>>> Hello Person!!");
    personLogger.info(">>>>> Hello Person!!");
    personLogger.config(">>>>> Hello Person!!");
    personLogger.fine(">>>>> Hello Person!!");
    personLogger.finer(">>>>> Hello Person!!");
    personLogger.finest(">>>>> Hello Person!!");
  }
}      

控制台和檔案中輸出内容如下:

一文詳解Java日志架構JUL
一文詳解Java日志架構JUL

自定義 Logger

我們可以針對某一個 Logger 進行單獨的配置,例如日志級别,關聯的 Handler 等,而不預設繼承父級的。

當然我們也可以為以包名為名稱的 Logger 進行配置,這樣這個包名下的所有子代 Logger 都能繼承此配置。

############################################################
# 全局屬性
############################################################

# 頂級RootLogger關聯的Handler,多個Handler使用逗号隔開
# 對于其他Logger,如果沒有指定自己的Handler,則預設繼承此
handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler

# 預設全局日志級别,Logger和Handler都可以設定自己的日志級别來覆寫此級别
.level= ALL
############################################################
# Handler 配置
############################################################

# FileHandler定義
# 日志檔案存儲位置
java.util.logging.FileHandler.pattern = ./jul%u.log
# 單個檔案的最大位元組數,0代表不限制
java.util.logging.FileHandler.limit = 50000
# 檔案數量上限
java.util.logging.FileHandler.count = 5
# 日志級别
java.util.logging.FileHandler.level = SEVERE
# 日志追加方式
java.util.logging.FileHandler.append = true
# Handler對象采用的字元集
java.util.logging.FileHandler.encoding = UTF-8
# 日志格式,使用系統預設的簡單格式
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter

# ConsoleHandler定義
# 日志級别
java.util.logging.ConsoleHandler.level = INFO
# Handler對象采用的字元集
java.util.logging.ConsoleHandler.encoding = UTF-8
# 日志格式,使用系統預設的簡單格式
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
############################################################
# Logger 配置
############################################################

# 設定名稱為com.chenpi.person的Logger對象的日志級别為WARNING
com.chenpi.person.level = WARNING
# 隻關聯FileHandler
com.chenpi.person.handlers = java.util.logging.FileHandler
# 關閉預設配置
com.chenpi.person.useParentHandlers = false
package com.chenpi;

import java.io.IOException;
import java.io.InputStream;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import org.junit.Test;

/**
* @author 陳皮
* @version 1.0
* @description
* @date 2022/3/2
*/
public class ChenPiJULMain {
 
    // JUL 日志架構示範
    @Test
    public void testLog() throws IOException {
 
        // 讀取配置檔案
        InputStream resourceAsStream = ChenPiJULMain.class.getClassLoader()
            .getResourceAsStream("logging.properties");
        // 擷取LogManager
        LogManager logManager = LogManager.getLogManager();
        // 記載配置檔案
        logManager.readConfiguration(resourceAsStream);
 
        // 擷取日志記錄器對象
        Logger logger = Logger.getLogger("com.chenpi.ChenPiJULMain");
 
        // 日志記錄輸出
        logger.severe(">>>>> Hello ChenPi!!");
        logger.warning(">>>>> Hello ChenPi!!");
        logger.info(">>>>> Hello ChenPi!!");
        logger.config(">>>>> Hello ChenPi!!");
        logger.fine(">>>>> Hello ChenPi!!");
        logger.finer(">>>>> Hello ChenPi!!");
        logger.finest(">>>>> Hello ChenPi!!");
 
        // 擷取日志記錄器對象
        Logger personLogger = Logger.getLogger("com.chenpi.person");
 
        // 日志記錄輸出
        personLogger.severe(">>>>> Hello Person!!");
        personLogger.warning(">>>>> Hello Person!!");
        personLogger.info(">>>>> Hello Person!!");
        personLogger.config(">>>>> Hello Person!!");
        personLogger.fine(">>>>> Hello Person!!");
        personLogger.finer(">>>>> Hello Person!!");
        personLogger.finest(">>>>> Hello Person!!");
    }
}      

上述例子中,對于名稱為com.chenpi.person的 Logger,隻将日志輸出到檔案中,其他 Logger 則會同時輸出到控制台和檔案中。

一文詳解Java日志架構JUL
一文詳解Java日志架構JUL
一文詳解Java日志架構JUL

自定義日志格式

以SimpleFormatter類源碼為例,再到LoggingSupport類源碼,發現首先判斷我們是否通過java.util.logging.SimpleFormatter.format屬性配置了格式,如果沒有則使用預設的日志格式。

一文詳解Java日志架構JUL
一文詳解Java日志架構JUL

是以我們可以在配置檔案中自定義日志記錄的格式。

############################################################
# 全局屬性
############################################################

# 頂級RootLogger關聯的Handler,多個Handler使用逗号隔開
# 對于其他Logger,如果沒有指定自己的Handler,則預設繼承此
handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler

# 預設全局日志級别,Logger和Handler都可以設定自己的日志級别來覆寫此級别
.level= ALL
############################################################
# Handler 配置
############################################################

# FileHandler定義
# 日志檔案存儲位置
java.util.logging.FileHandler.pattern = ./jul%u.log
# 單個檔案的最大位元組數,0代表不限制
java.util.logging.FileHandler.limit = 1024
# 檔案數量上限
java.util.logging.FileHandler.count = 5
# 日志級别
java.util.logging.FileHandler.level = FINE
# 日志追加方式
java.util.logging.FileHandler.append = true
# Handler對象采用的字元集
java.util.logging.FileHandler.encoding = UTF-8
# 日志格式,使用系統預設的簡單格式
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# 自定義SimpleFormatter的日志格式
java.util.logging.SimpleFormatter.format = %4$s: %5$s [%1$tc]%n

# ConsoleHandler定義
# 日志級别
java.util.logging.ConsoleHandler.level = INFO
# Handler對象采用的字元集
java.util.logging.ConsoleHandler.encoding = UTF-8
# 日志格式,使用系統預設的簡單格式
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
############################################################
# Logger 配置
############################################################

# 設定名稱為com.chenpi.person的Logger對象的日志級别為WARNING
com.chenpi.person.level = WARNING      

這樣,所有綁定了 SimpleFormatter 的 Handler 的所有日志記錄就使用了自定義的格式。

一文詳解Java日志架構JUL

當然通過源碼發現,日志格式類還有其他幾個實作,如下所示:

一文詳解Java日志架構JUL

日志過濾器

Filter,日志過濾器,用來對輸出的日志記錄進行過濾。我們可以根據多個次元進行過濾,例如隻輸出 message 包含某段文本資訊的日志,隻輸出某個方法中記錄的日志,某個級别的日志等等。

Logger 對象将日志資訊包裝成一個 LogRecord 對象,然後将該對象傳給 Handler 進行處理。每一個 Logger 和 Handler 都可以關聯一個 Filter。LogRecord 中包含了日志的文本資訊、日志生成的時間戳、日志來自于哪個類、日志來自于哪個方法、日志來自于哪個線程等等資訊。

Filter 源碼如下所示,我們隻需要建立一個 Filter 的實作類,重寫方法即可。

package java.util.logging;

/**
* A Filter can be used to provide fine grain control over
* what is logged, beyond the control provided by log levels.
* <p>
* Each Logger and each Handler can have a filter associated with it.
* The Logger or Handler will call the isLoggable method to check
* if a given LogRecord should be published.  If isLoggable returns
* false, the LogRecord will be discarded.
*
* @since 1.4
*/
@FunctionalInterface
public interface Filter {
 
    /**
    * Check if a given log record should be published.
    * @param record  a LogRecord
    * @return true if the log record should be published.
    */
    public boolean isLoggable(LogRecord record);
}      

我們實作一個 Filter ,如果日志消息包含暴力兩個字,則不予放行,即不記錄此條日志。

package com.chenpi;

import java.util.logging.Filter;
import java.util.logging.LogRecord;

/**
 * @author 陳皮
 * @version 1.0
 * @description
 * @date 2022/3/2
 */
public class MyLoggerFilter implements Filter {

  private static final String SENSITIVE_MESSAGE = "暴力";

  @Override
  public boolean isLoggable(LogRecord record) {
    String message = record.getMessage();
    return null == message || !message.contains(SENSITIVE_MESSAGE);
  }
}      
package com.chenpi;

import java.io.IOException;
import java.io.InputStream;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import org.junit.Test;

/**
 * @author 陳皮
 * @version 1.0
 * @description
 * @date 2022/3/2
 */
public class ChenPiJULMain {

  // JUL 日志架構示範
  @Test
  public void testLog() throws IOException {

    // 讀取配置檔案
    InputStream resourceAsStream = ChenPiJULMain.class.getClassLoader()
        .getResourceAsStream("logging.properties");
    // 擷取LogManager
    LogManager logManager = LogManager.getLogManager();
    // 記載配置檔案
    logManager.readConfiguration(resourceAsStream);

    // 擷取日志記錄器對象
    Logger logger = Logger.getLogger("com.chenpi.ChenPiJULMain");
    // Logger關聯過濾器
    logger.setFilter(new MyLoggerFilter());

    // 日志記錄輸出
    logger.info(">>>>> Hello ChenPi!!");
    logger.info(">>>>> 暴力小孩!!");
  }
}

// 輸出結果如下
資訊: >>>>> Hello ChenPi!! [星期三 三月 02 15:31:06 CST 2022]