学习Java日志框架之——搞懂JUL(java.util.logging)
创始人
2025-05-31 22:29:36
0

文章目录

  • 一、JUL简介
  • 二、JUL组件介绍
  • 三、代码实例
    • 1、入门案例
    • 2、日志级别
      • (1)默认日志级别源码分析
    • 3、自定义日志级别
    • 4、将日志输出到文件中
    • 5、Logger的父子关系
      • (1)父子关系源码分析
    • 6、使用配置文件
      • (1)默认配置文件位置
      • (2)默认配置文件(去掉注释)
      • (3)配置文件解析
      • (4)自定义读取配置文件
      • (5)自定义日志配置文件
  • 四、总结

一、JUL简介

JUL全称 Java Util Logging,核心类在java.util.logging包下,它是java原生的日志框架,使用时不需要另外引用第三方的类库,相对其他的框架使用方便,学习简单,主要是使用在小型应用中。

二、JUL组件介绍

在这里插入图片描述
Logger:被称为记录器,应用程序通过获取Logger对象,调用其API来发布日志信息。Logger通常被认为是访问日志系统的入口程序。

Handler:处理器,每个Logger都会关联一个或者是一组Handler,Logger会将日志交给关联的Handler去做处理,由Handler负责将日志做记录。Handler具体实现了日志的输出位置,比如可以输出到控制台或者是文件中等等。

Filter:过滤器,根据需要定制哪些信息会被记录,哪些信息会被略过。

Formatter:格式化组件,它负责对日志中的数据和信息进行转换和格式化,所以它决定了我们输出日志最终的形式。

Level:日志的输出级别,每条日志消息都有一个关联的级别。我们根据输出级别的设置,用来展现最终所呈现的日志信息。根据不同的需求,去设置不同的级别。

三、代码实例

本文使用的类,都是java.util.logging包下的类。

1、入门案例

// 入门案例
public static void test01() {// Logger创建方式,参数为当前类全路径字符串com.demo.logger.jul.JULTestLogger logger = Logger.getLogger(JULTest.class.getCanonicalName());/*第一种方式:直接调用日志级别相关的方法,方法中传递日志输出信息假设现在我们要输出info级别的日志信息*/logger.info("输出info信息");/*输出内容:三月 20, 2023 9:09:38 下午 com.demo.logger.jul.JULTest test01信息: 输出info信息*//*第二种方式:调用通用的log方法,然后在里面通过level类型来定义日志的级别参数,以及搭配日志输出信息的参数*/logger.log(Level.WARNING, "输出warning信息");/*输出内容:三月 20, 2023 9:14:09 下午 com.demo.logger.jul.JULTest test01警告: 输出warning信息*/// 动态输出数据,生产日志,使用占位符的方式进行操作String name = "zhangsan";int age = 23;// logger.log(Level.INFO, "姓名:" + name + "年龄:" + age);logger.log(Level.INFO, "姓名:{0}年龄:{1}", new Object[]{name, age});/*{0}和 {1}分别代表第一个占位符和第二个占位符,同时传递一个数组,代表参数的集合输出内容:三月 20, 2023 9:18:16 下午 com.demo.logger.jul.JULTest test01信息: 姓名:zhangsan年龄:23*/
}

2、日志级别

