TradeId:controller改造返回值中加上traceId
This commit is contained in:
parent
7039f43964
commit
58632033b5
@ -1,6 +1,7 @@
|
|||||||
package com.fastbee.common.core.domain;
|
package com.fastbee.common.core.domain;
|
||||||
|
|
||||||
import com.fastbee.common.constant.HttpStatus;
|
import com.fastbee.common.constant.HttpStatus;
|
||||||
|
import com.fastbee.common.trace.TraceContext;
|
||||||
import com.fastbee.common.utils.StringUtils;
|
import com.fastbee.common.utils.StringUtils;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -196,4 +197,20 @@ public class AjaxResult extends HashMap<String, Object> {
|
|||||||
super.put(key, value);
|
super.put(key, value);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附带当前链路 TraceId
|
||||||
|
*
|
||||||
|
* <p>用于绕过 ResponseBodyAdvice 直接写响应的场景(Security 认证失败、重复提交拦截等),
|
||||||
|
* 这类响应不经过 TraceIdResponseAdvice,需要手动携带 TraceId 供前端排查。
|
||||||
|
*
|
||||||
|
* @return 当前对象,便于链式调用
|
||||||
|
*/
|
||||||
|
public AjaxResult withTraceId() {
|
||||||
|
String traceId = TraceContext.getTraceId();
|
||||||
|
if (traceId != null) {
|
||||||
|
super.put(TraceContext.TRACE_ID_KEY, traceId);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,13 @@ public class CommonResult<T> implements Serializable {
|
|||||||
* @see ErrorCode#getMsg() ()
|
* @see ErrorCode#getMsg() ()
|
||||||
*/
|
*/
|
||||||
private String msg;
|
private String msg;
|
||||||
|
/**
|
||||||
|
* 链路追踪ID,由 TraceIdResponseAdvice 统一回填,用于前后端联查日志
|
||||||
|
*
|
||||||
|
* <p>属性名必须与 {@link com.fastbee.common.trace.TraceContext#TRACE_ID_KEY} 保持一致,
|
||||||
|
* 否则本类序列化出的 JSON 字段名会与 AjaxResult 注入的字段名不一致。
|
||||||
|
*/
|
||||||
|
private String traceId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将传入的 result 对象,转换成另外一个泛型结果的对象
|
* 将传入的 result 对象,转换成另外一个泛型结果的对象
|
||||||
|
|||||||
@ -25,6 +25,14 @@ public class R<T> implements Serializable {
|
|||||||
|
|
||||||
private T data;
|
private T data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 链路追踪ID,由 TraceIdResponseAdvice 统一回填,用于前后端联查日志
|
||||||
|
*
|
||||||
|
* <p>属性名必须与 {@link com.fastbee.common.trace.TraceContext#TRACE_ID_KEY} 保持一致,
|
||||||
|
* 否则本类序列化出的 JSON 字段名会与 AjaxResult 注入的字段名不一致。
|
||||||
|
*/
|
||||||
|
private String traceId;
|
||||||
|
|
||||||
public static <T> R<T> ok() {
|
public static <T> R<T> ok() {
|
||||||
return restResult(null, SUCCESS, "操作成功");
|
return restResult(null, SUCCESS, "操作成功");
|
||||||
}
|
}
|
||||||
@ -96,4 +104,12 @@ public class R<T> implements Serializable {
|
|||||||
public void setData(T data) {
|
public void setData(T data) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getTraceId() {
|
||||||
|
return traceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTraceId(String traceId) {
|
||||||
|
this.traceId = traceId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,14 @@ public class TableDataInfo implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private String msg;
|
private String msg;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 链路追踪ID,由 TraceIdResponseAdvice 统一回填,用于前后端联查日志
|
||||||
|
*
|
||||||
|
* <p>属性名必须与 {@link com.fastbee.common.trace.TraceContext#TRACE_ID_KEY} 保持一致,
|
||||||
|
* 否则本类序列化出的 JSON 字段名会与 AjaxResult 注入的字段名不一致。
|
||||||
|
*/
|
||||||
|
private String traceId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表格数据对象
|
* 表格数据对象
|
||||||
*/
|
*/
|
||||||
@ -79,4 +87,12 @@ public class TableDataInfo implements Serializable {
|
|||||||
public void setMsg(String msg) {
|
public void setMsg(String msg) {
|
||||||
this.msg = msg;
|
this.msg = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getTraceId() {
|
||||||
|
return traceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTraceId(String traceId) {
|
||||||
|
this.traceId = traceId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,10 +44,25 @@ import java.io.Serializable;
|
|||||||
public class TraceContext {
|
public class TraceContext {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TraceId 在 MDC 中的 key
|
* TraceId 的名称,同时用于日志 MDC 键名与 HTTP 响应体 JSON 字段名
|
||||||
|
*
|
||||||
|
* <p>两者刻意共用同一个名称:日志里 {@code grep 'traceId=xxx'} 与前端看到的
|
||||||
|
* {@code traceId: xxx} 是同一个值,避免排障时再做一次名称换算。
|
||||||
*/
|
*/
|
||||||
public static final String TRACE_ID_KEY = "traceId";
|
public static final String TRACE_ID_KEY = "traceId";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TraceId 在 HTTP Header / MQ Header 中的名称
|
||||||
|
* HTTP 传播沿用业界惯例的连字符风格,因此与 MDC / JSON 的字段名不同
|
||||||
|
*/
|
||||||
|
public static final String TRACE_ID_HEADER = "X-Trace-Id";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TraceId 的合法字符集与长度上限
|
||||||
|
* 用于校验上游传入的 TraceId,防止伪造或包含换行符导致日志注入
|
||||||
|
*/
|
||||||
|
public static final String TRACE_ID_PATTERN = "^[A-Za-z0-9_-]{1,64}$";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TenantId 在 MDC 中的 key
|
* TenantId 在 MDC 中的 key
|
||||||
* 预留字段,暂未使用
|
* 预留字段,暂未使用
|
||||||
|
|||||||
@ -2,12 +2,15 @@ package com.fastbee.framework.config;
|
|||||||
|
|
||||||
import com.fastbee.common.config.RuoYiConfig;
|
import com.fastbee.common.config.RuoYiConfig;
|
||||||
import com.fastbee.common.constant.Constants;
|
import com.fastbee.common.constant.Constants;
|
||||||
|
import com.fastbee.common.trace.TraceContext;
|
||||||
|
import com.fastbee.framework.filter.TraceIdFilter;
|
||||||
import com.fastbee.framework.interceptor.LanguageInterceptor;
|
import com.fastbee.framework.interceptor.LanguageInterceptor;
|
||||||
import com.fastbee.framework.interceptor.RepeatSubmitInterceptor;
|
import com.fastbee.framework.interceptor.RepeatSubmitInterceptor;
|
||||||
import com.fastbee.framework.interceptor.TraceIdInterceptor;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.http.CacheControl;
|
import org.springframework.http.CacheControl;
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
@ -30,8 +33,20 @@ public class ResourcesConfig implements WebMvcConfigurer {
|
|||||||
private RepeatSubmitInterceptor repeatSubmitInterceptor;
|
private RepeatSubmitInterceptor repeatSubmitInterceptor;
|
||||||
@Resource
|
@Resource
|
||||||
private LanguageInterceptor languageInterceptor;
|
private LanguageInterceptor languageInterceptor;
|
||||||
@Autowired
|
|
||||||
private TraceIdInterceptor traceIdInterceptor;
|
/**
|
||||||
|
* TraceId 过滤器注册
|
||||||
|
* 必须排在 Spring Security 过滤器链(-100)之前,保证 token 校验、鉴权失败等早期日志
|
||||||
|
* 与 Controller 返回的 traceId 属于同一条链路
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public FilterRegistrationBean<TraceIdFilter> traceIdFilterRegistration() {
|
||||||
|
FilterRegistrationBean<TraceIdFilter> registration = new FilterRegistrationBean<>(new TraceIdFilter());
|
||||||
|
registration.addUrlPatterns("/*");
|
||||||
|
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
|
||||||
|
registration.setName("traceIdFilter");
|
||||||
|
return registration;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
@ -51,8 +66,7 @@ public class ResourcesConfig implements WebMvcConfigurer {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
// TraceId拦截器放在最前面,确保所有请求都有TraceId
|
// TraceId 统一由 TraceIdFilter 在 Servlet 过滤器最前端生成,此处不再重复处理
|
||||||
registry.addInterceptor(traceIdInterceptor).addPathPatterns("/**").order(0);
|
|
||||||
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
|
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
|
||||||
//这里配置国际化拦截器的白名单
|
//这里配置国际化拦截器的白名单
|
||||||
registry.addInterceptor(languageInterceptor).addPathPatterns("/**").excludePathPatterns("/v2/api-docs",
|
registry.addInterceptor(languageInterceptor).addPathPatterns("/**").excludePathPatterns("/v2/api-docs",
|
||||||
@ -70,6 +84,8 @@ public class ResourcesConfig implements WebMvcConfigurer {
|
|||||||
config.addAllowedOriginPattern("*");
|
config.addAllowedOriginPattern("*");
|
||||||
// 设置访问源请求头
|
// 设置访问源请求头
|
||||||
config.addAllowedHeader("*");
|
config.addAllowedHeader("*");
|
||||||
|
// 暴露 TraceId 响应头,跨域场景下前端 JS 才能读取
|
||||||
|
config.addExposedHeader(TraceContext.TRACE_ID_HEADER);
|
||||||
// 设置访问源请求方法
|
// 设置访问源请求方法
|
||||||
config.addAllowedMethod("*");
|
config.addAllowedMethod("*");
|
||||||
// 有效期 1800秒
|
// 有效期 1800秒
|
||||||
|
|||||||
@ -0,0 +1,80 @@
|
|||||||
|
package com.fastbee.framework.filter;
|
||||||
|
|
||||||
|
import com.fastbee.common.trace.TraceContext;
|
||||||
|
import com.fastbee.common.trace.TraceIdGenerator;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
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;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TraceId 入口过滤器
|
||||||
|
*
|
||||||
|
* <p>放在 Servlet 过滤器链最前端(早于 Spring Security 的 JwtAuthenticationTokenFilter),
|
||||||
|
* 保证 token 校验、鉴权失败等早期日志也带有 TraceId,与 Controller 返回的 TraceId 一致。
|
||||||
|
*
|
||||||
|
* <p>职责:
|
||||||
|
* <ul>
|
||||||
|
* <li>请求头携带合法 X-Trace-Id 则继承,否则生成新的</li>
|
||||||
|
* <li>写入 TraceContext(自动同步 MDC)并回写响应头</li>
|
||||||
|
* <li>请求结束后清理 TraceContext,避免 Tomcat 线程复用导致 TraceId 串链</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author FastBee
|
||||||
|
*/
|
||||||
|
public class TraceIdFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(TraceIdFilter.class);
|
||||||
|
|
||||||
|
private static final Pattern TRACE_ID_MATCHER = Pattern.compile(TraceContext.TRACE_ID_PATTERN);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不打请求日志的路径前缀
|
||||||
|
* 过滤器作用域是 /*,静态资源与监控端点数量大且无排查价值,避免淹没业务日志
|
||||||
|
*/
|
||||||
|
private static final Set<String> NO_LOG_PATH_PREFIX = new HashSet<>(Arrays.asList(
|
||||||
|
"/profile", "/statics", "/webjars", "/actuator", "/druid", "/favicon.ico", "/ueditor"));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
String traceId = request.getHeader(TraceContext.TRACE_ID_HEADER);
|
||||||
|
// 上游 TraceId 不合法(含换行符等)时重新生成,避免日志注入
|
||||||
|
if (traceId == null || traceId.isEmpty() || !TRACE_ID_MATCHER.matcher(traceId).matches()) {
|
||||||
|
traceId = TraceIdGenerator.generate();
|
||||||
|
}
|
||||||
|
TraceContext.setTraceId(traceId);
|
||||||
|
response.setHeader(TraceContext.TRACE_ID_HEADER, traceId);
|
||||||
|
|
||||||
|
String uri = request.getRequestURI();
|
||||||
|
try {
|
||||||
|
if (isNeedLog(uri)) {
|
||||||
|
log.info("请求入口: {} {}", request.getMethod(), uri);
|
||||||
|
}
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
} finally {
|
||||||
|
TraceContext.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isNeedLog(String uri) {
|
||||||
|
if (uri == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (String prefix : NO_LOG_PATH_PREFIX) {
|
||||||
|
if (uri.startsWith(prefix)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -27,7 +27,7 @@ public abstract class RepeatSubmitInterceptor implements HandlerInterceptor {
|
|||||||
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
|
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
|
||||||
if (annotation != null) {
|
if (annotation != null) {
|
||||||
if (this.isRepeatSubmit(request, annotation)) {
|
if (this.isRepeatSubmit(request, annotation)) {
|
||||||
AjaxResult ajaxResult = AjaxResult.error(annotation.message());
|
AjaxResult ajaxResult = AjaxResult.error(annotation.message()).withTraceId();
|
||||||
ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
|
ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,53 +0,0 @@
|
|||||||
package com.fastbee.framework.interceptor;
|
|
||||||
|
|
||||||
import com.fastbee.common.trace.TraceContext;
|
|
||||||
import com.fastbee.common.trace.TraceIdGenerator;
|
|
||||||
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 {
|
|
||||||
|
|
||||||
private static final String TRACE_ID_HEADER = "X-Trace-Id";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
|
||||||
// 获取请求URI
|
|
||||||
String requestURI = request.getRequestURI();
|
|
||||||
|
|
||||||
// 尝试从请求头中获取TraceId(支持分布式追踪)
|
|
||||||
String traceId = request.getHeader(TRACE_ID_HEADER);
|
|
||||||
|
|
||||||
// 如果请求头中没有TraceId,则生成一个新的
|
|
||||||
if (traceId == null || traceId.isEmpty()) {
|
|
||||||
traceId = TraceIdGenerator.generate();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置到 TraceContext(自动同步到 MDC)
|
|
||||||
TraceContext.setTraceId(traceId);
|
|
||||||
|
|
||||||
// 将TraceId放入响应头,方便前端获取
|
|
||||||
response.setHeader(TRACE_ID_HEADER, 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 {
|
|
||||||
// 请求完成后清除 TraceContext,避免内存泄漏
|
|
||||||
TraceContext.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -23,20 +23,10 @@ import java.io.Serializable;
|
|||||||
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {
|
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {
|
||||||
private static final long serialVersionUID = -8970718410437077606L;
|
private static final long serialVersionUID = -8970718410437077606L;
|
||||||
|
|
||||||
public static boolean isAjaxRequest(HttpServletRequest request) {
|
|
||||||
String ajaxFlag = request.getHeader("X-Requested-With");
|
|
||||||
return ajaxFlag != null && "XMLHttpRequest".equals(ajaxFlag);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
|
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
|
||||||
throws IOException {
|
|
||||||
// if (isAjaxRequest(request)){
|
|
||||||
int code = HttpStatus.UNAUTHORIZED;
|
int code = HttpStatus.UNAUTHORIZED;
|
||||||
String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
|
String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
|
||||||
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
|
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg).withTraceId()));
|
||||||
// }else {
|
|
||||||
// response.sendRedirect("/oauth/login");
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,6 @@ public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
|
|||||||
// 记录用户退出日志
|
// 记录用户退出日志
|
||||||
AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功"));
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功"));
|
||||||
}
|
}
|
||||||
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success("退出成功")));
|
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success("退出成功").withTraceId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,140 @@
|
|||||||
|
package com.fastbee.framework.web.advice;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.fastbee.common.core.domain.CommonResult;
|
||||||
|
import com.fastbee.common.core.domain.R;
|
||||||
|
import com.fastbee.common.core.page.TableDataInfo;
|
||||||
|
import com.fastbee.common.trace.TraceContext;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
|
import org.springframework.http.server.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.ServerHttpResponse;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TraceId 响应体注入
|
||||||
|
*
|
||||||
|
* <p>所有经 JSON 序列化的 Controller 返回值统一带上 traceId 字段,前端报错时可直接把
|
||||||
|
* traceId 报给后端,用 {@code grep traceId=xxx logs/sys-all.log} 定位整条链路。
|
||||||
|
*
|
||||||
|
* <p>覆盖范围:AjaxResult、R、CommonResult、TableDataInfo 以及直接返回 VO 的接口,
|
||||||
|
* 同时包含 {@code @RestControllerAdvice} 异常处理返回的 AjaxResult。
|
||||||
|
*
|
||||||
|
* <p>处理策略:
|
||||||
|
* <ul>
|
||||||
|
* <li>AjaxResult(Map 子类):直接 put,零额外开销</li>
|
||||||
|
* <li>R / CommonResult / TableDataInfo:调用新增的 setTraceId,避免 JSON 树转换开销</li>
|
||||||
|
* <li>其余 POJO:转成 JSON 对象节点后追加字段,复用容器中的 ObjectMapper 保证序列化配置一致</li>
|
||||||
|
* <li>响应体为 null / JSON 数组 / 标量:无法追加字段,保持原样,由 X-Trace-Id 响应头兜底</li>
|
||||||
|
* <li>非 JSON 转换器(String、byte[]、文件、流):不介入,同样由响应头兜底</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author FastBee
|
||||||
|
*/
|
||||||
|
@RestControllerAdvice
|
||||||
|
// 最后执行:若有其它 ResponseBodyAdvice(如统一结果包装)先执行,本 advice 再在其结果上补 traceId
|
||||||
|
@Order()
|
||||||
|
public class TraceIdResponseAdvice implements ResponseBodyAdvice<Object> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不注入响应体的路径前缀
|
||||||
|
* Swagger/OpenAPI 文档是既定结构的元数据,注入业务字段会污染契约
|
||||||
|
*/
|
||||||
|
private static final List<String> EXCLUDE_PATH_PREFIX = Arrays.asList(
|
||||||
|
"/v2/api-docs", "/v3/api-docs", "/swagger-resources", "/webjars", "/actuator");
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(TraceIdResponseAdvice.class);
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public TraceIdResponseAdvice(ObjectMapper objectMapper) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||||
|
return MappingJackson2HttpMessageConverter.class.isAssignableFrom(converterType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
|
||||||
|
Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||||
|
ServerHttpRequest request, ServerHttpResponse response) {
|
||||||
|
String traceId = TraceContext.getTraceId();
|
||||||
|
if (traceId == null) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
// 响应头始终写入:即使响应体无法追加字段,前端仍可从 header 取到 TraceId
|
||||||
|
response.getHeaders().set(TraceContext.TRACE_ID_HEADER, traceId);
|
||||||
|
|
||||||
|
if (body == null || isExcludePath(request.getURI().getPath())) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
// Map 子类无需 JSON 树转换,直接进入零开销路径
|
||||||
|
if (body instanceof Map) {
|
||||||
|
writeToMap((Map<String, Object>) body, traceId, request.getURI().getPath());
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
if (body instanceof R) {
|
||||||
|
((R<?>) body).setTraceId(traceId);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
if (body instanceof CommonResult) {
|
||||||
|
((CommonResult<?>) body).setTraceId(traceId);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
if (body instanceof TableDataInfo) {
|
||||||
|
((TableDataInfo) body).setTraceId(traceId);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
// 直接返回 VO 的场景:复用容器 ObjectMapper 转成 JSON 对象节点,保证序列化配置一致
|
||||||
|
try {
|
||||||
|
JsonNode node = objectMapper.valueToTree(body);
|
||||||
|
if (node instanceof ObjectNode) {
|
||||||
|
((ObjectNode) node).put(TraceContext.TRACE_ID_KEY, traceId);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
// JSON 树转换失败(如存在循环引用)时保持原样,由响应头兜底,绝不让旁路功能影响业务响应
|
||||||
|
log.warn("响应体注入 traceId 失败,已降级到响应头,uri={}", request.getURI().getPath(), e);
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向 Map 响应体写入 traceId
|
||||||
|
* 不可变 Map(如 Collections.unmodifiableMap)会拒绝写入,此时由响应头兜底
|
||||||
|
*/
|
||||||
|
private void writeToMap(Map<String, Object> body, String traceId, String uri) {
|
||||||
|
try {
|
||||||
|
body.put(TraceContext.TRACE_ID_KEY, traceId);
|
||||||
|
} catch (UnsupportedOperationException e) {
|
||||||
|
log.debug("响应体为不可变 Map,traceId 仅输出到响应头,uri={}", uri, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isExcludePath(String path) {
|
||||||
|
if (path == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (String prefix : EXCLUDE_PATH_PREFIX) {
|
||||||
|
if (path.startsWith(prefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,17 @@ const codeMessage = {
|
|||||||
500: '服务器内部错误,无法完成请求',
|
500: '服务器内部错误,无法完成请求',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给错误提示附加 traceId,便于用户报错后到后端日志里检索整条链路
|
||||||
|
* 优先取响应体 traceId,其次取 X-Trace-Id 响应头
|
||||||
|
*/
|
||||||
|
const withTraceId = (msg, res) => {
|
||||||
|
// 小程序平台 response header 字段名不统一,两个都取
|
||||||
|
const header = res.header || res.headers || {};
|
||||||
|
const traceId = (res.data && res.data.traceId) || header['X-Trace-Id'] || header['x-trace-id'] || '';
|
||||||
|
return traceId ? `${msg} [traceId:${traceId}]` : msg;
|
||||||
|
};
|
||||||
|
|
||||||
const install = (Vue, vm) => {
|
const install = (Vue, vm) => {
|
||||||
// 这个配置是一次配置,全局通用的,具体参数见 https://www.uviewui.com/js/http.html
|
// 这个配置是一次配置,全局通用的,具体参数见 https://www.uviewui.com/js/http.html
|
||||||
uni.$u.http.setConfig((config) => {
|
uni.$u.http.setConfig((config) => {
|
||||||
@ -115,10 +126,10 @@ const install = (Vue, vm) => {
|
|||||||
return result
|
return result
|
||||||
} else {
|
} else {
|
||||||
console.log(result);
|
console.log(result);
|
||||||
vm.$u.toast(result.msg);
|
vm.$u.toast(withTraceId(result.msg, res));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
vm.$u.toast(res.data.msg);
|
vm.$u.toast(withTraceId((res.data && res.data.msg) || '', res));
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -71,8 +71,10 @@ axiosInstance.interceptors.response.use(
|
|||||||
return Promise.resolve(res.data)
|
return Promise.resolve(res.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提示错误
|
// 提示错误(附带 traceId,便于用户报错后按此 ID 检索后端整条链路日志)
|
||||||
window['$message'].error(window['$t']((res.data as any).msg))
|
const { msg, traceId } = res.data as { msg: string; traceId?: string }
|
||||||
|
const message = window['$t'](msg)
|
||||||
|
window['$message'].error(traceId ? `${message} [traceId:${traceId}]` : message)
|
||||||
return Promise.resolve(res.data)
|
return Promise.resolve(res.data)
|
||||||
},
|
},
|
||||||
(err: AxiosError) => {
|
(err: AxiosError) => {
|
||||||
|
|||||||
@ -11,6 +11,31 @@ let downloadLoadingInstance;
|
|||||||
// 是否显示重新登录
|
// 是否显示重新登录
|
||||||
export let isRelogin = { show: false };
|
export let isRelogin = { show: false };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取响应中的链路追踪ID
|
||||||
|
* 优先取响应体 traceId 字段,其次取 X-Trace-Id 响应头(二进制/网络异常场景)
|
||||||
|
*
|
||||||
|
* @param {Object} res axios 响应对象
|
||||||
|
* @returns {String} traceId,取不到时返回空串
|
||||||
|
*/
|
||||||
|
function getTraceId(res) {
|
||||||
|
if (!res) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return (res.data && res.data.traceId) || (res.headers && res.headers['x-trace-id']) || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给错误提示附加 traceId,便于用户报错后直接到日志里检索整条链路
|
||||||
|
*
|
||||||
|
* @param {String} message 原始提示
|
||||||
|
* @param {String} traceId 链路追踪ID
|
||||||
|
* @returns {String} 拼接后的提示
|
||||||
|
*/
|
||||||
|
function appendTraceId(message, traceId) {
|
||||||
|
return traceId ? `${message} [traceId:${traceId}]` : message;
|
||||||
|
}
|
||||||
|
|
||||||
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8';
|
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8';
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
const service = axios.create({
|
const service = axios.create({
|
||||||
@ -102,13 +127,13 @@ service.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
return Promise.reject('无效的会话,或者会话已过期,请重新登录。');
|
return Promise.reject('无效的会话,或者会话已过期,请重新登录。');
|
||||||
} else if (code === 500) {
|
} else if (code === 500) {
|
||||||
Message({ message: msg, type: 'error' });
|
Message({ message: appendTraceId(msg, getTraceId(res)), type: 'error', duration: 5 * 1000 });
|
||||||
return Promise.reject('error');
|
return Promise.reject('error');
|
||||||
} else if (code === 601) {
|
} else if (code === 601) {
|
||||||
Message({ message: msg, type: 'warning' });
|
Message({ message: appendTraceId(msg, getTraceId(res)), type: 'warning', duration: 5 * 1000 });
|
||||||
return Promise.reject('error');
|
return Promise.reject('error');
|
||||||
} else if (code !== 200) {
|
} else if (code !== 200) {
|
||||||
Notification.error({ title: msg });
|
Notification.error({ title: msg, message: getTraceId(res) ? 'traceId:' + getTraceId(res) : '', duration: 5 * 1000 });
|
||||||
return Promise.reject('error');
|
return Promise.reject('error');
|
||||||
} else {
|
} else {
|
||||||
return res.data;
|
return res.data;
|
||||||
@ -124,7 +149,7 @@ service.interceptors.response.use(
|
|||||||
} else if (message.includes('Request failed with status code')) {
|
} else if (message.includes('Request failed with status code')) {
|
||||||
message = '系统接口' + message.substr(message.length - 3) + '异常';
|
message = '系统接口' + message.substr(message.length - 3) + '异常';
|
||||||
}
|
}
|
||||||
Message({ message: message, type: 'error', duration: 5 * 1000 });
|
Message({ message: appendTraceId(message, getTraceId(error.response)), type: 'error', duration: 5 * 1000 });
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -152,7 +177,7 @@ export function download(url, params, filename, config) {
|
|||||||
const resText = await data.text();
|
const resText = await data.text();
|
||||||
const rspObj = JSON.parse(resText);
|
const rspObj = JSON.parse(resText);
|
||||||
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'];
|
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'];
|
||||||
Message.error(errMsg);
|
Message.error({ message: appendTraceId(errMsg, rspObj.traceId), duration: 5 * 1000 });
|
||||||
}
|
}
|
||||||
downloadLoadingInstance.close();
|
downloadLoadingInstance.close();
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user