likes
comments
collection
share

java.util.logging.LogManager

作者站长头像
站长
· 阅读数 8

本文章中涉及到的所有代码都已经上传到gitee仓库中:

gitee.com/sss123a/log…

Hello jul!

不需要引入任何依赖,直接上DEMO:

package com.matio.log.helloworld; 

import java.util.logging.Logger;

public class HelloWorld {  
    public static void main(String[] args) {  
        System.out.println("This is out!");  
        System.err.println("This is err!");  
        Logger.getLogger("X").info("Hello world!");  
    }  
}

打印结果如下: java.util.logging.LogManager

看到这样的打印结果,不知道大家是不是跟笔者一样好奇这样几个问题:

1.为什么打印格式长这样?能不能自己定制?

2.为什么使用System.err打印?

jul默认的配置文件是  ${java.home}/lib/logging.properties,对应笔者电脑上的C:\Program Files\Java\jdk1.8.0_271\jre\lib\logging.properties文件

jul各个组件详解

java.util.logging.LogManager

  1. Logger:记录器。应用程序通过获取 Logger 对象,调用其 API 来发布日志信息。Logger 对象通常是应用程序访问日志系统的入口程序;
  2. Level:每条日志消息都有一个关联的日志级别。该级别粗略指导了日志消息的重要性和紧迫;
  3. Formatter:它负责对日志事件中的数据进行转换和格式化。Formatter决定了数据在一条日志记录中的最终形式;
  4. Handler:每个 Logger都可以关联n个Handler。Logger会将日志交给关联的所有Handler处理,由它们负责输出日志,其具体的实现决定了日志记录的位置可以是控制台、文件等;
  5. Filter:过滤器。根据需要定制哪些消息会被记录,哪些消息会被放过。

Level

Level 类定义了一组可用来控制日志输出的标准日志级别。日志 Level 对象是有序的,并且是通过有序的整数来指定。在给定的级别上启用日志记录也就启用了所有较高级别的日志记录。

Jul 默认级别是 info,只要日志级别高于 info,都会输出

日志级别从高至低

  1. OFF 可用于关闭日志记录的特殊级别
  2. SEVERE 指示严重失败的消息级别 (highest value)
  3. WARNING 指示潜在问题的消息级别
  4. INFO 报告消息的消息级别
  5. CONFIG 用于静态配置消息的消息级别
  6. FINE 提供跟踪信息的消息级别
  7. FINER 指示一条相当详细的跟踪消息
  8. FINEST 指示一条最详细的跟踪消息 (lowest value)
  9. ALL 记录所有消息

如果是:off,那么就是关闭了日志级别输出;

如果是:all,那么就是开启了日志级别输出。

jul默认是没有提供ERROR级别的,当然我们可以自己定义

自定义Level

package com.matio.log.level;  
  
import java.util.logging.Level;  
import java.util.logging.Logger;  
  
public class CusLevelTest {  
    private static final ErrorLevel ERROR = new ErrorLevel();  

    public static void main(String[] args) {  
        Logger logger = Logger.getLogger(CusLevelTest.class.getName());  
        logger.log(ERROR, "123");  
        logger.info("info");  
    }  

    private static class ErrorLevel extends Level {  

        protected ErrorLevel() {  
            super("ERROR", 5000, "com.matio.log.level.logging");  
        }  
    }  
}

package com.matio.log.level;  
  
import java.util.ListResourceBundle;  
  
public final class logging extends ListResourceBundle {  
    public logging() {  
    }  

    protected final Object[][] getContents() {  
        return new Object[][]{{"ERROR", "错误XXX"}};  
    }  
}

打印结果如下:

三月 14, 2024 11:15:23 上午 com.matio.log.level.CusLevelTest main
错误XXX: 123
三月 14, 2024 11:15:23 上午 com.matio.log.level.CusLevelTest main
信息: info

Formatter

按照指定的格式去格式化我们的日志。jul默认提供了string和xml两种格式的输出

一般来说,每个日志记录 Handler 都有关联的 Formatter。Formatter 接受一个 LogRecord参数,并将它转换为一个字符串,这个字符串就是我们最终要打印的数据。

有些 formatter(如 XMLFormatter)需要围绕一组格式化记录来包装头部和尾部字符串。可以使用 getHeader 和 getTail 方法来获得这些字符串

java.util.logging.LogManager

logger.info("测试日志内容");

SimpleFormatter

jul默认使用这种formatter进行日志打印

package com.matio.log.formatter;  
  
import java.util.logging.ConsoleHandler;  
import java.util.logging.Logger;  
import java.util.logging.SimpleFormatter;  
  
