
接手一个跑了七八年的老系统时我最怕看到的不是看不懂的业务代码而是src/main/resources下那个躺了好几年、没人敢动的log4j.xml。删了怕日志全丢改一行怕线上出问题最后所有人绕着走。可真遇到日志文件不按天切同一个异常打了两遍生产环境只有控制台输出没有落盘这类问题时你还是得回到这个文件里一行一行地把它拆开看。这篇东西就是把这些年我在Log4j和Log4j.xml 配置上踩过的坑、验证过的写法整理出来。需要说明的是Log4j.xml 配置实际上指向两套完全不同的东西一套是 Log4j 1.x 时代的log4j.xml另一套是 Log4j 2.x 的log4j2.xml。两者的 XML 结构、元素名、加载机制都不一样把 1.x 的写法抄到 2.x 里配置文件会被直接忽略连报错都不给你。所以我会先把这两条线拆清楚再往下讲 appender 选型、logger 继承、格式串解析、日志分流和排错链路。适合正在维护老项目的后端同学也适合刚接触日志配置、想一次性搞明白原理的人。1. 先分清你手上的是 1.x 还是 2.x两套 XML 不能混用在动手改任何一行配置之前先花两分钟确认依赖树里到底是哪个日志实现。这一步看着废话但我见过太多人改了半天log4j2.xml结果项目实际加载的是log4j.xml因为pom.xml里引的是 1.2.x而 2.x 的配置文件在类路径上根本没被识别。判断方法很简单看依赖里是log4j:log4j:1.2.x还是org.apache.logging.log4j:log4j-core:2.x。前者配log4j.xml后者配log4j2.xml配置文件名本身就是错的内容写得再对也没用。1.1 DOCTYPE 和根节点是最好认的分水岭Log4j 1.x 的log4j.xml必须以 DOCTYPE 开头根节点是带命名空间的log4j:configuration而 Log4j 2.x 的log4j2.xml没有 DOCTYPE根节点就是普通的Configuration。这个区别不是风格问题是解析器层面的硬要求1.x 用的是DOMConfigurator它依赖 DTD 校验DOCTYPE 缺失或者写错整个文件会被判定为非法直接跳过2.x 用的是自己的插件化解析器不需要 DTD但有自己的一套元素注册表写错元素名会以未知元素的形式记在内部日志里。功能Log4j 1.xlog4j.xmlLog4j 2.xlog4j2.xml根节点log4j:configurationConfiguration是否需要 DOCTYPE需要且必须正确不需要输出目的地容器appenderAppenders下的具名元素参数写法param name valueXML 属性或Property布局layout class...PatternLayout pattern...日志器容器logger/categoryLoggers下的Logger根日志器rootRoot这张表建议收藏。跨项目切换时最容易出错的就是param和属性这两种写法体系——1.x 几乎所有可配置项都通过param传2.x 则优先用元素属性虽然 2.x 也兼容一部分param风格但支持的字段是有限的。1.2 配置文件是从哪儿被加载的Log4j 1.2 的加载逻辑大致是先看系统属性log4j.configuration有没有指定路径没指定就去类路径下找log4j.properties找不到再找log4j.xml。注意这个顺序——属性文件优先级高于 XML 文件。这就是我明明配了 log4j.xml 却不生效最经典的元凶某个依赖 jar 里塞了一个log4j.properties它先被加载了你的 XML 从头到尾没被读过。另外要留意log4j.configuration的值对 1.x 来说必须是能被URL解析的形式本地文件要写成file:/opt/app/conf/log4j.xml直接用/opt/app/conf/log4j.xml在某些版本下会被当成相对类路径找找不到就静默忽略。Log4j 2.x 的加载顺序则是按扩展名分优先级先是带-test后缀的log4j2-test.properties、log4j2-test.yaml/yml、log4j2-test.json/js、log4j2-test.xml然后才是正式的log4j2.properties、log4j2.yaml/yml、log4j2.json/js、log4j2.xml。同优先级下 properties 排在 xml 前面也就是说只要类路径上存在log4j2.properties你的log4j2.xml就永远轮不上。2.x 用系统属性log4j.configurationFile显式指定路径支持file:前缀也支持直接给绝对路径。一条通用经验永远不要在类路径根目录同时放多份日志配置文件。要么统一成一种格式要么在启动脚本里显式指定绝对路径把加载入口钉死别依赖框架的默认查找顺序。1.3 两份可以直接抄的最小可用配置先给 Log4j 1.x 的版本能同时输出到控制台和按天滚动的文件?xml version1.0 encodingUTF-8? !DOCTYPE log4j:configuration SYSTEM log4j.dtd log4j:configuration xmlns:log4jhttp://jakarta.apache.org/log4j/ debugfalse appender nameCONSOLE classorg.apache.log4j.ConsoleAppender param nameTarget valueSystem.out/ param nameEncoding valueUTF-8/ layout classorg.apache.log4j.PatternLayout param nameConversionPattern value%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p [%t] %c{1} - %m%n/ /layout /appender appender nameDAILY classorg.apache.log4j.DailyRollingFileAppender param nameFile value/data/logs/app/app.log/ param nameAppend valuetrue/ param nameEncoding valueUTF-8/ param nameDatePattern value.yyyy-MM-dd/ layout classorg.apache.log4j.PatternLayout param nameConversionPattern value%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p [%t] %c - %m%n/ /layout /appender logger namecom.example.order additivityfalse level valuedebug/ appender-ref refDAILY/ /logger root priority valueinfo/ appender-ref refCONSOLE/ appender-ref refDAILY/ /root /log4j:configuration再给 Log4j 2.x 的对应版本用RollingFile做按天加按大小的双策略滚动?xml version1.0 encodingUTF-8? Configuration statusWARN monitorInterval60 Properties Property nameLOG_HOME/data/logs/app/Property Property namePATTERN%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%t] %logger{36} - %msg%n/Property /Properties Appenders Console nameCONSOLE targetSYSTEM_OUT PatternLayout pattern${PATTERN}/ /Console RollingFile nameROLLING fileName${LOG_HOME}/app.log filePattern${LOG_HOME}/app-%d{yyyy-MM-dd}-%i.log.gz PatternLayout pattern${PATTERN}/ Policies TimeBasedTriggeringPolicy interval1 modulatetrue/ SizeBasedTriggeringPolicy size200 MB/ /Policies DefaultRolloverStrategy max30/ /RollingFile /Appenders Loggers Logger namecom.example.order leveldebug additivityfalse AppenderRef refROLLING/ /Logger Root levelinfo AppenderRef refCONSOLE/ AppenderRef refROLLING/ /Root /Loggers /Configuration这两份配置里的每一个点后面都会展开讲但有一处值得先点出来statusWARN这个属性是 Log4j 2.x 排查问题的第一把钥匙。它控制 Log4j 自身的内部日志级别默认是ERROR配置写错了基本什么都不说。改成TRACE之后启动一次控制台会把加载了哪个配置文件识别了哪些 logger哪个元素没认出来全部打出来排错效率提升非常明显。2. appender 选型控制台、按大小滚动、按天滚动的实际取舍用哪个 appender这个问题本质上是在回答三个子问题日志给谁看、写到哪里、写满了怎么办。本地开发环境给开发者看用ConsoleAppender就够生产环境要留痕、要能被日志采集程序读到就必须落盘而且必须滚动否则磁盘迟早被撑爆。很多人配错 appender不是因为不会写 XML而是没想清楚日志文件的最大生命周期是多少天单文件多大合适旧文件删除策略由谁负责。2.1 ConsoleAppender 不是配了就一定能看见Log4j 1.x 的ConsoleAppender默认目标是System.out可以通过param nameTarget valueSystem.err/改到标准错误流。这里有个坑很多日志采集容器会把stdout和stderr分开收集如果你把 ERROR 日志全丢到stderr、INFO 全丢到stdout采集端就得分两个源去拼反而麻烦。我的习惯是统一走System.out用日志级别字段去区分把分流工作交给采集规则不交给流。Log4j 2.x 里控制台 appender 是Console name... targetSYSTEM_OUT/注意这里的目标值是全大写带下划线的枚举名SYSTEM_OUT/SYSTEM_ERR写成System.out会直接报无效值。这个细节在从 1.x 迁移时特别容易翻车因为 1.x 那边写的就是System.out。另外一个高频问题是本地能打日志打成 jar 之后控制台空了。这通常不是 appender 的问题而是spring-boot-starter-log4j2或者容器的日志桥接把System.out重定向了需要先确认输出流有没有被别的框架接管。2.2 RollingFileAppenderMaxFileSize 和 MaxBackupIndex 必须成对出现Log4j 1.x 的org.apache.log4j.RollingFileAppender是按大小滚动的靠两个参数控制appender nameFILE classorg.apache.log4j.RollingFileAppender param nameFile value/data/logs/app/app.log/ param nameMaxFileSize value100MB/ param nameMaxBackupIndex value20/ param nameAppend valuetrue/ param nameEncoding valueUTF-8/ layout classorg.apache.log4j.PatternLayout param nameConversionPattern value%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p [%t] %c - %m%n/ /layout /appenderMaxFileSize是单个文件的上限支持KB、MB、GB单位MaxBackupIndex是保留的历史文件个数。这两个参数必须一起配只配前者不配后者滚动出来的app.log.1、app.log.2会无限累积。我见过一个线上例子MaxFileSize设了 10MB没设MaxBackupIndex结果一天下来堆了两千多个备份文件ls都刷屏。容量估算的小方法先假设单条日志平均 300 字节按业务峰值 QPS 估算每天的日志量。比如峰值 200 QPS、每条 300 字节、每天有效写入时长 8 小时那么单日约200 × 300 × 3600 × 8 ≈ 1.7 GB。如果磁盘给日志预留 50 GB那就得保留大约 30 天反推MaxFileSize200MB时MaxBackupIndex至少设到 8 到 10 才够再配合外部清理脚本。这只是粗算实际还要留出异常堆栈爆量的余量。还有一点必须提醒Log4j 1.x 的RollingFileAppender在多进程写同一个文件时是有风险的因为它不是进程安全的。同一个应用部署两个实例指向同一个日志文件滚动时可能出现内容交错甚至文件被截断。多实例要么把文件名带上实例标识如${sys:hostname}要么就让每个实例写自己的目录。2.3 DailyRollingFileAppender 的问题与替代思路DailyRollingFileAppender看着很美好——DatePattern写成.yyyy-MM-dd就能按天切。但它有两个绕不过去的缺陷一是没有最大保留数量的参数历史文件只能靠外部脚本清理二是它对跨天切换的判定依赖定时器如果进程在某天的午夜前后长时间 Full GC 或者被挂起切换时机可能偏移同一天产生两个文件。我的实际做法是在 Log4j 1.x 项目里尽量避免用DailyRollingFileAppender改用RollingFileAppender按大小滚动 外部清理脚本逻辑更确定。如果坚持要按天那至少要配一个日志清理任务。而在 Log4j 2.x 里就简单多了RollingFile可以直接挂时间策略加删除策略RollingFile nameROLLING fileName${LOG_HOME}/app.log filePattern${LOG_HOME}/app-%d{yyyy-MM-dd}-%i.log.gz PatternLayout pattern${PATTERN}/ Policies TimeBasedTriggeringPolicy interval1 modulatetrue/ SizeBasedTriggeringPolicy size200 MB/ /Policies DefaultRolloverStrategy Delete basePath${LOG_HOME} maxDepth1 IfFileName globapp-*.log.gz/ IfLastModified age30d/ /Delete /DefaultRolloverStrategy /RollingFile这里的modulatetrue是个容易被忽略但很有用的参数它让滚动边界对齐到时间区间的起点比如按天滚就对齐到 00:00而不是从应用启动时刻开始算 24 小时。不设这个参数凌晨三点启动的进程会在下午三点切文件看起来非常别扭。另外注意filePattern里必须同时包含%d或%i中的至少一个否则滚动策略无处落地压缩和删除都不会正常工作。加.gz后缀就会自动压缩历史文件体积能省一大半代价是写历史文件那一刻略有一点 CPU 开销。2.4 文件路径、编码、append 三个最容易配错的参数路径File参数写相对路径如logs/app.log时解析基准是 JVM 的工作目录不是类路径。你在 IDE 里跑工作目录是项目根目录日志出现在./logs打成 jar 之后用java -jar启动工作目录变成启动命令所在目录日志就跑到别的地方去了。用 systemd 或容器启动时更玄学工作目录可能是/。所以生产环境的日志路径一律写绝对路径或者用log4j2.xml的${sys:LOG_HOME}、${env:LOG_HOME}从环境变量注入让部署脚本控制落点。编码Encoding参数不写默认跟平台相关中文环境在 Windows 上一般是 GBK在 Linux 上取决于file.encoding。文件写了 UTF-8用 GBK 的编辑器打开中文全是乱码——这个问题下面第 4 章还会细讲。AppendLog4j 1.x 的FileAppender和RollingFileAppender默认Appendtrue也就是追加。设成false每次启动会清空日志文件。偶尔有人为了每次看到干净日志把它关了结果重启后事故现场全没了。这个参数除非有极其明确的理由否则不要动。Log4j 2.x 里对应的属性叫append同样是默认true。3. logger 的继承关系为什么你的日志打了两遍同一条日志打了两遍是我在群里被问得最多的问题之一九成以上原因都是additivity没设对。要弄明白它得先理解 Log4j 的 logger 命名规则——它不是一个平铺的名字列表而是一棵倒过来的树。3.1 logger 的名字就是包名前缀继承链按点拆分Log4j 把 logger 名字按.拆成层级。配置里写了一个叫com.example.order的 logger那么在代码里用LoggerFactory.getLogger(com.example.order.service.PayService)或者直接LoggerFactory.getLogger(PayService.class)拿到的 logger都会命中这个配置。查找规则是这样给定 logger 名a.b.c.d日志组件会依次往上找a.b.c.d、a.b.c、a.b、a、最后到root第一个在配置里显式声明了级别的就决定这条日志是否输出。appender 则是另一个维度从命中的那个 logger 开始把它的 appender 全部执行一遍然后如果additivity为true继续往上找父级 logger 的 appender一直追加到 root 为止。这套机制的好处是配置极省只要在 root 上配一个 appender所有 logger 自动继承不用一个个写。坏处也正是它——很多人没意识到继承包含 appender 的累加。3.2 additivityfalse 关掉的到底是什么看这个典型误配appender nameCONSOLE classorg.apache.log4j.ConsoleAppender layout classorg.apache.log4j.PatternLayout param nameConversionPattern value%-5p %c - %m%n/ /layout /appender appender nameFILE classorg.apache.log4j.RollingFileAppender param nameFile value/data/logs/app/order.log/ layout classorg.apache.log4j.PatternLayout param nameConversionPattern value%-5p %c - %m%n/ /layout /appender logger namecom.example.order level valuedebug/ appender-ref refFILE/ /logger root priority valueinfo/ appender-ref refCONSOLE/ /rootcom.example.order下的日志会先写进FILE然后因为additivity默认是true继续往上走到 root又写了一遍CONSOLE。如果 root 上也挂了FILE那就是同一个文件被写两遍。加上additivityfalse这条继承链就在这个 logger 处断掉只走它自己的 appender。注意additivity只影响 appender 的继承不影响级别的继承。设成false不会让子 logger 的级别失效。判断要不要设我的经验是只要一个 logger 显式声明了自己专属的 appender并且这个 appender 的用途和 root 的 appender 有重叠比如都写文件就把 additivity 设成 false。像只在 root 上配 appender、logger 里只改级别的场景就不要动它否则日志会彻底断掉。3.3 root 不是默认而是兜底配错会全丢日志有个很隐蔽的故障日志文件是空的但程序运行正常也没有任何报错。排查到最后发现 root 上只写了rootpriority valueinfo//root一个appender-ref都没有。这种情况下所有没被专门配置的 logger 都无处输出日志就这么凭空消失了而且不会抛异常。Log4j 1.x 在这种情况下会在启动时打印类似log4j:WARN No appenders could be found for logger (...)的提示很多人把它当成噪音直接忽略。看到这类提示就该警觉。Log4j 2.x 更直白如果压根找不到配置文件会打印No log4j2 configuration file found. Using default configuration: logging only errors to the console.意思是只把 ERROR 打到控制台其他全部丢弃——这也是为什么升级到 2.x 之后突然觉得日志变少了其实是一直在用默认配置在跑。4. PatternLayout 逐字段拆解每个百分号都有代价格式串是log4j.xml里改得最频繁的部分也是性能问题最容易被埋进去的地方。很多人习惯性地把能加的都加进去格式串写成一长串等到 QPS 上去了才发现日志本身成了瓶颈。4.1 四个基础转换符的写法与含义转换符含义常用写法说明%d时间%d{yyyy-MM-dd HH:mm:ss.SSS}省略格式时用默认ISO8601%-5p/%-5level级别%-5p负号表示左对齐5 是固定宽度%t/%thread线程名%t排查并发问题几乎必备%c/%loggerlogger 名%c{1}{1}只取最后一段{1.}会做首字母缩写%m/%msg消息体%m2.x 里推荐用%msg%n换行%n不要直接写\n跨平台有差异%%字面量百分号%%单独一个%会解析失败%c{1}是实战里最实用的技巧之一。业务日志的类名常常是com.example.order.service.impl.PayServiceImpl原样打出来会把日志行撑得很长%c{1}只留PayServiceImpl可读性和文件体积都明显改善。%c{1.}更激进会把包名也缩写成一个字母比如c.e.o.s.i.PayServiceImpl适合日志量极大的场景。%-5p里的左对齐加固定宽度是个排版细节INFO是四位ERROR是五位不做对齐的话日志行会参差不齐肉眼扫起来很累。写成%-5p后每个级别都占五格日志左侧就齐了。4.2 %l、%C、%L、%M 这些看起来很香的转换符%l输出的是调用位置的完整信息包括类名、方法名、文件名、行号。看着非常有用——直接告诉你哪一行打的日志。但它有一个致命问题JVM 在抛出异常时才能拿到精确的调用栈为了取行号日志组件必须构造一个Throwable并调用getStackTrace()这是一个极其昂贵的操作。我在压测环境实测过格式串里加上%l后单机日志吞吐量大概会掉到原来的十分之一左右%C、%F、%L、%M是同一类问题只是各自取的字段不同转换符输出内容代价%C/%class调用者类名高需取栈%F/%file调用者文件名高需取栈%L/%line行号高需取栈%M/%method方法名高需取栈%l/%location以上四项的组合最高%t/%thread线程名低%X/%mdcMDC 值低结论很简单这几个转换符只用在本地调试和排查阶段生产环境的格式串里不要出现。如果你真的需要知道日志是哪个类打的用%c{1}就够定位到类行号可以在 IDE 里配合 grep 找。4.3 %X 与 MDC给日志打上链路标记分布式系统里一条日志只写支付失败是没用的你需要知道是哪一笔请求、哪个用户、哪个订单。做法是往 MDCMapped Diagnostic Context1.x 和 slf4j 的叫法或 ThreadContext2.x 的叫法里塞上下文然后在格式串里用%X{key}取出来。// 入口处放入上下文 MDC.put(traceId, requestId); try { // 业务逻辑 } finally { MDC.remove(traceId); // 必须清理 }param nameConversionPattern value%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p [%t] [%X{traceId}] %c{1} - %m%n/这里有三个实操要点。第一MDC.remove一定要放在finally里否则线程池复用线程时上一个请求的 traceId 会带到下一个请求上日志串得离谱。第二MDC 基于ThreadLocal异步线程比如Async、线程池、CompletableFuture里是取不到父线程的 MDC 的需要手动传递或者用专门的任务装饰器。第三2.x 里%X{key}和%mdc{key}都可用取不到值时不会报错会原样输出所以别指望它帮你发现上下文丢失。4.4 中文乱码三个编码位置要同时对齐中文乱码几乎都出在编码没有全链路统一。涉及的位置有三个控制台输出编码ConsoleAppender的Encoding参数不写就跟随file.encoding文件输出编码文件类 appender 的Encoding参数同样建议显式写UTF-8查看端编码用less、编辑器或者日志平台打开时的解码设置。很多人的问题出在第三点日志文件本身是标准 UTF-8但服务器上locale是POSIXless app.log出来的中文全是问号。判断到底是哪一环出问题最简单的办法是file -i app.log看编码声明再用iconv转一次验证。如果文件确实是 UTF-8那就是查看端的问题不用改配置。另外Windows 控制台默认代码页是 GBK用System.out输出 UTF-8 字符串必然乱码。解决方式是启动参数加上-Dfile.encodingUTF-8并在控制台执行chcp 65001切到 UTF-8 代码页。这个组合我在多个项目里验证过稳定有效。5. 中大型项目的日志分流按包名、级别、文件切开项目小的时候一份配置走天下项目大了之后日志混杂在一起就没法用了——框架的 DEBUG 日志把业务日志埋掉一个文件几百 MB出问题根本翻不动。日志分流的核心思路是按来源分流哪个包的日志、按严重程度分流什么级别、按生命周期分流保留多久。5.1 把框架噪音压下去Spring、MyBatis、Netty、HTTP 客户端这几个是日志噪音的主要来源。配置思路是给它们单独设一个较高的级别把它们关小但不完全关掉!-- Log4j 2.x -- Logger nameorg.springframework levelwarn/ Logger nameorg.apache.ibatis levelwarn/ Logger nameio.netty levelwarn/ Logger nameorg.apache.http levelwarn/ Logger namecom.zaxxer.hikari levelwarn/MyBatis 的 SQL 日志是个特例调试 SQL 时把com.example.mapper打到debug最直接因为 MyBatis 会把 mapper 接口的全限定名当作 logger 名。但要注意开启 SQL 日志会让日志量暴涨十倍以上因为每条语句的参数都会完整打出来长文本字段比如文章正文会被原样写进去。我的做法是只在排障时临时开排完立刻关或者单独写到一个生命周期很短的文件里。还有一个常被忽略的点同一个包前缀的 logger 不要配两条冲突的规则。比如同时存在Logger nameorg.springframework levelwarn/和Logger nameorg.springframework.web leveldebug/后者会覆盖前者的效果因为查找是最长前缀优先。这在合并多人提交的配置时特别容易发生排查为什么关了还是打一堆 DEBUG时先把所有 logger 名的前缀列一遍。5.2 Filter 和 threshold两种按级别过滤的区别很多人会把某个 appender 只收 ERROR和某个 logger 只输出 ERROR混为一谈。前者是 appender 层的过滤后者是 logger 层的级别控制。Log4j 2.x 里给 appender 加过滤用ThresholdFilterRollingFile nameERROR_FILE fileName${LOG_HOME}/error.log filePattern${LOG_HOME}/error-%d{yyyy-MM-dd}.log.gz PatternLayout pattern${PATTERN}/ ThresholdFilter levelERROR onMatchACCEPT onMismatchDENY/ Policies TimeBasedTriggeringPolicy/ /Policies /RollingFile这样挂到 root 上之后error.log里只会出现 ERROR 及以上级别的日志而app.log保留全量形成了全量 错误单独一份的双文件模式。这个模式在生产环境非常实用日常看app.log告警和巡检看error.log不用在大文件里 grep。如果要做区间过滤比如只把 WARN 和 ERROR 收进某个文件用LevelRangeFilterLevelRangeFilter minLevelWARN maxLevelERROR onMatchACCEPT onMismatchDENY/Log4j 1.x 里的对应物是filter classorg.apache.log4j.varia.LevelRangeFilter参数名是LevelMin、LevelMax、AcceptOnMatch。注意 1.x 的 filter 是挂在 appender 上的子元素而且写成filter而不是param写错位置会被静默忽略。5.3 不重启改日志级别两种可行路径线上出问题时来不及发版只想把某个包的日志临时调到 DEBUG。Log4j 2.x 提供了两条路。第一条是配置自动重载在根节点加monitorInterval60Log4j 会每 60 秒检查一次配置文件的时间戳发现变化就重新加载。把配置文件放在容器外的挂载目录里改完等一分钟就生效。要注意monitorInterval重载是整体重载如果新配置有语法错误重载会失败并且继续用旧配置同时把错误打进内部日志——所以改之前一定要在本地验证过。第二条是代码直接操作 LoggerContextLoggerContext ctx (LoggerContext) LogManager.getContext(false); Configuration cfg ctx.getConfiguration(); cfg.getLoggerConfig(com.example.order).setLevel(Level.DEBUG); ctx.updateLoggers();把这段逻辑包一个内部接口加上鉴权就是一个简易的动态调级开关。Log4j 1.x 的做法更简单粗暴LogManager.getLogger(com.example.order).setLevel(Level.DEBUG)直接生效不需要刷新上下文。提示动态调级是内存态重启就失效。别把它当成长期配置手段用完记得调回去否则日志量会在不知不觉中吃掉磁盘。5.4 异步日志AsyncAppender 和 AsyncLogger 的取舍日志写盘是同步 IO高并发下确实可能成为瓶颈。Log4j 2.x 提供了两种异步方案机制完全不同。AsyncAppender是 appender 级的包装用法是把真正的 appender 挂到它下面Async nameASYNC_FILE bufferSize8192 blockingfalse AppenderRef refROLLING/ /Async它的原理是把日志事件丢进一个队列由单独的线程消费。blockingfalse时队列满了直接丢弃日志会记一条内部日志blockingtrue时则阻塞业务线程——两者都有代价前者丢日志后者可能拖慢业务。AsyncLogger是无锁的基于 LMAX Disruptor性能更好但需要额外引入com.lmax:disruptor依赖否则会退化或者报错。用法上要混用AsyncRoot和AsyncLoggerAsyncLogger namecom.example.order levelinfo additivityfalse AppenderRef refROLLING/ /AsyncLogger AsyncRoot levelinfo AppenderRef refCONSOLE/ /AsyncRoot关键注意点异步日志在 JVM 异常退出kill -9、OOM、断电时会丢队列里还没落盘的日志。对日志有强审计要求的场景比如金融交易流水宁可同步写或者关键业务单独走同步 appender。这个取舍必须由业务方拍板不能由运维随手加个Async决定。6. 配置不生效的完整排查链路前面讲的都是怎么配这一节讲配了没用怎么办。我把这些年遇到的故障按排查顺序整理成一条链路照着走基本能定位到问题。6.1 第一步永远是确认加载了哪个文件绝大多数配置不生效根源就是加载了别的文件。Log4j 1.x 在启动参数里加-Dlog4j.debugtrue控制台会打印它在找哪个文件、最终用哪个 URL 初始化了仓库。Log4j 2.x 对应的是-Dlog4j2.debugtrue或者直接在配置里写statusTRACE会输出完整的内部状态日志包括每一个被识别的 logger 和 appender 名字。我一般的排查顺序是这样的步骤动作判断依据1打印内部状态日志看实际加载的配置文件路径2列出类路径下所有同名文件jar tf或者find找重复项3确认配置文件在打包产物里的位置是否在BOOT-INF/classes下4确认依赖树里日志实现的版本mvn dependency:tree5确认启动参数有没有覆盖检查-D参数和环境变量第 2 步经常有惊喜。曾经有个项目业务模块的 jar 里带了一份log4j2.xml公共模块也带了一份最终哪份生效取决于 jar 在类路径上的顺序——这种情况下配置文件的内容对错根本无所谓。6.2 那些不报错、只会静默失效的写法Log4j 1.x 的DOMConfigurator有个很坑的特性param的name属性如果拼写错误它不会报错只是这个参数不生效。比如把MaxFileSize写成MaxFileSizee配置照常加载日志照常输出就是文件永远不滚动。这类问题只能靠对照官方文档逐个字段核对。2.x 稍好一些未知的元素和属性会在内部状态日志里以Unknown element或类似形式提示但前提是你打开了status。默认statusERROR的情况下这些提示都被吞掉了。另外几个静默失效的高频场景logger的name写了一个不存在的包名配置没错只是没命中additivity设成了false但忘了挂appender-ref日志直接断掉一条不出文件路径的目录不存在且没有创建权限appender 初始化失败日志被丢弃。这些都要靠打开内部日志 核对名字来解决。6.3 打包与部署阶段的配置覆盖Spring Boot 项目打包成 fat jar 之后配置文件的位置变了。日志配置文件要么放在src/main/resources下被塞进BOOT-INF/classes要么放在外部目录通过logging.config或者log4j.configurationFile指过去。推荐后者因为外部配置改完不用重新打包配合monitorInterval就能做到热更新。容器化部署还有个细节镜像里的配置是构建时烘进去的如果想要同一镜像在不同环境用不同日志级别就得把配置目录挂载出来或者在启动命令里用-Dlog4j2.configurationFile${LOG_CONFIG}注入。别在镜像里改文件再重新 commit那样版本管理会失控。6.4 依赖冲突日志桥接包让配置彻底失效这是最隐蔽的一类问题。SLF4J 生态里有一堆桥接包slf4j-log4j12、log4j-slf4j-impl、log4j-over-slf4j、jcl-over-slf4j、log4j-jcl。它们的作用是把不同门面的调用转发到同一个实现上但如果同时引入了互相转发的两个包就会形成死循环或者其中一方被彻底绕过。经典组合是log4j-over-slf4j和slf4j-log4j12同时存在。前者把org.apache.log4j的调用转给 SLF4J后者又把 SLF4J 转回 Log4j 1.x结果是日志在两者之间来回弹或者直接抛栈溢出。排查方法是mvn dependency:tree -Dincludesorg.slf4j,log4j把输出的所有日志相关依赖列出来对照以下规则清理整个工程只保留一个日志实现log4j-core 或 logback-classic 二选一其余全部用exclusions排掉。SLF4J 会在启动时打印Class path contains multiple SLF4J bindings的警告这个警告绝对不能当噪音忽略——它就是在告诉你你现在不知道日志写到哪去了。一些我自己的使用习惯写到这里配置本身的东西基本说完了剩下的都是些个人习惯不一定对但确实帮我省过不少事。我现在的项目里日志配置一律走外部文件通过启动参数指定绝对路径monitorInterval设 60 秒格式串固定成时间 级别 线程 logger 短名 traceId 消息绝不放%l那类取栈的字段。appender 只留三个控制台、全量滚动文件、ERROR 单独一份滚动文件。ERROR 那份配IfLastModified age30d自动清理全量那份保留 7 天磁盘占用可控。还有一个习惯是每次改完日志配置先本地跑一次完整启动看一遍内部状态日志。Log4j 2.x 开statusTRACE的输出虽然啰嗦但它会明确告诉你哪个 logger 被识别了、级别是多少、挂了哪些 appender。花两分钟看这一屏字比上线之后从几 GB 日志里找问题划算太多。踩过的坑多了之后你会发现日志配置这件事本身不难难的是它出错的时候通常一声不响。