diff --git a/ruoyi-admin/src/main/resources/logback-plus.xml b/ruoyi-admin/src/main/resources/logback-plus.xml index 2d786f9a9..a51e42cc7 100644 --- a/ruoyi-admin/src/main/resources/logback-plus.xml +++ b/ruoyi-admin/src/main/resources/logback-plus.xml @@ -2,8 +2,8 @@ - + value="%cyan(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) %magenta([traceId:%X{traceId:-}]) - %msg%n"/> + diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java index 155a8d071..3f02333fe 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java @@ -3,6 +3,7 @@ package org.dromara.common.web.config; import jakarta.servlet.DispatcherType; import org.dromara.common.web.config.properties.XssProperties; import org.dromara.common.web.filter.RepeatableFilter; +import org.dromara.common.web.filter.TraceIdFilter; import org.dromara.common.web.filter.XssFilter; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -10,6 +11,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.web.servlet.FilterRegistration; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; +import org.springframework.core.Ordered; /** * Filter配置 @@ -38,4 +40,15 @@ public class FilterConfig { return new RepeatableFilter(); } + @Bean + public FilterRegistrationBean traceIdFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setDispatcherTypes(DispatcherType.REQUEST); + registration.setFilter(new TraceIdFilter()); + registration.addUrlPatterns("/*"); + registration.setName("traceIdFilter"); + registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 10); + return registration; + } + } diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/context/TraceContext.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/context/TraceContext.java new file mode 100644 index 000000000..7c62ca691 --- /dev/null +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/context/TraceContext.java @@ -0,0 +1,89 @@ +package org.dromara.common.web.context; + +import cn.hutool.core.util.IdUtil; +import org.slf4j.MDC; + +import java.util.Map; + +/** + * TraceId 上下文工具类(基于 MDC + ThreadLocal 双保险) + */ +public final class TraceContext { + + private static final String TRACE_ID_KEY = "traceId"; + + private TraceContext() { + } + + /** + * 获取当前 traceId(不存在返回 null) + */ + public static String getTraceId() { + return MDC.get(TRACE_ID_KEY); + } + + /** + * 获取或创建 traceId(不存在时自动生成) + */ + public static String getOrCreateTraceId() { + String traceId = getTraceId(); + if (traceId == null) { + traceId = generateTraceId(); + setTraceId(traceId); + } + return traceId; + } + + /** + * 设置 traceId 到 MDC + */ + public static void setTraceId(String traceId) { + if (traceId != null) { + MDC.put(TRACE_ID_KEY, traceId); + } + } + + /** + * 清理当前线程的 traceId(非常重要!) + */ + public static void clear() { + MDC.remove(TRACE_ID_KEY); + } + + /** + * 生成 traceId 的规则 + */ + public static String generateTraceId() { + return IdUtil.getSnowflake().nextIdStr(); + } + + /** + * 捕获当前线程的完整 MDC 上下文快照 + *

适用于手动创建 Thread、CompletableFuture、自定义线程池等场景

+ */ + public static Map captureMdc() { + return MDC.getCopyOfContextMap(); + } + + /** + * 在新线程/任务中恢复完整的 MDC 上下文 + *

建议在新线程的开头调用,在 finally 块中调用 clear()

+ */ + public static void restoreMdc() { + Map contextMap = TraceContext.captureMdc(); + if (contextMap != null) { + MDC.setContextMap(contextMap); + } + } + + /** + * 在新线程/任务中只恢复 traceId + */ + public static void restoreTraceId(String traceId) { + MDC.remove(TRACE_ID_KEY); + if (traceId != null) { + MDC.put(TRACE_ID_KEY, traceId); + } + } + +} diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/TraceIdFilter.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/TraceIdFilter.java new file mode 100644 index 000000000..3e842593a --- /dev/null +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/TraceIdFilter.java @@ -0,0 +1,43 @@ +package org.dromara.common.web.filter; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.dromara.common.web.context.TraceContext; +import org.springframework.util.StringUtils; + +import java.io.IOException; + +/** + * Author: miracle + * Date: 2026/1/14 下午1:41 + */ +public class TraceIdFilter implements Filter { + + private static final String TRACE_ID_HEADER = "X-Trace-Id"; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + + String traceId = req.getHeader(TRACE_ID_HEADER); + + // 前端传了就用前端的,没传就自己生成 + if (!StringUtils.hasText(traceId)) { + traceId = TraceContext.generateTraceId(); + } + + try { + TraceContext.setTraceId(traceId); + // 可选:把 traceId 写回响应头,方便前端/测试工具看到 + resp.setHeader(TRACE_ID_HEADER, traceId); + chain.doFilter(request, response); + } finally { + // 必须清理!防止线程池复用导致上下文泄漏 + TraceContext.clear(); + } + } +}