public class StringFormatterTest {  
  
    public static void main(String[] args) {  
        Logger logger = Logger.getLogger(StringFormatterTest.class.getName());  
        logger.setUseParentHandlers(false);  

        // 将日志输出到控制台上  
        ConsoleHandler handler = new ConsoleHandler();  
        handler.setFormatter(new SimpleFormatter());  
        logger.addHandler(handler);  

        logger.info("test SimpleFormatter");  
    }  
}

打印结果如下

三月 07, 2024 2:11:27 下午 com.matio.log.formatter.StringFormatterTest main
信息: test SimpleFormatter

不知道大家有没有想过这样的一个问题:为什么打印结果的格式是这个样子的?能不能自定义格式

其实在SimpleFormatter类中一个静态属性format,这个就来定义日志按照何种格式打印的

public class SimpleFormatter extends Formatter { 
    // format string for printing the log record  
    private static final String format = LoggingSupport.getSimpleFormat();
}

static String getSimpleFormat(boolean var0) {  
    String var1 = (String)AccessController.doPrivileged(new PrivilegedAction<String>() {  
        public String run() {  
            return System.getProperty("java.util.logging.SimpleFormatter.format");  
    }  
    });  
    if (var0 && proxy != null && var1 == null) {  
        var1 = proxy.getProperty("java.util.logging.SimpleFormatter.format");  
    }  
    if (var1 != null) {  
        try {  
            String.format(var1, new Date(), "", "", "", "", "");  
        } catch (IllegalArgumentException var3) {  
            var1 = "%1$tb %1$td, %1$tY %1$tl:%1$tM:%1$tS %1$Tp %2$s%n%4$s: %5$s%6$s%n";  
        }  
    } else {  
        var1 = "%1$tb %1$td, %1$tY %1$tl:%1$tM:%1$tS %1$Tp %2$s%n%4$s: %5$s%6$s%n";  
    } 
    return var1;  
}

可以看出java提供了java.util.logging.SimpleFormatter.format系统属性帮助用户自定义格式输出,:

System.setProperty("java.util.logging.SimpleFormatter.format", "时间:%s 类方法名:%s 日志名称:%s 日志级别:%s 日志内容:%s\n");

注意:

上面一行代码会影响所有的SimpleFormatter,因为SimpleFormatter.format是个静态属性。当然我们可以定义自己的Formatter避免受到影响

XMLFormatter

顾名思义,就是将日志内容按照xml格式输出,使用极少。往往打印一行日志时会附加很多额外属性信息

handler.setFormatter(new XMLFormatter());

打印结果如下

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
  <date>2024-03-07T14:11:48</date>
  <millis>1709791908177</millis>
  <sequence>0</sequence>
  <logger>com.matio.log.formatter.StringFormatterTest</logger>
  <level>INFO</level>
  <class>com.matio.log.formatter.XmlFormatterTest</class>
  <method>main</method>
  <thread>1</thread>
  <message>test XMLFormatter</message>
</record>

自定义Formatter

自定义一个Formatter去继承jul中的Formatter,然后设置给目标handler即可

package com.matio.log.formatter;  
  
import java.util.logging.*;  
  
public class CusFormatterTest {  
  
    public static void main(String[] args) {  
        Logger logger = Logger.getLogger(CusFormatterTest.class.getName());  
        logger.setUseParentHandlers(false);  

        // 将日志输出到控制台上  
        ConsoleHandler handler = new ConsoleHandler();  
        handler.setFormatter(new CusFormatter());  
        logger.addHandler(handler);  

        logger.info("test CusFormatter");  
    }  

    private static class CusFormatter extends Formatter {  

        @Override  
        public String format(LogRecord record) {  
            return record.getLevel().getName() + " : " + record.getMessage();  
        }  
    }  
}

打印结果如下

INFO : test CusFormatter

Handler

Handler 对象从 Logger 中获取日志信息,并将这些信息导出。例如,它可将这些信息写入控制台或文件中,也可以将这些信息发送到网络日志服务中,或将其转发到操作系统日志中。

每一个logger可以通过 addHandler(Handler) 添加多个Handler对象,每一个Handler对会被顺序执行。另外可通过执行 setLevel(Level.OFF) 来禁用 Handler ,并可通过执行适当级别的 setLevel 来重新启用。

-Djava.util.logging.config.file=myfile

见静态代码块,主要目的就是初始化一个单例的LogManager对象


// The global LogManager object  
private static final LogManager manager;

