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 标记}}// 假设这是一个普通的业务方法,不会设置 logTagpublic 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 实现日志文件分离,动态指定文件