最新国产好看的视频,伊人天堂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ā)中,一個(gè)請(qǐng)求往往要經(jīng)過多個(gè)層次的處理:

@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í),需要從日志中找到這個(gè)請(qǐng)求的完整執(zhí)行鏈路。但如果日志中沒有統(tǒng)一的標(biāo)識(shí),只能通過時(shí)間、orderId 等信息去匹配,效率很低。

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

基本思路

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

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

MDC.put("traceId", "123456");
log.info("這條日志會(huì)自動(dòng)帶上 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

但直接在每個(gè)方法里寫 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 存儲(chǔ)上下文:

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 請(qǐng)求統(tǒng)一處理

通過 Filter 在請(qǐng)求入口生成 traceId,請(qǐng)求結(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 {
            // 從請(qǐng)求頭獲取 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 層的所有日志都會(huì)自動(dòng)帶上 traceId,無需手動(dòng)處理。

跨線程場(chǎng)景

普通的 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(() -> {
            // 這里的日志會(huì)自動(dòng)帶上父線程的 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) {
        // 這里的日志會(huì)自動(dòng)帶上主線程的 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("接收查詢請(qǐng)求, orderId={}", id);  // 有 traceId
        return orderService.getById(id);
    }

    @PostMapping
    public Order create(@RequestBody OrderCreateDTO dto) {
        log.info("接收創(chuàng)建請(qǐ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 - 接收查詢請(qǐng)求, 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 必須在請(qǐng)求結(jié)束時(shí)清理,否則可能會(huì)導(dǎo)致內(nèi)存泄漏。Filter 中的 finally 塊很重要:

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

2. 線程池命名

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

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

3. MDC 的性能

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

4. traceId 生成策略

幾種常見方案:

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

大多數(shù)場(chǎng)景用 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 攔截請(qǐng)求,生成 traceId
  • 線程傳遞:用 TTL 解決跨線程問題
  • 服務(wù)傳遞:HTTP 調(diào)用通過請(qǐng)求頭傳遞 traceId
  • 出口清理:請(qǐng)求結(jié)束時(shí)清理 ThreadLocal

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

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

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

相關(guān)文章

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

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

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

    Java遍歷Map的方法匯總

    大家平時(shí)在使用Java開發(fā)時(shí),經(jīng)常會(huì)遇到遍歷Map對(duì)象的問題,本文就給大家介紹幾種Java遍歷Map對(duì)象的方法,并簡(jiǎn)單分析一下每種方法的效率,需要的朋友可以參考下
    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à)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2025-04-04
  • java實(shí)現(xiàn)簡(jiǎn)單聊天軟件

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

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

    hadoop之MapReduce框架原理

    這篇文章主要介紹了hadoop的MapReduce框架原理,MapReduce是分為兩個(gè)階段的,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ì)列中每一個(gè)元素都被分配到一個(gè)優(yōu)先權(quán)值,出隊(duì)順序按照優(yōu)先權(quán)值來劃分。一般有兩種出隊(duì)順序高優(yōu)先權(quán)出隊(duì)或低優(yōu)先權(quán)出隊(duì),想了解具體內(nèi)容的小伙伴可以參考下文內(nèi)容,希望對(duì)你有所幫助
    2021-12-12
  • Java接口回調(diào)的本質(zhì)詳解

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

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

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

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

最新評(píng)論

宝应县| 江陵县| 舞钢市| 邢台市| 镇安县| 济南市| 康马县| 泸州市| 墨玉县| 四会市| 武宣县| 霍林郭勒市| 永嘉县| 德格县| 滦平县| 松阳县| 庄浪县| 杭锦旗| 衢州市| 和顺县| 绥德县| 阜新市| 桃园县| 新竹县| 焉耆| 峨眉山市| 酉阳| 健康| 和田市| 从江县| 吕梁市| 内乡县| 罗源县| 大理市| 游戏| 万宁市| 吴桥县| 蚌埠市| 齐河县| 方正县| 奉贤区|