static {  
    manager = AccessController.doPrivileged(new PrivilegedAction<LogManager>() {  
        @Override  
        public LogManager run() {  
            LogManager mgr = null;  
            String cname = System.getProperty("java.util.logging.manager"); 
            if (cname != null) {  
                Class<?> clz = ClassLoader.getSystemClassLoader().loadClass(cname); 
                mgr = (LogManager) clz.newInstance();    
            } 
            if (mgr == null) {  
                mgr = new LogManager();  
            }  
            return mgr;  
        }  
    });  
}

protected LogManager() {  
    this(checkSubclassPermissions());  
}  
  
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.  
}  
}

扩展点:自定义LogManager

从以上代码中知道,我们可以通过自定义一个LogManager

System.setProperty("java.util.logging.manager", "com.matio.log.logmanager.CustomLogManager");  
LogManager logManager = LogManager.getLogManager();  
// com.matio.log.logmanager.CustomLogManager@7f31245a  
System.out.println(logManager);
package com.matio.log.logmanager;   
import java.util.logging.LogManager;  

public class CustomLogManager extends LogManager {  
  
}

java.util.logging.LogManager

ConsoleHandler

将日志内容标准错误输出到控制台上,就是类似这种

System.err.println("三月 08, 2024 2:04:27 下午 com.matio.log.handler.ConsoleHandlerTest main");  
System.err.println("信息: console handler!");

上下两个代码输出的内容一致

package com.matio.log.handler;  
  
import java.io.UnsupportedEncodingException;  
import java.util.logging.*;  
  
public class ConsoleHandlerTest {  
  
    public static void main(String[] args) throws UnsupportedEncodingException {  
        Logger logger = Logger.getLogger(ConsoleHandlerTest.class.getName());  
        logger.setUseParentHandlers(false);  

        ConsoleHandler handler = new ConsoleHandler();  
        handler.setFormatter(new SimpleFormatter());  
        // handler.setLevel(Level.ALL);  
        // handler.setEncoding("UTF-8");  

        // 可以给每个handler设置一个ErrorManager,用来处理日志记录期间处理程序上发生的任何错误。  
        // 在处理日志记录输出时,如果处理程序遇到问题,那么处理程序不应该将异常返回  
        // 给日志记录调用的发出者(不太可能感兴趣),而应该调用其相关的ErrorManager。  
        // handler.setErrorManager(new ErrorManager());  

        handler.setFilter(null);  
        logger.addHandler(handler);  
        
        logger.info("console handler!");  
    }  
}

支持以下配置

private void configure() {  
    LogManager manager = LogManager.getLogManager();  
    String cname = getClass().getName();
    setLevel(manager.getLevelProperty(cname +".level", Level.INFO));  
    setFilter(manager.getFilterProperty(cname +".filter", null));  
    setFormatter(manager.getFormatterProperty(cname +".formatter", new SimpleFormatter()));  
    setEncoding(manager.getStringProperty(cname +".encoding", null));
}
  1. java.util.logging.ConsoleHandler.level 为 Handler 指定默认的级别(默认为 INFO )。
  2. java.util.logging.ConsoleHandler.filter 指定要使用的 Filter 类的名称(默认为null)。
  3. java.util.logging.ConsoleHandler.formatter 指定要使用的 Formatter 类的名称(默认为 java.util.logging.SimpleFormatter )。
  4. java.util.logging.ConsoleHandler.encoding 指定要使用的字符集编码的名称(默认为使用默认平台的编码)。

SocketHandler

开启一个socket客户端,将日志发送给指定的host和port

示例代码:

package com.matio.log.handler;  
  
import java.io.*;  
import java.net.ServerSocket;  
import java.net.Socket;  
import java.util.logging.Level;  
import java.util.logging.Logger;  
import java.util.logging.SimpleFormatter;  
import java.util.logging.SocketHandler;  
  
public class SocketHandlerTest {  
  
    public static void main(String[] args) throws InterruptedException {  
        // 模拟启动一个socketserver准备接受日志  
        new Thread(SocketHandlerTest::openServer).start();  
        //  等待socketserver启动完成
        Thread.sleep(3000);  

        Logger logger = Logger.getLogger(SocketHandlerTest.class.getName());  
        logger.setUseParentHandlers(false);  

        // 把日志内容通过socket发送给远端  
        SocketHandler handler = null;  
        try {  
            handler = new SocketHandler("localhost", 12345);  
            handler.setFormatter(new SimpleFormatter());  
        } catch (IOException e) {  
            throw new RuntimeException(e);  
        }  
        handler.setLevel(Level.ALL);  

        logger.addHandler(handler);  
        logger.info("第一条日志内容");  
        logger.info("第二条日志内容");  
    }  

