最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot實(shí)現(xiàn)統(tǒng)一日志上下文的實(shí)戰(zhàn)指南

 更新時(shí)間:2026年01月08日 09:29:15   作者:風(fēng)象南  
這篇文章主要為大家詳細(xì)介紹了SpringBoot實(shí)現(xiàn)統(tǒng)一日志上下文的相關(guān)方法,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的小伙伴可以了解下

在項(xiàng)目開發(fā)中,一個請求往往要經(jīng)過多個層次的處理:

@GetMapping("/order/{id}")
public Order getOrder(@PathVariable Long id) {
    log.info("查詢訂單, orderId={}", id);  // Controller
    return orderService.getById(id);
}

@Service
public class OrderService {
    public Order getById(Long id) {
        log.info("開始查詢, id={}", id);  // Service
        return orderMapper.selectById(id);
    }
}

當(dāng)線上出現(xiàn)問題時(shí),需要從日志中找到這個請求的完整執(zhí)行鏈路。但如果日志中沒有統(tǒng)一的標(biāo)識,只能通過時(shí)間、orderId 等信息去匹配,效率很低。

更復(fù)雜的情況是,請求處理過程中會啟動異步任務(wù)、使用線程池、調(diào)用下游服務(wù),這些日志如何關(guān)聯(lián)起來?

基本思路

在日志中引入統(tǒng)一的上下文信息,最關(guān)鍵的是 traceId(追蹤 ID)。一個請求的所有日志都帶上相同的 traceId,就能通過 grep 或日志系統(tǒng)快速定位問題。

SLF4J 提供了 MDC(Mapped Diagnostic Context)機(jī)制來解決這個問題:

MDC.put("traceId", "123456");
log.info("這條日志會自動帶上 traceId");
MDC.clear();

配置 logback 輸出格式:

<pattern>%d{HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>

輸出效果:

12:34:56.789 [http-nio-8080-exec-1] [123456] INFO  c.e.OrderController - 查詢訂單, orderId=1
12:34:56.890 [http-nio-8080-exec-1] [123456] INFO  c.e.OrderService - 開始查詢, id=1

但直接在每個方法里寫 MDC.put/clear 不現(xiàn)實(shí),需要統(tǒng)一處理。

設(shè)計(jì)上下文數(shù)據(jù)結(jié)構(gòu)

先定義上下文要包含哪些信息:

@Getter
@Setter
public class LogContext {

    private String traceId;
    private String userId;
    private String clientId;
    private String uri;

    public static LogContext create() {
        LogContext context = new LogContext();
        context.setTraceId(UUID.randomUUID().toString().replace("-", ""));
        return context;
    }

    public static LogContext from(String traceId) {
        LogContext context = new LogContext();
        context.setTraceId(traceId);
        return context;
    }
}

用 ThreadLocal 存儲上下文:

public class LogContextHolder {

    private static final ThreadLocal<LogContext> CONTEXT = new ThreadLocal<>();

    public static void setContext(LogContext context) {
        CONTEXT.set(context);
        updateMDC(context);
    }

    public static LogContext getContext() {
        return CONTEXT.get();
    }

    public static String getTraceId() {
        LogContext context = CONTEXT.get();
        return context != null ? context.getTraceId() : null;
    }

    public static void clear() {
        CONTEXT.remove();
        MDC.clear();
    }

    private static void updateMDC(LogContext context) {
        if (context != null) {
            MDC.put("traceId", context.getTraceId());
            MDC.put("userId", context.getUserId());
            MDC.put("clientId", context.getClientId());
        }
    }
}

Web 請求統(tǒng)一處理

通過 Filter 在請求入口生成 traceId,請求結(jié)束時(shí)清理:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class LogContextFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        try {
            // 從請求頭獲取 traceId,沒有則生成新的
            String traceId = req.getHeader("traceId");
            LogContext context = StringUtils.hasText(traceId)
                ? LogContext.from(traceId)
                : LogContext.create();

            // 設(shè)置其他上下文信息
            context.setUri(req.getRequestURI());
            String userId = getUserIdFromRequest(req);
            if (userId != null) {
                context.setUserId(userId);
            }

            LogContextHolder.setContext(context);

            // 將 traceId 寫入響應(yīng)頭,方便前端追蹤
            res.setHeader("traceId", context.getTraceId());

            chain.doFilter(request, response);

        } finally {
            LogContextHolder.clear();
        }
    }

    private String getUserIdFromRequest(HttpServletRequest request) {
        // 從 token 或 session 中獲取 userId
        // ...
        return null;
    }
}

