20260326:traceId链路追踪

This commit is contained in:
zhugongping 2026-03-29 11:31:31 +08:00
parent ea1d179c41
commit 24c1120105
7 changed files with 85 additions and 139 deletions

View File

@ -5,79 +5,36 @@ import org.slf4j.MDC;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadLocalRandom;
/**
* 日志追踪ID工具类
* 统一生成TraceId的逻辑,使用IdWorker和ThreadLocalRandom
* 统一生成 TraceId 并通过 MDC 传递,使同一请求/消息链路的所有日志可关联。
*
* @author Sumuxi
*/
public class TraceIdUtils {
/**
* TraceId在MDC中的key
*/
public static final String TRACE_ID_KEY = "traceId";
/**
* IdWorker实例(单例模式)
* 生成一个新的 TraceId(纯生成,不写入 MDC)。
* 使用 32 位 UUID(去横线),全局唯一且无溢出风险。
*/
private static volatile IdUtils idWorker;
/**
* 获取IdWorker实例(懒加载单例)
*
* @return IdWorker实例
*/
private static IdUtils getIdWorker() {
if (idWorker == null) {
synchronized (TraceIdUtils.class) {
if (idWorker == null) {
// 使用机器ID,可以根据实际情况配置
// 这里使用1作为默认workerId,可以通过配置文件读取
// IdUtils构造函数:workerId, datacenterId,-1表示随机生成
idWorker = new IdUtils(-1L, -1L);
}
}
}
return idWorker;
public static String generateTraceId() {
return IdUtils.fastSimpleUUID();
}
/**
* 生成TraceId
* 格式:IdWorker.nextId() * 1000 + ThreadLocalRandom.nextInt(1000)
* 参考:generateClueId() 的实现方式
* 使用Math.abs确保TraceId为正数
*
* @return TraceId
* 生成 TraceId 并写入当前线程 MDC,返回生成的值。
*/
public static long generateTraceId() {
long id = getIdWorker().nextId();
// 使用绝对值确保TraceId为正数,避免负数显示
// 注意:Math.abs(id) * 1000 可能会溢出,所以需要对最终结果再次使用Math.abs()
long traceId = Math.abs(id) * 1000 + ThreadLocalRandom.current().nextInt(1000);
// 确保最终结果为非负数(处理溢出情况)
traceId = Math.abs(traceId);
MDC.put(TRACE_ID_KEY, String.valueOf(traceId));
public static String generateAndSetTraceId() {
String traceId = generateTraceId();
MDC.put(TRACE_ID_KEY, traceId);
return traceId;
}
/**
* 生成TraceId并放入MDC
* 返回字符串格式的TraceId
*
* @return TraceId字符串
*/
public static String generateTraceIdString() {
long traceId = generateTraceId();
return String.valueOf(traceId);
}
/**
* 设置TraceId到MDC
*
* @param traceId TraceId
* 设置指定 TraceId 到 MDC
*/
public static void setTraceId(String traceId) {
if (traceId != null && !traceId.isEmpty()) {
@ -86,18 +43,7 @@ public class TraceIdUtils {
}
/**
* 设置TraceId到MDC(long类型)
*
* @param traceId TraceId
*/
public static void setTraceId(long traceId) {
MDC.put(TRACE_ID_KEY, String.valueOf(traceId));
}
/**
* 从MDC获取TraceId
*
* @return TraceId
* 从 MDC 获取当前线程的 TraceId
*/
public static String getTraceId() {
return MDC.get(TRACE_ID_KEY);
@ -105,20 +51,19 @@ public class TraceIdUtils {
/**
* 确保当前线程 MDC 中存在 TraceId。
* - 已存在:直接返回
* - 不存在:生成并写入 MDC
* 已存在则返回,不存在则生成并写入 MDC。
*/
public static String ensureTraceId() {
String traceId = getTraceId();
if (traceId == null || traceId.isEmpty()) {
traceId = generateTraceIdString();
traceId = generateAndSetTraceId();
}
return traceId;
}
/**
* 使用指定 traceId 覆盖当前线程 MDC(为空则生成)。
* 主要用于跨线程/跨进程透传后的落地。
* 用于跨线程/跨进程透传后的落地。
*/
public static String ensureTraceId(String traceId) {
if (traceId == null || traceId.isEmpty()) {
@ -129,7 +74,15 @@ public class TraceIdUtils {
}
/**
* 包装 Runnable:透传提交线程的 MDC,上下文中无 traceId 则补齐;执行完自动清理/还原。
* 清除 MDC 中的 TraceId
*/
public static void clearTraceId() {
MDC.remove(TRACE_ID_KEY);
}
/**
* 包装 Runnable:透传提交线程的 MDC 上下文到执行线程,
* 无 traceId 则补齐;执行完自动还原,避免线程复用污染。
*/
public static Runnable wrapWithMdc(Runnable task) {
final Map<String, String> captured = MDC.getCopyOfContextMap();
@ -154,7 +107,8 @@ public class TraceIdUtils {
}
/**
* 包装 Callable:透传提交线程的 MDC,上下文中无 traceId 则补齐;执行完自动清理/还原。
* 包装 Callable:透传提交线程的 MDC 上下文到执行线程,
* 无 traceId 则补齐;执行完自动还原,避免线程复用污染。
*/
public static <T> Callable<T> wrapWithMdc(Callable<T> task) {
final Map<String, String> captured = MDC.getCopyOfContextMap();
@ -177,12 +131,4 @@ public class TraceIdUtils {
}
};
}
/**
* 清除MDC中的TraceId
*/
public static void clearTraceId() {
MDC.remove(TRACE_ID_KEY);
}
}

View File

@ -4,7 +4,6 @@ import com.fastbee.common.config.RuoYiConfig;
import com.fastbee.common.constant.Constants;
import com.fastbee.framework.interceptor.LanguageInterceptor;
import com.fastbee.framework.interceptor.RepeatSubmitInterceptor;
import com.fastbee.framework.interceptor.TraceIdInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -30,8 +29,6 @@ public class ResourcesConfig implements WebMvcConfigurer {
private RepeatSubmitInterceptor repeatSubmitInterceptor;
@Resource
private LanguageInterceptor languageInterceptor;
@Autowired
private TraceIdInterceptor traceIdInterceptor;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
@ -51,8 +48,6 @@ public class ResourcesConfig implements WebMvcConfigurer {
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// TraceId拦截器放在最前面,确保所有请求都有TraceId
registry.addInterceptor(traceIdInterceptor).addPathPatterns("/**").order(0);
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
//这里配置国际化拦截器的白名单
registry.addInterceptor(languageInterceptor).addPathPatterns("/**").excludePathPatterns("/v2/api-docs",

View File

@ -2,6 +2,7 @@ package com.fastbee.framework.config;
import com.fastbee.framework.config.properties.PermitAllUrlProperties;
import com.fastbee.framework.security.filter.JwtAuthenticationTokenFilter;
import com.fastbee.framework.security.filter.TraceIdFilter;
import com.fastbee.framework.security.handle.AuthenticationEntryPointImpl;
import com.fastbee.framework.security.handle.LogoutSuccessHandlerImpl;
import org.springframework.beans.factory.annotation.Autowired;
@ -18,6 +19,7 @@ import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.web.filter.CorsFilter;
/**
@ -52,6 +54,12 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* TraceId 过滤器(最高优先级,保证整个请求链路 TraceId 一致)
*/
@Autowired
private TraceIdFilter traceIdFilter;
/**
* 跨域过滤器
*/
@ -152,6 +160,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
// TraceId filter 放在 Security 过滤链最前面,确保所有日志都有同一个 TraceId
httpSecurity.addFilterBefore(traceIdFilter, SecurityContextPersistenceFilter.class);
}
/**

View File

@ -1,51 +0,0 @@
package com.fastbee.framework.interceptor;
import com.fastbee.common.utils.TraceIdUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 日志追踪ID拦截器
* 用于生成和传递TraceId,方便同一个链路调用追查问题
*
* @author Sumuxi
*/
@Component
public class TraceIdInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取请求URI
String requestURI = request.getRequestURI();
// 尝试从请求头中获取TraceId(支持分布式追踪)
String traceId = request.getHeader("X-Trace-Id");
// 如果请求头中没有TraceId,则生成一个新的
if (traceId == null || traceId.isEmpty()) {
traceId = TraceIdUtils.generateTraceIdString();
} else {
// 如果请求头中有TraceId,设置到MDC
TraceIdUtils.setTraceId(traceId);
}
// 将TraceId放入响应头,方便前端获取
response.setHeader("X-Trace-Id", traceId);
// 打印请求路径日志
org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TraceIdInterceptor.class);
log.info("拦截请求路径: {}", requestURI);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 请求完成后清除MDC中的TraceId,避免内存泄漏
TraceIdUtils.clearTraceId();
}
}

View File

@ -0,0 +1,48 @@
package com.fastbee.framework.security.filter;
import com.fastbee.common.utils.TraceIdUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* TraceId 过滤器 — 替代原 TraceIdInterceptor。
* <p>
* 作为 Servlet Filter 注册在 Spring Security 过滤链最前端,
* 保证整个请求生命周期(包括 Security 过滤器、JWT 验证等)的日志
* 都携带同一个 TraceId。
*
* @author Sumuxi
*/
@Component
public class TraceIdFilter extends OncePerRequestFilter {
private static final String TRACE_HEADER = "X-Trace-Id";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
String traceId = request.getHeader(TRACE_HEADER);
if (traceId == null || traceId.isEmpty()) {
traceId = TraceIdUtils.generateAndSetTraceId();
} else {
TraceIdUtils.setTraceId(traceId);
}
response.setHeader(TRACE_HEADER, traceId);
filterChain.doFilter(request, response);
} finally {
TraceIdUtils.clearTraceId();
}
}
}

View File

@ -56,8 +56,7 @@ public abstract class DeviceAbstractQuartzJob implements Job {
protected void before(JobExecutionContext context, DeviceJob deviceJob) {
threadLocal.set(new Date());
// 为设备定时任务生成TraceId,使用统一的生成逻辑
TraceIdUtils.generateTraceId();
TraceIdUtils.generateAndSetTraceId();
log.info("设备定时任务开始执行: {} - {}", deviceJob.getJobName(), deviceJob.getJobGroup());
}

View File

@ -56,8 +56,7 @@ public abstract class AbstractQuartzJob implements Job {
protected void before(JobExecutionContext context, SysJob sysJob) {
threadLocal.set(new Date());
// 为定时任务生成TraceId,使用统一的生成逻辑
TraceIdUtils.generateTraceId();
TraceIdUtils.generateAndSetTraceId();
log.info("定时任务开始执行: {} - {}", sysJob.getJobName(), sysJob.getJobGroup());
}