    // 使用文心一言写的demo  
    private static void openServer() {  
        int portNumber = 12345;  
        try (  
            ServerSocket serverSocket = new ServerSocket(portNumber);  
            Socket clientSocket = serverSocket.accept();  
            PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);  
            BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));  
        ) {  
            String inputLine;  
            while ((inputLine = in.readLine()) != null) {  
                System.out.println("Received: " + inputLine);  
                if ("exit".equalsIgnoreCase(inputLine)) {  
                    break;  
                }  
                out.println("Welcome to the server!");  
            }  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
  
}

打印结果如下:

Received: 三月 12, 2024 1:52:04 下午 com.matio.log.handler.SocketHandlerTest main
Received: 信息: 第一条日志内容
Received: 三月 12, 2024 1:52:04 下午 com.matio.log.handler.SocketHandlerTest main
Received: 信息: 第二条日志内容

支持以下配置

private void configure() {  
    LogManager manager = LogManager.getLogManager();  
    String cname = getClass().getName();  
    setLevel(manager.getLevelProperty(cname +".level", Level.ALL));  
    setFilter(manager.getFilterProperty(cname +".filter", null));  
    setFormatter(manager.getFormatterProperty(cname +".formatter", new XMLFormatter()));  
    setEncoding(manager.getStringProperty(cname +".encoding", null));  
    port = manager.getIntProperty(cname + ".port", 0);  
    host = manager.getStringProperty(cname + ".host", null);  
}
  1. java.util.logging.SocketHandler.level 指定该 Handler 的默认日志输出级别(默认值为 Level.ALL )。
  2. java.util.logging.SocketHandler.filter 指定要使用的 Filter 类的名称(默认值非 Filter )。
  3. java.util.logging.SocketHandler.formatter 指定要使用的 Formatter (默认值为 java.util.logging.XMLFormatter )。
  4. java.util.logging.SocketHandler.encoding 要使用的字符集编码的名称(默认值为默认平台编码)。
  5. java.util.logging.SocketHandler.host 指定要连接到的目标主机名(无默认值)。
  6. java.util.logging.SocketHandler.port 指定要使用的目标 TCP 端口(无默认值)。

FileHandler

FileHandler 可以写入指定的文件,也可以写入指定n个轮换文件集合。

对于循环文件集合而言,到达每个文件的给定大小限制后,就关闭该文件,将其轮换出去,并打开新的文件。通过在基本文件名中添加 "0"、"1"、"2" 等来依次命名旧文件。

默认情况下,IO 库中启用了缓冲,但当缓冲完成时,每个日志记录都要被刷新

示例代码:

package com.matio.log.handler;  
  
import java.io.File;  
import java.io.IOException;  
import java.lang.reflect.Method;  
import java.util.logging.FileHandler;  
import java.util.logging.Level;  
import java.util.logging.Logger;  
import java.util.logging.SimpleFormatter;  
  
public class FileHandlerTest {  
  
    public static void main(String[] args) throws IOException {  
        Logger logger = Logger.getLogger(FileHandlerTest.class.getName()); 
        logger.setUseParentHandlers(false);  

        // 将日志文件保存在D:/test/目录下  
        // 限制单个日志文件大小不超过100M,且最多产生200个日志文件  
        FileHandler fileHandler = new FileHandler("D:/test/log%g-%u.log", 100 * 1024 * 1024, 200);  
        fileHandler.setFormatter(new SimpleFormatter());  
        fileHandler.setLevel(Level.ALL);  
        fileHandler.setEncoding("UTF-8");  

        // 打印出我们的日志实际保存在哪个目录  
        printLogPath(fileHandler);  

        logger.addHandler(fileHandler);  
        logger.info("test FileHandler");  

    }  

