
1. 别急着抄配置先弄明白 Log4j.xml 在管什么很多人第一次接触Log4j.xml都是在接手一个老项目的时候——项目跑起来日志乱成一团控制台刷屏、磁盘被日志文件塞满、线上出问题翻不到关键信息最后发现所有问题的源头都指向src/main/resources下的那个log4j.xml。这个文件看起来不长但它是整个应用日志行为的唯一指挥中心。改一个level属性可能让 QPS 从两千掉到八百少写一个additivity同一条日志就能打两遍MaxFileSize写大了一个日志文件几百 MB运维想 grep 都费劲。这篇文章不会只给你一份可以复制粘贴的模板。我打算把Log4j.xml拆成几个层次来讲它由哪些元素构成、每个元素背后的设计意图是什么、生产环境的配置应该怎么根据磁盘容量和检索需求算出来、从 1.x 迁移到 2.x 时哪些写法会直接失效、以及那些只有踩过一次才知道的坑。假设你手头有一个 Java 项目日志框架用的是 Log4j 或者 Log4j2配置方式是 XML那么这篇内容基本可以覆盖你 90% 的日常场景。如果你是刚入门的新手前面几节会把基本概念补齐如果你已经写过几份配置可以重点看第 3 节往后那里全是参数计算和排查路径。1.1 三种日志落地方式的取舍日志最终要输出到某个地方这件事在 Java 世界里无非三条路System.out.println、自己封装一个写文件的工具类、以及用成熟的日志框架。第一种在调试小 demo 时没问题但一旦进了并发场景多线程交叉输出会把日志搅成一锅粥而且没有级别概念线上想关掉就只能在代码里到处删。第二种看起来可控实际要自己处理文件滚动、并发加锁、异常堆栈格式化写着写着就变成了一个半成品轮子还容易在磁盘写满时把业务线程阻塞住。日志框架的价值在于把这几个问题一次性解决掉Logger负责在代码里埋点Appender负责往目标介质写Layout负责格式化三者通过配置文件解耦。业务代码只依赖Logger这一个抽象明天想把日志从本地文件换成按天切割再压缩归档只需要改 XML一行 Java 代码都不用动。Log4j.xml就是这套解耦机制的配置文件载体。XML 和 properties 两种格式在 Log4j 1.x 里是等价的框架会优先找log4j.xml找不到才退回到log4j.properties。选择 XML 的理由其实很现实层级结构直观、支持嵌套这对Appender内部的Layout、Filter尤其重要、可以写注释说明每个参数为什么这么配。properties 是扁平的键值对表达嵌套关系要靠log4j.appender.xxx.layout.ConversionPattern这种长前缀配到第十行就开始眼花。团队协作时XML 的可读性优势会随着配置复杂度上升而放大。1.2 配置文件被加载的那一瞬间发生了什么理解加载流程排查问题时能少走很多弯路。Log4j 1.x 在类初始化时会做一次静态初始化按照这个顺序找配置系统属性log4j.configuration指定的路径、classpath 下的log4j.xml、classpath 下的log4j.properties如果全都没有会打印一条 log4j:WARN No appenders could be found 的警告然后什么都不输出。这条警告是新手最容易忽略的提示看到它基本就说明配置文件没被找到。Log4j2 的加载机制更复杂一些它会依次检查log4j2-test.xml、log4j2.xml等配置文件同时还支持 JSON、YAML、properties 多种格式甚至可以通过log4j.configurationFile系统属性指定一个绝对路径。这就带来一个隐蔽的问题如果你的测试资源目录里放了一份log4j2-test.xml打包时不小心带进了生产包线上跑的就是测试配置级别可能是 debug日志量直接翻十倍。我遇到过一次生产事故排查到最后发现是构建脚本把src/test/resources也打进了 jar 包。提示在应用启动日志里主动打印一行当前生效的配置文件路径比事后猜测要省事得多。Log4j2 可以配合Configuration元素上的statustrace临时开启内部日志它会告诉你配置是从哪个文件加载的。2. Log4j.xml 的结构骨架一个都不能少的部分抛开具体写法不谈任何一份能跑起来的Log4j.xml都在回答三个问题日志写到哪、哪些日志写进去、写成什么样子。对应的三个核心元素分别是appender、logger含root、layout。这三者的关系不是并列的而是层层引用logger引用appenderappender内部持有layout。理解了这条引用链配置文件再长你也能一眼看出脉络。2.1 appender 决定日志去哪儿也是最容易配错的地方appender是日志的出口常见类型有这么几种。文件类里用得最多的是RollingFileAppender按大小滚动和DailyRollingFileAppender按时间滚动FileAppender是最基础的写文件但不滚动基本只适合临时调试。控制台类就是ConsoleAppender。还有一类是把日志转发出去的比如AsyncAppender用于异步化或者配合其他系统做集中收集。这里有个新手经常问的问题为什么不能只用DailyRollingFileAppender因为 Log4j 1.x 的按天滚动有两个硬伤。第一它只负责重命名文件不负责删除旧文件跑上一年之后你的日志目录里会躺着 365 个文件磁盘迟早爆掉。第二跨天那一刻如果恰好多线程或集群多进程同时写同一个文件重命名操作会出现竞争结果是日志丢了一段或者文件名变成奇怪的样子。所以我个人的习惯是生产环境一律用RollingFileAppender按大小切配合运维层的定时清理脚本或者直接升级到 Log4j2 用它的DefaultRolloverStrategy做自动清理。appender的属性命名也值得注意。1.x 里用的是param namexxx valueyyy/2.x 则把这些参数变成了子元素或者直接写成属性。name属性是所有appender必须有的它是logger引用时的唯一标识起名建议带上用途比如console、fileInfo、fileError别用appender1这种半年后自己都看不懂。2.2 logger 和 root 决定日志按什么规则分流logger是日志的分类器通常按包名或类名来划分。比如你可以给com.example.order包单独配一个级别让它输出 info 以上同时给com.example.common配成 warn把那些噪音比较大的工具类日志压下去。name属性支持前缀匹配配置com.example会同时作用于它下面所有的子包这比逐个类去配省事得多。root是整棵树的根节点所有没有匹配到具体logger的日志最终都会落到root上。它的用法和普通logger几乎一样区别在于名字固定为root且不能被additivity影响。生产配置里我一般把root设成warn或者info加一个兜底的appender然后在具体业务包上按需放开debug。这样做的好处是任何新引入的第三方库只要不在我的包名范围内默认走的就是相对安静的那一档不会莫名其妙刷屏。级别本身有个容易被误解的点levelinfo表示 info 及更高级别warn、error、fatal都会输出而不是只输出 info。很多人第一次配的时候会想我只要 error 怎么办那就把级别设成error这样 info 和 warn 会被过滤掉。级别从低到高依次是 trace、debug、info、warn、error、fatal还有两个特殊值 all 和 off。2.3 layout 决定日志长什么样也决定写入性能Layout负责把日志事件格式化成一行字符串最常用的是PatternLayout。它的ConversionPattern属性用一堆占位符拼出最终格式常用的几个我在下面列个表配的时候对着查就行。占位符含义性能开销%d{yyyy-MM-dd HH:mm:ss.SSS}时间戳可自定义格式低但有格式化成本%p或%-5p日志级别-5表示左对齐补空格极低%c或%c{2}logger 名称{2}表示只保留最后两段低%t线程名低%m日志消息本体极低%n换行符极低%M方法名高需要构造堆栈%L行号高需要构造堆栈%l完整位置信息等价于%C.%M(%F:%L)非常高%X{key}MDC 中的自定义字段低表里那两个标高的占位符是我强烈建议在生产环境避开的。%L和%l的实现原理是从当前线程的堆栈里往上找调用者一次日志就要构造一次堆栈快照在压测里这部分开销能占到日志总耗时的三分之一以上。如果确实需要行号定位只在本地开发环境开生产环境把定位信息统一换成一个业务追踪 ID通过 MDC 传进去性能和安全都更好。%c{2}这个写法也值得说一说。默认的%c会输出完整的 logger 名称比如com.example.order.service.impl.OrderServiceImpl一行占掉几十个字符。改成%c{1}只保留最后一段日志立刻清爽代价是可能有同名类不好区分。折中方案是%c{2}输出impl.OrderServiceImpl大部分情况下够用。2.4 additivity一个让日志翻倍的开关additivity是logger元素上的一个布尔属性默认值是true。它的含义是这条日志除了写进当前logger引用的appender还要不要继续向上传递给父级logger。听起来有点绕看个例子就明白了。假设你的root上挂了一个consoleappender又给com.example配了一个logger同样引用了console。这时候com.example包下的日志会先写一次因为additivity默认是true它继续往上走到root又写一次结果是控制台里每条日志出现两遍。这种情况在排查日志为什么重复时占了绝大多数。解决办法就是把additivity设成false让日志在这一层终止。我自己的习惯是只要一个logger配置了独立的appender就顺手把additivityfalse写上除非明确需要它同时汇总到root。注意additivity控制的是是否向上传递到父级 logger而不是是否输出到当前 appender。搞反这个逻辑会导致配置怎么改都不对。3. 一份可以直接落地的生产配置前面讲的是零件这一节把它们组装起来。我准备了两份完整配置一份对应 Log4j 1.2很多存量项目还在用一份对应 Log4j2。两份都是我从实际项目里抽出来的做过删减但保留了关键结构你可以直接拿去改成自己的包名和路径。3.1 Log4j 1.2 版本的完整配置?xml version1.0 encodingUTF-8? !DOCTYPE log4j:configuration SYSTEM log4j.dtd log4j:configuration xmlns:log4jhttp://jakarta.apache.org/log4j/ !-- 控制台输出仅用于本地开发 -- appender nameconsole classorg.apache.log4j.ConsoleAppender param nameTarget valueSystem.out/ param nameThreshold valueDEBUG/ layout classorg.apache.log4j.PatternLayout param nameConversionPattern value%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5p %c{2} - %m%n/ /layout /appender !-- 业务日志按大小滚动 -- appender namefileInfo classorg.apache.log4j.RollingFileAppender param nameFile value${log.home}/app-info.log/ param nameAppend valuetrue/ param nameEncoding valueUTF-8/ param nameThreshold valueINFO/ param nameMaxFileSize value100MB/ param nameMaxBackupIndex value20/ layout classorg.apache.log4j.PatternLayout param nameConversionPattern value%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5p %c{2} [%X{traceId}] - %m%n/ /layout /appender !-- 错误日志单独归档方便快速定位 -- appender namefileError classorg.apache.log4j.RollingFileAppender param nameFile value${log.home}/app-error.log/ param nameAppend valuetrue/ param nameEncoding valueUTF-8/ param nameThreshold valueERROR/ param nameMaxFileSize value50MB/ param nameMaxBackupIndex value10/ layout classorg.apache.log4j.PatternLayout param nameConversionPattern value%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5p %c{2} - %m%n/ /layout /appender !-- 业务包独立级别 -- logger namecom.example.order additivityfalse level valueINFO/ appender-ref reffileInfo/ appender-ref reffileError/ /logger !-- 第三方库压到 WARN避免噪音 -- logger nameorg.springframework level valueWARN/ /logger logger namecom.zaxxer.hikari level valueWARN/ /logger !-- 根节点兜底 -- root level valueINFO/ appender-ref refconsole/ appender-ref reffileInfo/ appender-ref reffileError/ /root /log4j:configuration这份配置里有几个设计决策值得展开说。fileError用Threshold属性卡在 ERROR意味着只有错误级别才会写进去它和fileInfo是并存关系而不是替代关系一条 ERROR 日志会同时出现在两个文件里。这看起来冗余但在实际运维中价值很大出问题时你不需要在几百 MB 的 info 日志里翻找直接打开体积小得多的 error 文件就行。${log.home}是 Log4j 1.x 支持的系统属性占位符启动时通过-Dlog.home/data/logs传入。用占位符而不是写死绝对路径是为了让同一份配置能在开发、测试、生产三套环境通用。需要注意的是如果这个属性没传Log4j 不会报错而是会把${log.home}当成字面量拼进路径结果就是在项目根目录下生成一个名字里带美元符号的文件夹。这个坑我踩过排查了半天才反应过来。3.2 Log4j2 版本的完整配置Log4j2 的写法变化很大元素名从appender变成了Appenders下的具名节点参数从param变成了属性或子元素同时还多了Properties、Policies、DefaultRolloverStrategy这些能力。?xml version1.0 encodingUTF-8? Configuration statusWARN monitorInterval30 Properties Property nameLOG_HOME/data/logs/Property Property namePATTERN%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} [%X{traceId}] - %msg%n/Property /Properties Appenders Console nameConsole targetSYSTEM_OUT PatternLayout pattern${PATTERN}/ /Console RollingFile nameRollingFile fileName${LOG_HOME}/app.log filePattern${LOG_HOME}/app-%d{yyyy-MM-dd}-%i.log.gz PatternLayout pattern${PATTERN}/ Policies TimeBasedTriggeringPolicy interval1 modulatetrue/ SizeBasedTriggeringPolicy size100 MB/ /Policies DefaultRolloverStrategy max50 Delete basePath${LOG_HOME} maxDepth1 IfFileName globapp-*.log.gz/ IfLastModified age30d/ /Delete /DefaultRolloverStrategy /RollingFile RollingFile nameErrorFile fileName${LOG_HOME}/app-error.log filePattern${LOG_HOME}/app-error-%d{yyyy-MM-dd}-%i.log.gz PatternLayout pattern${PATTERN}/ Filters ThresholdFilter levelERROR onMatchACCEPT onMismatchDENY/ /Filters Policies TimeBasedTriggeringPolicy interval1/ SizeBasedTriggeringPolicy size50 MB/ /Policies /RollingFile /Appenders Loggers Logger namecom.example.order levelinfo additivityfalse AppenderRef refRollingFile/ AppenderRef refErrorFile/ /Logger Logger nameorg.springframework levelwarn/ Logger namecom.zaxxer.hikari levelwarn/ Root levelinfo AppenderRef refConsole/ AppenderRef refRollingFile/ AppenderRef refErrorFile/ /Root /Loggers /Configuration几个关键差异必须点出来。filePattern里的%i是同一时间段内的序号占位符配合%d{yyyy-MM-dd}就能实现同一天内按大小切、跨天重新计数的效果这个组合在 1.x 里是做不到的。DefaultRolloverStrategy里的Delete子节点实现了自动清理age30d表示删除 30 天前的文件这一条配置直接省掉了运维的定时脚本。monitorInterval30表示每 30 秒检测一次配置文件是否改动改完不用重启应用就能生效。这个特性在线上调试时非常好用临时把某个包的级别从 info 调到 debug看几眼日志再调回来全程不需要发版。要注意的是它只对文件系统上的配置文件生效如果配置被打进了 jar 包内部检测机制是看不到变化的。3.3 滚动策略的参数到底怎么算MaxFileSize和MaxBackupIndex这两个数不是随便填的它们决定了日志的磁盘占用上限和检索难度。先说检索难度单个文件太大用less打开会卡用grep扫一遍耗时也长单个文件太小一天切出几百个文件找起来要不停切换。我一般把单文件控制在 50MB 到 200MB 之间100MB 是个比较舒服的值。再说磁盘上限。在 Log4j 1.x 里MaxBackupIndex表示保留的历史文件数量所以单个 appender 的最大占用约等于MaxFileSize × (MaxBackupIndex 1)。按上面的配置算100MB × 21 ≈ 2.1GB。这个值是硬上限不管跑多久都不会超过。如果你希望日志保留更长时间比如按每天 5GB 的写入量保留一周那就要算一下如果只靠大小滚动一周产生 35GB除以单文件 100MB需要 350 个备份文件MaxBackupIndex就得设成 350总占用 35GB。Log4j2 因为同时有max和Delete两套机制控制更灵活。max限制的是同一时间段的滚动文件数量Delete的age限制的是保存时长两者是或的关系哪个先触发就按哪个删。我个人更倾向用age来管理因为它是按时间维度表达的跟日志保留 30 天这种业务要求直接对应不用反推算文件数。提示日志目录一定要和系统盘分开。见过好几个案例应用日志把根分区写满导致整个服务不可用。如果没法独立挂盘至少给日志目录设一个 quota或者用Delete策略把上限压死。3.4 运行期动态调整日志级别生产环境最怕的情况是线上出了问题但当前级别是 info需要的 debug 日志没打出来。重启应用改配置代价太大这时候就需要动态调整能力。Log4j 1.x 原生没有提供这个能力常见做法是暴露一个内部接口拿到Logger对象后调用setLevel()。代码大概是这个意思LogManager.getLogger(com.example.order) .setLevel(Level.DEBUG);这个操作是即时生效的因为它直接改的是内存里 logger 对象的级别。要注意的是它不会持久化应用重启后仍然回到 XML 里配置的值这个特性刚好符合临时排查的需求。Log4j2 提供了更正规的方案通过 JMX 或者内置的LoggerContextAPI 来调整。如果你用的是 Spring Boot它自带的loggers端点可以直接在运行时查看和修改任意 logger 的级别返回结果也很直观。我在几个项目里都保留了这个能力但会加上权限校验和操作审计毕竟改错了级别可能把磁盘打满。还有一个更省事的办法把root级别设成info但在需要详细排查的包上预留一个debug的 logger默认不挂任何 appender 或者挂一个单独的文件。平时它不产生输出需要的时候动态改级别或者挂上 appender。4. 升级、依赖和那些绕不开的坑4.1 从 1.x 迁到 2.x 的配置对照迁移时最直接的问题是元素名对不上我整理了一张对照表对着改基本不会漏。用途Log4j 1.x 写法Log4j2 写法根配置节点log4j:configurationConfigurationappender 容器平铺的appender节点Appenders包裹具名节点appender 声明nameclass属性直接用元素名如RollingFile namex参数传递param namea valueb/属性RollingFile ab按大小滚动RollingFileAppenderMaxFileSizeRollingFileSizeBasedTriggeringPolicy按时间滚动DailyRollingFileAppenderDatePatternRollingFileTimeBasedTriggeringPolicy级别设置level valueINFO/levelinfo属性appender 引用appender-ref refx/AppenderRef refx/异步输出AsyncAppenderAsyncLogger或AsyncAppender文件清理不支持DefaultRolloverStrategyDelete除了写法语义上也有几处变化容易踩。2.x 的级别值是小写的写大写虽然多数情况下也能识别但最好统一成小写。2.x 的ThresholdFilter语义比 1.x 的Threshold更明确onMatch和onMismatch要显式写出来不写会走默认行为可能和预期不一致。还有%d的默认时间格式在 2.x 里变了如果依赖默认值格式化的结果会跟 1.x 差一截。4.2 依赖冲突排查Class path contains multiple SLF4J bindings这条警告信息相信很多人都见过。它的意思是 classpath 里存在多个 SLF4J 的实现绑定SLF4J 不知道该用哪个只能随便选一个结果就是你的log4j.xml明明配了日志却按另一个框架的配置在输出。排查方法是打开项目依赖树看看日志相关的 jar 都有哪些。Maven 项目执行mvn dependency:tree然后过滤日志相关关键字就行。最常见的组合是引入某个 SDK 时顺带带进来了 logback而项目本身用的是 log4j2两个绑定同时存在。解决办法是从源头排除。比如依赖了某个 starter它默认带 logback那就加一个 excludedependency groupIdcom.example/groupId artifactIdsome-sdk/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-logging/artifactId /exclusion /exclusions /dependency排查这类问题时有个经验不要只看直接依赖要看最终打进包里的 jar 列表。有时候依赖树上看不出问题是因为传递依赖被别的路径又拉进来了。直接解压产物包看WEB-INF/lib或者BOOT-INF/lib下有哪些日志 jar一目了然。4.3 桥接包的作用和选择Java 生态里的日志框架不止一个历史上还有 JUL、JCL 这些第三方库可能各用各的。桥接包的作用是把这些框架的调用统一转发到我们想用的那一个上避免出现一部分日志进了文件、另一部分不见了的情况。常见的几个方向和用途把 JCL 的调用转到 SLF4J把 JUL 的调用转到 SLF4J把 log4j 1.x 的 API 调用转到 log4j2。最后一个在升级场景里特别有用因为很多老依赖直接调的是org.apache.log4j.Logger如果不做桥接这些日志就不会走 log4j2 的配置。配置桥接包的时候要注意方向不能出现循环。比如同时引入了log4j 转 slf4j和slf4j 转 log4j两个包互相转发栈会直接爆掉。原则是让所有框架的日志最终汇聚到一个实现上中间可以有桥接但路径必须是一条单向的线。5. 出问题时按这个顺序查5.1 配置文件没生效的四步排查法第一步看启动日志里有没有log4j:WARN No appenders could be found或者类似提示。有的话就是文件没被加载检查文件名和位置是否正确log4j.xml必须放在 classpath 根目录下也就是编译产物的根目录而不是某个子目录里。第二步看配置文件的编码。XML 开头写了encodingUTF-8但文件本身如果是 GBK 保存的解析会直接失败而且报错信息可能被吞掉。用编辑器确认一下实际编码。第三步看是否有同名文件冲突。比如log4j.xml和log4j2.xml同时存在或者src/test/resources下也有一份被带进了产物。这种情况最隐蔽因为两个文件都能解析成功只是生效的不是你以为的那个。第四步看级别是否被覆盖。有些框架会在启动时通过代码重新设置级别覆盖掉 XML 里的配置。这种情况在引入了日志门面适配层时比较容易出现检查一下有没有代码在启动阶段调用了setLevel。5.2 日志重复打印的两种典型原因一种是前面说的additivity没关。判断方法是看重复的日志是不是来自同一个 logger 名称如果是并且你能在配置里找到两个都引用了同一个 appender 的层级那就是它。另一种是 appender 被重复引用。比如一个logger引用了fileInforoot也引用了fileInfo同时additivity是true结果一条日志被写了两次。这种重复的特征是文件里出现完全相同的两行时间戳都一样。还有一类不太常见但很难查的情况应用被部署了两份或者热部署工具导致类加载器重复加载配置被初始化了两次。判断方法是看日志有没有成对出现且数量稳定翻倍同时检查类加载器数量。5.3 常见问题速查表现象可能原因处理方向完全没有日志输出配置文件未加载检查 classpath 和文件名日志重复两遍additivity为 true设为 false中文乱码未指定 Encoding加Encoding参数磁盘被写满无清理策略加Delete或设上限行号缺失未使用%L开发环境开启生产慎用级别改了不生效代码覆盖或缓存检查启动逻辑日志文件不滚动策略配置错误检查 Policy 参数异步日志丢失队列满且丢弃策略为丢弃检查队列大小和策略5.4 性能相关的几个注意事项日志对性能的影响主要体现在三块字符串拼接、堆栈获取、磁盘 IO。第一块最容易被忽视很多人写log.debug(order: order.toString())即使当前级别是 info这个字符串拼接照样执行因为参数在方法调用前就求值好了。正确写法是用占位符log.debug(order id: {}, status: {}, orderId, status);这样只有在 debug 级别开启时才会真正做格式化。第二块是前面提过的定位占位符生产环境别用。第三块是磁盘 IO同步写日志会阻塞业务线程量大的时候必须上异步。Log4j2 的异步日志性能很好但要注意它是基于队列的队列满了之后的策略必须明确DiscardingThreshold设成 0 表示不丢弃代价是可能阻塞。这块要根据业务能接受丢日志还是能接受卡顿来权衡没有标准答案。6. 我这些年配 Log4j.xml 踩出来的经验先说一个最现实的不要指望一份配置能通用所有环境。我见过团队把生产和开发的配置写成同一份结果是要么开发时日志太少没法调试要么生产日志量太大把磁盘打满。正确做法是准备多份配置通过构建脚本或者启动参数切换让每套环境各取所需。第二个经验是关于分包的。项目初期大家图省事所有日志都往root上挂等到业务复杂了再想按模块分流发现日志文件已经混在一起没法拆。我的建议是项目一开始就按业务域划分几个关键包每个包配独立的 logger哪怕暂时用的是同一个 appender。这样等哪天需要把订单日志单独归档时只需要改一行配置。第三个经验是给日志加上下文标识。光靠时间戳和线程名在高并发场景下很难把一次请求的日志串起来。在入口处生成一个请求 ID 放进 MDC配置里用%X{traceId}打出来排查效率能提升好几倍。这一步的成本极低但收益非常高。第四个经验是定期回顾配置。日志配置很容易变成一次写完再也不看的文件随着依赖不断引入里面可能塞了一堆已经不存在的包名或者级别被临时调过忘了改回来。我一般在每个季度的技术债清理里加一条过一遍日志配置删掉无效的 logger核对级别设置。最后一个提醒关于版本。如果你维护的还是 1.x 的项目尽快安排升级到 2.x除了功能上的优势更重要的是 1.x 已经停止维护很多年出了安全问题时得不到修复而 2.x 早期版本也曾因为某个内置功能的默认行为出过一次影响范围很大的安全事件这两件事叠加起来足以说明升级这件事不该再拖。升级过程中重点验证的是桥接包配置和自定义 appender 的兼容性其他部分照着前面的对照表改一般不会有大问题。