news 2026/9/29 1:57:58

Log4j.xml与log4j2.xml配置实战:加载、滚动、继承与排错

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Log4j.xml与log4j2.xml配置实战:加载、滚动、继承与排错

接手一个跑了七八年的老系统时,我最怕看到的不是看不懂的业务代码,而是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.x(log4j.xml)Log4j 2.x(log4j2.xml)
根节点<log4j:configuration><Configuration>
是否需要 DOCTYPE需要,且必须正确不需要
输出目的地容器<appender><Appenders>下的具名元素
参数写法<param name value>XML 属性或<Property>
布局<layout class="..."><PatternLayout pattern="...">
日志器容器<logger>/<category><Loggers>下的<Logger>
根日志器<root><Root>

这张表建议收藏。跨项目切换时,最容易出错的就是<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 version="1.0" encoding="UTF-8"?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false"> <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender"> <param name="Target" value="System.out"/> <param name="Encoding" value="UTF-8"/> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p [%t] %c{1} - %m%n"/> </layout> </appender> <appender name="DAILY" class="org.apache.log4j.DailyRollingFileAppender"> <param name="File" value="/data/logs/app/app.log"/> <param name="Append" value="true"/> <param name="Encoding" value="UTF-8"/> <param name="DatePattern" value="'.'yyyy-MM-dd"/> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p [%t] %c - %m%n"/> </layout> </appender> <logger name="com.example.order" additivity="false"> <level value="debug"/> <appender-ref ref="DAILY"/> </logger> <root> <priority value="info"/> <appender-ref ref="CONSOLE"/> <appender-ref ref="DAILY"/> </root> </log4j:configuration>

再给 Log4j 2.x 的对应版本,用RollingFile做按天加按大小的双策略滚动:

<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN" monitorInterval="60"> <Properties> <Property name="LOG_HOME">/data/logs/app</Property> <Property name="PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%t] %logger{36} - %msg%n</Property> </Properties> <Appenders> <Console name="CONSOLE" target="SYSTEM_OUT"> <PatternLayout pattern="${PATTERN}"/> </Console> <RollingFile name="ROLLING" fileName="${LOG_HOME}/app.log" filePattern="${LOG_HOME}/app-%d{yyyy-MM-dd}-%i.log.gz"> <PatternLayout pattern="${PATTERN}"/> <Policies> <TimeBasedTriggeringPolicy interval="1" modulate="true"/> <SizeBasedTriggeringPolicy size="200 MB"/> </Policies> <DefaultRolloverStrategy max="30"/> </RollingFile> </Appenders> <Loggers> <Logger name="com.example.order" level="debug" additivity="false"> <AppenderRef ref="ROLLING"/> </Logger> <Root level="info"> <AppenderRef ref="CONSOLE"/> <AppenderRef ref="ROLLING"/> </Root> </Loggers> </Configuration>

这两份配置里的每一个点后面都会展开讲,但有一处值得先点出来:status="WARN"这个属性是 Log4j 2.x 排查问题的第一把钥匙。它控制 Log4j 自身的内部日志级别,默认是ERROR,配置写错了基本什么都不说。改成TRACE之后启动一次,控制台会把"加载了哪个配置文件""识别了哪些 logger""哪个元素没认出来"全部打出来,排错效率提升非常明显。

2. appender 选型:控制台、按大小滚动、按天滚动的实际取舍

"用哪个 appender"这个问题,本质上是在回答三个子问题:日志给谁看、写到哪里、写满了怎么办。本地开发环境给开发者看,用ConsoleAppender就够;生产环境要留痕、要能被日志采集程序读到,就必须落盘,而且必须滚动,否则磁盘迟早被撑爆。很多人配错 appender,不是因为不会写 XML,而是没想清楚"日志文件的最大生命周期是多少天""单文件多大合适""旧文件删除策略由谁负责"。

2.1 ConsoleAppender 不是配了就一定能看见

Log4j 1.x 的ConsoleAppender默认目标是System.out,可以通过<param name="Target" value="System.err"/>改到标准错误流。这里有个坑:很多日志采集容器会把stdout和stderr分开收集,如果你把 ERROR 日志全丢到stderr、INFO 全丢到stdout,采集端就得分两个源去拼,反而麻烦。我的习惯是统一走System.out,用日志级别字段去区分,把分流工作交给采集规则,不交给流。

Log4j 2.x 里控制台 appender 是<Console name="..." target="SYSTEM_OUT"/>,注意这里的目标值是全大写带下划线的枚举名SYSTEM_OUT/SYSTEM_ERR,写成System.out会直接报无效值。这个细节在从 1.x 迁移时特别容易翻车,因为 1.x 那边写的就是System.out。