    // 打印出我们的日志实际保存在哪个目录  
    private static void printLogPath(FileHandler fileHandler) {  
        System.out.println(System.getProperty("user.home"));  
        System.out.println(System.getProperty("java.io.tmpdir"));  
        try {  
            Method method = FileHandler.class.getDeclaredMethod("generate", String.class, int.class, int.class);  
        method.setAccessible(true);  
            File file = (File) method.invoke(fileHandler, "D:/test/log%g-%u.log", 1, 2);  
            System.out.println("日志保存位置:" + file.getAbsolutePath());  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

支持以下配置

  1. java.util.logging.FileHandler.level 为 Handler 指定默认的级别(默认为 Level.ALL )。
  2. java.util.logging.FileHandler.filter 指定要使用的 Filter 类的名称(默认为null)。
  3. java.util.logging.FileHandler.formatter 指定要使用的 Formatter 类的名称(默认为 java.util.logging.XMLFormatter )。
  4. java.util.logging.FileHandler.encoding 指定要使用的字符集编码的名称(默认使用默认的平台编码)。
  5. java.util.logging.FileHandler.limit 指定要写入到任意文件的近似最大量(以字节为单位)。如果该数为 0,则没有限制(默认为无限制)。
  6. java.util.logging.FileHandler.count 指定有多少输出文件参与循环(默认为 1)。
  7. java.util.logging.FileHandler.pattern 为生成的输出文件名称指定一个模式。有关细节请参见以下内容(默认为 "%h/java%u.log")。
  8. java.util.logging.FileHandler.append 指定是否应该将 FileHandler 追加到任何现有文件上(默认为 false)。

pattern具体由包括以下特殊字符串组成(可以参考java.util.logging.FileHandler#generate):

  • "/" 本地路径名分隔符
  • "%t" 系统临时目录
  • "%h" "user.home" 系统属性的值
  • "%g" 区分循环日志的生成号
  • "%u" 解决冲突的惟一号码
  • "%%" 转换为单个百分数符号"%"

如果未指定 "%g" 字段,并且文件计数大于 1,那么生成号将被添加到所生成文件名末尾的小数点后面。

例如,文件计数为 2 的 "%t/java%g.log" 模式通常导致在 Solaris 系统中将日志文件写入 /var/tmp/java0.log 和 /var/tmp/java1.log,而在 Windows 95 中,则将其写入 C:\TEMP\java0.log 和 C:\TEMP\java1.log。

按照 0、1、2 等的序列安排生成号。

通常,将惟一字段 "%u" 设置为 0。但是如果 FileHandler 试图打开文件名并查找当前被另一个进程使用的文件,则增加惟一的字段号并再次重试。重复此操作直到 FileHandler 找到当前没有被使用的文件名。如果有冲突并且没有指定 "%u" 字段,则将该字段添加到文件名末尾的小数点后(它将位于所有自动添加的生成号后面)。

因此,如果三个进程都试图将日志记录到 fred%u.%g.txt,那么它们可能将 fred0.0.txt、fred1.0.txt、fred2.0.txt 作为其循环序列中的首个文件而结束。

注意,使用本地磁盘文件系统时,使用惟一的 id 以避免冲突是系统可靠运行的惟一保证。

比如pattern=D:/test/log%g-%u.log就是将日志保存在D:/test/目录下

保存日志的目录必须要提前创建好,否则会抛出以下异常:

Exception in thread "main" java.nio.file.NoSuchFileException: D:\test\log0-0.log.lck
	at sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:79)
	at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:97)
	at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:102)
	at sun.nio.fs.WindowsFileSystemProvider.newFileChannel(WindowsFileSystemProvider.java:115)
	at java.nio.channels.FileChannel.open(FileChannel.java:287)
	at java.nio.channels.FileChannel.open(FileChannel.java:335)
	at java.util.logging.FileHandler.openFiles(FileHandler.java:478)
at java.util.logging.FileHandler.<init>(FileHandler.java:310)
	at com.matio.log.handler.FileHandlerTest.main(FileHandlerTest.java:18)

MemoryHandler

底层维护了一个默认长度1000的环形数组用来缓冲处理日志。当用户调用logger.info("test");输出日志时都会生成一个LogRecord存放在这个环形数组上。当满足一定条件时才会遍历这个环形数组然后调用真正的Handler去输出日志。如果数组满了会覆盖旧的日志

当满足以下三种条件中的任意一个时,会触发缓冲区的 push 操作

  • 传入的 LogRecord 类型大于预先定义的 pushLevel 级别。
  • 外部类显式地调用 push 方法。
  • 如果记录符合所需的某些标准,则子类重写 log 方法,并扫描每个传入的 LogRecord ,调用 push 。
public synchronized void publish(LogRecord record) {  
    if (!isLoggable(record)) {  
        return;  
    }  
    int ix = (start+count)%buffer.length;  
    buffer[ix] = record;  
    if (count < buffer.length) {  
        count++;  
    } else {  
        start++;  
        start %= buffer.length;  
    }  
    if (record.getLevel().intValue() >= pushLevel.intValue()) {  
        push();  
    }  
}

public synchronized void push() {  
    for (int i = 0; i < count; i++) {  
        int ix = (start+i)%buffer.length;  
        LogRecord record = buffer[ix];  
        target.publish(record);  
    }  
    // Empty the buffer.  
    start = 0;  
    count = 0;  
}

支持以下配置:

