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 时期各框架各自造轮子的局面。
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 统一体系。
