Java-01-基础篇-JDK日志(JUL)
一,JUL
JDK 自带的日志(java.util.logging,简称 JUL)这是 Java 标准库自带的日志框架,无需额外依赖
1.1 基本实现
package com.toast.learn.spring.core.logger.jdk;import java.util.logging.Level;
import java.util.logging.Logger;/*** @author toast* @time 2025/9/11* @remark*/
public class JDKLoggingSimpleExample {private static final Logger logger = Logger.getLogger(JdkLoggingExample.class.getName());public static void main(String[] args) {logger.info("This is an info message");logger.warning("This is a warning message");logger.severe("This is a severe (error) message");// 可设置日志级别logger.setLevel(Level.ALL);}
}
输出结果
> Task :spring-learn:run
9月 11, 2025 6:05:51 下午 com.toast.learn.spring.core.logger.jdk.JDKLoggingExample01 main
信息: This is an info message
9月 11, 2025 6:05:51 下午 com.toast.learn.spring.core.logger.jdk.JDKLoggingExample01 main
警告: This is a warning message
9月 11, 2025 6:05:51 下午 com.toast.learn.spring.core.logger.jdk.JDKLoggingExample01 main
严重: This is a severe (error) message
可以看出,JDK自带的支持国际化,会根据本地的Locale 选择输出对应的日志级别内容
二,日志级别
JDK的日志级别,定义在 java.util.logging.Level 类中,部分代码如下:
package java.util.logging;public class Level implements Serializable {private static final String defaultBundle = "sun.util.logging.resources.logging";private final String name;private final int value;private final String resourceBundleName;private transient String localizedLevelName;private transient Locale cachedLocale;public static final Level OFF = new Level("OFF", Integer.MAX_VALUE, "sun.util.logging.resources.logging");public static final Level SEVERE = new Level("SEVERE", 1000, "sun.util.logging.resources.logging");public static final Level WARNING = new Level("WARNING", 900, "sun.util.logging.resources.logging");public static final Level INFO = new Level("INFO", 800, "sun.util.logging.resources.logging");public static final Level CONFIG = new Level("CONFIG", 700, "sun.util.logging.resources.logging");public static final Level FINE = new Level("FINE", 500, "sun.util.logging.resources.logging");public static final Level FINER = new Level("FINER", 400, "sun.util.logging.resources.logging");public static final Level FINEST = new Level("FINEST", 300, "sun.util.logging.resources.logging");public static final Level ALL = new Level("ALL", Integer.MIN_VALUE, "sun.util.logging.resources.logging");private static final Level[] standardLevels;private static final long serialVersionUID = -8176160795706313070L;/** 忽略其他代码 */
}
从这里也可以看出日志级别的国际化并没有直接国际化的内容,日志级别的国际化的相关资源都存储在指定的位置。
日志级别的国际化资源包存储位置:"sun.util.logging.resources.logging"
第三个参数是资源包名(ResourceBundle),用于:
本地化级别名称(如中文环境下显示“信息”而不是“INFO”)
提供标准错误消息模板 messages_zh_CN.properties 。
public final class logging_zh_CN extends ListResourceBundle {public logging_zh_CN() {}protected final Object[][] getContents() {return new Object[][]{{"ALL", "全部"}, {"CONFIG", "配置"}, {"FINE", "详细"}, {"FINER", "较详细"}, {"FINEST", "非常详细"}, {"INFO", "信息"}, {"OFF", "禁用"}, {"SEVERE", "严重"}, {"WARNING", "警告"}};}
}
2.1 完整级别列表(按严重程度从高到低排序)
|
| 关闭所有日志,最高“级别”,实际用于禁用日志输出 | 临时关闭日志、性能压测 |
| 1000 | 严重错误,系统不可用、关键功能失败(如数据库连接失败、空指针未捕获) | 生产环境必须记录 |
| 900 | 警告,潜在问题但系统仍可运行(如配置缺失使用默认值、重试成功) | 需要关注,可能预示未来故障 |
| 800 | 信息性消息,记录系统关键节点(如启动完成、用户登录、定时任务触发) | 生产环境默认开启 |
| 700 | 配置信息,记录初始化参数、环境配置(如端口、数据源URL、线程池大小) | 调试部署问题、环境差异 |
| 500 | 调试信息(基本),方法入口/出口、关键变量值 | 开发/测试环境 |
| 400 | 更细粒度调试,循环内部、分支判断、中间状态 | 详细调试复杂逻辑 |
| 300 | 最详细调试,每行关键代码、对象内部状态、网络包内容 | 深度排查问题,性能开销大 |
|
| 记录所有级别,最低“级别”,用于开启全部日志 | 本地调试、问题复现 |
✅ 数值越小,级别越低,日志越详细
❗ 只有当日志级别 ≥ Logger 设置的级别时,才会被记录
2.2 日志级别输出案例
package com.toast.learn.spring.core.logger.jdk;import java.util.logging.Level;
import java.util.logging.Logger;/*** @author toast* @time 2025/9/11* @remark*/
public class LogLevelExample {private static final Logger logger = Logger.getLogger(LogLevelExample.class.getName());public static void main(String[] args) {// 设置日志级别为 INFO(默认通常是 INFO)logger.setLevel(Level.INFO);logger.severe("数据库连接失败!"); // ✅ 输出(SEVERE >= INFO)logger.warning("缓存未命中,使用默认值"); // ✅ 输出(WARNING >= INFO)logger.info("系统启动完成"); // ✅ 输出(INFO >= INFO)logger.config("监听端口: 8080"); // ❌ 不输出(CONFIG=700 < INFO=800)logger.fine("进入 doSomething 方法"); // ❌ 不输出logger.finest("变量 x = 100"); // ❌ 不输出}
}
输出结果:
9月 11, 2025 6:18:03 下午 com.toast.learn.spring.core.logger.jdk.LogLevelExample main
严重: 数据库连接失败!
9月 11, 2025 6:18:03 下午 com.toast.learn.spring.core.logger.jdk.LogLevelExample main
警告: 缓存未命中,使用默认值
9月 11, 2025 6:18:03 下午 com.toast.learn.spring.core.logger.jdk.LogLevelExample main
信息: 系统启动完成
三,Handler 日志处理
JDK 自带的日志系统(java.util.logging, JUL)完全支持将日志输出到文件,甚至可以配置多个输出目标(如同时输出到控制台和文件)、按大小或时间滚动、设置编码等。
日志的默认输出是控制台,是因为默认配置只启用了 ConsoleHandler
。我们可以通过配置 FileHandler
来输出到文件。
package com.toast.learn.spring.core.logger.jdk;import java.io.IOException;
import java.util.logging.*;/*** @author toast* @time 2025/9/11* @remark*/
public class JdkLoggingToFileExample {private static final Logger logger = Logger.getLogger(JdkLoggingToFileExample.class.getName());public static void main(String[] args) {try {// 创建 logs 目录java.nio.file.Files.createDirectories(java.nio.file.Paths.get("./logs"));// 创建 FileHandlerFileHandler fileHandler = new FileHandler("./logs/app.log", 10 * 1024 * 1024, 5, true);fileHandler.setEncoding("UTF-8");fileHandler.setLevel(Level.ALL);// 设置格式SimpleFormatter formatter = new SimpleFormatter() {@Overridepublic String format(LogRecord record) {return String.format("[%1$tF %1$tT] [%2$-7s] %3$s - %4$s%n",record.getMillis(),record.getLevel().getLocalizedName(),record.getLoggerName(),record.getMessage());}};fileHandler.setFormatter(formatter);// 添加到 Logger(建议添加到 root logger)Logger rootLogger = Logger.getLogger("");rootLogger.addHandler(fileHandler);// 可选:关闭父级处理器避免重复(如果不需要控制台输出)// logger.setUseParentHandlers(false);logger.info("This is an info message");logger.warning("This is a warning message");logger.severe("This is a severe (error) message");} catch (IOException e) {e.printStackTrace();}}
}
输出结果-控制台:
9月 11, 2025 6:32:24 下午 com.toast.learn.spring.core.logger.jdk.JdkLoggingToFileExample main
信息: This is an info message
9月 11, 2025 6:32:24 下午 com.toast.learn.spring.core.logger.jdk.JdkLoggingToFileExample main
警告: This is a warning message
9月 11, 2025 6:32:24 下午 com.toast.learn.spring.core.logger.jdk.JdkLoggingToFileExample main
严重: This is a severe (error) message
输出结果-文件:
可以发现每一个Handler 日志输出处理都有一个独立日志格式属性。上述代码并且设置控制台的日志格式,但是文件的输出设置日志格式。
JDK 除了提供的控制台,File输出,还提供了MemoryHandler,SocketHandler,StreamHandler,一个是往内存输出,一个是通过网络输出出去,和最后OutputStream 流输出
四,日志格式化
JDK 自带的日志支持日志自定义格式的。
package com.toast.learn.spring.core.logger.jdk;import java.util.logging.*;/*** @author toast* @time 2025/9/11* @remark*/
public class LogFormatExample {private static final Logger logger = Logger.getLogger(JdkLoggingExample.class.getName());public static void main(String[] args) {// 获取控制台处理器Handler[] handlers = logger.getParent().getHandlers(); // 通常从 root logger 获取for (Handler handler : handlers) {if (handler instanceof ConsoleHandler) {// 创建自定义格式SimpleFormatter formatter = new SimpleFormatter() {@Overridepublic String format(LogRecord record) {return String.format("[%1$tF %1$tT] [%2$-7s] %3$s - %4$s%n",record.getMillis(),record.getLevel().getLocalizedName(),record.getLoggerName(),record.getMessage());}};handler.setFormatter(formatter);}}logger.info("This is an info message");logger.warning("This is a warning message");logger.severe("This is a severe (error) message");}
}
输出内容:
[2025-09-11 18:28:48] [信息 ] com.toast.learn.spring.core.logger.jdk.JdkLoggingExample - This is an info message
[2025-09-11 18:28:48] [警告 ] com.toast.learn.spring.core.logger.jdk.JdkLoggingExample - This is a warning message
[2025-09-11 18:28:48] [严重 ] com.toast.learn.spring.core.logger.jdk.JdkLoggingExample - This is a severe (error) message
五,JDK 日志设置挺全面的,为什么流行不起来
你说:“JDK 自带的日志设置得也并不错,为什么会不流行使用?”
✅ 你的观察是对的 —— 从功能上讲,JUL(java.util.logging)确实“不差”:
- 支持级别控制(INFO/WARN/SEVERE…)
- 支持 Handler(输出到控制台、文件、网络等)
- 支持 Formatter(自定义格式)
- 支持 Filter(过滤日志)
- 支持 ResourceBundle(国际化)
- 线程安全、模块化支持、配置文件驱动
但“功能完整” ≠ “好用” ≠ “流行”。
🚫 为什么 JUL 不流行?—— 7 大核心原因
1️⃣ 🧩 配置复杂、不够灵活(最大痛点)
- JUL 依赖
logging.properties
,语法老旧、不直观。 - 想按天滚动日志?❌ 不支持,只能按大小。
- 想输出 JSON 格式?❌ 很难,需自定义 Formatter。
- 想异步输出避免阻塞?❌ 无内置支持。
- 想动态修改日志级别(不重启)?❌ 需要自己写代码或依赖 JMX。
🆚 Logback/Log4j2:支持 XML/YAML/JSON 配置、按天/小时滚动、异步日志、热更新、JSON 格式、条件配置等。
2️⃣ 🐢 性能一般,尤其高并发下
- JUL 的 Handler 默认是同步阻塞写入(如写文件、网络)。
- 没有内置异步 Appender(Log4j2 有
AsyncAppender
,Logback 有AsyncAppender
)。 - 在微服务、高吞吐场景下,容易成为性能瓶颈。
📊 性能对比(粗略):
- Log4j2 (异步) > Logback > JUL
3️⃣ 🧱 扩展性差,生态弱
- 想输出到 Kafka、Elasticsearch、Graylog?❌ JUL 没有官方支持。
- 想集成 MDC(Mapped Diagnostic Context,如 traceId)?❌ 需要自己封装。
- 社区几乎没有第三方 Handler/Formatter 插件。
🆚 Logback/Log4j2:海量插件、开箱即用、社区活跃。
4️⃣ 🤝 与主流框架集成差
- Spring Boot 默认用 Logback,不加载 JUL 的
logging.properties
。 - 如果你想在 Spring Boot 里用 JUL,需要手动排除默认日志、桥接、配置 —— 非常麻烦。
- 大部分开源库(如 Hibernate、Netty、Apache Commons)默认使用 SLF4J —— 如果你用 JUL,需要桥接(
jul-to-slf4j
),增加复杂度。
✅ SLF4J + Logback 成为事实标准 → JUL 被边缘化。
5️⃣ 📜 API 设计不够友好
JUL 的 API 冗长:
- java1Loggerlogger = Logger.getLogger(MyClass.class.getName()); // 冗长
vs SLF4J:
- java1Loggerlogger = LoggerFactory.getLogger(MyClass.class); // 简洁
不支持 {}
占位符(容易造成字符串拼接性能浪费):
- java1234⌄// JUL:必须手动判断是否启用if (logger.isLoggable(Level.FINE)) { logger.fine("User " + name + " logged in at " + time); // 性能差!}
vs SLF4J:
- java1logger.debug("User {} logged in at {}", name, time); // 自动判断,无性能损耗
6️⃣ 🌐 历史包袱 & 社区惯性
- JUL 是 2002 年随 JDK 1.4 推出的,设计较早。
- 2006 年 Log4j 作者又推出 SLF4J + Logback,设计更现代、性能更好。
- 2014 年 Log4j2 诞生,性能碾压 JUL。
- 开发者一旦习惯 SLF4J,就不会回头用 JUL —— 生态一旦形成,很难逆转。
📈 趋势:JUL → Log4j → SLF4J + Logback/Log4j2
7️⃣ 🛠 调试和运维不友好
JUL 默认输出格式不统一、不美观:
- 129月 11, 2025 5:07:41 下午 com.example.MyClass main信息: This is an info message
Logback/Log4j2 默认格式清晰、可读性强:
- 12025-09-11 17:07:41.123 [main] INFO com.example.MyClass - This is an info message
- 缺少“开箱即用”的生产级配置(如按天滚动、压缩归档、错误分离等)。
✅ 什么时候适合用 JUL?
虽然不推荐新项目使用,但以下场景 JUL 仍有价值:
JDK 工具/小型工具类 | 不想引入第三方依赖,JUL 零依赖 |
Android 开发(早期) | 早期 Android 支持 JUL(现在推荐 Timber + SLF4J) |
阅读 JDK 源码 | JDK 内部大量使用 JUL,理解它有助于阅读源码 |
维护老项目 | 有些老系统仍在用 JUL,需要维护 |
📊 流行度对比(2025 年)
SLF4J + Logback | ✅✅✅ 80%+ | Spring Boot 默认,生态最成熟 |
SLF4J + Log4j2 | ✅✅ 15% | 性能最强,适合高并发 |
JUL (java.util.logging) | ❌ <5% | 仅用于 JDK 内部或老项目 |
Log4j 1.x | ❌ 已淘汰 | 安全漏洞,不再推荐 |
📉 JUL 的使用率逐年下降,尤其在云原生、微服务架构下几乎绝迹。
💡 总结:为什么“功能不差”却不流行?
✍️ “JUL 是一辆功能齐全的老爷车 —— 有方向盘、有刹车、能跑,但没人想开它上高速。”
- 它诞生太早,设计不够现代。
- 性能、扩展性、易用性被后起之秀全面超越。
- 生态和社区惯性让它逐渐边缘化。