Spring Boot自定义日志Starter:如何从HTTP请求动态填充Logback自定义字段?
解决方案:利用MDC + 请求拦截器动态注入tenantId
这个需求是日志标准化场景里的常见问题,核心思路是借助**MDC(Mapped Diagnostic Context)**存储请求上下文变量,结合Spring的请求拦截机制来实现动态tenantId的注入,具体步骤如下:
1. 编写请求拦截器,提取tenantId并存入MDC
MDC是Logback提供的线程上下文存储工具,能让我们在日志中动态获取当前线程的上下文变量。我们需要先从HTTP请求中提取tenantId(可根据实际从请求头、参数或Cookie中获取),存入MDC后,在请求结束时清理MDC,避免线程池复用导致的脏数据。
创建拦截器类:
import org.slf4j.MDC; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class TenantIdInterceptor implements HandlerInterceptor { // 自定义tenantId的请求头key,可根据业务调整 private static final String TENANT_ID_HEADER = "X-Tenant-Id"; private static final String MDC_TENANT_KEY = "tenantId"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从请求头获取tenantId,也可改为从请求参数、Cookie等位置获取 String tenantId = request.getHeader(TENANT_ID_HEADER); // 为空时设置默认值,避免日志字段缺失 if (tenantId == null || tenantId.trim().isEmpty()) { tenantId = "unknown"; } // 将tenantId存入MDC MDC.put(MDC_TENANT_KEY, tenantId); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 请求结束后移除MDC中的tenantId,防止线程复用导致数据污染 MDC.remove(MDC_TENANT_KEY); } }
2. 注册拦截器到Spring容器
需要把上面的拦截器配置到Spring Boot的WebMvc上下文,让它对请求生效:
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 注册拦截器,拦截所有请求路径,可根据业务调整拦截范围 registry.addInterceptor(new TenantIdInterceptor()) .addPathPatterns("/**"); } }
3. 修改logback.xml中的customFields配置
把原来硬编码的tenantId替换为MDC中的变量,LogstashEncoder支持用%X{key}引用MDC值,还可以通过:-设置默认值:
<encoder class="net.logstash.logback.encoder.LogstashEncoder"> <!-- 从MDC中获取tenantId,无值时用unknown兜底 --> <customFields>{"tenantId":"%X{tenantId:-unknown}"}</customFields> </encoder>
额外注意事项
- 异步线程场景:如果项目中使用
@Async等异步方法,MDC上下文不会自动传递到异步线程。由于你已经集成了Sleuth,它默认会帮你传递MDC上下文,但如果是自定义线程池,需要手动配置TaskDecorator来传递MDC:
import org.slf4j.MDC; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.TaskDecorator; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.Map; import java.util.concurrent.Executor; @Configuration public class AsyncConfig { @Bean(name = "asyncExecutor") public Executor asyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 配置线程池基础参数 executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(20); executor.setThreadNamePrefix("Async-"); // 添加TaskDecorator传递MDC上下文 executor.setTaskDecorator(runnable -> { Map<String, String> mdcContext = MDC.getCopyOfContextMap(); return () -> { try { if (mdcContext != null) { MDC.setContextMap(mdcContext); } runnable.run(); } finally { MDC.clear(); } }; }); executor.initialize(); return executor; } }
- 合法性校验:如果tenantId是敏感业务字段,建议在拦截器中添加校验逻辑,确保传入的tenantId符合业务规范,避免恶意注入。
完成以上配置后,下游项目引入你的Starter时,只要请求携带对应字段的tenantId,日志中的tenantId就会自动替换为请求中的值,无值时则使用默认的unknown填充。
内容的提问来源于stack exchange,提问作者keepmoving
相关产品推荐
相关产品推荐

