学习Java日志框架之——搞懂JUL(java.util.logging)

系列文章目录

学习Java日志框架之——搞懂JUL(java.util.logging)
学习Java日志框架之——搞懂log4j
学习Java日志框架之——搞懂日志门面(JCL+SLF4J)
学习日志框架之——搞懂logback
学习日志框架之——log4j2入门
log4j2扩展——打印自定义日志输出格式,将日志输出为json或自定义

一、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.JULTest
    Logger 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(); // 获取LogManager
    SecurityManager 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<Object>() {
                @Override
                public 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 manager
                    owner.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); // 文件中的日志级别为ALL
    handler2.setLevel(Level.CONFIG); // 控制台的日志级别为CONFIG

    logger.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对象的顶层logger
    Logger 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.test
    System.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);

    //儿子做打印,会输出ALL
    logger2.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 penalty
        loadLoggerHandlers(logger, name, name + ".handlers");
        return true;
    } else {
        return false;
    }
}
class LoggerContext {
    // Table of named Loggers that maps names to Loggers.
    private final Hashtable<String,LoggerWeakRef> namedLoggers = new Hashtable<>();
    // Tree of named Loggers
    private final LogNode root;
    private LoggerContext() {
        this.root = new LogNode(null, this);
    }

final class LoggerWeakRef extends WeakReference<Logger> {
    private String                name;       // for namedLoggers cleanup
    private 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();  // save for namedLoggers cleanup
    }

private static class LogNode {
    HashMap<String,LogNode> 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 phase
                if (System.out == null) {
                    return;
                }
                readPrimordialConfiguration = true;

                try {
                    AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
                            @Override
                            public Void run() throws Exception {
                                readConfiguration(); // 读取配置

                                // Platform loggers begin to delegate to java.util.logging.Logger
                                sun.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.properties
        fname = 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= INFO

java.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.XMLFormatter

java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

com.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是用来格式化输出的。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秃了也弱了。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值