欢迎光临
我们一直在努力

Java深入解析篇十三之Java日志API(JUL)详解

Java日志API(JUL)详解


目录

  • 概述与历史
  • 核心架构与组件关系
  • Logger详解
  • Level日志级别
  • Handler输出处理器
  • Formatter格式化器
  • Filter过滤器
  • LogRecord与日志流转
  • LogManager与配置文件
  • 自定义Handler与Formatter
  • 与SLF4J桥接
  • 最佳实践与常见陷阱
  • 版本演进

  • 1. 概述与历史

    1.1 什么是JUL

    JUL(Java Util Logging)是 Java 标准库自带的日志框架,全部类位于 java.util.logging 包。它是 JDK 1.4(2002年,JSR 47) 引入的,目标是让 Java 平台拥有一个标准日志设施,避免 Log4j 1.x 时期各框架各自造轮子的局面。

    // 最小可用示例:JDK内置,无需任何第三方依赖
    import java.util.logging.Logger;

    public class JulHelloWorld {
    // 推荐:存为字段,避免被GC(见陷阱章节)
    private static final Logger logger = Logger.getLogger(JulHelloWorld.class.getName());

    public static void main(String[] args) {
    logger.info("Hello, JUL!"); // 输出到控制台(System.err)
    logger.warning("这是一条警告");
    logger.severe("这是一条严重错误");
    }
    }
    /*
    运行输出(默认SimpleFormatter):
    五月 20, 2024 10:30:15 上午 JulHelloWorld main
    信息: Hello, JUL!
    五月 20, 2024 10:30:15 上午 JulHelloWorld main
    警告: 这是一条警告
    五月 20, 2024 10:30:15 上午 JulHelloWorld main
    严重: 这是一条严重错误
    */

    1.2 设计背景与定位

    时间事件
    1996 Log4j 0.x 出现,成为事实标准
    2002 JDK 1.4 引入 java.util.logging(JSR 47)
    2004 SLF4J / Logback 出现,解决门面与实现分离问题
    2012 Log4j2 发布,性能大幅超越
    今天 JUL 仍是 JDK 内置设施,轻量场景与库开发常用

    JUL 的优势:零依赖、JDK 内置、无需选型。劣势:配置能力弱、无占位符 {}、无 MDC、无原生异步、性能与易用性逊于 Log4j2/Logback。


    2. 核心架构与组件关系

    2.1 五大核心组件

    ┌──────────────────────────────────────────────────────────────┐
    │ 应用代码 │
    │ logger.info("用户" + id + "登录") │
    └────────────┬─────────────────────────────────────────────────┘

    ┌─────────────────────────┐
    │ Logger(记录器) │ 判断级别 → 构建LogRecord → 分发
    │ ├─ Level(级别阈值) │
    │ ├─ Filter(过滤器) │
    │ └─ Handler[](处理器) │
    └────────────┬────────────┘
    ▼(可能向父Logger传播)
    ┌─────────────────────────┐
    │ Handler(处理器) │ 再次判断级别 → 调用Formatter → 输出
    │ ├─ Level(级别阈值) │
    │ ├─ Filter(过滤器) │
    │ └─ Formatter(格式化器)│
    └────────────┬────────────┘

    控制台 / 文件 / 网络 / 缓冲

    组件职责类比
    Logger 应用获取的日志入口,按名称形成层级树 收件人
    Level 控制日志严重程度的门槛 安检等级
    Filter 细粒度的发布判断 安检员
    Handler 决定日志写到哪里(控制台/文件/网络) 投递渠道
    Formatter 决定日志的输出格式 包装方式
    LogRecord 在组件之间传递的日志事件数据 包裹
    LogManager 全局管理器:Logger注册表 + 配置 邮局总局

    3. Logger详解

    3.1 获取Logger与层级树

    Logger 通过 LogManager 全局注册表按名称缓存。名称采用点分层级(与包名无关,只是字符串前缀约定),子 Logger 默认继承父 Logger 的有效级别和 Handler 传播。

    import java.util.logging.Logger;

    public class LoggerHierarchyDemo {
    public static void main(String[] args) {
    // 推荐命名:用类的全限定名,天然形成层级
    Logger daoLogger = Logger.getLogger("com.example.dao");
    Logger appLogger = Logger.getLogger("com.example");

    // 同名getLogger返回同一实例(注册表缓存)
    System.out.println(Logger.getLogger("com.example.dao") == daoLogger); // true

    // 层级关系:com.example.dao 的父是 com.example
    System.out.println(daoLogger.getParent().getName()); // com.example

    // 根Logger名为 "",默认挂一个INFO级别的ConsoleHandler
    Logger root = Logger.getLogger("");
    System.out.println(root.getHandlers().length); // 1

    // JDK7+ 全局Logger
    Logger global = Logger.getGlobal();
    global.info("全局日志");
    }
    }

    关键陷阱:LogManager 对 Logger 只持有弱引用。如果在方法里 Logger.getLogger(…) 用完就丢,该 Logger 可能被 GC 回收,其上配置(Handler、Level)随之丢失。必须把 Logger 存为字段。

    3.2 记录日志的完整API

    import java.util.logging.Level;
    import java.util.logging.Logger;

    public class LoggerApiDemo {
    private static final Logger logger = Logger.getLogger("com.example.order");

    public static void main(String[] args) {
    // 1) 七个便捷方法(按级别命名)
    logger.severe("系统崩溃"); // SEVERE 1000
    logger.warning("磁盘空间不足"); // WARNING 900
    logger.info("订单创建成功"); // INFO 800
    logger.config("线程池大小=8"); // CONFIG 700
    logger.fine("缓存命中 key=1001"); // FINE 500
    logger.finer("进入方法 process"); // FINER 400
    logger.finest("循环第3次迭代"); // FINEST 300

    // 2) 通用log方法 + 参数化消息({0}{1}占位符,MessageFormat语法)
    logger.log(Level.INFO, "用户{0}下单,金额{1}元", new Object[]{"张三", 199.5});

    // 3) 带异常堆栈
    try {
    int r = 10 / 0;
    } catch (ArithmeticException e) {
    logger.log(Level.SEVERE, "计算失败", e); // 第三个参数为Throwable,会打印堆栈
    }

    // 4) 方法进入/退出追踪(FINER级别,调试调用链很有用)
    process("参数A");

    // 5) 先判断再记录(避免昂贵的参数计算)
    if (logger.isLoggable(Level.FINEST)) {
    logger.finest("大对象快照: " + buildExpensiveSnapshot());
    }
    }

    private static void process(String arg) {
    logger.entering("LoggerApiDemo", "process", arg); // FINER: ENTRY + 参数
    try {
    // 业务逻辑…
    } finally {
    logger.exiting("LoggerApiDemo", "process"); // FINER: RETURN
    }
    }

    private static String buildExpensiveSnapshot() {
    return "耗时构建的快照";
    }
    }

    3.3 级别继承与Handler传播

    import java.util.logging.ConsoleHandler;
    import java.util.logging.Level;
    import java.util.logging.Logger;

    public class InheritanceDemo {
    public static void main(String[] args) {
    Logger parent = Logger.getLogger("com.demo");
    Logger child = Logger.getLogger("com.demo.service");

    // child未设置Level(null)→ 有效级别继承父级
    parent.setLevel(Level.FINE);
    System.out.println(child.isLoggable(new java.util.logging.LogRecord(Level.FINE, "x"))); // true

    // 默认行为:日志同时交给自身Handler和父Handler(向上传播)
    // 若child也addHandler,可能出现重复输出!
    child.addHandler(new ConsoleHandler());
    child.info("这条会打印两次:child自己的Handler + 根Logger的默认Handler");

    // 解决:关闭向父级传播
    child.setUseParentHandlers(false);
    child.info("现在只打印一次");
    }
    }


    4. Level日志级别

    4.1 标准级别一览

    JUL 的 Level 是数值,数值越大越严重。介于标准级别之间的数值也可自定义。

    Level数值语义≈SLF4J
    OFF Integer.MAX_VALUE 关闭日志
    SEVERE 1000 致命错误 ERROR
    WARNING 900 警告 WARN
    INFO 800 一般信息 INFO
    CONFIG 700 配置信息 —(近似INFO)
    FINE 500 调试信息 DEBUG
    FINER 400 较细调试(entering/exiting用) TRACE
    FINEST 300 最细粒度 TRACE
    ALL Integer.MIN_VALUE 全部打开

    4.2 双重级别门槛

    一条日志要最终输出,必须同时通过 Logger 级别和 Handler 级别两道门槛:

    import java.util.logging.ConsoleHandler;
    import java.util.logging.Level;
    import java.util.logging.Logger;

    public class LevelGateDemo {
    public static void main(String[] args) {
    Logger logger = Logger.getLogger("gate.demo");
    logger.setUseParentHandlers(false); // 去掉默认Handler,避免干扰

    ConsoleHandler handler = new ConsoleHandler();
    logger.addHandler(handler);

    // 场景1:Logger=FINE,Handler=INFO(默认) → FINE日志仍然看不到!
    logger.setLevel(Level.FINE);
    logger.fine("看不到我:被Handler的INFO级别挡住");

    // 场景2:Handler也放开
    handler.setLevel(Level.ALL);
    logger.fine("现在能看到了:两道门槛都通过");
    }
    }

    4.3 自定义级别

    import java.util.logging.Level;
    import java.util.logging.Logger;

    // 自定义级别:继承Level,提供 name + value + resourceKey
    class AuditLevel extends Level {
    // AUDENT审计级别:介于INFO(800)和WARNING(900)之间
    public static final AuditLevel AUDIT = new AuditLevel("AUDIT", 850);

    protected AuditLevel(String name, int value) {
    super(name, value);
    }
    }

    public class CustomLevelDemo {
    private static final Logger logger = Logger.getLogger("audit.demo");

    public static void main(String[] args) {
    logger.setUseParentHandlers(false);
    java.util.logging.ConsoleHandler h = new java.util.logging.ConsoleHandler();
    h.setLevel(Level.ALL);
    logger.addHandler(h);
    logger.setLevel(Level.ALL);

    logger.log(AuditLevel.AUDIT, "管理员删除了用户1001");
    }
    }


    5. Handler输出处理器

    5.1 Handler继承体系

    java.util.logging.Handler(抽象)
    ├── StreamHandler ── 写入OutputStream(UTF-8,不自动flush)
    │ ├── ConsoleHandler ── 写入System.err,自动flush
    │ ├── FileHandler ── 写入文件,支持滚动
    │ └── SocketHandler ── 写入TCP套接字,自动flush
    └── MemoryHandler ── 内存缓冲后转发给目标Handler

    5.2 ConsoleHandler 与 StreamHandler

    import java.io.OutputStream;
    import java.util.logging.*;

    public class ConsoleHandlerDemo {
    public static void main(String[] args) {
    Logger logger = Logger.getLogger("console.demo");
    logger.setUseParentHandlers(false);

    // ConsoleHandler 写入 System.err(红色输出,与正常输出区分)
    ConsoleHandler console = new ConsoleHandler();
    console.setLevel(Level.ALL);
    logger.addHandler(console);

    // StreamHandler 可写任意OutputStream(这里写System.out)
    // 注意:StreamHandler不会自动flush,一条日志可能滞留在缓冲
    StreamHandler out = new StreamHandler(System.out, new SimpleFormatter()) {
    @Override
    public synchronized void publish(LogRecord record) {
    super.publish(record);
    flush(); // 逐条刷新,否则看不到输出
    }
    };
    out.setLevel(Level.ALL);
    logger.addHandler(out);

    logger.info("同时输出到stderr和stdout");
    }
    }

    5.3 FileHandler 与滚动文件

    import java.io.IOException;
    import java.util.logging.*;

    public class FileHandlerDemo {
    public static void main(String[] args) throws IOException {
    Logger logger = Logger.getLogger("file.demo");
    logger.setUseParentHandlers(false);

    // 滚动文件:%g是序号,最多5个文件,每个上限50KB
    // 写满后 app.log.0 → app.log.1,依次轮转
    FileHandler fh = new FileHandler(
    "logs/app%g.log", // 路径模式
    50 * 1024, // limit:单文件字节上限(0=不限)
    5, // count:滚动文件数
    true); // append:是否追加
    fh.setLevel(Level.ALL);
    fh.setFormatter(new SimpleFormatter()); // FileHandler默认是XMLFormatter
    logger.addHandler(fh);

    for (int i = 0; i < 100; i++) {
    logger.info("第 " + i + " 条日志");
    }
    fh.close(); // 必须close,否则可能丢失尾部数据
    }
    }

    模式占位符速查:

    占位符含义示例
    %h 用户主目录 user.home C:\\Users\\dev
    %t 系统临时目录 java.io.tmpdir /tmp
    %g 滚动序号 app0.log
    %u 唯一数(多实例防冲突) app1.log
    %d 日期 app2024-05-20.log

    5.4 MemoryHandler(缓冲转发)

    import java.util.logging.*;

    public class MemoryHandlerDemo {
    public static void main(String[] args) {
    Logger logger = Logger.getLogger("memory.demo");
    logger.setUseParentHandlers(false);

    // 目标:文件(模拟:这里用控制台)
    ConsoleHandler target = new ConsoleHandler();
    target.setLevel(Level.ALL);

    // MemoryHandler:缓冲100条;遇到WARNING及以上立即push;也可手动push
    MemoryHandler memory = new MemoryHandler(target, 100, Level.WARNING);
    logger.addHandler(memory);
    logger.setLevel(Level.ALL);

    logger.info("先缓冲,暂不输出"); // 进缓冲区
    logger.fine("继续缓冲"); // 进缓冲区
    logger.warning("触发push,前面的全部输出"); // ≥WARNING → 缓冲区整体推给target

    memory.push(); // 手动强制输出剩余缓冲
    }
    }


    6. Formatter格式化器

    6.1 SimpleFormatter 与格式定制

    SimpleFormatter 输出文本格式。默认输出较啰嗦,可通过系统属性 -Djava.util.logging.SimpleFormatter.format 定制:

    import java.util.logging.*;

    public class SimpleFormatterDemo {
    public static void main(String[] args) {
    // 也可在代码里设置(需在Logger使用前)
    System.setProperty("java.util.logging.SimpleFormatter.format",
    // %1$tF=日期 %1$tT=时间 %4$s=级别 %3$s=Logger %5$s=消息 %6$s=异常
    "%1$tF %1$tT [%4$s] %3$s – %5$s%6$s%n");

    Logger logger = Logger.getLogger("format.demo");
    logger.setUseParentHandlers(false);
    ConsoleHandler h = new ConsoleHandler();
    h.setLevel(Level.ALL);
    h.setFormatter(new SimpleFormatter());
    logger.addHandler(h);

    logger.info("单行输出,清爽多了");
    }
    }
    // 输出:2024-05-20 10:30:15 [信息] format.demo – 单行输出,清爽多了

    占位符说明(MessageFormat语法):

    占位符内容
    %1$ 日期时间(LogRecord.millis)
    %2$ 源类名与方法名
    %3$ Logger名称
    %4$ 级别(本地化名称)
    %5$ 消息
    %6$ 异常堆栈

    6.2 XMLFormatter

    FileHandler 的默认格式化器,输出结构化 XML,适合机器解析:

    import java.util.logging.*;

    public class XmlFormatterDemo {
    public static void main(String[] args) throws Exception {
    Logger logger = Logger.getLogger("xml.demo");
    logger.setUseParentHandlers(false);
    FileHandler fh = new FileHandler("logs/xml%u.log");
    fh.setFormatter(new XMLFormatter()); // 其实这是默认值
    logger.addHandler(fh);

    logger.info("XML格式日志");
    fh.close();
    }
    }
    /*
    输出文件内容:
    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <log>
    <record>
    <date>2024-05-20T10:30:15.123456789Z</date>
    <millis>1716198615123</millis>
    <sequence>0</sequence>
    <logger>xml.demo</logger>
    <level>INFO</level>
    <class>XmlFormatterDemo</class>
    <method>main</method>
    <thread>1</thread>
    <message>XML格式日志</message>
    </record>
    </log>
    */

    6.3 自定义Formatter

    import java.time.Instant;
    import java.time.ZoneId;
    import java.time.format.DateTimeFormatter;
    import java.util.logging.*;

    // 自定义Formatter:只需实现 format(LogRecord)
    public class JsonFormatter extends Formatter {
    private static final DateTimeFormatter FMT =
    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
    .withZone(ZoneId.systemDefault());

    @Override
    public String format(LogRecord record) {
    StringBuilder sb = new StringBuilder(128);
    sb.append("{");
    sb.append("\\"time\\":\\"").append(FMT.format(Instant.ofEpochMilli(record.getMillis()))).append("\\",");
    sb.append("\\"level\\":\\"").append(record.getLevel().getName()).append("\\",");
    sb.append("\\"logger\\":\\"").append(record.getLoggerName()).append("\\",");
    sb.append("\\"msg\\":\\"").append(escape(formatMessage(record))).append("\\"");
    // 有异常时附上堆栈
    if (record.getThrown() != null) {
    java.io.StringWriter sw = new java.io.StringWriter();
    record.getThrown().printStackTrace(new java.io.PrintWriter(sw));
    sb.append(",\\"exception\\":\\"").append(escape(sw.toString())).append("\\"");
    }
    sb.append("}\\n");
    return sb.toString();
    }

    // 简单转义,防止JSON破坏
    private String escape(String s) {
    return s.replace("\\\\", "\\\\\\\\").replace("\\"", "\\\\\\"")
    .replace("\\n", "\\\\n").replace("\\r", "\\\\r");
    }
    }


    7. Filter过滤器

    JDK 8 起 Filter 是函数式接口,可直接用 Lambda。Logger 和 Handler 各可挂一个,两者都通过才发布。

    import java.util.logging.*;

    public class FilterDemo {
    public static void main(String[] args) {
    Logger logger = Logger.getLogger("filter.demo");
    logger.setUseParentHandlers(false);
    ConsoleHandler h = new ConsoleHandler();
    h.setLevel(Level.ALL);
    logger.addHandler(h);
    logger.setLevel(Level.ALL);

    // 1) Logger级过滤:屏蔽包含"heartbeat"的嘈杂日志
    logger.setFilter(record ->
    !record.getMessage().contains("heartbeat"));

    logger.info("正常业务日志"); // 输出
    logger.info("heartbeat check ok"); // 被过滤

    // 2) Handler级过滤:只输出SEVERE
    logger.setFilter(null); // 移除Logger级过滤
    h.setFilter(record -> record.getLevel().intValue() >= Level.SEVERE.intValue());
    logger.warning("警告不输出");
    logger.severe("严重错误才输出");

    // 3) 采样过滤:每3条记1条(降低高频日志量)
    final java.util.concurrent.atomic.AtomicInteger counter =
    new java.util.concurrent.atomic.AtomicInteger();
    h.setFilter(record -> counter.incrementAndGet() % 3 == 0);
    }
    }


    8. LogRecord与日志流转

    LogRecord 是日志事件的载体,封装了一次日志调用的全部信息:

    import java.util.logging.Level;
    import java.util.logging.LogRecord;

    public class LogRecordDemo {
    public static void main(String[] args) {
    LogRecord record = new LogRecord(Level.INFO, "用户{0}登录");
    record.setParameters(new Object[]{"张三"}); // {0}参数

    System.out.println("级别: " + record.getLevel());
    System.out.println("消息: " + record.getMessage());
    System.out.println("参数: " + java.util.Arrays.toString(record.getParameters()));
    System.out.println("时间: " + record.getMillis());
    System.out.println("线程: " + record.getThreadID());
    System.out.println("序号: " + record.getSequenceNumber()); // 全局递增
    System.out.println("源类: " + record.getSourceClassName());
    System.out.println("源方法: " + record.getSourceMethodName());
    }
    }

    注意:Handler 的 publish(LogRecord) 收到的 record 与其他 Handler 是同一对象引用。如果自定义 Handler 修改了 record(如改消息),会影响后续 Handler。异步使用时更要小心生命周期。


    9. LogManager与配置文件

    9.1 配置加载优先级

    ① 系统属性 java.util.logging.config.class 指定的配置类(最高优先级)
    ② 系统属性 java.util.logging.config.file 指定的文件
    ③ $JAVA_HOME/conf/logging.properties(JDK9+;JDK8为 jre/lib/logging.properties)
    ④ 都没有 → 使用内置默认配置(根Logger=INFO + ConsoleHandler)

    启动方式:

    # 方式1:外部配置文件
    java -Djava.util.logging.config.file=my-logging.properties -jar app.jar

    # 方式2:定制SimpleFormatter格式
    java -Djava.util.logging.SimpleFormatter.format="%1$tF %1$tT [%4$s] %5$s%6$s%n" -jar app.jar

    9.2 logging.properties 完整示例

    # ============================================
    # logging.properties 示例
    # ============================================

    # 根Logger挂载的Handler列表(逗号分隔)
    handlers = java.util.logging.ConsoleHandler, java.util.logging.FileHandler

    # 根Logger级别
    .level = INFO

    # —- 按包名/Logger名设置级别(子级继承) —-
    com.example.level = FINE
    com.example.dao.level = FINEST
    org.hibernate.level = WARNING

    # —- ConsoleHandler 配置 —-
    java.util.logging.ConsoleHandler.level = ALL
    java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

    # —- FileHandler 配置 —-
    java.util.logging.FileHandler.pattern = %h/myapp%u.log
    java.util.logging.FileHandler.limit = 5000000
    java.util.logging.FileHandler.count = 10
    java.util.logging.FileHandler.append = true
    java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter

    # —- SimpleFormatter 单行格式 —-
    java.util.logging.SimpleFormatter.format = %1$tF %1$tT [%4$s] %3$s – %5$s%6$s%n

    9.3 编程式配置

    import java.io.ByteArrayInputStream;
    import java.nio.charset.StandardCharsets;
    import java.util.logging.LogManager;
    import java.util.logging.Logger;

    public class ProgrammaticConfigDemo {
    public static void main(String[] args) throws Exception {
    // 用代码方式提供properties内容
    String config = """
    handlers = java.util.logging.ConsoleHandler
    .level = INFO
    java.util.logging.ConsoleHandler.level = ALL
    java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
    com.example.level = FINE
    """
    ;
    LogManager.getLogManager().readConfiguration(
    new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8)));

    Logger logger = Logger.getLogger("com.example");
    logger.fine("FINE级别现在可见了");
    }
    }


    10. 自定义Handler与Formatter

    将日志写入数据库的典型场景:

    import java.util.logging.*;

    /**
    * 自定义Handler:将SEVERE级日志写入数据库(此处用内存List模拟)
    */

    public class DatabaseHandler extends Handler {
    // 实际项目中注入DataSource,这里用List模拟数据库表
    private final java.util.List<String> logTable =
    java.util.Collections.synchronizedList(new java.util.ArrayList<>());

    @Override
    public void publish(LogRecord record) {
    // 1) 级别与过滤判断(Handler基类不会自动做,需手动调用)
    if (!isLoggable(record)) {
    return;
    }
    // 2) 格式化
    String message = getFormatter() != null
    ? getFormatter().formatMessage(record)
    : record.getMessage();
    // 3) 写入目标(此处为INSERT log_table VALUES(…))
    logTable.add(record.getLevel() + " | " + message);
    }

    @Override
    public void flush() { /* 数据库提交 */ }

    @Override
    public void close() throws SecurityException { /* 释放连接 */ }

    public java.util.List<String> getLogTable() { return logTable; }

    // 演示
    public static void main(String[] args) {
    Logger logger = Logger.getLogger("db.audit");
    logger.setUseParentHandlers(false);
    DatabaseHandler dbHandler = new DatabaseHandler();
    dbHandler.setLevel(Level.SEVERE); // 只入库严重错误
    dbHandler.setFormatter(new SimpleFormatter());
    logger.addHandler(dbHandler);

    logger.warning("警告不入库");
    logger.severe("支付失败,需要人工介入"); // 入库

    System.out.println("数据库中的日志: " + dbHandler.getLogTable());
    }
    }


    11. 与SLF4J桥接

    11.1 为什么需要桥接

    许多 JDK 内部组件和第三方库(如 HttpClient、部分旧版框架)直接使用 JUL。如果应用主日志走 SLF4J + Logback,就需要把 JUL 日志统一汇入,否则日志散落两处。

    11.2 jul-to-slf4j 桥接步骤

    <!– pom.xml:引入SLF4J桥接器 –>
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jul-to-slf4j</artifactId>
    <version>2.0.13</version>
    </dependency>
    <dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.14</version>
    </dependency>

    import org.slf4j.bridge.SLF4JBridgeHandler;
    import java.util.logging.Logger;

    public class JulToSlf4jBridgeDemo {
    public static void main(String[] args) {
    // 1) 移除根Logger默认的ConsoleHandler(否则JUL日志双份输出)
    Logger root = Logger.getLogger("");
    for (java.util.logging.Handler h : root.getHandlers()) {
    root.removeHandler(h);
    }
    // 2) 安装桥接Handler:JUL日志 → SLF4J → Logback
    SLF4JBridgeHandler.install();
    // 3) 放开JUL级别,让SLF4J端控制过滤
    root.setLevel(java.util.logging.Level.FINEST);

    // 之后所有JUL日志都走Logback的格式与输出目标
    Logger.getLogger("legacy.lib").info("这条JUL日志现在由Logback输出");
    }
    }

    性能注意:桥接通过反射从 LogRecord 提取调用点信息,有额外开销。SLF4J 官方文档建议:在桥接场景配合级别控制(根 Logger 级别不要太低),可减少无效转发。Logback 也提供了 LevelChangePropagator 来把 Logback 的级别变更传播回 JUL,降低开销。

    11.3 反向:SLF4J → JUL?

    不常见。若有库使用 SLF4J-api,而你想用 JUL 输出,可引入 slf4j-jdk14 绑定(SLF4J 1.x 风格)。现代项目一般反过来统一。


    12. 最佳实践与常见陷阱

    12.1 最佳实践清单

    // ✅ 1. Logger存为static final字段(防GC + 复用)
    private static final Logger logger = Logger.getLogger(MyService.class.getName());

    // ✅ 2. 用参数化消息代替字符串拼接(级别不够时不格式化)
    logger.log(Level.INFO, "用户{0}消费{1}元", new Object[]{user, amount});
    // ❌ logger.info("用户" + user + "消费" + amount + "元");

    // ✅ 3. 昂贵参数先做级别判断
    if (logger.isLoggable(Level.FINEST)) {
    logger.finest("快照: " + expensiveToString(obj));
    }

    // ✅ 4. 异常日志带Throwable,保留完整堆栈
    logger.log(Level.SEVERE, "保存订单失败, orderId=" + id, exception);

    // ✅ 5. 关闭向父Handler传播,避免重复输出
    childLogger.setUseParentHandlers(false);

    12.2 常见陷阱

    陷阱现象解决
    Logger未存字段 日志"随机消失",被GC回收 存为 static final 字段
    Handler级别未放开 logger.fine() 无输出 handler.setLevel(Level.ALL)
    忘记关传播 同一条日志打印多次 setUseParentHandlers(false)
    FileHandler未close 日志文件缺尾部/锁文件残留 try-with-resources 或显式close
    输出到err困惑 ConsoleHandler写System.err 了解即可,或换自定义StreamHandler
    占位符语法错 {0} 不生效 JUL用MessageFormat语法,非SLF4J的{}
    默认配置覆盖 自定义配置不生效 检查加载优先级,用-Djava.util.logging.config.file

    12.3 什么场景适合JUL

    • 轻量工具/脚本:不想引入第三方依赖
    • 库开发:JDK 内置,不给使用方强加日志实现(但更推荐 SLF4J-api)
    • 受管控环境:只允许 JDK 标准库
    • 应用主日志:不推荐,建议 SLF4J + Logback/Log4j2

    13. 版本演进

    版本年份变化
    JDK 1.4 2002 首次引入 java.util.logging 全套API(JSR 47):Logger、Handler、Formatter、Level、Filter、LogManager、LogRecord
    JDK 5 2004 Logger.getAnonymousLogger() 等小幅完善
    JDK 7 2011 Logger.getGlobal();SimpleFormatter.format 系统属性支持自定义格式
    JDK 8 2014 Filter 成为函数式接口,支持 Lambda
    JDK 9 2017 模块化:归入 java.logging 模块;默认配置文件路径变为 $JAVA_HOME/conf/logging.properties;引入 PlatformLogger(JDK内部日志)
    JDK 10 2018 PlatformLogger.LoggerFinder SPI:可替换 JDK 内部日志后端(如把JDK内部日志导向自定义实现)
    JDK 21 2023 API 保持稳定,无重大变更;与虚拟线程等并发特性自然兼容

    总结:JUL 自 1.4 引入后 API 高度稳定,演进以小幅增强为主。它的价值在于"永远存在、零依赖";企业级应用通常通过桥接将其纳入 SLF4J 统一体系。

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » Java深入解析篇十三之Java日志API(JUL)详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址