Spring Boot 3实战案例锦集》PDF电子书已更新至130篇!

🎉🎉《Spring Boot实战案例合集》目前已更新224个案例,我们将持续不断的更新。文末有电子书目录。→ 现在就订阅合集


环境:Spring Boot 3.5.0


1. 简介

在微服务高并发业务场景中,接口往往需要并行调用多个第三方接口聚合数据,以此缩短响应耗时。SpringBoot 默认异步线程池基于 ThreadLocal 实现的 MDC 上下文,无法自动传递至子线程,导致请求链路 traceId 丢失。不同异步线程日志无法关联同一请求,线上问题排查、链路溯源变得异常困难。

本文基于自定义 ThreadPoolTaskExecutor 线程池,结合 MDC 任务装饰器实现上下文自动透传,解决跨线程日志链路追踪问题,适配多接口并行聚合的生产实战场景。

2.实战案例

在线程池能够传播 MDC 之前,我们需要先设置 traceId。如果请求中不存在唯一的ID,那么就在Filter中生成唯一的ID。

@Componentpublic class TraceIdFilter extends OncePerRequestFilter {  private static final String TRACE_ID = "traceId";  @Override  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)      throws IOException, ServletException {    // 从请求header中读取或者生成唯一ID    String traceId = request.getHeader("X-Trace-Id");    if (traceId == null || traceId.isEmpty()) {      traceId = UUID.randomUUID().toString();    }    MDC.put(TRACE_ID, traceId);    response.setHeader("X-Trace-Id", traceId);    try {      chain.doFilter(request, response);    } finally {      MDC.clear();    }  }}

2.2 配置线程池

我们需要自定义线程池来装饰线程执行的任务。

@Configuration@EnableAsyncpublic class TaskExecutorConfig {  @Value("${price.pool.core-size:3}")  private int corePoolSize;  @Value("${price.pool.max-size:10}")  private int maxPoolSize;  @Value("${price.pool.queue-capacity:100}")  private int queueCapacity;  @Value("${price.pool.thread-name-prefix:price-fetch-}")  private String threadNamePrefix;  @Bean(name = "packTaskExecutor")  Executor priceTaskExecutor() {    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();    executor.setCorePoolSize(corePoolSize);    executor.setMaxPoolSize(maxPoolSize);    executor.setQueueCapacity(queueCapacity);    executor.setThreadNamePrefix(threadNamePrefix);    // 自定义线程工厂    executor.setThreadFactory(new ThreadFactory() {      private final AtomicInteger counter = new AtomicInteger(1);      @Override      public Thread newThread(Runnable r) {        Thread thread = new Thread(r, threadNamePrefix + counter.getAndIncrement());        thread.setDaemon(false);        return thread;      }    });    // 装饰线程执行的任务Runnable    executor.setTaskDecorator(new MdcTaskDecorator());    executor.setWaitForTasksToCompleteOnShutdown(true);    executor.setAwaitTerminationSeconds(60) ;    executor.initialize();    return executor;  }}

任务装饰器

public class MdcTaskDecorator implements TaskDecorator {  @Override  public Runnable decorate(Runnable runnable) {    // 1.获取当前线程的上下文数据(父线程)    Map<String, String> parentMdcContext = MDC.getCopyOfContextMap();    return () -> {      // 2.在子线程中保留现有的 MDC      Map<String, String> originalChildMdcContext = MDC.getCopyOfContextMap();      try {        // 3.在子线程中恢复父线程的 MDC        if (parentMdcContext != null) {          MDC.setContextMap(parentMdcContext);        } else {          MDC.clear();        }        // 4.执行原始任务        runnable.run();      } finally {        // 5.恢复子线程原有的 MDC(防止泄漏)        if (originalChildMdcContext != null) {          MDC.setContextMap(originalChildMdcContext);        } else {          MDC.clear();        }      }    };  }}

总结说明:

  • 提交任务前:捕获父线程的 MDC上下文(如 traceId=abc-123)

  • 当任务在子线程中启动时:保存任何现有的 MDC(以防万一),然后恢复父线程的 MDC

  • 任务执行时:现在,在子线程中 MDC.get("traceId") 返回 "abc-123"!

  • 任务完成后:恢复子线程原有的 MDC,以防止内存泄漏

2.3 异步聚合任务

我们这里模拟分别从:淘宝、京东、拼多多 获取商品价格。

@Servicepublic class PriceAggregateService {  private static final Logger log = LoggerFactory.getLogger(PriceAggregateService.class);  private final Executor packTaskExecutor;  public PriceAggregateService(@Qualifier("packTaskExecutor") Executor packTaskExecutor) {    this.packTaskExecutor = packTaskExecutor;  }  public CompletableFuture<String> getTaobaoPrice(String productId) {    return CompletableFuture.supplyAsync(() -> {      log.info("[淘宝] 查询商品价格,productId:{}", productId);      // 模拟调用接口 500ms      Thread.sleep(500);      return "淘宝:¥5999";    }, this.packTaskExecutor);  }  public CompletableFuture<String> getJdPrice(String productId) {    return CompletableFuture.supplyAsync(() -> {      log.info("[京东] 查询商品价格,productId:{}", productId);      Thread.sleep(500);      return "京东:¥5899";    }, this.packTaskExecutor);  }  public CompletableFuture<String> getPddPrice(String productId) {    return CompletableFuture.supplyAsync(() -> {      log.info("[拼多多] 查询商品价格,productId:{}", productId);      Thread.sleep(500);      return "拼多多:¥5299";    }, this.packTaskExecutor);  }}

定义Controller接口,我们将在接口中并行调用上的3个方法:

@RestController@RequestMapping("/api/price")public class PriceAggregateController {  private static final Logger log = LoggerFactory.getLogger(PriceAggregateController.class);  private final PriceAggregateService priceAggregateService;  public PriceAggregateController(PriceAggregateService priceAggregateService) {    this.priceAggregateService = priceAggregateService;  }  @GetMapping("/{productId}")  public Map<String, Object> getPrice(@PathVariable String productId) {    log.info("接收商品比价请求,productId:{}", productId);    // 并行调用    CompletableFuture<String> taobao = priceAggregateService.getTaobaoPrice(productId);    CompletableFuture<String> jd = priceAggregateService.getJdPrice(productId);    CompletableFuture<String> pdd = priceAggregateService.getPddPrice(productId);    // 等待所有结果返回    CompletableFuture.allOf(taobao, jd, pdd).join();    Map<String, Object> result = new HashMap<>();    try {      result.put("taobao", taobao.get());      result.put("jd", jd.get());      result.put("pdd", pdd.get());    }    log.info("商品比价完成,productId:{}", productId);    return result;  }}

2.4 配置日志

要让 traceId 出现在每条日志行中,我们需要在配置 logback-spring.xml 中进行如下的配置:

<configuration scan="false" scanPeriod="15 seconds" debug="false">  <contextName>common</contextName>  <property name="COMMON_PATTERN"             value="%green(%d{23:mm:ss}) [%yellow(%thread)][%red(traceId=%X{traceId:-N/A})] %highlight(%-5level) %logger{36} - %msg%n"/>  <appender name="TRACEX" class="ch.qos.logback.core.ConsoleAppender">    <encoder>      <pattern>${COMMON_PATTERN}</pattern>      <charset>UTF-8</charset>    </encoder>  </appender>  <springProfile name="dev | default">    <root level="INFO">      <appender-ref ref="TRACEX" />    </root>  </springProfile></configuration>

说明:%X{traceId:-N/A} 从MDC获取 (如果没有设置则显示:"N/A")。

2.5 测试

图片

控制台日志输出:

图片

2.5 微服务调用透传

微服务之间的调用我们通常会采用openfeign,那么要实现traceId的传递,我们需要自定义拦截器:

public class FeignTraceInterceptor implements RequestInterceptor {  public static final String TRACE_ID = "traceId";  public static final String X_TRACE_ID = "X-Trace-Id";  @Override  public void apply(RequestTemplate template) {    // 1. 从 MDC 获取当前线程的 traceId    String traceId = MDC.get(TRACE_ID);    // 2. 如果存在,放入 Feign 调用的请求头中    if (traceId != null && !traceId.isEmpty()) {      template.header(X_TRACE_ID, traceId);    }  }}// 配置Feign@EnableFeignClients(defaultConfiguration = FeignDefaultConfiguration.class)public class FeignConfig {}@Configurationpublic class FeignDefaultConfiguration {  @Bean  FeignTraceInterceptor feignTraceInterceptor() {    return new FeignTraceInterceptor() ;  }}

2.6 全局Rest Client透传

当我们直接使用RestTemplate或者RestClient调用接口时,那么我们可以通过如下的方式定义全局的拦截器:

自定义RestClient:

@Componentpublic class CustomRestClient implements RestClientCustomizer {  public static final String TRACE_ID = "traceId";  public static final String X_TRACE_ID = "X-Trace-Id";
  @Override  public void customize(Builder restClientBuilder) {    restClientBuilder.requestInterceptor(new ClientHttpRequestInterceptor() {      @Override      public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)          throws IOException {        String traceId = MDC.get(TRACE_ID);        request.getHeaders().add(X_TRACE_ID, traceId) ;        return execution.execute(request, body) ;      }    }) ;  }}

自定义RestTemplate:

@Componentpublic class CustomRestTemplate implements RestTemplateRequestCustomizer<ClientHttpRequest> {  public static final String TRACE_ID = "traceId";  public static final String X_TRACE_ID = "X-Trace-Id";
  @Override  public void customize(ClientHttpRequest request) {    String traceId = MDC.get(TRACE_ID);    request.getHeaders().add(X_TRACE_ID, traceId) ;  }}

以上是本篇文章的全部内容,如对你有帮助帮忙点赞+转发+收藏

拿捏配置!Spring Boot 七种属性获取方式

Spring Boot+WebSocket+Redis:毫秒级实时在线系统

高级开发!手撕Controller底层调用链,深度功能扩展、性能优化

SpringBoot+Nginx+Lua接口性能提升N倍

基于Spring Boot实现不一样的通用DAO,值得学习

确保Spring Boot定时任务只执行一次方案

Spring Boot中通过3种方式初始化数据,你们如何选择?

自定义注解+SpEL实现强大的权限管理

考察你对 Spring 基本功掌握能力

自己动手实现Agent统计API接口调用耗时

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