CodeGym /課程 /JAVA 25 SELF /在多執行緒與 Web 應用中的日誌紀錄

在多執行緒與 Web 應用中的日誌紀錄

JAVA 25 SELF
等級 63 , 課堂 2
開放

1. 日誌紀錄的執行緒安全

在單執行緒程式中一切很簡單:只有一個執行緒寫日誌,沒人干擾它。但在真實的應用——Web 服務、微服務——往往同時運轉著數十甚至上百個執行緒。想像一下,如果好幾個人同時在筆記本的同一行用筆書寫——結果可以說是非常難以辨認。

執行緒安全(thread safety)是指,即使有 100500 個執行緒同時寫日誌,訊息也不會混淆、不會黏在一起、也不會遺失。

在各個庫中如何實作?

現代的日誌庫(Log4j 2Logbackjava.util.logging)一開始就以執行緒安全為目標來設計。這表示:

  • 每個執行緒都可以安全地呼叫 logger 的方法。
  • 庫內部透過同步與佇列,讓訊息彼此不會干擾。
  • 即使多個執行緒同時寫入同一個檔案,日誌也不會彼此混在一起。

重要:logger 本身(例如 SLF4JLog4jLogger 物件)可以在任何類別中作為 static final 欄位使用——這不會導致執行緒問題。

範例

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MultiThreadedLoggerExample {
    private static final Logger logger = LoggerFactory.getLogger(MultiThreadedLoggerExample.class);

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 5; i++) {
                logger.info("執行緒 {} 正在寫入訊息 {}", Thread.currentThread().getName(), i);
            }
        };

        Thread t1 = new Thread(task, "第一");
        Thread t2 = new Thread(task, "第二");
        t1.start();
        t2.start();
    }
}

在日誌中你會看到兩個執行緒的訊息井然有序——不會糊成一團或互相覆蓋。

2. 日誌上下文:MDC(Mapped Diagnostic Context)

想像你的應用同時處理上百個請求,每個各自在自己的執行緒中。日誌訊息飛來飛去,但看不出屬於哪個請求。你不只想知道「發生了什麼」,還想知道是對哪個請求、與誰、在什麼情境中發生。

MDC(Mapped Diagnostic Context)是一種機制,可將與目前執行緒相關的額外資訊「附加」到日誌上。該執行緒寫出的每條訊息都會自動攜帶這些額外資料。

範例:記錄請求識別碼

在 Web 應用中可為每個請求指派唯一 ID(例如 UUID)。透過 MDC,這個 ID 會自動加到處理該請求之執行緒所寫出的所有日誌。

程式碼長這樣(SLF4J + Logback):

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import java.util.UUID;

public class MdcExample {
    private static final Logger logger = LoggerFactory.getLogger(MdcExample.class);

    public static void main(String[] args) {
        Runnable task = () -> {
            // 產生請求的唯一識別碼
            String requestId = UUID.randomUUID().toString();
            MDC.put("requestId", requestId); // 加入到 MDC

            logger.info("正在處理請求");
            doSomeWork();
            logger.info("處理完成");

            MDC.clear(); // 結束後務必清除!
        };

        Thread t1 = new Thread(task, "執行緒-1");
        Thread t2 = new Thread(task, "執行緒-2");
        t1.start();
        t2.start();
    }

    static void doSomeWork() {
        logger.debug("正在執行工作...");
    }
}

設定日誌格式(例如,logback.xml):

<encoder>
    <pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} [requestId=%X{requestId}] - %msg%n</pattern>
</encoder>

結果:

12:01:23 [執行緒-1] INFO  MdcExample [requestId=ad8d...f3] - 正在處理請求
12:01:23 [執行緒-1] DEBUG MdcExample [requestId=ad8d...f3] - 正在執行工作...
12:01:23 [執行緒-1] INFO  MdcExample [requestId=ad8d...f3] - 處理完成

重點!

  • MDC 只在單一執行緒範圍內運作。如果你把工作交給另一個執行緒(例如透過執行緒池),需要手動傳遞 MDC 的值(或使用能自動處理的專用庫)。
  • 別忘了清除 MDC!若未清除,資料可能「外洩」到同一執行緒處理的下一個請求(例如 Web 伺服器的執行緒池)。請在 finally 區塊呼叫 MDC.clear()

3. Web 應用中的日誌紀錄

Web 應用不只是啟動後就跑的程式,而是一條真正的生產線:請求進來、被處理、回應送出,而且是同時、大量進行。在這裡,日誌不是奢侈品,而是必要工具!

在 Web 應用中要記錄什麼?

  • HTTP 請求與回應:方法、URL、參數、回應狀態、處理時間。
  • 錯誤與例外:所有非預期失敗與 stack trace。
  • 業務事件:註冊、登入、下單、付款等。
  • 技術細節:與資料庫、外部服務的互動,操作耗時等。

核心原則:請用能讓你在一個月後凌晨三點出事時,仍能看懂到底發生什麼的方式來記錄日誌。

範例:HTTP 請求日誌(Spring Boot)

最簡單的做法是使用過濾器或切面,為每個進來的請求做日誌。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;

@Component
public class RequestLoggingFilter implements Filter {
    private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String requestId = UUID.randomUUID().toString();
        MDC.put("requestId", requestId);

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        logger.info("請求:{} {}", httpRequest.getMethod(), httpRequest.getRequestURI());

        long start = System.currentTimeMillis();
        try {
            chain.doFilter(request, response); // 交給後續鏈(到控制器)
        } finally {
            long duration = System.currentTimeMillis() - start;
            logger.info("回應已送出,處理時間:{} 毫秒", duration);
            MDC.clear();
        }
    }
}

