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

Java日志API(JUL)详解


目录

  1. 概述与历史
  2. 核心架构与组件关系
  3. Logger详解
  4. Level日志级别
  5. Handler输出处理器
  6. Formatter格式化器
  7. Filter过滤器
  8. LogRecord与日志流转
  9. LogManager与配置文件
  10. 自定义Handler与Formatter
  11. 与SLF4J桥接
  12. 最佳实践与常见陷阱
  13. 版本演进

1. 概述与历史

1.1 什么是JUL

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

java 复制代码
// 最小可用示例: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 传播。

java 复制代码
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

java 复制代码
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传播

java 复制代码
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 级别两道门槛:

java 复制代码
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 自定义级别

java 复制代码
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

java 复制代码
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 与滚动文件

java 复制代码
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(缓冲转发)

java 复制代码
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 定制:

java 复制代码
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,适合机器解析:

java 复制代码
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

java 复制代码
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 各可挂一个,两者都通过才发布

java 复制代码
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 是日志事件的载体,封装了一次日志调用的全部信息:

java 复制代码
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)

启动方式:

bash 复制代码
# 方式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 完整示例

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 编程式配置

java 复制代码
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

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

java 复制代码
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 桥接步骤

xml 复制代码
<!-- 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>
java 复制代码
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 最佳实践清单

java 复制代码
// ✅ 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 统一体系。

相关推荐
随遇而安zx29 分钟前
Spring Cloud Gateway 微服务网关 设计思想与深度解析
java·spring cloud·微服务·架构
catino37 分钟前
spring-事务@Transactional
java·数据库·spring
luj_176842 分钟前
毒素驱动的生物自毁机制探析
服务器·c语言·开发语言·经验分享·算法
万年咸鱼1 小时前
Java List 接口详解:从基础到实战
java·windows·list
海兰1 小时前
【 Python 量化交易】第6章:数据可视化
开发语言·python·信息可视化
多多鼠1 小时前
Tool Calling的信任边界:从协议校验到执行沙箱的完整链路设计
运维·开发语言·网络·人工智能·python·langchain
Java的搬运工1 小时前
Spring boot 事件监听
java·spring boot·spring·编程语言
啊阿狸不会拉杆1 小时前
《计算机网络-自顶向下方法》1.5 协议层次及其服务模型 读书笔记
开发语言·计算机网络·php
tryxr1 小时前
Chat2Excel 项目网关服务开发
java·gateway·网关模块开发