Java日志框架这个话题,看起来简单,实际上每个Java项目都会遇到,而且一出问题就让人头大。我见过太多同事在排查线上问题时,因为日志配置不对、框架冲突导致关键日志打不出来,硬生生把半小时能解决的问题拖成了半天。这篇东西我会把Java日志体系的来龙去脉、主流框架选型、实际配置技巧、冲突排查思路以及面试高频考点一次性讲透,这些都是我在实际项目中踩坑踩出来的经验,希望能帮你少走弯路。
很多刚入行的同学对日志框架的认知停留在“会用log.info打印信息就行”,但真实项目里的日志问题远比这个复杂。为什么Spring Boot默认用Logback?为什么你的项目里会出现SLF4J的警告?为什么明明配置了日志文件却找不到输出?这些问题如果搞不清楚,遇到线上故障时你会非常被动。
1. Java日志框架的演进:从各自为战到统一门面
1.1 Java日志体系的前世今生
Java日志框架的发展史,本质上是一段“重复造轮子”然后“被迫统一”的历史。早年间Java生态里最流行的日志方案是Log4j 1.x,那时候几乎每个项目都在用,配置简单,功能也算够用。但问题是JDK自己又搞了一套java.util.logging(也就是JUL),Sun官方并不希望你完全依赖第三方库,于是两套方案并存,项目里经常出现“一部分代码用JUL打印,一部分代码用Log4j打印”的混乱局面。
后来Apache又搞出了Apache Commons Logging(也叫JCL),想做一个统一的日志门面,解决“底层用哪个实现”的问题,但JCL的类加载机制设计得比较复杂,在某些应用服务器环境里会出现ClassLoader相关的诡异问题。再后来Ceki Gülcü大神离开了Log4j项目,创造了SLF4J和Logback,SLF4J同样是一个门面,但设计上比JCL干净不少,Logback则是作为SLF4J的原生实现,性能好、功能强,逐渐成了主流。再后来Log4j项目组又推出了Log4j 2.x,走的是异步高性能路线,在某些极端场景下性能表现比Logback更亮眼。
这里有一条很关键的历史线:SLF4J和Logback出自同一个人之手,所以它们俩配合最默契。Log4j 2.x是Apache从零重写的版本,和Log4j 1.x在API上完全不兼容,很多人以为Log4j 2就是Log4j 1的升级版,直接改个版本号就能用,结果一启动就报ClassNotFoundException,这就是不了解历史沿革踩的坑。
1.2 门面框架和实现框架到底怎么分工
很多初学者分不清门面(Facade)和实现(Implementation)的关系,我用一个生活化的类比来解释。门面就像是餐厅的点餐台,你只需要对着点餐台下单,不需要关心后厨是用燃气灶还是电磁炉做的菜。实现框架就是后厨那套具体的烹饪设备。你点的是“鱼香肉丝”,不管是哪个厨师用什么锅炒出来的,端上来的菜要能满足你的需求就行。
对应到Java里,SLF4J就是那个点餐台,它只定义了一套统一的日志API,比如Logger logger = LoggerFactory.getLogger(Xxx.class)、logger.info("xxx")。而Logback、Log4j2、JUL这些就是后厨设备,负责真正把日志写到控制台、文件或者其他地方。你的业务代码只依赖SLF4J的API,底层用哪个实现可以在部署时通过classpath里的jar包来决定,这就是“面向接口编程”思想在日志领域的落地。
注意:门面本身不做日志输出,它只负责把调用转发给真正的实现。如果你项目里只引入了slf4j-api,没有引入任何实现框架,日志代码不会报错,但所有日志都会静默丢失,这个问题在面试里经常被拿来当陷阱题。
2. SLF4J绑定机制与桥接:为什么你会在启动日志里看到一堆警告
2.1 编译期绑定:SLF4J的实现查找原理
SLF4J最核心的机制是“编译期绑定”,这在Java生态里算是比较独特的设计。它不像Spring那样在运行时通过配置去扫描实现类,而是在编译阶段就通过StaticLoggerBinder这个类把门面和实现绑定在一起。简单来说,slf4j-api在编译时会调用LoggerFactory.getLogger()方法,这个方法内部会尝试加载org.slf4j.impl.StaticLoggerBinder类,这个类存在于具体的日志实现jar包里。
所以问题的关键在于:你的classpath下到底放了哪个日志实现jar。如果放的是logback-classic,它里面就有StaticLoggerBinder;如果放的是log4j-slf4j-impl,它也有;如果同时放了两个,classpath里出现了两个StaticLoggerBinder类,SLF4J会随机选一个,然后在启动时打印警告信息告诉你检测到多个绑定。这正是很多项目里所谓的“日志框架冲突警告”的根源。
我在实际项目里见过最典型的场景是:Spring Boot应用(默认自带Logback)被人手动加了一个Log4j2的依赖,然后启动时控制台刷出一大堆SLF4J: Class path contains multiple SLF4J bindings的警告,然后日志输出的行为变得不可控,一会儿输出到控制台,一会儿不输出,一会儿文件里有内容一会儿没有。这种问题你用代码去排查是查不出任何逻辑错误的,纯粹是classpath治理出了问题。
2.2 桥接机制:让老项目里的日志调用全部“改道”
除了绑定机制,SLF4J还提供了一套很聪明的桥接方案,专门用来解决项目里其他依赖库还在用老日志API的问题。比如你引入了一个第三方jar包,这个jar里的代码是用Log4j 1.x API写的,直接在代码里调org.apache.log4j.Logger。如果这个jar包里真的带上了log4j的依赖,你的项目里就等于混入了两套日志实现,非常混乱。
正确的做法是:把原有的log4j依赖排除掉,替换成log4j-over-slf4j这个桥接jar。这个jar包里提供了org.apache.log4j.Logger类,但内部实现只是简单地把所有调用转发给SLF4J API。这样一来,老代码依然编译、运行正常,但日志输出已经统一走底层的Logback或者Log4j2了。
桥接这种方案不是SLF4J一家在用,JCL和JUL也有对应的桥接包,比如jcl-over-slf4j和jul-to-slf4j。这里面有一个需要留意的细节:绝对不要同时把一个桥接包和它对应的原生日志实现jar放在一起。举个例子,log4j-over-slf4j和log4j不能同时存在于classpath,否则会无限循环调用导致栈溢出。这种问题在Maven依赖冲突里极其隐蔽,因为两个jar包表面上是不同groupId的,排除的时候很容易漏掉。
3. 主流日志实现框架对比与选型建议
3.1 Logback、Log4j2、JUL的核心参数对比
说到选型,很多团队在技术评审时都会纠结到底用Logback还是Log4j2。我根据自己的使用经验,把几个主流实现的性能和功能特点整理成一个对比表格,方便你快速做判断。
| 维度 | Logback | Log4j 2.x | JUL |
|---|---|---|---|
| 性能(同步) | 优秀 | 优秀 | 一般 |
| 性能(异步) | 良好 | 极佳(使用LMAX无锁队列) | 较差 |
| 配置方式 | XML | XML / JSON / YAML | properties / XML |
| 与SLF4J集成 | 原生支持 | 需要适配包 | 需要桥接包 |
| 自动重载配置 | 支持(有扫描机制) | 支持 | 不支持 |
| 故障隔离 | 一般 | 支持(重写策略) | 一般 |
| 热度与社区 | 很高 | 高 | 低 |
单从性能数字上看,Log4j2在异步模式下的吞吐量确实能压过Logback一头,尤其适合超高并发、日志量极大的场景。但大部分业务系统的日志量根本到不了那个量级,对绝大多数项目来说,Logback配合Spring Boot使用是最顺手、最省心的方案,因为Spring Boot默认就集成好了,基本零配置导入即用。
Log4j2的优势在于支持无垃圾(Garbage-Free)日志、异步日志性能极其强悍,如果你做的是类似日志采集网关这种日志写入量每秒几万条甚至更高的系统,那Log4j2基本是标配。不过它的配置参数比Logback多不少,学习成本也更高,小项目里用它有点杀鸡用牛刀的感觉。
3.2 异步日志到底怎么就“快”了
很多面试官喜欢问异步日志的原理,其实核心只有一句话:日志的I/O操作本质上是把数据写入文件或者网络,这个过程中线程会阻塞在磁盘I/O或网络I/O上,异步日志就是把这些I/O操作丢给独立的后台线程池去处理,业务线程只负责把日志事件放进队列里,立刻返回继续干活。
这里有一个必须讲清楚的点:异步日志不是没有代价的。它的代价主要在于“缓存区”。如果业务线程往队列里丢日志的速度快过后台线程写入文件的速度,队列就会积压,积压到一定程度就一定会触发丢弃策略或者阻塞策略。Logback的异步Appender默认情况下如果队列满了,会丢弃TRACE、DEBUG、INFO级别的日志,只保留WARN和ERROR级别,保证重要的日志不丢。而Log4j2默认的行为是根据配置的重写策略,选择丢弃日志或者让业务线程阻塞等待。
实际生产中我建议:异步Appender的队列大小不要用默认值,Logback默认是256,对于日志量大的系统太小了,建议调成至少1024或2048。同时要关注线上监控里的队列剩余容量指标,如果长期处于拥挤状态,说明写入能力跟不上,加大队列只是治标不治本,真正要排查的是磁盘写入速度或者日志刷盘频率。
3.3 我个人的选型建议
不用过分纠结选型,大部分情况下我的建议就三条:
如果是新项目,并且用了Spring Boot,直接用默认的Logback,把配置文件从默认的logback-spring.xml改成你自己定义的格式,完全够用。如果你对性能有极致追求,比如要做日志采集系统、高频事件流记录,选Log4j2,但要接受它更复杂的配置体系和学习成本。至于JUL,我基本只在写JDK内置工具或者无法引入第三方依赖的极简环境里才会考虑,日常项目里别用它,配置繁琐而且功能单薄得可怜。
还有一个特殊场景要注意:如果项目里既有老代码用Log4j1,又有新代码想用SLF4J,优先级最高的方案不是强行升级老代码API,而是通过桥接统一到SLF4J,同时保留日志格式和输出目标的一致性。改API容易,但输出格式变化带来的运维成本很容易被忽略。
4. Logback实战配置:从控制台到滚动文件再到异步
4.1 核心组件拆解:Logger、Appender、Layout
Logback的体系继承了Log4j1的三件套设计:Logger负责接收日志请求,Appender负责定义日志输出目的地,Layout负责把日志格式化成一行的字符串。三者分工极其清晰:Logger是你在代码里拿到的对象,通过LoggerFactory.getLogger(Xxx.class)获取,它在配置里对应<logger>标签,可以单独指定某个包的日志级别;Appender则决定日志从哪儿出去,常见的包括ConsoleAppender(控制台)、RollingFileAppender(滚动文件)、AsyncAppender(异步包装);Layout就是那个<pattern>标签里的内容,比如%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n。
大部分项目的最简配置只需要一个根<root>级别定义加一两个Appender。但如果你需要按照业务模块把日志拆分到不同文件,那就要通过<logger>指定对应的包路径,并单独关联一个<appender-ref>。这个功能在排查问题时特别有用,比如你要单独记录订单相关的日志,可以把订单服务的包路径下的日志全部输出到order.log,并且这个文件的日志级别只受这个Logger配置控制,不影响全局配置。
4.2 滚动策略配置:线上日志为什么会写爆磁盘
我觉得滚动策略是Logback配置里最容易踩坑的地方,也是线上故障高发区。如果滚动配置配错了,最直接的后果就是单个日志文件无限增长,直到把磁盘给写满。我遇到过不止一次,一台服务器因为日志文件撑爆磁盘导致应用直接宕机,而那台机器上部署的服务本身TP99都很好,纯粹是被日志拖垮的。
最常用的滚动策略是SizeAndTimeBasedRollingPolicy,也就是按时间和大小双重维度滚动。配置模板通常是这样的:
<appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/app.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>logs/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxFileSize>100MB</maxFileSize> <maxHistory>30</maxHistory> <totalSizeCap>5GB</totalSizeCap> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender>逐项来解释这几个关键参数。fileNamePattern里的%d{yyyy-MM-dd}和%i是滚动命名的核心,%d按日期切换文件,%i用来区分同一天内因大小限制产生的多个文件,两者必须配合使用,否则配置不合法。maxFileSize严格控制单个文件的体积,超过就滚动新文件。maxHistory决定保留多少天的历史文件,这个是配置里最需要和运维对齐的一个值,保留天数太长存储成本高,太短出了问题找日志时可能已经被清了。totalSizeCap是总容量上限,所有日志文件加起来超过这个值,老文件就会被自动删除,相当于一个兜底保险。
我在项目里看到很多新人配置滚动策略时只设置了maxFileSize和maxHistory,不设置totalSizeCap,结果遇到业务高峰期时每天滚动出几十个文件,一个月下来历史文件积累了几百个,磁盘占用比预期大得多。所以我强烈建议这三个参数配套使用,别偷懒。
4.3 生产环境必不可少的Pattern细节
日志格式看起来只是字符串拼拼接接,但配得好不好直接关系到排查问题的效率。一个合格的日志Pattern至少要包含时间、线程名、日志级别、Logger名称和日志正文。但更推荐在生产环境里加上%X{traceId}这类MDC转译符,这在高并发系统里是链路追踪的基础。
MDC(Mapped Diagnostic Context)是SLF4J提供的一个线程上下文容器,你可以在请求入口把traceId放进去,然后在日志Pattern里用%X{traceId}输出。这样从日志文件里按traceId过滤,一次请求经过的所有日志都能完整串起来。实际使用时要特别注意一个点:如果是异步处理任务,子线程默认拿不到父线程的MDC上下文,需要用MDC.getCopyOfContextMap()手动传递,否则子线程打印的日志会少一块关键的上下文信息。
注意:Pattern里不要放太多无意义的信息,比如
%class、%method、%line这种,它们虽然能精确到代码行号,但代价是开启ConversionPattern时需要额外计算调用栈信息,性能影响比较大。线上日志如果量大,建议只保留%logger{36}这种粗粒度信息,性能和安全兼顾。
4.4 多环境配置:如何做到开发测试生产“三套配置一套代码”
Spring Boot环境下,很多人以为需要在application.yml里配置logging相关的属性,其实Logback自己有一套独立的配置文件体系。如果你想针对不同环境使用不同的日志级别、输出路径、保留策略,推荐的做法是用logback-spring.xml这个名字,配合<springProfile>标签做环境区分。
比如开发环境里希望打印DEBUG级别的SQL日志,生产环境只想保留WARN级别以上的日志,可以在同一个配置文件里这样写:
<springProfile name="dev"> <root level="DEBUG"> <appender-ref ref="CONSOLE"/> </root> </springProfile> <springProfile name="prod"> <root level="WARN"> <appender-ref ref="ROLLING"/> </root> </springProfile>这里有个技术点需要明确:用springProfile的配置文件必须命名为logback-spring.xml,而不是logback.xml,因为logback.xml在加载时不经过Spring Boot的Environment处理,springProfile标签不会生效。如果你用了logback.xml还想做环境区分,大概率会出现“配置了但没作用”的诡异现象。
5. 日志冲突排查实录:那些年一起踩过的NoSuchMethodError
5.1 冲突的底层逻辑:ClassLoader和依赖树
日志框架冲突的底层问题,可以归结为一句话:classpath里出现了同一个类的不同版本,或者多个日志实现jar包互相干扰。Java的ClassLoader默认是“谁先加载谁生效”的逻辑,不同jar包里的同名类会出现先到先得的覆盖效应,这就导致你代码里引用的方法签名和实际加载的类不一致,从而抛出NoSuchMethodError或者ClassNotFoundException。
要排查这类问题,最经典的手段就是Maven依赖树分析。在项目根目录执行:
mvn dependency:tree -Dincludes=ch.qos.logback,log4j,org.slf4j这条命令能快速列出所有跟日志相关的依赖,你就能一眼看出哪些jar包被重复引入了、版本是否冲突。IDEA里更方便,直接在pom.xml右键选择Diagrams -> Show Dependencies,可以可视化地看到依赖关系图,定位冲突路径会更直观。
5.2 典型的冲突场景和解决方案
场景一:项目里既有logback又引入了log4j2依赖。
问题现象是启动时SLF4J打印多个绑定警告。解决方案是剔除不需要的实现依赖,保留一个即可。如果是Spring Boot项目,想切换成Log4j2,正确的做法是先在spring-boot-starter里排除spring-boot-starter-logging,再引入spring-boot-starter-log4j2,同时确保引入的Log4j2适配包是log4j-slf4j-impl。
场景二:某个第三方SDK传递依赖了老版本log4j,导致项目里出现Log4j1和SLF4J桥接共存。
解决思路是在引入这个SDK时用<exclusions>把它内部的log4j依赖排除掉,再统一加上log4j-over-slf4j桥接包。这里有个容易犯的错:只排除了log4j:log4j,但SDK可能传递了org.slf4j:slf4j-log4j12,这个包也会造成绑定冲突,排除的时候最好把两者都排查干净。
场景三:业务代码里用了新版SLF4J API,但某个依赖强制传递了旧版slf4j-api。
这个问题的典型报错是NoSuchMethodError: org.slf4j.LoggerFactory.getLogger(...),原因就是旧版slf4j-api根本没有getLogger这样的方法。解决方法是把依赖里的slf4j-api排除掉,然后在pom里显式声明一个较高的版本。
5.3 重复日志问题的排查思路
重复日志和框架冲突不太一样,它通常是因为多条日志路径同时生效导致的。最常见的表现是:一条info日志既打印到了控制台又写进了文件,而且文件里出现了两条一模一样的信息。这个问题的根源大多数出在两个地方。
第一个是<logger>和<root>的Appender重复。比如你在根级别挂了CONSOLEAppender,又在某个包的Logger上挂了同一个CONSOLEAppender,同时没有设置additivity="false",那么这条日志会沿着日志链向上传递,被两个Appender各处理一次。解决方案就是在自定义Logger上设置additivity="false",显式终止向上传递。
第二个是Spring Boot的配置覆盖。Spring Boot的application.yml里也可以配置日志输出文件和Level,这些配置在运行时会覆盖Logback配置里的某些属性。如果你同时在logback-spring.xml和application.yml里配置了日志文件路径,两个配置会叠加生效,导致一份日志被写了两次。这个坑我自己踩过,后来养成了一个习惯:能在外置配置里做的尽量统一放在一个地方,别两处都写。
5.4 故障隔离:日志系统自身挂掉怎么办
这里分享一个容易被忽略的经验。日志框架本身虽然很成熟,但要是配置不当,它反而会成为应用崩溃的元凶。比如日志文件写入失败、磁盘空间满、异步队列无限积压,这些都可能反过来拖垮业务线程。Logback和Log4j2近几年都引入了故障隔离功能,简单说就是当写日志出现异常时,日志系统会主动降级,不再阻塞业务线程,保证主流程不受影响。
这块内容在面试时经常被拔高作为加分项,所以建议你要有一个基本认知:日志是辅助手段,故障隔离的基本逻辑是“宁可丢日志,不能丢业务”。不过这个设计理念也存在一个权衡,比如在审计类、金额交易类的业务里,日志往往具备审计追溯的价值,那就不适合盲目使用丢弃策略,这类系统通常会把日志写入做成同步模式,或者使用独立的可靠通道,确保不丢。
6. 面试高频追问:日志框架的“八股文”和实战鉴别
6.1 经典八股文问题与回答要点
问题一:为什么不能用System.out.println打印日志?
这个问题看起来简单,但回答要是只落在“不好看”“没级别”上,就显得比较浅。比较好的回答要覆盖几个维度:没有分级过滤机制,生产环境没办法控制输出量;没有持久化能力,日志丢失在控制台;严重阻塞,println内部是同步写,线上高并发时影响QPS;没有格式化能力,无法按上下文追踪。最后再补一句“System.out本身还可能有线程安全问题,在JDK里它是通过synchronized控制的,锁竞争直接影响性能”。
问题二:SLF4J的绑定原理是什么?
回答时要抓住“编译期静态绑定”这个关键词。SLF4J在编译时通过StaticLoggerBinder类将门面和实现绑定,这个类存在于实现jar包里,所以切换底层日志框架只需替换依赖jar包,代码不用改一行。然后可以举一个实际案例:Spring Boot默认绑定了Logback,如果想让项目改用Log4j2,只需要处理依赖即可,业务代码里LoggerFactory的调用方式完全不用动。
问题三:Logback和Log4j2比哪个好?
没有绝对的好,只有适不适合。先把两者各自的核心优势讲清楚:Logback和SLF4J同源,配置简洁,天然无缝集成,对Spring Boot最友好;Log4j2支持无锁异步、无垃圾日志、插件化配置,在极端并发日志量下性能更优。再从项目规模、日志量、维护成本、团队熟悉度几个维度说明选型依据,最后给一个经验答案:绝大多数业务系统选Logback就够用,只有日志写入规模极大或者对延迟极度敏感时才值得上Log4j2。
问题四:日志异步就一定快吗?
这个问题要把权衡讲透。异步日志通过队列实现业务线程和I/O线程的解耦,理论上吞吐量确实比同步高。但代价是日志写入有延迟,极端情况下会丢日志(队列满时),同时增加了内存消耗,还可能带来上下文传递的问题。面试官问这个问题的深度其实是想考察你是否理解“技术选型要基于场景”,不要只背结论。
6.2 面试官追问:线上日志突然不打印了怎么办
这个问题很实战,回答时可以按照从现象到根因的排查顺序来组织。先看一眼磁盘空间是否满了,df -h命令查看,这是最容易被忽略的第一原因;然后检查日志文件是否配置了滚动策略,文件是否达到大小上限,因为有些配置下达到上限后应用不会自动切换文件,会直接罢工;再看代码里是否有人动了日志级别配置,比如把某个包调整成了ERROR级别;最后用jstack看一下业务线程是否阻塞在日志输出上,这个能定位到日志I/O是不是成了瓶颈。
这种问题没有标准答案,重点在于展示排查思路的条理性。我在团队里反复强调:平时就要维护一份日志故障排查手册,把过去踩过的坑沉淀下来,出现问题时按清单逐项排查,比临时瞎猜要高效得多。
7. 从日志格式到运维协作:容易被忽视的“最后一步”
7.1 对接日志采集平台时的格式规范
日志不只是写给人看的,现在很多系统都接入了ELK、Splunk等日志采集平台,日志格式是否符合采集端的要求就成了一个值得认真对待的问题。如果日志格式混乱,采集端解析规则就难写,业务字段提取也容易出错,最终导致告警失灵或者检索困难。
我在对接日志平台时常用的约定是:日志统一使用JSON格式输出,或者至少在每行日志里带上固定的键值对形式。Logback的PatternLayoutEncoder很难直接输出JSON,但代码里用net.logstash.logback.encoder.LogstashEncoder可以很轻松地输出结构化JSON。使用JSON日志之后,采集端只需要建立对应的索引映射即可直接使用,不需要为每个应用单独定制解析规则。
7.2 日志容量规划和监控告警
最后再说一个运维层面容易被忽略的事。日志不是无限存储的,需要做容量规划。比如一台机器日志盘大小是100GB,每天日志生成量大约2GB,那至少应该给日志保留30天以内的空间,但这就算得非常紧张。容量规划最佳实践是按实际观察到的写入速率来反推,观察一周峰值,预留至少2倍的余量。
配合容量规划,一定要设置日志磁盘使用率的监控告警,75%就要预警,85%就要强制介入清理。很多公司在这个环节做得不够,等到磁盘真的写满才触发告警,此时应用已经拉胯了。日志监控看似只是运维的事,但对开发者来说,了解这些可以在设计日志策略时更有全局视角,不至于只盯着自己那几行配置代码。
8. 我踩过的几个坑,写给你避雷
日志框架相关的坑,很难在教科书里找到答案,基本都是遇到一次才能沉淀下来的经验。我最后再分享几个自己真实踩过、也帮团队同事排过的坑,希望能给你提个醒。
第一个是配置文件位置问题。Spring Boot项目里如果同时存在logback.xml和logback-spring.xml,很多人不清楚到底哪个生效。Spring Boot的LoggingInitializationContext对这两种文件名的优先级是不同的,logback-spring.xml拥有更高优先级,但如果你用的老版本Spring Boot,行为又可能不一样。最稳妥的做法是项目里只保留一个Logback配置文件,别两个都放。
第二个是MDC丢上下文的问题。在高并发场景下,用线程池做异步处理时,需要显式传递MDC上下文。我见过很多人在本地测试时一切正常,一上生产环境就发现异步部分的日志查不到traceId,排查了半天最后发现就是没做MDC传递。这个坑的隐蔽性很强,因为代码层面不会报任何错误。
第三个是控制台日志在生产环境造成的隐形成本。很多人习惯在配置里保留ConsoleAppender,方便开发时排查问题。但生产环境如果继续保留这个Appender,日志会源源不断地写到标准输出,被容器平台收集的话还会产生额外的存储和网络开销,而且控制台输出的性能比写文件差得多。我的习惯是开发环境保留控制台,测试和生产环境只保留滚动文件输出。
说到底,Java日志框架不是一门高深的技术,但细节非常多。把日志体系彻底搞懂,你会发现排查线上问题的效率会提升一个档次,面试时遇到相关话题也不再会心虚。希望这篇东西能帮你把日志这套体系理清楚,以后在项目里和日志相关的问题,都能少走一点弯路。