錯誤與例外的日誌

在 Web 框架(例如 Spring)中,通常會使用專門的錯誤處理器(@ExceptionHandler)來優雅地記錄所有非預期的故障。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(Exception.class)
    public String handleException(Exception ex) {
        logger.error("發生錯誤:", ex); // 記錄完整的 stack trace!
        return "error"; // 回傳錯誤頁面
    }
}

與 Web 框架的整合

幾乎所有現代 Web 框架(SpringJakarta EEMicronaut 等)都能「開箱即用」地與 logger 整合。通常只要把 SLF4J/Logback 依賴加入專案——啟動、請求處理、錯誤等標準訊息就會自動被記錄。

4. 實作:在多執行緒任務中的日誌範例

讓我們在教學應用(例如訂單處理服務)中加入多執行緒處理,看看日誌如何幫忙不致手忙腳亂。

範例:使用多執行緒與 MDC 處理訂單

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class OrderProcessingApp {
    private static final Logger logger = LoggerFactory.getLogger(OrderProcessingApp.class);

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        for (int i = 1; i <= 5; i++) {
            final int orderId = i;
            executor.submit(() -> {
                String requestId = UUID.randomUUID().toString();
                MDC.put("requestId", requestId);

                try {
                    logger.info("開始處理訂單 {}", orderId);
                    processOrder(orderId);
                    logger.info("訂單 {} 處理成功", orderId);
                } catch (Exception ex) {
                    logger.error("處理訂單時發生錯誤 " + orderId, ex);
                } finally {
                    MDC.clear();
                }
            });
        }
        executor.shutdown();
    }

    static void processOrder(int orderId) throws InterruptedException {
        if (orderId % 2 == 0) {
            throw new RuntimeException("對偶數訂單的錯誤模擬");
        }
        Thread.sleep(500); // 模擬工作
    }
}

發生了什麼:

  • 每筆訂單在獨立的執行緒中處理。
  • 每個執行緒都會建立唯一的 requestId(透過 MDC)。
  • 可以用這個識別碼把同一筆訂單的所有日誌串在一起查找。
  • 錯誤會連同完整堆疊一起記錄。

日誌格式已設定為顯示 requestId

5. 重要細節與注意事項

  • 執行緒、執行緒池與 MDC。 如果使用執行緒池(你很可能會用),請記住:池中的執行緒會被重複利用!若忘了清除 MDC,一個請求的資料可能會流入另一個請求的日誌。總是在工作結束時呼叫 MDC.clear()
  • MDC 與非同步任務。 在非同步的 Web 框架(例如 Spring WebFlux)中,MDC 不一定能「開箱即用」,因為請求處理可能在不同執行緒間跳動。可使用專用的擴充或轉接器。
  • 微服務中的日誌。 在微服務架構中,通常不只記錄本地請求識別碼,還會記錄全域(traceId),並在服務之間傳遞。這能追蹤請求在整個系統中的路徑(分散式追蹤)。常見系統有 ZipkinJaegerOpenTelemetry

6. 示範:System.out.println 與日誌的差異

System.out.println 只是把字串印到主控台。在多執行緒環境中:

  • 訊息可能彼此交錯。
  • 沒有時間、執行緒、層級、上下文等資訊。
  • 無法設定輸出到檔案、格式、或依層級過濾。

Logger 會輸出結構化訊息,考量執行緒、層級、格式,並支援輸出到不同目的地(檔案、主控台、網路)。

比較範例

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PrintVsLogger {
    private static final Logger logger = LoggerFactory.getLogger(PrintVsLogger.class);

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("System.out: " + Thread.currentThread().getName() + " 步驟 " + i);
                logger.info("Logger: 步驟 {}", i);
            }
        };
        new Thread(task, "T1").start();
        new Thread(task, "T2").start();
    }
}

結論:

  • System.out —— 訊息可能交錯,沒有時間與層級。
  • Logger —— 每條訊息都包含時間、執行緒、層級,可過濾並快速定位。

7. 多執行緒與 Web 應用中日誌的常見錯誤

常見錯誤 1:使用 System.out.println 取代 logger。 在多執行緒環境會造成主控台「一團亂」,無法依層級過濾,也缺乏上下文資訊。

常見錯誤 2:忽略或錯用 MDC。 若不使用 MDC 傳遞請求/使用者識別碼,日誌會失去意義——無法判斷錯誤屬於哪個請求。若忘了清除 MDC,資料可能「外洩」到其他請求。

常見錯誤 3:把 logger 建成區域變數。 請使用 private static finalLogger——這樣每個類別只建立一次,不浪費記憶體,也更不易出錯。

常見錯誤 4:記錄敏感資料。 密碼、信用卡、個資等都不應該寫入日誌——這是資安問題!

常見錯誤 5:只用「INFO」或只用「ERROR」。 請用合適層級:DEBUG 用於除錯、INFO 用於業務事件、ERROR 用於錯誤。不要全部寫成同一層級,否則日誌會失去價值。

常見錯誤 6:未記錄例外的 stack trace。 如果只寫 logger.error("錯誤: " + ex.getMessage()),就會遺失錯誤原因的關鍵資訊。請務必記錄完整例外:logger.error("錯誤", ex)

常見錯誤 7:自製但非執行緒安全的 logger。 若有人「自己寫 logger」但沒有做好同步——在多執行緒環境幾乎可保證會遺失或破壞日誌。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION