在Spring项目里引入Log4j记录日志,通常是为了统一输出格式、按级别过滤、把日志持久化到文件,方便线上问题追踪。但实际配置时,经常碰到日志不输出、输出两次、启动时报找不到Appender等情况。这些问题的根源往往不在Log4j本身,而在依赖选择、配置文件命名和日志继承关系上。本文从基础配置讲起,重点分析几个高频误区,并给出可直接落地的排查思路。

Log4j在Spring中的角色与基础配置
Log4j是Apache开源的日志框架,分为1.x和2.x两个主要版本,二者配置格式和核心包完全不一样。Spring本身只依赖SLF4J日志门面,并不绑定具体实现。想要用Log4j输出日志,需要额外引入对应的桥接包。如果是传统Spring项目使用Log4j 1.x,一般加入slf4j-log4j12;如果使用Log4j 2.x,则加入log4j-slf4j-impl和log4j-core。这个步骤如果少了,SLF4J会在启动时给出警告,提示找不到日志实现。对于Spring Boot项目,默认使用的是Logback,要切换到Log4j2必须显式排除spring-boot-starter-logging并引入spring-boot-starter-log4j2,否则同时存在两个实现会让SLF4J选择不可预期。
先看一个Log4j 1.x的基础配置,通常放在classpath根目录下的log4j.properties文件中。
log4j.rootLogger=INFO, console, file
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Target=System.out
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.File=logs/app.log
log4j.appender.file.MaxFileSize=10MB
log4j.appender.file.MaxBackupIndex=10
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
log4j.logger.com.example=DEBUG
配置中rootLogger定义了全局级别和挂载的Appender,console输出到控制台,file使用RollingFileAppender做文件滚动。PatternLayout里的%d表示时间,%-5p表示日志级别左对齐占5位,%c{1}输出类名最后一段,%L输出行号,%m是日志内容,%n换行。这类配置适用于传统Servlet或Spring MVC项目。如果使用Log4j 2.x,配置文件名必须是log4j2.xml,且同样要放在资源根目录。
对于Spring Boot项目,切换Log4j2的典型依赖配置如下。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
</dependencies>
这里通过exclusion排除了默认的Logback,再引入Log4j2 starter。启动成功后,在resources目录下放置log4j2.xml即可生效。如果缺少排除步骤,classpath中会同时存在Logback和Log4j2,SLF4J会绑定其中一个,具体绑定结果取决于依赖顺序,经常导致日志配置不按预期工作。
Spring中使用Log4j的常见误区
第一个误区是依赖不全或者冲突。很多人只在pom.xml中加了log4j-core,没有加log4j-slf4j-impl或log4j-api。这样SLF4J在初始化时找不到对应实现,会打印类似SLF4J: Failed to load class org.slf4j.impl.StaticLoggerBinder的警告,然后回退到无操作实现,所有日志全部丢失。反过来,如果同时保留Logback和Log4j2,SLF4J会警告Class path contains multiple SLF4J bindings,并选择其中一个。排查这类问题最直接的办法是执行mvn dependency:tree查看日志相关的依赖树,确认只存在一个slf4j绑定包。
第二个误区是配置文件没被加载。Log4j 1.x默认寻找classpath根目录下的log4j.properties,Log4j 2.x默认寻找log4j2.xml。如果文件放在src/main/java目录而非src/main/resources目录,编译后不会出现在classpath根下,自然无法加载。此时启动会看到log4j:WARN No appenders could be found for logger这样的提示。Spring Boot还支持通过logging.config属性指定配置文件位置,但指定时要注意文件后缀必须对应实现类型,否则依旧不生效。确认是否加载成功,可以在启动日志中观察是否存在Log4j2的初始化信息,或者故意写错一个Appender名称看启动是否报错。
第三个误区是additivity属性理解错误导致日志重复输出。Log4j中的Logger具有继承关系,子Logger默认会把日志传递给父Logger的Appender。例如根Logger挂载了console,com.example包下的Logger如果也挂载console,并且没有关闭additivity,那么com.example下的日志会在两个Appender中各输出一次,有时甚至出现三份。解决方式是在子Logger上设置additivity=false。Log4j 1.x的写法是log4j.additivity.com.example=false;Log4j 2.x则在Logger元素上直接写additivity="false"。下面是一个Log4j 2.x的修复片段。
<Logger name="com.example" level="DEBUG" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
第四个误区是日志级别设置不合理。开发阶段为了调试方便把根Logger设为DEBUG,上线时忘记改回INFO或WARN,导致大量调试信息写入磁盘,不仅拖慢性能,还可能覆盖掉关键错误。反过来,把根Logger设为OFF会直接关闭所有日志,线上出问题后只能靠猜。比较合理的做法是生产环境根级别设INFO,针对频繁调试的包单独设DEBUG,并在发布前检查配置文件是否与当前环境一致。此外,避免在循环中打印DEBUG日志,尽量使用参数化消息而不是字符串拼接,减少运行开销。
实用配置与排查技巧
生产环境推荐使用滚动文件Appender,防止单个日志文件无限增长。下面展示一个Log4j 2.x的RollingFile配置,基于文件大小和时间两个维度滚动,并自动压缩旧文件,保留最近20个归档。
<RollingFile name="RollingFile" fileName="logs/app.log"
filePattern="logs/app-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
<Policies>
<SizeBasedTriggeringPolicy size="10 MB"/>
<TimeBasedTriggeringPolicy interval="1"/>
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingFile>
这个配置会按天滚动,单个文件超过10MB也会触发滚动。旧文件压缩为.gz格式,最多保留20个。注意filePattern中的%d和%i是Log4j2的占位符,日期格式和归档序号要与Policies配合使用,否则可能产生归档文件名冲突。对于Log4j 1.x,可以使用DailyRollingFileAppender或RollingFileAppender,但2.x的配置更灵活,建议新项目直接使用2.x。
如果需要在分布式或高并发场景下追踪同一个请求的完整日志链路,可以使用MDC。MDC是SLF4J提供的上下文容器,能在一个线程内保存键值对。在Spring Web应用中,通过Filter把请求ID放入MDC,日志输出时用%X{requestId}打印,就能把同一请求的所有日志串起来。下面是一个简单的Filter实现。
import org.slf4j.MDC;
import javax.servlet.*;
import java.io.IOException;
public class RequestIdFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String requestId = java.util.UUID.randomUUID().toString();
MDC.put("requestId", requestId);
try {
chain.doFilter(request, response);
} finally {
MDC.remove("requestId");
}
}
}
在PatternLayout中加上%X{requestId}后,每一行日志都会带上本次请求的唯一标识。注意在finally块中必须调用MDC.remove,否则线程池复用线程时会导致requestId串掉。这个技巧在排查线上并发问题时非常有用。
最后补充几个排查方向。如果日志文件一直不出现,除了检查配置文件和依赖,还要看进程是否有目标目录的写权限,以及日志文件路径是否使用了相对路径导致写到了启动目录而非预期目录。启动时加上-Dlog4j.debug=true可以查看Log4j 1.x的配置加载过程,Log4j 2.x对应-Dorg.apache.logging.log4j.simplelog.StatusLogger.level=TRACE。通过这些内部状态输出,可以快速定位是配置文件没找到、Appender初始化失败,还是依赖绑定问题。总之,Spring项目使用Log4j记录日志并不复杂,但依赖、命名、继承、级别这四个环节必须逐一核对,才能避开常见坑,让日志真正成为排障的利器。