Spring Boot Logback:实现定时任务日志与业务日志隔离
1. 问题背景
在 Spring Boot 应用中,我们通常使用@EnableScheduling启用定时任务。这些定时任务在执行过程中,可能会调用 Mapper 方法与数据库交互,产生大量的 SQL 日志。默认情况下,这些日志会与普通业务请求的日志一起输出到日志文件或控制台。
现在希望实现以下目标:
- 定时任务类及其调用链路中的所有日志(包括 SQL 日志),能够被单独输出到一个或多个指定文件。
 - 普通业务调用 Mapper 产生的 SQL 日志,保持原有的输出位置不变(例如
app.log)。 - 支持多个定时任务,每个任务的日志可以独立输出到不同的文件。
 - 未设置特定日志标签的日志,应默认输出到
app.log,不被定时任务日志分流机制影响。 
2. 核心思路:MDC 与 Logback Filter
要实现日志的精准隔离,关键在于能够区分日志事件的来源。Logback 提供了 MDC (Mapped Diagnostic Context) 机制,允许我们在当前线程上下文中存储键值对信息。日志事件在被处理时,可以访问这些 MDC 信息,从而实现基于上下文的日志过滤和路由。
3. 实现步骤
3.1 添加 Janino 依赖
首先,在pom.xml中添加 Janino 依赖:
<!-- 用于 Logback EvaluatorFilter 中的表达式解析 -->
<dependency>
    <groupId>org.codehaus.janino</groupId>
    <artifactId>janino</artifactId>
    <version>3.1.9</version> <!-- 请使用最新稳定版本 -->
</dependency>
Janino 是一个 Java 编译器库,后面会用来判断 MDC 上下文中的值,结合 EvaluatorFilter 实现基于 MDC 的日志过滤。
3.2 在每个定时任务入口设置唯一的logTag
为每个定时任务设置一个唯一的logTag,例如jobA、jobB。
import org.slf4j.MDC;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component
public class MyJobs {
    private static final Logger logger = LoggerFactory.getLogger(MyJobs.class);
    @Scheduled(cron = "0 0/1 * * * ?") // 每分钟执行一次
    public void jobA() {
        MDC.put("logTag", "jobA"); // 设置任务 A 的 MDC 标记
        try {
            logger.info("执行任务 A...");
            // 假设这里调用了 Mapper 方法,SQL 日志会进入 jobA.log
            // userMapper.selectById(1);
        } finally {
            MDC.remove("logTag"); // 清理 MDC 标记
        }
    }
    @Scheduled(cron = "0 0/2 * * * ?") // 每两分钟执行一次
    public void jobB() {
        MDC.put("logTag", "jobB"); // 设置任务 B 的 MDC 标记
        try {
            logger.info("执行任务 B...");
            // 假设这里调用了 Mapper 方法,SQL 日志会进入 jobB.log
            // productMapper.selectByName("test");
        } finally {
            MDC.remove("logTag"); // 清理 MDC 标记
        }
    }
    // 假设这是一个普通的业务方法,不会设置 logTag
    public void normalBusinessMethod() {
        logger.info("执行普通业务方法...");
    }
}
3.3 配置logback-spring.xml使用SiftingAppender
这里需要注意MDCBasedDiscriminator必须设置defaultValue属性。我们将其设置为一个特殊值(如none),然后在sift内部的appender中添加一个EvaluatorFilter来过滤掉这个特殊值,这样在没有设置logTag时,不会输出到none.log中。
<configuration>
    <!-- 普通业务日志 app.log -->
    <appender name="APP" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/app.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
        <!-- 如果有 logTag,不输出到 app.log -->
        <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
            <evaluator class="ch.qos.logback.classic.boolex.JaninoEventEvaluator">
                <expression>
                    return mdc.get("logTag") != null;
                </expression>
            </evaluator>
            <OnMatch>DENY</OnMatch>
            <OnMismatch>NEUTRAL</OnMismatch>
        </filter>
    </appender>
    <!-- 定时任务日志,按 logTag 拆分 -->
    <appender name="JOB_SIFT" class="ch.qos.logback.classic.sift.SiftingAppender">
        <discriminator class="ch.qos.logback.classic.sift.MDCBasedDiscriminator">
            <!-- 用 logTag 作为分片键 -->
            <key>logTag</key>
            <!-- 必须设置 defaultValue,但我们设置为 none,后续会过滤掉 -->
            <defaultValue>none</defaultValue>
        </discriminator>
        <sift>
            <!-- 动态创建的 Appender,名称和文件路径包含 logTag -->
            <appender name="JOB-${logTag}" class="ch.qos.logback.core.rolling.RollingFileAppender">
                <file>logs/${logTag}.log</file>
                <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                    <fileNamePattern>logs/${logTag}.%d{yyyy-MM-dd}.log</fileNamePattern>
                    <maxHistory>30</maxHistory>
                </rollingPolicy>
                <encoder>
                    <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{logTag}] - %msg%n</pattern>
                </encoder>
                <!-- 过滤掉 defaultValue=none 的日志,避免生成 none.log -->
                <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
                    <evaluator class="ch.qos.logback.classic.boolex.JaninoEventEvaluator">
                        <expression>
                            return mdc == null || mdc.get("jobTag") == null || "none".equals(mdc.get("jobTag"));
                        </expression>
                    </evaluator>
                    <OnMatch>DENY</OnMatch> <!-- 匹配到空或为 none 的 logTag 则拒绝 -->
                    <OnMismatch>NEUTRAL</OnMismatch> <!-- 匹配到非 none 的 logTag 则中立,继续处理 -->
                </filter>
            </appender>
        </sift>
    </appender>
    <!-- Root Logger 配置:所有日志都先经过这里,再由 Appender 的 Filter 进行分流 -->
    <root level="INFO">
        <appender-ref ref="APP"/>
        <appender-ref ref="JOB_SIFT"/>
    </root>
</configuration>
效果:
- 普通业务日志(无
logTag)将只写入app.log,不会生成none.log。 - 定时任务 A(
logTag=jobA)的日志将只写入jobA.log。 - 定时任务 B(
logTag=jobB)的日志将只写入jobB.log。 - 定时任务中调用的 Mapper 产生的 SQL 日志,也会跟随其所属任务的
logTag写入对应的任务日志文件。 
参考:
Logback Manual - MDC
Logback Manual - SiftingAppender
Logback Manual - EvaluatorFilter
Logback 使用 SiftingAppender、MDC 实现日志文件分离,动态指定文件
                
            
        
浙公网安备 33010602011771号