jul避坑:看懂它的底层逻辑
jul避坑的关键不是背API,而是看懂它的运行链路:Logger接收日志,Level决定放不放行,Handler决定往哪写,Formatter决定长什么样。很多“为什么没输出”“为什么重复打印”的问题,都卡在这条链上。
先给结论:JUL是四段式流水线
想真正 jul避坑,要把 JUL 想成一条流水线:你的代码调用 Logger,Logger 看级别;通过后交给 Handler,Handler 再看自己的级别;最后 Formatter 把 LogRecord 变成文本或 XML。
很多人只盯着 Logger,以为 setLevel 就完事。实际 JUL 里 Logger 和 Handler 都能拦日志,所以同一条 FINE 日志可能在 Logger 放行后,又被 ConsoleHandler 拦掉。这就是新手最常见的“明明配置了却没输出”。
坑一:父子Logger会让日志重复
JUL 的 Logger 按名字形成层级,比如 com.demo 是 com.demo.service 的父级。子 Logger 默认会把日志往父级传,也就是 useParentHandlers=true。
如果你给子 Logger 加了一个 ConsoleHandler,父 Logger 本身也有 ConsoleHandler,一条日志就可能打两遍。解决方式很简单:要么别乱给子 Logger 加 Handler,要么明确 logger.setUseParentHandlers(false)。重复日志别硬忍,越到线上越烦。
坑二:级别继承不是你想的那样
Logger 的 level 可以是 null,表示继承父级。这个设计挺灵活,但排查时也容易绕。你以为某个类是 INFO,实际它可能继承了根 Logger 的 WARNING。
我的做法是:小项目直接配置根 Logger 和根 Handler;稍大的项目按包名配,比如 com.company.service=FINE,其他保持 INFO。不要给每个类单独设级别,配置会碎成渣。
坑三:配置文件加载时机很要命
logging.properties 不是你放进项目就自动生效。常见方式是启动参数加 -Djava.util.logging.config.file=/path/logging.properties。若程序里已经提前创建过 Logger,再加载配置,表现可能和预期不一致。
更稳的做法是让配置在应用启动最早阶段生效。命令行工具可以在 main 方法开头调用 LogManager.getLogManager().readConfiguration(...),服务端项目则尽量交给启动脚本或容器参数。
坑四:Formatter决定排障体验
JUL 默认格式能提供信息,但不一定适合团队排查。单行格式通常比多行更适合 grep、日志平台检索;异常堆栈又需要完整保留,不能为了“好看”把 throwable 丢掉。
如果你自定义 Formatter,记得处理 record.getThrown()。我见过有人只 format 时间、级别、消息,结果线上异常只有一句 failed,堆栈全没了。日志最贵的不是磁盘,是关键时刻没信息。
最后:别和JUL的边界较劲
JUL 的底层逻辑很清楚:轻量、标准库、可配置,但不负责帮你解决所有现代日志问题。它可以完成基础记录、级别过滤、控制台和文件输出,却不擅长复杂结构化日志和大规模日志治理。
所以 jul避坑的终极建议是:用它的简单,别硬凹复杂。小项目把 Logger、Handler、Formatter 理顺就够了;项目一旦进入多人协作、链路追踪、日志平台接入阶段,早点引入 SLF4J 体系,别拖到代码里满地都是 JUL 再迁移。
常见问题
jul避坑最先要检查什么?
先检查 Logger 和 Handler 的级别是否都放行,再检查 useParentHandlers 是否导致重复输出,最后看配置文件是否真的被加载。
JUL为什么会打印两遍日志?
通常是子 Logger 自己有 Handler,同时又把日志传给父 Logger。可以移除多余 Handler,或设置 setUseParentHandlers(false)。
JUL自定义Formatter要注意什么?
必须处理异常对象 record.getThrown(),否则堆栈可能丢失。时间、线程、级别、logger名也建议保留,方便检索。