這樣,Controller、Service、Repository 層的所有日志都會自動帶上 traceId,無需手動處理。

跨線程場景

普通的 ThreadLocal 在子線程中無法獲取父線程的值,需要特殊處理。

更多ThreadLocal父子線程傳值方案可參考:SpringBoot實(shí)現(xiàn)ThreadLocal父子線程傳值的幾種方式

線程池裝飾器

使用阿里巴巴的 TransmittableThreadLocal(TTL):

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.3</version>
</dependency>

改造 LogContextHolder:

public class LogContextHolder {

    // 改用 TransmittableThreadLocal
    private static final TransmittableThreadLocal<LogContext> CONTEXT =
        new TransmittableThreadLocal<>();

    // 其他方法不變
}

創(chuàng)建 TTL 裝飾的線程池:

@Configuration
public class ExecutorConfig {

    @Bean("ttlExecutor")
    public Executor ttlExecutor() {
        TtlExecutors.getTtlExecutor(new ThreadPoolExecutor(
            10, 20, 60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100),
            new ThreadFactoryBuilder().setNameFormat("ttl-pool-%d").build()
        ));
    }
}

使用裝飾后的線程池:

@Service
public class OrderService {

    @Autowired
    @Qualifier("ttlExecutor")
    private Executor ttlExecutor;

    public void processAsync(Long orderId) {
        ttlExecutor.execute(() -> {
            // 這里的日志會自動帶上父線程的 traceId
            log.info("異步處理訂單, orderId={}", orderId);
        });
    }
}

CompletableFuture

CompletableFuture 需要用 TTL 包裝:

public CompletableFuture<Order> getOrderAsync(Long id) {
    return TtlCompletable.get(() ->
        CompletableFuture.supplyAsync(() -> {
            log.info("異步查詢訂單, id={}", id);  // 有 traceId
            return orderMapper.selectById(id);
        }, ttlExecutor)
    );
}

@Async 異步任務(wù)

Spring 的 @Async 需要配置線程池裝飾器:

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        return TtlExecutors.getTtlExecutor(new ThreadPoolExecutor(
            5, 10, 60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100)
        ));
    }
}

使用:

@Service
public class OrderService {

    @Async
    public void sendNotification(Long orderId) {
        // 這里的日志會自動帶上主線程的 traceId
        log.info("發(fā)送訂單通知, orderId={}", orderId);
    }
}

HTTP 調(diào)用傳遞 traceId

調(diào)用下游服務(wù)時(shí),需要把 traceId 傳遞過去,形成完整的調(diào)用鏈。

RestTemplate

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate() {
        RestTemplate template = new RestTemplate();
        template.setInterceptors(Collections.singletonList(new TraceIdInterceptor()));
        return template;
    }
}

public class TraceIdInterceptor implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) {
        String traceId = LogContextHolder.getTraceId();
        if (traceId != null) {
            request.getHeaders().set("traceId", traceId);
        }
        return execution.execute(request, body);
    }
}

WebClient

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient() {
        return WebClient.builder()
            .filter((request, next) -> {
                String traceId = LogContextHolder.getTraceId();
                if (traceId != null) {
                    request.headers().set("traceId", traceId);
                }
                return next.exchange(request);
            })
            .build();
    }
}

Feign

@Configuration
public class FeignConfig {

    @Bean
    public RequestInterceptor traceIdInterceptor() {
        return template -> {
            String traceId = LogContextHolder.getTraceId();
            if (traceId != null) {
                template.header("traceId", traceId);
            }
        };
    }
}

完整示例