另外一个高频问题是"本地能打日志,打成 jar 之后控制台空了"。这通常不是 appender 的问题,而是spring-boot-starter-log4j2或者容器的日志桥接把System.out重定向了,需要先确认输出流有没有被别的框架接管。

2.2 RollingFileAppender:MaxFileSize 和 MaxBackupIndex 必须成对出现

Log4j 1.x 的org.apache.log4j.RollingFileAppender是按大小滚动的,靠两个参数控制:

<appender name="FILE" class="org.apache.log4j.RollingFileAppender"> <param name="File" value="/data/logs/app/app.log"/> <param name="MaxFileSize" value="100MB"/> <param name="MaxBackupIndex" value="20"/> <param name="Append" value="true"/> <param name="Encoding" value="UTF-8"/> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p [%t] %c - %m%n"/> </layout> </appender>

MaxFileSize是单个文件的上限,支持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 天,反推MaxFileSize=200MB时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 name="ROLLING" fileName="${LOG_HOME}/app.log" filePattern="${LOG_HOME}/app-%d{yyyy-MM-dd}-%i.log.gz"> <PatternLayout pattern="${PATTERN}"/> <Policies> <TimeBasedTriggeringPolicy interval="1" modulate="true"/> <SizeBasedTriggeringPolicy size="200 MB"/> </Policies> <DefaultRolloverStrategy> <Delete basePath="${LOG_HOME}" maxDepth="1"> <IfFileName glob="app-*.log.gz"/> <IfLastModified age="30d"/> </Delete> </DefaultRolloverStrategy> </RollingFile>