  1. java.util.logging.MemoryHandler.level 指定 Handler 的级别(默认为 Level.ALL )。
  2. java.util.logging.MemoryHandler.filter 指定要使用的 Filter 类的名称(默认为无null)。
  3. java.util.logging.MemoryHandler.size 定义缓冲区的大小(默认为 1000)。
  4. java.util.logging.MemoryHandler.push 定义 pushLevel (默认为 level.SEVERE )。
  5. java.util.logging.MemoryHandler.target 指定目标 Handler 类的名称(无默认值)。
private void configure() {  
    LogManager manager = LogManager.getLogManager();  
    String cname = getClass().getName();  
    pushLevel = manager.getLevelProperty(cname +".push", Level.SEVERE);  
    size = manager.getIntProperty(cname + ".size", 1000);  
    if (size <= 0) {  
        size = 1000;  
    }  
    setLevel(manager.getLevelProperty(cname +".level", Level.ALL));  
    setFilter(manager.getFilterProperty(cname +".filter", null));  
    setFormatter(manager.getFormatterProperty(cname +".formatter", new SimpleFormatter()));
}

示例代码

package com.matio.log.handler;  
  
import java.io.UnsupportedEncodingException;  
import java.util.logging.*;  
  
public class MemoryHandlerTest {  
    public static void main(String[] args) throws UnsupportedEncodingException {  
        Logger logger = Logger.getLogger(MemoryHandlerTest.class.getName());  
        logger.setUseParentHandlers(false);  

        // memoryHandler中的target  
        ConsoleHandler consoleHandler = new ConsoleHandler();  
        consoleHandler.setLevel(Level.ALL);  
        consoleHandler.setFormatter(new SimpleFormatter());  

        MemoryHandler memoryHandler = new MemoryHandler(consoleHandler, 10, Level.WARNING);  
        memoryHandler.setLevel(Level.INFO);  
        logger.addHandler(memoryHandler);  

        // 这句并不会打印日志,因为日志还在缓冲区内  
        logger.info("warning msg");  

        // 主动刷新缓存,输出日志  
        memoryHandler.push();  
    }  
}

自定义Handler

package com.matio.log.handler;  
  
import java.util.logging.Handler;  
import java.util.logging.LogRecord;  
import java.util.logging.Logger;  
  
public class CusHandlerTest {  
  
    public static void main(String[] args) {  
        Logger logger = Logger.getLogger(CusHandlerTest.class.getName());  
        logger.setUseParentHandlers(false);  
        logger.addHandler(new CusHandler());  
        logger.info("哈哈哈");  
    }  

    private static class CusHandler extends Handler {  

        @Override  
        public void publish(LogRecord record) {  
            System.out.println(record.getLevel().getName() + " : " + record.getMessage());  
        }  

        @Override  
        public void flush() {  
        }  

        @Override  
        public void close() throws SecurityException {  
        }  

    }  
}

Filter

Filter 可用于为记录内容提供比记录级别所提供的更细粒度的控制。

每个 Logger 和 Handler 都可以关联一个过滤器。Logger 或 Handler 可以调用 isLoggable 方法来检查是否应该发布给定的 LogRecord。如果 isLoggable 返回 false,则丢弃 LogRecord。

接口定义如下

public interface Filter {  
    boolean isLoggable(LogRecord record);  
}

示例代码

package com.matio.log.filter;  
  
import java.io.IOException;  
import java.util.logging.*;  
  
/**  
* 过滤器Filter可用于提供对所记录内容的精细控制,超出了日志级别提供的控制范围。  
* 每个Logger和每个Handler都可以有一个与其关联的筛选器。Logger或Handler将调用isLogable方法来检查是否应该发布给定的LogRecord。  
* 如果isLoggable返回false,则LogRecord将被丢弃。  
*/  
public class FilterTest {  
    public static void main(String[] args) throws IOException {  
        Logger logger = Logger.getLogger("FilterTest");  
        logger.setUseParentHandlers(false);  

        // 输出到控制台上  
        ConsoleHandler handler = new ConsoleHandler();  
        handler.setFormatter(new SimpleFormatter());  
        // 可以给每一个handler设置一个filter  
        handler.setFilter(new CusFilter1());  
        logger.addHandler(handler);  

        // 将日志文件保存在D:/test/目录下  
        FileHandler fileHandler = new FileHandler("D:/test/log%g-%u.log", 100 * 1024 * 1024, 200);  
        fileHandler.setFormatter(new SimpleFormatter());  
        fileHandler.setLevel(Level.ALL);  
        fileHandler.setFilter(new CusFilter2());  
        fileHandler.setEncoding("UTF-8");  

        // 可以给每一个logger设置一个filter  
        logger.setFilter(new CusFilter1());  
        logger.info("test filter");  
    }  