Controller:

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    @GetMapping("/{id}")
    public Order getOrder(@PathVariable Long id) {
        log.info("接收查詢請求, orderId={}", id);  // 有 traceId
        return orderService.getById(id);
    }

    @PostMapping
    public Order create(@RequestBody OrderCreateDTO dto) {
        log.info("接收創(chuàng)建請求, dto={}", dto);  // 有 traceId
        return orderService.create(dto);
    }
}

Service:

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    @Qualifier("ttlExecutor")
    private Executor executor;

    public Order getById(Long id) {
        log.info("開始查詢, id={}", id);  // 有 traceId
        Order order = orderMapper.selectById(id);

        // 異步記錄日志
        executor.execute(() -> {
            log.info("記錄查詢?nèi)罩? orderId={}", order.getId());  // 有 traceId
        });

        return order;
    }
}

日志輸出:

2025-01-08 12:34:56.789 [http-nio-8080-exec-1] [a1b2c3d4e5f6] INFO  c.e.OrderController - 接收查詢請求, orderId=1
2025-01-08 12:34:56.890 [http-nio-8080-exec-1] [a1b2c3d4e5f6] INFO  c.e.OrderService - 開始查詢, id=1
2025-01-08 12:34:56.990 [ttl-pool-1] [a1b2c3d4e5f6] INFO  c.e.OrderService - 記錄查詢?nèi)罩? orderId=1

三條日志的 traceId 都是 a1b2c3d4e5f6,可以快速關(guān)聯(lián)。

注意事項(xiàng)

1. 內(nèi)存泄漏

ThreadLocal 必須在請求結(jié)束時(shí)清理,否則可能會導(dǎo)致內(nèi)存泄漏。Filter 中的 finally 塊很重要:

try {
    // 處理請求
} finally {
    LogContextHolder.clear();  // 必須清理
}

2. 線程池命名

給線程池設(shè)置有意義的名字,方便從日志中識別:

new ThreadFactoryBuilder().setNameFormat("order-pool-%d").build()

3. MDC 的性能

MDC 讀寫有少量開銷,但對性能影響很小。如果是超高并發(fā)場景,可以考慮按需開啟。

4. traceId 生成策略

幾種常見方案:

方案優(yōu)點(diǎn)缺點(diǎn)
UUID簡單、無需協(xié)調(diào)較長、無序
雪花算法性能高、趨勢遞增需要機(jī)器 ID 配置
Redis incr全局唯一、遞增依賴 Redis
請求入口自增簡單單機(jī)內(nèi)唯一

大多數(shù)場景用 UUID 就夠了。

5. 分布式追蹤

如果是微服務(wù)架構(gòu),可以考慮接入 SkyWalking、Zipkin 等分布式追蹤系統(tǒng),它們提供更強(qiáng)大的鏈路追蹤能力,包括調(diào)用關(guān)系圖、耗時(shí)分析等。

但接入成本較高,中小項(xiàng)目用 MDC + traceId 通常就夠了。

總結(jié)

統(tǒng)一日志上下文的核心是:

  • 入口生成:Filter/Interceptor 攔截請求,生成 traceId
  • 線程傳遞:用 TTL 解決跨線程問題
  • 服務(wù)傳遞:HTTP 調(diào)用通過請求頭傳遞 traceId
  • 出口清理:請求結(jié)束時(shí)清理 ThreadLocal

這套方案不依賴第三方框架,實(shí)現(xiàn)簡單,適合大多數(shù)單體應(yīng)用或中小型微服務(wù)項(xiàng)目。

如果項(xiàng)目規(guī)模較大,對鏈路追蹤要求高,可以考慮接入專業(yè)的 APM 系統(tǒng)。