这里的modulate="true"是个容易被忽略但很有用的参数:它让滚动边界对齐到时间区间的起点(比如按天滚就对齐到 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 章还会细讲。

Append:Log4j 1.x 的FileAppender和RollingFileAppender默认Append=true,也就是追加。设成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 additivity="false" 关掉的到底是什么

看这个典型误配:

<appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender"> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%-5p %c - %m%n"/> </layout> </appender> <appender name="FILE" class="org.apache.log4j.RollingFileAppender"> <param name="File" value="/data/logs/app/order.log"/> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%-5p %c - %m%n"/> </layout> </appender> <logger name="com.example.order"> <level value="debug"/> <appender-ref ref="FILE"/> </logger> <root> <priority value="info"/> <appender-ref ref="CONSOLE"/> </root>

com.example.order下的日志会先写进FILE,然后因为additivity默认是true,继续往上走到 root,又写了一遍CONSOLE。如果 root 上也挂了FILE,那就是同一个文件被写两遍。加上additivity="false",这条继承链就在这个 logger 处断掉,只走它自己的 appender。

注意:additivity只影响 appender 的继承,不影响级别的继承。设成false不会让子 logger 的级别失效。

判断要不要设,我的经验是:只要一个 logger 显式声明了自己专属的 appender,并且这个 appender 的用途和 root 的 appender 有重叠(比如都写文件),就把 additivity 设成 false。像"只在 root 上配 appender、logger 里只改级别"的场景,就不要动它,否则日志会彻底断掉。

3.3 root 不是"默认"而是"兜底",配错会全丢日志

有个很隐蔽的故障:日志文件是空的,但程序运行正常,也没有任何报错。排查到最后发现 root 上只写了<root><priority value="info"/></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:给日志打上链路标记

分布式系统里,一条日志只写"支付失败"是没用的,你需要知道是哪一笔请求、哪个用户、哪个订单。做法是往 MDC(Mapped Diagnostic Context,1.x 和 slf4j 的叫法)或 ThreadContext(2.x 的叫法)里塞上下文,然后在格式串里用%X{key}取出来。

// 入口处放入上下文 MDC.put("traceId", requestId); try { // 业务逻辑 } finally { MDC.remove("traceId"); // 必须清理 }
<param name="ConversionPattern" 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 中文乱码:三个编码位置要同时对齐

中文乱码几乎都出在编码没有全链路统一。涉及的位置有三个:

  1. 控制台输出编码:ConsoleAppender的Encoding参数,不写就跟随file.encoding;
  2. 文件输出编码:文件类 appender 的Encoding参数,同样建议显式写UTF-8;
  3. 查看端编码:用less、编辑器或者日志平台打开时的解码设置。

很多人的问题出在第三点:日志文件本身是标准 UTF-8,但服务器上locale是POSIX,less app.log出来的中文全是问号。判断到底是哪一环出问题,最简单的办法是file -i app.log看编码声明,再用iconv转一次验证。如果文件确实是 UTF-8,那就是查看端的问题,不用改配置。

另外,Windows 控制台默认代码页是 GBK,用System.out输出 UTF-8 字符串必然乱码。解决方式是启动参数加上-Dfile.encoding=UTF-8,并在控制台执行chcp 65001切到 UTF-8 代码页。这个组合我在多个项目里验证过,稳定有效。

5. 中大型项目的日志分流:按包名、级别、文件切开

项目小的时候一份配置走天下,项目大了之后,日志混杂在一起就没法用了——框架的 DEBUG 日志把业务日志埋掉,一个文件几百 MB,出问题根本翻不动。日志分流的核心思路是:按来源分流(哪个包的日志)、按严重程度分流(什么级别)、按生命周期分流(保留多久)。

5.1 把框架噪音压下去

Spring、MyBatis、Netty、HTTP 客户端这几个是日志噪音的主要来源。配置思路是给它们单独设一个较高的级别,把它们关小但不完全关掉:

<!-- Log4j 2.x --> <Logger name="org.springframework" level="warn"/> <Logger name="org.apache.ibatis" level="warn"/> <Logger name="io.netty" level="warn"/> <Logger name="org.apache.http" level="warn"/> <Logger name="com.zaxxer.hikari" level="warn"/>

MyBatis 的 SQL 日志是个特例:调试 SQL 时把com.example.mapper打到debug最直接,因为 MyBatis 会把 mapper 接口的全限定名当作 logger 名。但要注意开启 SQL 日志会让日志量暴涨十倍以上,因为每条语句的参数都会完整打出来,长文本字段(比如文章正文)会被原样写进去。我的做法是只在排障时临时开,排完立刻关,或者单独写到一个生命周期很短的文件里。

还有一个常被忽略的点:同一个包前缀的 logger 不要配两条冲突的规则。比如同时存在<Logger name="org.springframework" level="warn"/>和<Logger name="org.springframework.web" level="debug"/>,后者会覆盖前者的效果,因为查找是"最长前缀优先"。这在合并多人提交的配置时特别容易发生,排查"为什么关了还是打一堆 DEBUG"时,先把所有 logger 名的前缀列一遍。

5.2 Filter 和 threshold:两种"按级别过滤"的区别

很多人会把"某个 appender 只收 ERROR"和"某个 logger 只输出 ERROR"混为一谈。前者是 appender 层的过滤,后者是 logger 层的级别控制。

Log4j 2.x 里给 appender 加过滤用ThresholdFilter:

<RollingFile name="ERROR_FILE" fileName="${LOG_HOME}/error.log" filePattern="${LOG_HOME}/error-%d{yyyy-MM-dd}.log.gz"> <PatternLayout pattern="${PATTERN}"/> <ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/> <Policies> <TimeBasedTriggeringPolicy/> </Policies> </RollingFile>

这样挂到 root 上之后,error.log里只会出现 ERROR 及以上级别的日志,而app.log保留全量,形成了"全量 + 错误单独一份"的双文件模式。这个模式在生产环境非常实用:日常看app.log,告警和巡检看error.log,不用在大文件里 grep。

如果要做"区间过滤",比如只把 WARN 和 ERROR 收进某个文件,用LevelRangeFilter:

<LevelRangeFilter minLevel="WARN" maxLevel="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>

Log4j 1.x 里的对应物是<filter class="org.apache.log4j.varia.LevelRangeFilter">,参数名是LevelMin、LevelMax、AcceptOnMatch。注意 1.x 的 filter 是挂在 appender 上的子元素,而且写成<filter>而不是<param>,写错位置会被静默忽略。

5.3 不重启改日志级别:两种可行路径

线上出问题时,来不及发版,只想把某个包的日志临时调到 DEBUG。Log4j 2.x 提供了两条路。

第一条是配置自动重载,在根节点加monitorInterval="60",Log4j 会每 60 秒检查一次配置文件的时间戳,发现变化就重新加载。把配置文件放在容器外的挂载目录里,改完等一分钟就生效。要注意:monitorInterval重载是整体重载,如果新配置有语法错误,重载会失败并且继续用旧配置,同时把错误打进内部日志——所以改之前一定要在本地验证过。

第二条是代码直接操作 LoggerContext:

LoggerContext 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 name="ASYNC_FILE" bufferSize="8192" blocking="false"> <AppenderRef ref="ROLLING"/> </Async>

它的原理是把日志事件丢进一个队列,由单独的线程消费。blocking="false"时队列满了直接丢弃日志(会记一条内部日志),blocking="true"时则阻塞业务线程——两者都有代价,前者丢日志,后者可能拖慢业务。

AsyncLogger是无锁的,基于 LMAX Disruptor,性能更好,但需要额外引入com.lmax:disruptor依赖,否则会退化或者报错。用法上要混用<AsyncRoot>和<AsyncLogger>:

<AsyncLogger name="com.example.order" level="info" additivity="false"> <AppenderRef ref="ROLLING"/> </AsyncLogger> <AsyncRoot level="info"> <AppenderRef ref="CONSOLE"/> </AsyncRoot>

关键注意点:异步日志在 JVM 异常退出(kill -9、OOM、断电)时会丢队列里还没落盘的日志。对日志有强审计要求的场景,比如金融交易流水,宁可同步写,或者关键业务单独走同步 appender。这个取舍必须由业务方拍板,不能由运维随手加个<Async>决定。

6. 配置不生效的完整排查链路

前面讲的都是"怎么配",这一节讲"配了没用怎么办"。我把这些年遇到的故障按排查顺序整理成一条链路,照着走基本能定位到问题。

6.1 第一步永远是确认加载了哪个文件

绝大多数"配置不生效",根源就是加载了别的文件。Log4j 1.x 在启动参数里加-Dlog4j.debug=true,控制台会打印它在找哪个文件、最终用哪个 URL 初始化了仓库。Log4j 2.x 对应的是-Dlog4j2.debug=true,或者直接在配置里写status="TRACE",会输出完整的内部状态日志,包括每一个被识别的 logger 和 appender 名字。

我一般的排查顺序是这样的:

步骤动作判断依据
1打印内部状态日志看实际加载的配置文件路径
2列出类路径下所有同名文件jar tf或者find找重复项
3确认配置文件在打包产物里的位置是否在BOOT-INF/classes下
4确认依赖树里日志实现的版本mvn dependency:tree
5确认启动参数有没有覆盖检查-D参数和环境变量

第 2 步经常有惊喜。曾经有个项目,业务模块的 jar 里带了一份log4j2.xml,公共模块也带了一份,最终哪份生效取决于 jar 在类路径上的顺序——这种情况下配置文件的内容对错根本无所谓。

6.2 那些不报错、只会静默失效的写法

Log4j 1.x 的DOMConfigurator有个很坑的特性:<param>的name属性如果拼写错误,它不会报错,只是这个参数不生效。比如把MaxFileSize写成MaxFileSizee,配置照常加载,日志照常输出,就是文件永远不滚动。这类问题只能靠对照官方文档逐个字段核对。

2.x 稍好一些,未知的元素和属性会在内部状态日志里以Unknown element或类似形式提示,但前提是你打开了status。默认status="ERROR"的情况下,这些提示都被吞掉了。

另外几个静默失效的高频场景:<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 -Dincludes=org.slf4j,log4j,把输出的所有日志相关依赖列出来,对照以下规则清理:整个工程只保留一个日志实现(log4j-core 或 logback-classic 二选一),其余全部用<exclusions>排掉。SLF4J 会在启动时打印Class path contains multiple SLF4J bindings的警告,这个警告绝对不能当噪音忽略——它就是在告诉你"你现在不知道日志写到哪去了"。

一些我自己的使用习惯

写到这里,配置本身的东西基本说完了,剩下的都是些个人习惯,不一定对,但确实帮我省过不少事。

我现在的项目里,日志配置一律走外部文件,通过启动参数指定绝对路径,monitorInterval设 60 秒,格式串固定成"时间 + 级别 + 线程 + logger 短名 + traceId + 消息",绝不放%l那类取栈的字段。appender 只留三个:控制台、全量滚动文件、ERROR 单独一份滚动文件。ERROR 那份配IfLastModified age="30d"自动清理,全量那份保留 7 天,磁盘占用可控。

还有一个习惯是每次改完日志配置,先本地跑一次完整启动,看一遍内部状态日志。Log4j 2.x 开status="TRACE"的输出虽然啰嗦,但它会明确告诉你哪个 logger 被识别了、级别是多少、挂了哪些 appender。花两分钟看这一屏字,比上线之后从几 GB 日志里找问题划算太多。踩过的坑多了之后你会发现,日志配置这件事本身不难,难的是它出错的时候通常一声不响。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 1:56:53

MoE论文合集整理指南:算法、系统与应用三条主线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 1:56:36

PCIe电学规范第8章拆解:从链路预算到信号完整性实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 1:53:57

单相PWM整流器拓扑详解:从二极管整流到H桥主动式前端设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 1:53:55

多目标优化实战:epsilon-约束法原理、代码实现与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 1:53:14

LVDS信号原理与FPGA/RK3566实战调试指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华