    private static class CusFilter1 implements Filter {  
        @Override  
        public boolean isLoggable(LogRecord record) {  
            return true;  
        }  
    }  

    private static class CusFilter2 implements Filter {  
        @Override  
        public boolean isLoggable(LogRecord record) {  
            return false;  
        }  
    }  
}

LogManager

存在一个单一的全局 LogManager 对象,它可用于维护 Logger 和日志服务的一组共享状态

自定义LogManager

具体可以参考LogManager类中的静态代码块

示例代码:

package com.matio.log.logmanager;  
  
import java.util.logging.LogManager;  
import java.util.logging.Logger;  
  
public class LogManagerTest0 {  
    public static void main(String[] args) {  
        // 因为LogManager在类加载的时候就会生成唯一实例  
        // 所以需要保证这行代码在LogManager类被加载前执行  
        System.setProperty("java.util.logging.manager", "com.matio.log.logmanager.CustomLogManager");  

        System.out.println(LogManager.getLogManager());  

        Logger logger = Logger.getLogger(LogManagerTest0.class.getName());  
        logger.info("info");  
    }  
}

package com.matio.log.logmanager;  
  
import java.io.IOException;  
import java.io.InputStream;  
import java.util.logging.LogManager;  
import java.util.logging.Logger;  
// 自定义LogManager,不使用原生的了  
public class CustomLogManager extends LogManager {  
  
    public CustomLogManager() {  
    System.out.println("CustomLogManager is loaded!");  
    }  

    @Override  
    public void readConfiguration() throws IOException, SecurityException {  
        // java.util.logging.config.class  
        // java.util.logging.config.file  
        super.readConfiguration();  

        // todo 我们可以加载自定义配置文件  
    }  

    @Override  
    public boolean addLogger(Logger logger) {  
        System.out.println("注册 : " + logger.getName());  
        // todo 我们可以配置任意的logger的属性,比如:  
        /*logger.setLevel(null);  
        logger.setResourceBundle(null);  
        logger.setUseParentHandlers(false);  
        logger.setFilter(null);  
        logger.setParent(null);*/  
        return super.addLogger(logger);  
    }  

    @Override  
    public void readConfiguration(InputStream ins) throws IOException, SecurityException {  
        super.readConfiguration(ins);  
    }  
  
}

另外,LogManager 使用两个可选的允许更好地控制初始配置读取的系统属性:

  1. java.util.logging.config.class
  2. java.util.logging.config.file

(其实jul默认的配置文件是 ${java.home}/lib/logging.properties,对应笔者电脑上的C:\Program Files\Java\jdk1.8.0_271\jre\lib\logging.properties文件)

如果设置了 "java.util.logging.config.class" 属性,则会把属性值当作类名。给定的类将会被加载,并会实例化一个对象,该对象的构造方法负责读取初始配置。(此对象可以使用其他系统属性来控制自己的配置。)此备用配置类可使用 readConfiguration(InputStream) 来定义 LogManager 中的属性。

如果未设置 "java.util.logging.config.class" 属性,则会使用 "java.util.logging.config.file" 系统属性来指定一个properties文件。从此文件读取初始日志配置。

如果这两个属性都没有定义,则如上所述,LogManager 将默认从 ${java.home}/lib/logging.properties 中读取其初始配置。具体可以参考

java.util.logging.LogManager#readConfiguration()

这里附带上 ${java.home}/lib/logging.properties 文件内容:

############################################################
#  	Default Logging Configuration File
#
# You can use a different file by specifying a filename
# with the java.util.logging.config.file system property.  
# For example java -Djava.util.logging.config.file=myfile
############################################################

############################################################
#  	Global properties
############################################################

# "handlers" specifies a comma separated list of log Handler 
# classes.  These handlers will be installed during VM startup.
# Note that these classes must be on the system classpath.
# By default we only configure a ConsoleHandler, which will only
# show messages at the INFO and above levels.
handlers= java.util.logging.ConsoleHandler

# To also add the FileHandler, use the following line instead.
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler

# Default global logging level.
# This specifies which kinds of events are logged across
# all loggers.  For any given facility this global level
# can be overriden by a facility specific level
# Note that the ConsoleHandler also has a separate level
# setting to limit messages printed to the console.
.level= INFO

############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################

# default file output is in user's home directory.
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

# Limit the message that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

# Example to customize the SimpleFormatter output format 
# to print one-line log message like this:
#     <level>: <log message> [<date/time>]
#
# java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n

############################################################
# Facility specific properties.
# Provides extra control for each logger.
############################################################

# For example, set the com.xyz.foo logger to only log SEVERE
# messages:
com.xyz.foo.level = SEVERE

全局日志属性可以包括:

  • handlers:该属性为 handler 类定义类名的空白分隔列表,以便作为处理程序在根 Logger(该 Logger 名为 "")中加载和注册。每个类名必须用于具有默认构造方法的 Handler 类。
  • ${loggerName}.handlers :该属性为 handler 类定义空白分隔或逗号分隔的列表,以便作为处理程序加载和注册到指定的 logger。每个类名必须用于一个具有默认构造方法的 Handler 类。
  • ${loggerName}.useParentHandlers:该属性定义一个 boolean 值。默认情况下,每个 logger 除了自己处理日志消息外,还可能调用其父级来处理,这往往也会导致根 logger 来处理消息。将此属性设置为 false 时,需要为此 logger 配置 Handler,否则不传递任何消息。
  • config:此属性允许运行任意配置代码。该属性定义类名的空白分隔的列表。为每个指定类创建新实例。每个类的默认构造方法都可以执行任意代码来更新日志配置,如设置 logger 级别、添加处理程序、添加过滤器,等等。

注意,在 LogManager 配置期间加载的所有类,其搜索顺序是先从系统类路径中搜索,然后才从用户类中搜索。这包括 LogManager 类、任何 config 类和任何 handler 类。

Logger 是按其圆点分隔的名称被组织到命名层次结构中的。因此,"a.b.c" 是 "a.b" 的子级,但 "a.b1" 和 a.b2" 属于同一级。

假定所有以 ".level" 结尾的名称的属性为 Logger 定义日志级别。因此,"foo.level" 就为名称为 "foo" 的 logger 定义了日志级别,进而为指定层次结构中它的所有子级也逐个定义了日志级别。日志级别是按其在属性文件中的定义顺序应用的。因此,树中子节点的级别设置应该迟于其父级设置。属性名 ".level" 可用于设置树的根级。

LogManager 对象上的所有方法都是多线程安全的。

java.util.logging.config.class

如果设置了 "java.util.logging.config.class" 属性,则会把属性值当作类名。给定的类将会被加载,并会实例化一个对象,该对象的构造方法负责读取初始配置。(此对象可以使用其他系统属性来控制自己的配置。)此备用配置类可使用 readConfiguration(InputStream) 来定义 LogManager 中的属性。 示例代码如下:

package com.matio.log.logmanager;  
  
import java.util.logging.Logger;  
  
public class LogManagerTest1 {  
    public static void main(String[] args) {  
        // 自定义配置文件 
        System.setProperty("java.util.logging.config.class", "com.matio.log.logmanager.JulConfig");  

        Logger logger = Logger.getLogger(LogManagerTest1.class.getName());  
        logger.info("info");  
    }  
}

package com.matio.log.logmanager;  
  
import java.io.FileInputStream;  
import java.io.IOException;  
import java.util.logging.LogManager;  
  
public class JulConfig {  
  
    public JulConfig() {  
        init();  
    }  

    private void init() {  
        String path = "E:\\WorkspaceIdea\\01src\\log\\matio-jul\\src\\main\\resources\\logging.properties";  
        System.out.println("init jul by " + path);  
        try {  
            FileInputStream fileInputStream = new FileInputStream(path);  
            LogManager.getLogManager().readConfiguration(fileInputStream);  
        } catch (IOException e) {  
            throw new RuntimeException(e);  
        }  
    }  

}

java.util.logging.config.file

如果未设置 "java.util.logging.config.class" 属性,则会使用 "java.util.logging.config.file" 系统属性来指定一个Properties文件。从此文件读取初始日志配置

package com.matio.log.logmanager;  
  
import java.util.logging.Logger;  
  
public class LogManagerTest2 {  
    public static void main(String[] args) {  
        // 指定jul的默认配置文件路径  
        System.setProperty("java.util.logging.config.file", "E:\\WorkspaceIdea\\01src\\log\\matio-jul\\src\\main\\resources\\logging.properties");  
        Logger logger = Logger.getLogger(LogManagerTest2.class.getName());  
        logger.info("info");  
    }  
}