到此這篇關(guān)于SpringBoot實(shí)現(xiàn)統(tǒng)一日志上下文的實(shí)戰(zhàn)指南的文章就介紹到這了,更多相關(guān)SpringBoot統(tǒng)一日志上下文內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • JavaWeb框架MVC設(shè)計(jì)思想詳解

    JavaWeb框架MVC設(shè)計(jì)思想詳解

    這篇文章主要介紹了JavaWeb框架MVC設(shè)計(jì)思想詳解的相關(guān)資料,非常不錯,具有參考借鑒價(jià)值,需要的朋友可以參考下
    2016-07-07
  • Java遍歷Map的方法匯總

    Java遍歷Map的方法匯總

    大家平時(shí)在使用Java開發(fā)時(shí),經(jīng)常會遇到遍歷Map對象的問題,本文就給大家介紹幾種Java遍歷Map對象的方法,并簡單分析一下每種方法的效率,需要的朋友可以參考下
    2023-12-12
  • ReentrantLock 非公平鎖實(shí)現(xiàn)原理詳解

    ReentrantLock 非公平鎖實(shí)現(xiàn)原理詳解

    這篇文章主要為大家介紹了ReentrantLock 非公平鎖實(shí)現(xiàn)原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-12-12
  • Win10 IDEA遠(yuǎn)程連接HBase教程

    Win10 IDEA遠(yuǎn)程連接HBase教程

    在Windows 10上,通過IDEA連接到虛擬機(jī)中的Hadoop和HBase需要關(guān)閉虛擬機(jī)防火墻,并修改相關(guān)配置文件中的IP地址,此外,創(chuàng)建Maven項(xiàng)目并添加依賴是必要步驟,最后,通過Java代碼和HBase Shell命令進(jìn)行操作,此過程涉及的技術(shù)包括虛擬機(jī)配置、防火墻管理、文件編輯和項(xiàng)目管理等
    2024-11-11
  • Spring?@Configuration?proxyBeanMethods=false問題

    Spring?@Configuration?proxyBeanMethods=false問題

    這篇文章主要介紹了Spring?@Configuration?proxyBeanMethods=false問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2025-04-04
  • java實(shí)現(xiàn)簡單聊天軟件

    java實(shí)現(xiàn)簡單聊天軟件

    這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)簡單的聊天軟件,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-07-07
  • hadoop之MapReduce框架原理

    hadoop之MapReduce框架原理

    這篇文章主要介紹了hadoop的MapReduce框架原理,MapReduce是分為兩個階段的,MapperTask階段,和ReduceTask階段。如果有感興趣的小伙伴可以借鑒參考
    2023-03-03
  • Java優(yōu)先隊(duì)列?priority?queue

    Java優(yōu)先隊(duì)列?priority?queue

    本文主要介紹了Java優(yōu)先隊(duì)列?priority?queue,優(yōu)先隊(duì)列是一種特殊的數(shù)據(jù)結(jié)構(gòu)隊(duì)列中每一個元素都被分配到一個優(yōu)先權(quán)值,出隊(duì)順序按照優(yōu)先權(quán)值來劃分。一般有兩種出隊(duì)順序高優(yōu)先權(quán)出隊(duì)或低優(yōu)先權(quán)出隊(duì),想了解具體內(nèi)容的小伙伴可以參考下文內(nèi)容,希望對你有所幫助
    2021-12-12
  • Java接口回調(diào)的本質(zhì)詳解

    Java接口回調(diào)的本質(zhì)詳解

    大家好,本篇文章主要講的是Java接口回調(diào)的本質(zhì)詳解,感興趣的同學(xué)趕快來看一看吧,對你有幫助的話記得收藏一下
    2022-02-02
  • 詳解Java 虛擬機(jī)垃圾收集機(jī)制

    詳解Java 虛擬機(jī)垃圾收集機(jī)制

    這篇文章主要介紹了Java 虛擬機(jī)垃圾收集機(jī)制的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)Java虛擬機(jī)的相關(guān)知識,感興趣的朋友可以了解下
    2020-12-12

最新評論

北川| 宁河县| 甘洛县| 赤峰市| 临洮县| 防城港市| 涟源市| 余姚市| 阿瓦提县| 银川市| 邹平县| 将乐县| 清徐县| 汉中市| 凌源市| 进贤县| 菏泽市| 新乡市| 安康市| 孝昌县| 永兴县| 常熟市| 秀山| 弋阳县| 顺平县| 永吉县| 蛟河市| 南皮县| 铜川市| 丰原市| 普兰店市| 澄江县| 九龙城区| 松江区| 仙居县| 肥乡县| 永福县| 象山县| 西丰县| 孝感市| 琼结县|