摘要:JUL(Java util logging),Java原生日志架構,不需要引入第三方依賴包,使用簡單友善。
本文分享自華為雲社群《Java 日志架構 JUL 詳解大全》,作者: 陳皮的JavaLib 。
JUL 簡介
JUL(Java util logging),Java 原生日志架構,不需要引入第三方依賴包,使用簡單友善,一般在小型應用中使用,主流項目中現在很少使用了。
JUL 架構
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsQTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cGcq5yNzcTN2UDO0MDO1MGN5cjNyYzXxIDN0QTM2IzLcRDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.jpg)
- 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。控制台輸出結果如下:
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 級别以上的日志記錄。
以下是控制台輸出的日志結果:
以下是日志檔案jul.log中輸出的日志結果:
日志配置檔案
通過 debug 調試,按以下方法順序,可以發現,如果我們沒有配置 JUL 的配置檔案,系統預設從 JDK 的安裝目錄下的 lib 目錄下讀取預設的配置檔案logging.properties。
getLogger() -> demandLogger() -> LogManager.getLogManager() -> ensureLogManagerInitialized() -> owner.readPrimordialConfiguration() -> readConfiguration()
以下是 JDK 自帶的 JUL 預設日志配置檔案内容:
對于配置選項有哪些,其實可以通過相應類的源碼找出,例如 Logger 類的源碼如下:
FileHandler 類的源碼如下:
是以我們可以拷貝預設的配置檔案到我們工程的 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!!");
}
}
控制台和檔案中輸出内容如下:
自定義 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 則會同時輸出到控制台和檔案中。
自定義日志格式
以SimpleFormatter類源碼為例,再到LoggingSupport類源碼,發現首先判斷我們是否通過java.util.logging.SimpleFormatter.format屬性配置了格式,如果沒有則使用預設的日志格式。
是以我們可以在配置檔案中自定義日志記錄的格式。
############################################################
# 全局屬性
############################################################
# 頂級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 的所有日志記錄就使用了自定義的格式。
當然通過源碼發現,日志格式類還有其他幾個實作,如下所示:
日志過濾器
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]