资讯动态

Spring Boot整合log4j2:排除Logback到异步日志配置

发布时间:2026/10/20 23:43:27 来源:尧图企业网站定制
1. 项目概述不只是换一个日志框架那么简单先说结论Spring Boot 默认用的是 Logback这一点很多刚接触的人可能都不知道。当你兴冲冲地引入 log4j2 依赖结果启动时发现日志还是在用 Logback 输出然后一脸懵地翻官方文档才发现 Spring Boot 的spring-boot-starter-logging里默认捆绑了 Logback你得先把它排除掉才行。这个坑我当年踩过一次印象极其深刻。所以这篇博文要做的就是把 Spring Boot 整合 log4j2 的完整过程拆开揉碎讲清楚。从为什么选 log4j2、怎么排除默认日志组件、怎么写 log4j2.xml 配置文件到异步日志、滚动策略、生产环境调优这些实操细节全部覆盖。适合正在做 Spring Boot 项目、对日志管理有进阶需求的开发者以及那些受够了 Logback 复杂配置、想换一个更趁手日志方案的朋友。用一句话概括这篇内容的核心价值配置好 log4j2你的日志系统会变成一把趁手的瑞士军刀——按天滚动、按大小切分、自动清理旧日志、异步打印不阻塞业务线程这些能力在实际项目中是真的能救命。1.1 核心需求解析为什么会有“Spring Boot 整合 log4j2”这个需求我观察到的普遍场景有三类第一类是性能敏感型项目。log4j2 的异步日志记录器采用了无锁化的 LMAX Disruptor 环形缓冲区技术在高并发写入场景下吞吐量比 Logback 高一个数量级。如果你的接口 QPS 上千、日志量大同步刷盘会明显拖慢接口响应时间这时候换 log4j2 是合理的选择。第二类是配置洁癖型项目。log4j2 语法上支持${sys:property}占位符、多个配置文件组合、Lookup 动态取值等高级特性对于需要根据环境切换配置的团队来说灵活度高很多。第三类是踩坑后的迁移需求。Logback 在处理某些复杂 pattern 时会出现性能抖动加上 log4j2 的RoutingAppender、RewriteAppender等组件在日志分流方面确实灵活所以很多项目后期会主动切换到 log4j2。2. 环境准备版本选型和依赖管理的那些坑2.1 版本选择别傻乎乎全用最新版很多初学者喜欢直接拉最新版本但日志框架这东西版本兼容性极其重要。Spring Boot 2.x 和 3.x 对 log4j2 的版本要求完全不同随便选版本轻则配置文件不生效重则直接启动报错。以目前主流的 Spring Boot 版本举例Spring Boot 版本对应 log4j2 版本通过 BOM 管理注意事项2.3.x ~ 2.6.x2.13.x ~ 2.17.x相对成熟但要注意 CVE 漏洞升级2.7.x2.17.x 以上推荐用 2.17.2 以上版本3.0.x ~ 3.2.x2.20.x 以上需要 JDK 17这里有个重要原则不要手动指定 log4j2 版本除非你有强制需求。因为 Spring Boot 的spring-boot-dependenciesBOM 文件会统一管理所有依赖版本你手动指定版本反而可能破坏依赖一致性。正确的做法是引入spring-boot-starter-log4j2时不做版本声明让它跟随 Spring Boot 父级 BOM 走。2.2 排除默认日志组件Spring Boot 的默认日志方案是 Logback它通过spring-boot-starter-logging这个模块引入。所以整合 log4j2 的第一步就是把spring-boot-starter-logging从依赖中排除掉。Maven 项目里你需要在spring-boot-starter-web或其他 starter里添加 exclusionsdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-logging/artifactId /exclusion /exclusions /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-log4j2/artifactId /dependencyGradle 项目则这样处理implementation(org.springframework.boot:spring-boot-starter-web) { exclude group: org.springframework.boot, module: spring-boot-starter-logging } implementation org.springframework.boot:spring-boot-starter-log4j2提示如果你用的是较新版本的 Spring Boot3.x其实也支持在配置文件里指定logging.log4j2.config参数来加载自定义配置文件但排除默认日志组件这一步依然是必须的否则会出现 Logback 和 log4j2 同时存在导致的类冲突。2.3 验证是否排除成功排除完依赖之后很多人直接跑项目发现日志照常输出就以为自己配置成功了。但注意这里有个很容易被忽略的验证步骤必须在启动日志里确认到底用的是哪个日志框架。启动 Spring Boot 项目观察控制台前几行输出的格式。Logback 默认会打印类似这样的 INFO 日志[ main] com.example.DemoApplication : Starting DemoApplication on ...而 log4j2 启动成功后会加载你的配置文件并且在日志中如果有输出格式会按照你定义的 pattern 来走不会出现明显差异的话可能加载失败。这个时候最靠谱的验证方式是打印一个日志并在配置里刻意把输出模式改成一种特殊的格式比如 %d{HH:mm:ss} %-5level %logger{30} %msg%n。如果能输出开头的日志行说明 log4j2 配置生效了。3. log4j2.xml 配置详解从入门到生产可用3.1 配置文件基本结构log4j2 的配置文件是一棵 XML 树核心结构分成三大块properties可选、appenders输出目标和loggers日志记录器规则。一个最简单的配置长这样?xml version1.0 encodingUTF-8? configuration statusWARN appenders Console nameConsole targetSYSTEM_OUT PatternLayout pattern%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n/ /Console /appenders loggers root levelinfo appender-ref refConsole/ /root /loggers /configuration我把这段配置放在这里是想先帮你建立一个整体认知。下面再逐步解释每个节点。configuration根节点上的statusWARN属性表示 log4j2 内部日志的输出级别设置为 WARN 可以避免控制台刷太多没用的调试信息同时又能在配置出问题时保留关键错误提示。注意这个属性和你业务日志的级别没有任何关系它只决定 log4j2 自身系统状态日志的展示级别。Console节点定义了一个输出到控制台的 appenderPatternLayout里的%d是时间、%-5level是日志级别左对齐占 5 位、%logger{36}是类名、%msg是日志消息、%n换行符。loggers节点下至少有一个root记录器它是所有日志的兜底入口level属性决定默认接收哪些级别的日志appender-ref指定要输出到哪些目标。3.2 生产环境的多文件配置方案真正的生产项目单一控制台输出远远不够。你需要按天滚动保存的文件日志、按大小切分的独立错误日志、完整保留调用链信息的 Trace 日志以及一个清理策略来避免磁盘被占满。我日常用得最多的一套配置结构如下configuration statusWARN monitorInterval30 properties property nameLOG_HOME/data/logs/your-app/property property namePATTERN%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/property /properties appenders Console nameConsole targetSYSTEM_OUT PatternLayout pattern${PATTERN}/ /Console !-- 所有日志按天滚动 -- RollingFile nameRollingFileAll fileName${LOG_HOME}/all.log filePattern${LOG_HOME}/all-%d{yyyy-MM-dd}-%i.log.gz PatternLayout pattern${PATTERN}/ Policies TimeBasedTriggeringPolicy/ SizeBasedTriggeringPolicy size100 MB/ /Policies DefaultRolloverStrategy max30/ /RollingFile !-- 错误日志单独切割 -- RollingFile nameRollingFileError fileName${LOG_HOME}/error.log filePattern${LOG_HOME}/error-%d{yyyy-MM-dd}-%i.log.gz PatternLayout pattern${PATTERN}/ LevelRangeFilter minLevelERROR maxLevelERROR onMatchACCEPT onMismatchDENY/ Policies TimeBasedTriggeringPolicy/ SizeBasedTriggeringPolicy size100 MB/ /Policies DefaultRolloverStrategy max30/ /RollingFile /appenders loggers root levelinfo appender-ref refConsole/ appender-ref refRollingFileAll/ appender-ref refRollingFileError/ /root /loggers /configuration这套配置里有三个细节值得展开讲。3.3 滚动策略与文件保留策略详解TimeBasedTriggeringPolicy表示按时间触发滚动默认根据filePattern里的%d{yyyy-MM-dd}来确定最小滚动粒度。这里写的是按天滚动也就是当天的all.log写满到第二天零点时会被重命名成all-2024-01-01-0.log.gz-0表示当天第 0 个滚动文件然后新建一个all.log继续写。SizeBasedTriggeringPolicy size100 MB增加了按大小滚动的能力。意思是即使还没到第二天零点只要当前文件达到 100MB也会触发滚动重命名成带-i序号的文件。这里-i序号的递增规则和压缩策略有关filePattern末尾要写-i.log.gz否则多个按大小触发的滚动文件会互相覆盖。注意如果你同时用了TimeBasedTriggeringPolicy和SizeBasedTriggeringPolicy那么filePattern里必须同时包含%d和%i两个占位符。很多新手漏了%i结果按大小触发的滚动文件总是被覆盖日志直接丢数据排查半天也找不到原因。DefaultRolloverStrategy max30是最关键的配置之一它决定了最多保留多少个滚动归档文件。超过这个数量后最老的文件会被自动删除。上述配置里max30配合按天滚动意味着你最多保留 30 天如果每天只滚动一次或 30 个文件这很符合生产实践保留一个月的日志既能追溯问题又不会让磁盘吃紧。3.4 用 LevelRangeFilter 做日志分流错误日志单独放一个文件这是生产环境最基本的要求。上面的RollingFileErrorappender 里有一个LevelRangeFilter它的含义是minLevelERROR maxLevelERROR限定了只接受 ERROR 级别onMatchACCEPT表示命中的日志直接接受onMismatchDENY表示不命中的日志直接拒绝不让它进入该 appender有的教程会用ThresholdFilter比如ThresholdFilter levelERROR但那个是“大于等于 ERROR 就接受”会把 ERROR 以上如 FATAL也放进来。如果你确实只想单独收集 ERRORLevelRangeFilter更精准。还有一个容易踩的坑每个 appender 的filter是可以叠加多个的filter 的执行顺序按照在 XML 里的声明顺序执行。所以我个人习惯把 filter 写在 PatternLayout 和 Policies 之前避免出现过滤规则还没生效就触发了滚动把无用日志也写进错误文件里的情况。4. 异步日志性能提升的核心手段4.1 为什么同步日志会拖慢业务线程如果不做任何配置log4j2 默认是同步写日志的。也就是说业务线程执行到logger.info(...)这一行时要做的事情包括格式化日志消息、拼接 pattern 字符串、拿到文件锁、写入 IO 缓冲区、甚至触发磁盘刷盘。这些操作加起来虽然只有几毫秒但在高并发场景下线程会被频繁阻塞接口的 P99 延迟明显拉高。打个比方你开了一家奶茶店顾客点单业务线程后店员要现场煮茶、加盖、打包写日志全部做完才接下一个顾客。这显然效率很低。异步日志就是让店员把订单丢给后厨的传送窗口然后立刻接待下一个顾客后厨做好了自己送货。4.2 AsyncRoot 与 AsyncAppender 两种异步方案log4j2 提供两种异步玩法我需要你把它们区分清楚。第一种是AsyncRoot它替换掉root节点loggers AsyncRoot levelinfo appender-ref refConsole/ appender-ref refRollingFileAll/ appender-ref refRollingFileError/ /AsyncRoot /loggers在这种方案下日志事件会先进入一个无锁化的环形缓冲区基于 LMAX Disruptor然后由后台线程组统一消费并执行写入操作。业务线程调用日志接口几乎只做入队操作耗时忽略不计。第二种是AsyncAppender它是包在某个同步 appender 外面的一层“异步包装器”Async nameAsyncRollingFileAll appender-ref refRollingFileAll/ /Async把appender-ref指向AsyncRollingFileAll而不是RollingFileAll就能让后续的 RollingFile 写文件操作转为异步执行。这两种方案的核心区别是AsyncRoot 是整个记录链路的异步化AsyncAppender 只是单个 appender 层面的异步化。如果只是局部日志量大用 AsyncAppender 足够如果整体 QPS 高直接上 AsyncRoot。4.3 异步日志必须额外配置的两个系统参数这部分是最容易翻车的。用 AsyncRoot 其实没那么简单log4j2 的异步日志器依赖 Disruptor 技术而这个技术在应用启动时需要通过 Java 系统属性去指定底层实现。不加两个参数你会踩到生产环境启动时直接报Unable to load class com.lmax.disruptor.BlockingWaitStrategy或者干脆日志不输出的黑坑。需要在 JVM 启动参数中追加-Dlog4j2.contextSelectororg.apache.logging.log4j.core.async.AsyncLoggerContextSelector -Dlog4j2.asyncQueueFullPolicydiscard另外还要在 pom.xml 中引入 Disruptor 依赖dependency groupIdcom.lmax/groupId artifactIddisruptor/artifactId version3.4.4/version /dependency这里解释一下两个系统参数的含义。AsyncLoggerContextSelector让全链路走异步记录器的上下文asyncQueueFullPolicydiscard会在环形缓冲区满的时候直接丢弃日志而不是阻塞业务线程。丢弃日志在多数业务场景下是可以接受的——你宁愿日志少一条也不希望业务接口因为日志堆积被拖垮。提示如果你不想丢失任何日志可以把discard改成block但这样缓冲区满时业务线程还是会被阻塞。取舍逻辑很简单要么保性能丢日志要么保日志损失性能。我在生产项目中核心链路日志量大的服务选discard对日志完整性要求极高的财务类系统选block。4.4 异步日志的代价与适用场景异步日志不是银弹它有一个天然代价日志写入与业务执行不在同一个线程导致日志输出的先后顺序不完全等同于业务代码的执行顺序排查时序问题时可能会困扰你。另外异步日志的缓冲区如果设置得太小默认是 4MB 左右高并发量下频繁触发discard策略会导致日志大量丢失。可以针对性地调大系统属性-Dlog4j2.asyncQueueFullPolicydiscard只是策略缓冲区大小需要通过AsyncLoggerConfig里的RingBufferSize属性调整。AsyncLogger namecom.example levelinfo includeLocationfalse appender-ref refRollingFileAll/ /AsyncLoggerRingBufferSize可以通过systemProperty配置-DAsyncLoggerConfig.RingBufferSize1048576这个值表示缓冲区可以容纳 1048576 个日志事件够绝大多数项目用了。调大它会占用更多内存每一条日志事件大约占用几百字节所以 1048576 条大概就是几百 MB 级的内存占用这个背景你要知晓。4.5 别在异步日志里用 location 信息我在生产环境经常看到有人把pattern写成%class %method %line这种格式一旦开了 async这些 location 信息的获取成本会飙到正常日志的 10 到 100 倍性能直接崩盘。原因很简单获取调用方的类名、方法名、行号需要遍历线程堆栈这个过程在同步模式下已经不算便宜到了异步模式下因为线程切换堆栈遍历更是开销巨大。所以我的建议是日志 pattern 里不要带 location 相关的占位符除了在本地开发调试阶段临时用一下。如果你确实需要定位日志来自哪个类用%logger{36}加上类名就够了它只是读取 logger 名称没有堆栈遍历的开销。5. 生产环境的进阶配置实录5.1 全局日志级别与环境切换配置文件的灵活切换是 log4j2 相比 Logback 的一个显著优势。你可以借助 Spring Boot 的 profile 机制为不同环境维护不同的配置文件log4j2-dev.xml log4j2-prod.xml log4j2-test.xml然后在application.yml里指定激活哪个logging: config: classpath:log4j2-${spring.profiles.active}.xml或者在启动参数里指定-Dlogging.configclasspath:log4j2-prod.xml这种做法的好处是本地开发用控制台友好格式 DEBUG 级别生产环境用 JSON 格式 INFO 级别 异步滚动文件规则互相隔离不用担心改错了影响生产。5.2 pattern 里的魔法参数与隐藏坑照顾到很多朋友是直接复制网上的 pattern我把常用占位符整理成一张速查表占位符含义示例输出%d{yyyy-MM-dd HH:mm:ss.SSS}时间戳2024-01-15 14:30:22.123%thread线程名http-nio-8080-exec-7%-5level日志级别左对齐INFO , ERROR%logger{36}类名缩写com.example.service.UserService%msg日志消息用户登录成功%n换行无%X{requestId}MDC 中的值8f4a9c2e-b7b3-4d31-9e5a-5d5f5a1d5c31%ex异常堆栈java.lang.Exception...%logger{36}里的数字 36 表示类名最大显示 36 个字符超过部分会用点号智能省略比如c.e.s.UserService这样日志里不会出现一长串包名挤到换行。强烈建议在 pattern 里加入%X{requestId}也就是 MDC 中的值。MDCMapped Diagnostic Context是日志框架提供的一种线程级上下文变量你可以在请求入口设置一个唯一的 traceId然后在所有日志输出中植入这个 ID。线上排查问题时从网关入口拿到的 traceId 直接 grep 日志文件整个调用链一目了然效率成倍提升。我建议在下面这个位置加 MDC 过滤器和过滤器配置PatternLayout pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} %X{requestId} - %msg%n/配套在业务代码里用 AOP 或拦截器设置Component public class TraceIdFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { String traceId UUID.randomUUID().toString().replace(-, ); MDC.put(requestId, traceId); try { chain.doFilter(request, response); } finally { MDC.remove(requestId); } } }记住finally里一定要remove不然线程池里的线程复用时MDC 会被下一次请求的 traceId 污染。5.3 JSON 日志输出给日志收集系统吃的口粮如果你在团队里接了日志采集系统比如 ELK、Loki 这类控制台输出的PatternLayout格式就是灾难每条日志字段位置不固定采集端解析规则写到你怀疑人生。这时候直接输出 JSON 格式是最好的解法。log4j2 内置了JsonLayout配置如下Console nameConsoleJson targetSYSTEM_OUT JsonLayout compacttrue eventEoltrue propertiestrue KeyValuePair keyservice valueuser-service/ KeyValuePair keyinstance value${sys:instanceId}/ /JsonLayout /ConsoleJsonLayout输出的样子大概是这样{timeMillis:1705308622123,thread:http-nio-8080-exec-7,level:INFO,loggerName:com.example.service.UserService,message:用户登录成功,service:user-service}compacttrue表示紧凑输出每条 JSON 单行方便采集端按行监听eventEoltrue表示事件结束后换行propertiestrue会把 MDC 中的内容也一并打成 JSON 字段。KeyValuePair可以往 JSON 里追加自定义字段比如服务名、实例 ID、环境标识等这些字段对于多维度的日志检索非常重要。5.4 动态修改日志级别不用重启的热更新这个技巧被很多人忽视了但它真的是神器。log4j2 的configuration节点上有一个monitorInterval属性configuration statusWARN monitorInterval30它的含义是日志框架会每隔 30 秒检查一次配置文件是否有改动如果有自动热加载新配置完全不需要重启应用。这意味着你在排查线上问题时可以把某个logger的级别从 INFO 临时调到 DEBUG保存配置后 30 秒内自动生效定位完问题再调回去。具体操作是在 log4j2 配置里给某个包单独设置 loggerloggers Logger namecom.example.troubleshoot leveldebug additivityfalse appender-ref refConsole/ /Logger root levelinfo appender-ref refConsole/ appender-ref refRollingFileAll/ /root /loggersadditivityfalse很关键它表示该 logger 的日志不会继续向父 loggerroot传递避免同一日志被输出两遍。如果你不懂这个属性调试时就会看到日志重复而且误以为配置有问题。注意热更新配置只针对文件路径的配置变更。如果你的配置文件里用了${sys:property}这类运行时占位符改了系统属性之后热更新不会自动刷新占位符值。这种场景下我还是老老实实重启一次。6. 常见问题与排查技巧实录6.1 问题排查一键对照表我把过往项目中遇到的 log4j2 高频问题整理成了一张速查表收藏好有问题直接照着排查。现象可能原因解决方案引入依赖后日志没变化没排除spring-boot-starter-loggingpom 或 gradle 里排除该依赖控制台疯狂打印 log4j2 自身 DEBUGstatus属性值太低把statusWARN或改为statusERROR日志文件按天不滚动TimeBasedTriggeringPolicy没配置确认filePattern里有%d{yyyy-MM-dd}且时间粒度匹配异步日志启动报Unable to load class com.lmax.disruptor.*没引入 Disruptor 依赖添加 disruptor 依赖日志重复输出additivity没设为 false子 logger 设置additivityfalse异步日志大量丢日志asyncQueueFullPolicy配置不当调整为block或增大RingBufferSize日志里没有 traceId 字段没加 MDC 或 pattern 里没写%X{requestId}配置拦截器写入 MDC 并调整 pattern滚动文件总是被覆盖filePattern里缺少%i添加%i编号占位符线上日志里出现乱码编码不一致Console 和 RollingFile 均配置charsetUTF-86.2 实战排查流程一次线上日志丢失案例说个真实案例。有一次线上接口突然超时量飙升我打开日志文件一看居然有一段 10 分钟的日志是空的后面日志又恢复正常了。所有人第一反应是日志框架出 BUG各种搜资料没结果。我当时的排查思路是这样的先确认是不是滚动策略问题检查滚动文件是否正常生成。打开 log4j2 自身状态把status改成 DEBUG看内部日志有没有异常输出。发现堆满了日志文件的文件句柄异常最终定位到是日志写入线程被 GC 停顿长时间阻塞导致环形缓冲区瞬间打满按照discard策略开始丢日志。这个案例的根因其实是应用本身出现了一次长时间的 Full GC把日志线程给卡死了。但 log4j2 的discard策略在这里起到了保护作用——业务线程没有被日志阻塞拖死接口虽然超时了几分钟但系统没有整体雪崩。之后我在配置里加了一个监控项把status调整成 WARN同时用日志本身记录异步队列饱和的次数AsyncLoggerConfig支持注册饱和回调这样再出现类似情况就能提前收到告警而不是事后救火。6.3 文件编码和文件中文乱码这是个很小但很烦人的问题。window 下默认编码是 GBKLinux 是 UTF-8如果你把配置里 Console 的编码不写死同样的配置在两种平台上跑出来的日志写入到同一个文件中文就会出现乱码。在 appender 里显式声明编码Console nameConsole targetSYSTEM_OUT PatternLayout charsetUTF-8 pattern${PATTERN}/ /ConsoleRollingFile 也一样支持 charset 属性。写清楚之后不管是本地开发还是生产部署日志中文统一正常。6.4 配置文件不生效的终极排查大法如果你改了 log4j2.xml 保存后发现完全没有变化不要急着怀疑人生按这个顺序排查检查文件是否放在 classpath 根目录下默认读取classpath:log4j2.xml。检查application.yml或application.properties里是否有logging.config指向了其他文件如果有优先级会覆盖默认路径。检查 IDEA 或 Eclipse 的 target / classes 目录下是否仍然残留旧的配置文件没有同步刷新。在启动参数里强制指定配置文件路径-Dlogging.configclasspath:log4j2-prod.xml。最后大招在 Spring Boot 启动类里加一行代码LoggerContext context (LoggerContext) LogManager.getContext(false);然后打印context.getConfiguration()的完整内容看实际生效的配置是什么。这个排查顺序基本上百试百灵我还没见过哪次定位不到问题的。7. 一个我能给的实操总结我个人在实际操作中的体会是Spring Boot 整合 log4j2 本身不难难点全在细节里排除默认依赖要记得、异步方案要理解、过滤器和滚动策略要配套、生产配置要区分环境。任何一环甩锅给“配置不生效”多半是前面哪个前置条件没满足。最后再分享一个小技巧保存一份基础可用的 log4j2-prod.xml 在项目模板里当公版配置新项目直接拷贝改改路径就能用。省去每次从零查文档的时间同时还能保证团队内部日志格式的统一规范。后面如果需求扩展再往里面加 RoutingAppender 做业务日志分流、加 Kafka/HTTP 自研 appender 做日志传输基本骨架都不用动。这套东西我已经在不同项目里反复用了很多轮按上面这些配置来不敢说绝对不踩新的坑但常见的那些雷大概率都能避开。如果你配置完后跑起来能看到日志按照你设定的 pattern 输出、按天滚动文件正常生成、异步日志性能提升明显那这篇教程的价值就到位了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