// 日志级别
public static void test02() {/*日志的级别,总共七级Level.SEVERE:(最高级)错误Level.WARNING:警告Level.INFO:(默认级别)消息Level.CONFIG:配置级别Level.FINE:详细信息(少)Level.FINER:详细信息(中)Level.FINEST:(最低级)详细信息(多)两个特殊的级别:Level.OFF;可用来关闭日志记录Level.ALL:启用所有日志记录对于日志的级别,我们重点关注的是new对象时第二个参数,是一个数值:public static final Level OFF = new Level("OFF",Integer.MAX_VALUE, defaultBundle);public static final Level SEVERE = new Level("SEVERE",1000, defaultBundle);public static final Level WARNING = new Level("WARNING", 900, defaultBundle);public static final Level INFO = new Level("INFO", 800, defaultBundle);public static final Level CONFIG = new Level("CONFIG", 700, defaultBundle);public static final Level FINE = new Level("FINE", 500, defaultBundle);public static final Level FINER = new Level("FINER", 400, defaultBundle);public static final Level FINEST = new Level("FINEST", 300, defaultBundle);public static final Level ALL = new Level("ALL", Integer.MIN_VALUE, defaultBundle);这个数值的意义在于:如果我们设置的日志级别是800,那么最终展现的日志信息,比如是数值大于800的所有日志信息。*/Logger logger = Logger.getLogger(JULTest.class.getCanonicalName());logger.severe("severe信息");logger.warning("warning信息");logger.info("info信息");logger.config("config信息");logger.fine("fine信息");logger.finer("finer信息");logger.finest("finest信息");/*输出内容:我们看到,默认是输出info及比info信息级别高的信息三月 20, 2023 9:47:27 下午 com.demo.logger.jul.JULTest test02严重: severe信息三月 20, 2023 9:47:27 下午 com.demo.logger.jul.JULTest test02警告: warning信息三月 20, 2023 9:47:27 下午 com.demo.logger.jul.JULTest test02信息: info信息*/
}

(1)默认日志级别源码分析

我们进入Logger的getLogger方法:

// java.util.logging.Logger#getLogger(java.lang.String)
@CallerSensitive
public static Logger getLogger(String name) {// This method is intentionally not a wrapper around a call// to getLogger(name, resourceBundleName). If it were then// this sequence:////     getLogger("Foo", "resourceBundleForFoo");//     getLogger("Foo");//// would throw an IllegalArgumentException in the second call// because the wrapper would result in an attempt to replace// the existing "resourceBundleForFoo" with null.return demandLogger(name, null, Reflection.getCallerClass());
}// java.util.logging.Logger#demandLogger
private static Logger demandLogger(String name, String resourceBundleName, Class caller) {LogManager manager = LogManager.getLogManager(); // 获取LogManagerSecurityManager sm = System.getSecurityManager();if (sm != null && !SystemLoggerHelper.disableCallerCheck) {if (caller.getClassLoader() == null) {return manager.demandSystemLogger(name, resourceBundleName);}}return manager.demandLogger(name, resourceBundleName, caller);// ends up calling new Logger(name, resourceBundleName, caller)// iff the logger doesn't exist already
}

在初始化LogManager时,初始化了默认的日志级别为Level.INFO:

// java.util.logging.LogManager#ensureLogManagerInitialized
final void ensureLogManagerInitialized() {final LogManager owner = this;if (initializationDone || owner != manager) {// we don't want to do this twice, and we don't want to do// this on private manager instances.return;}// Maybe another thread has called ensureLogManagerInitialized()// before us and is still executing it. If so we will block until// the log manager has finished initialized, then acquire the monitor,// notice that initializationDone is now true and return.// Otherwise - we have come here first! We will acquire the monitor,// see that initializationDone is still false, and perform the// initialization.//synchronized(this) {// If initializedCalled is true it means that we're already in// the process of initializing the LogManager in this thread.// There has been a recursive call to ensureLogManagerInitialized().final boolean isRecursiveInitialization = (initializedCalled == true);assert initializedCalled || !initializationDone: "Initialization can't be done if initialized has not been called!";if (isRecursiveInitialization || initializationDone) {// If isRecursiveInitialization is true it means that we're// already in the process of initializing the LogManager in// this thread. There has been a recursive call to// ensureLogManagerInitialized(). We should not proceed as// it would lead to infinite recursion.//// If initializationDone is true then it means the manager// has finished initializing; just return: we're done.return;}// Calling addLogger below will in turn call requiresDefaultLogger()// which will call ensureLogManagerInitialized().// We use initializedCalled to break the recursion.initializedCalled = true;try {AccessController.doPrivileged(new PrivilegedAction() {@Overridepublic Object run() {assert rootLogger == null;assert initializedCalled && !initializationDone;// Read configuration.owner.readPrimordialConfiguration();// Create and retain Logger for the root of the namespace.owner.rootLogger = owner.new RootLogger();owner.addLogger(owner.rootLogger);// 设置默认的日志级别Level defaultLevel = Level.INFO;if (!owner.rootLogger.isLevelInitialized()) {owner.rootLogger.setLevel(defaultLevel);}// Adding the global Logger.// Do not call Logger.getGlobal() here as this might trigger// subtle inter-dependency issues.@SuppressWarnings("deprecation")final Logger global = Logger.global;// Make sure the global logger will be registered in the// global managerowner.addLogger(global);return null;}});} finally {initializationDone = true;}}
}
 

3、自定义日志级别

JUL对于日志级别的自定义有些麻烦,要先排除掉默认的Handler,然后替换为自定义的Handler。

// 自定义日志级别
public static void test03(){//日志记录器Logger logger = Logger.getLogger(JULTest.class.getCanonicalName());//将默认的日志打印方式关闭掉//参数设置为false,我们打印日志的方式就不会按照父logger默认的方式去进行操作logger.setUseParentHandlers(false);//处理器Handler//在此我们使用的是控制台日志处理器,取得处理器对象ConsoleHandler handler = new ConsoleHandler();//创建日志格式化组件对象SimpleFormatter formatter = new SimpleFormatter();//在处理器中设置输出格式handler.setFormatter(formatter);//在记录器中添加处理器logger.addHandler(handler);//设置日志的打印级别//此处必须将日志记录器和处理器的级别进行统一的设置,才会达到日志显示相应级别的效果//logger.setLevel(Level.CONFIG);//handler.setLevel(Level.CONFIG);logger.setLevel(Level.ALL);handler.setLevel(Level.ALL);logger.severe("severe信息");logger.warning("warning信息");logger.info("info信息");logger.config("config信息");logger.fine("fine信息");logger.finer("finer信息");logger.finest("finest信息");/*输出结果:三月 21, 2023 10:48:40 上午 com.demo.logger.jul.JULTest test03严重: severe信息三月 21, 2023 10:48:40 上午 com.demo.logger.jul.JULTest test03警告: warning信息三月 21, 2023 10:48:40 上午 com.demo.logger.jul.JULTest test03信息: info信息三月 21, 2023 10:48:40 上午 com.demo.logger.jul.JULTest test03配置: config信息三月 21, 2023 10:48:40 上午 com.demo.logger.jul.JULTest test03详细: fine信息三月 21, 2023 10:48:40 上午 com.demo.logger.jul.JULTest test03较详细: finer信息三月 21, 2023 10:48:40 上午 com.demo.logger.jul.JULTest test03非常详细: finest信息*/
}

4、将日志输出到文件中

用户使用Logger来进行日志的记录,Logger可以持有多个处理器Handler(日志的记录使用的是Logger,日志的输出使用的是Handler)

添加了哪些handler对象,就相当于需要根据所添加的handler将日志输出到指定的位置上,例如控制台、文件等

public static void test04() throws IOException {/*将日志输出到具体的磁盘文件中这样做相当于是做了日志的持久化操作*/Logger logger = Logger.getLogger(JULTest.class.getCanonicalName());logger.setUseParentHandlers(false);//文件日志处理器,输出到指定目录下FileHandler handler = new FileHandler("D:\\test\\myLogTest.log");SimpleFormatter formatter = new SimpleFormatter();handler.setFormatter(formatter);logger.addHandler(handler);//也可以同时在控制台和文件中进行打印ConsoleHandler handler2 = new ConsoleHandler();handler2.setFormatter(formatter);logger.addHandler(handler2); //可以在记录器中同时添加多个处理器logger.setLevel(Level.ALL);handler.setLevel(Level.ALL); // 文件中的日志级别为ALLhandler2.setLevel(Level.CONFIG); // 控制台的日志级别为CONFIGlogger.severe("severe信息");logger.warning("warning信息");logger.info("info信息");logger.config("config信息");logger.fine("fine信息");logger.finer("finer信息");logger.finest("finest信息");
}

5、Logger的父子关系

JUL中Logger之间是存在"父子"关系的,值得注意的是,这种父子关系不是我们普遍认为的类之间的继承关系,这种关系是通过树状结构存储的。

public static void test05(){/*从下面创建的两个logger对象看来我们可以认为logger1是logger2的父亲*///父亲是RootLogger,名称默认是一个空的字符串//RootLogger可以被称之为所有logger对象的顶层loggerLogger logger1 = Logger.getLogger("com.demo.test");Logger logger2 = Logger.getLogger("com.demo.test.JULTest");System.out.println(logger2.getParent()==logger1); //true// logger1的父Logger引用为:java.util.logging.LogManager$RootLogger@31ef45e3; 名称为com.demo.test; 父亲的名称为System.out.println("logger1的父Logger引用为:"+logger1.getParent()+"; 名称为"+logger1.getName()+"; 父亲的名称为"+logger1.getParent().getName());// logger2的父Logger引用为:java.util.logging.Logger@598067a5; 名称为com.demo.test.JULTest; 父亲的名称为com.demo.testSystem.out.println("logger2的父Logger引用为:"+logger2.getParent()+"; 名称为"+logger2.getName()+"; 父亲的名称为"+logger2.getParent().getName());/*父亲所做的设置,也能够同时作用于儿子对logger1做日志打印相关的设置,然后我们使用logger2进行日志的打印*///父亲做设置logger1.setUseParentHandlers(false);ConsoleHandler handler = new ConsoleHandler();SimpleFormatter formatter = new SimpleFormatter();handler.setFormatter(formatter);logger1.addHandler(handler);handler.setLevel(Level.ALL);logger1.setLevel(Level.ALL);//儿子做打印,会输出ALLlogger2.severe("severe信息");logger2.warning("warning信息");logger2.info("info信息");logger2.config("config信息");logger2.fine("fine信息");logger2.finer("finer信息");logger2.finest("finest信息");
}

(1)父子关系源码分析

JUL在初始化时会创建一个顶层RootLogger作为所有Logger的父Logger:

//java.util.logging.LogManager#ensureLogManagerInitialized
owner.rootLogger = owner.new RootLogger();
owner.addLogger(owner.rootLogger);
if (!owner.rootLogger.isLevelInitialized()) {owner.rootLogger.setLevel(defaultLevel);
}

RootLogger其实是LogManager的内部类,默认的名称是空字符串。

以上的RootLogger对象作为树状结构的根节点存在的,将来自定义的父子关系通过路径来进行关联,父子关系,同时也是节点之间的挂载关系。
通过owner.addLogger(owner.rootLogger);来进行根节点的挂载。

// java.util.logging.LogManager#addLogger
public boolean addLogger(Logger logger) {final String name = logger.getName();if (name == null) {throw new NullPointerException();}drainLoggerRefQueueBounded();LoggerContext cx = getUserContext(); // 用来保存节点的Map关系if (cx.addLocalLogger(logger)) {// Do we have a per logger handler too?// Note: this will add a 200ms penaltyloadLoggerHandlers(logger, name, name + ".handlers");return true;} else {return false;}
}
class LoggerContext {// Table of named Loggers that maps names to Loggers.private final Hashtable namedLoggers = new Hashtable<>();// Tree of named Loggersprivate final LogNode root;private LoggerContext() {this.root = new LogNode(null, this);}final class LoggerWeakRef extends WeakReference {private String                name;       // for namedLoggers cleanupprivate LogNode               node;       // for loggerRef cleanupprivate WeakReference parentRef;  // for kids cleanupprivate boolean disposed = false;         // avoid calling dispose twiceLoggerWeakRef(Logger logger) {super(logger, loggerRefQueue);name = logger.getName();  // save for namedLoggers cleanup}private static class LogNode {HashMap children;LoggerWeakRef loggerRef;LogNode parent;final LoggerContext context;

由LoggerContext 的存储数据结构我们也可以看出,是存在父子关系的。

6、使用配置文件

(1)默认配置文件位置

如果我们没有自己添加配置文件,则会使用系统默认的配置文件:
在java.util.logging.LogManager#ensureLogManagerInitialized方法中,执行了owner.readPrimordialConfiguration();方法:

// java.util.logging.LogManager#readPrimordialConfiguration
private void readPrimordialConfiguration() {if (!readPrimordialConfiguration) {synchronized (this) {if (!readPrimordialConfiguration) {// If System.in/out/err are null, it's a good// indication that we're still in the// bootstrapping phaseif (System.out == null) {return;}readPrimordialConfiguration = true;try {AccessController.doPrivileged(new PrivilegedExceptionAction() {@Overridepublic Void run() throws Exception {readConfiguration(); // 读取配置// Platform loggers begin to delegate to java.util.logging.Loggersun.util.logging.PlatformLogger.redirectPlatformLoggers();return null;}});} catch (Exception ex) {assert false : "Exception raised while reading logging configuration: " + ex;}}}}
}// java.util.logging.LogManager#readConfiguration()
public void readConfiguration() throws IOException, SecurityException {checkPermission();// if a configuration class is specified, load it and use it.String cname = System.getProperty("java.util.logging.config.class");if (cname != null) {try {// Instantiate the named class.  It is its constructor's// responsibility to initialize the logging configuration, by// calling readConfiguration(InputStream) with a suitable stream.try {Class clz = ClassLoader.getSystemClassLoader().loadClass(cname);clz.newInstance();return;} catch (ClassNotFoundException ex) {Class clz = Thread.currentThread().getContextClassLoader().loadClass(cname);clz.newInstance();return;}} catch (Exception ex) {System.err.println("Logging configuration class \"" + cname + "\" failed");System.err.println("" + ex);// keep going and useful config file.}}String fname = System.getProperty("java.util.logging.config.file");if (fname == null) { // 如果配置为null,就会找java.home --> 找到jre文件夹 --> lib --> logging.propertiesfname = System.getProperty("java.home");if (fname == null) {throw new Error("Can't find java.home ??");}File f = new File(fname, "lib");f = new File(f, "logging.properties");fname = f.getCanonicalPath();}try (final InputStream in = new FileInputStream(fname)) {final BufferedInputStream bin = new BufferedInputStream(in);readConfiguration(bin);}
}

也就是说,如果我们没有指定配置文件的话,JUL也是会读取默认的配置文件,读取的文件是java.homo目录中,jre文件夹下,lib目录中的logging.properties文件。

(2)默认配置文件(去掉注释)

handlers= java.util.logging.ConsoleHandler.level= INFOjava.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatterjava.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormattercom.xyz.foo.level = SEVERE

(3)配置文件解析

# RootLogger使用的处理器,在获取RootLogger对象时进行的设置
# 默认情况下,配置的是控制台处理器,只能在控制台上进行输出操作
# 如果想要其他的处理器,在处理器类后面通过以逗号的形式进行分割
handlers= java.util.logging.ConsoleHandler# 根节点RootLogger的日志级别
# 默认情况下,这是全局的日志级别,如果不手动配置其他的日志级别,则默认输出下述配置的级别及更高的级别
.level= INFO# 文件处理器属性的设置
# 输出日志文件的路径
java.util.logging.FileHandler.pattern = %h/java%u.log
# 输出日志文件的限制(默认50000个字节)
java.util.logging.FileHandler.limit = 50000
# 设置日志文件的数量
java.util.logging.FileHandler.count = 1
# 输出日志的格式
# 默认是以XML的方式输出
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter# 控制台处理器的属性设置
# 控制台输出默认级别
java.util.logging.ConsoleHandler.level = INFO
# 控制台默认输出日志的格式
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter# 也可以将日志级别设定到具体的某个包下
#com.xyz.foo.level = SEVERE

(4)自定义读取配置文件

可以将logging.properties文件稍作修改,验证有效性。

InputStream input = new FileInputStream("E:\\logging.properties");//取得日志管理器对象
LogManager logManager = LogManager.getLogManager();//读取自定义的配置文件
logManager.readConfiguration(input);Logger logger = Logger.getLogger(JULTest.class.getCanonicalName());logger.severe("severe信息");
logger.warning("warning信息");
logger.info("info信息");
logger.config("config信息");
logger.fine("fine信息");
logger.finer("finer信息");
logger.finest("finest信息");

(5)自定义日志配置文件

以下配置可以自定义FileHandler的设置:

# 自定义Logger
com.demo.logger.jul.handlers=java.util.logging.FileHandler
# 自定义Logger日志等级
com.demo.logger.jul.level=CONFIG
# 屏蔽掉父Logger的日志设置
com.demo.logger.jul.useParentHandlers=false

默认使用的是XMLFormatter,我们可以改成容易读的:

java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter

文件默认保存在用户的home目录的java0.log文件。

默认的日志文件是覆盖的方式,我们可以设置为追加的方式:

# 输出日志文件,是否追加
java.util.logging.FileHandler.append=true

四、总结

1.初始化LogManager,加载logging.properties配置文件,将Logger添加到LogManager中。
2.从单例的LogManager获取Logger。

// 构造方法是protected的
protected LogManager() {this(checkSubclassPermissions());
}
// 只能通过静态方法获取唯一实例
public static LogManager getLogManager() {if (manager != null) {manager.ensureLogManagerInitialized();}return manager;
}

3.设置日志级别Level,在打印的过程中使用到了日志记录的LogRecord类。

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

4.Filter作为过滤器提供了日志级别之外更细粒度的控制。
5.Handler日志处理器,决定日志的输出位置,例如控制台、文件…
6.Formatter是用来格式化输出的。

相关内容

热门资讯

用再接再厉造句 用再接再厉造句  再接再厉。接:接战、迎战;厉:磨快,引申为奋勉,努力。指雄鸡相斗,每次交锋以前先磨...
游子的解释及造句 游子的解释及造句  游子拼音  【注音】: you zi  游子解释  【意思】:(yóuzǐ)<书...
恼恨的解释及造句 恼恨的解释及造句  恼恨拼音  【注音】: nao hen  恼恨解释  【意思】:生气和怨恨:我说...
“美妙”造句 51、一条清澈见得的溪流犹如织女那银得发亮的秀发铺在崎岖的山路上,静静地流淌着。那美妙的流水生配上那...
甚至意思和造句 甚至意思和造句  甚至是我们生活中一个常用的词,下面就是小编为您收集整理的甚至意思和造句的相关文章,...
用只不过造句 用只不过造句  造句的坚持训练有助于学生积累大量的写作素材、并且能够降低写作的畏难情绪,以下是小编整...
洛阳纸贵造句 洛阳纸贵造句(精选50句)  造句指懂得并使用字词,按照一定的句法规则造出字词通顺、意思完整、符合逻...
“综治办”造句 1、近日,措美县综治办、法院在措美镇举办了一次别开生面的法律知识竞赛。2、中央综治委副主任、中央政法...
用既也造句 用既也造句  造句指懂得并使用字词,按照一定的句法规则造出字词通顺、意思完整、符合逻辑的句子。依据现...
不可言状造句   不可言状造句  1、无论是决口,还是改道,其造成的灾难都不可言状。  2、恩格斯指出,在当时的德...
胡扯解释造句 胡扯解释造句  胡扯拼音  【注音】: hu che  胡扯解释  【意思】:闲谈;瞎说。  胡扯造...
露面拼音解释及造句 露面拼音解释及造句  露面拼音  【注音】: lu mian  露面解释  【意思】:(~儿)显露出...
词语过渡怎么造句 词语过渡怎么造句  过渡拼音  【注音】: guo du  过渡解释  【意思】:事物由一个阶段逐渐...
好像的造句 好像的造句(精选70句)  引导语:造句是一件非常有意思的事情,亦有很多人造过句,那么使用“好像”要...
cinderella什么意思...   cinderella你知道怎么解释吗? cinderella可以如何造句呢?请阅读以下文章,跟着...
惊诧的解释及造句 惊诧的解释及造句  【注音】: jing cha  【意思】:惊讶诧异:这是意料中的事,我们并不感到...
“日新月异”造句 1、写诗贵在能够灵活,灵活才能日新月异。2、那最神圣恒久而又日新月异的,那最使我们感到惊奇和震撼的两...
“急中生智”造句 1、 突遇危险,他急中生智化险为夷了。2、 大火马上就要烧进来了,艳艳急中生智,披着一条沾湿的被子冲...
一知半解的意思及造句 一知半解的意思及造句  【一知半解的拼音】:  yī zhī bàn jiě  【一知半解的意思】:...
“囟门”造句 1、 结论前囟门皮样囊肿是良性先天性发育异常疾病,诊断明确后手术治疗效果好。2、 小儿囟门未闭合时,...