Java OO & Functional
11/38
Bài 11 / 38~12 phútException HandlingLộ trình · chặng 04/21Miễn phí… lượt xem

Đọc stack trace và cause chain — giữ dấu vết lỗi qua mọi layer

Cách đọc stack trace production, cơ chế fillInStackTrace của JVM, kỹ thuật giữ nguyên nhân gốc qua cause chain và suppressed exception.

TL;DR: Stack trace là nhật ký ghi lại hành trình lan truyền của ngoại lệ qua Call Stack. JVM tạo dựng stack trace ngay tại thời điểm new Exception() thông qua hàm gốc native fillInStackTrace(). Khi bắt ngoại lệ ở tầng thấp và ném ngoại lệ mới ở tầng cao, bắt buộc phải truyền exception gốc vào tham số cause để bảo toàn chuỗi nguyên nhân (Cause Chain). Cơ chế Suppressed Exception (Java 7+) ngăn ngừa việc ngoại lệ đóng tài nguyên ghi đè mất ngoại lệ nghiệp vụ chính.

Ở bài trước, bạn đã nắm vững cơ chế Stack Unwinding khi một ngoại lệ được ném ra và leo ngược qua các tầng stack frame. Bài học này sẽ giúp bạn giải mã chính xác bức tranh dấu vết mà JVM để lại trong log hệ thống khi quá trình này diễn ra.

Khi kiểm tra nhật ký vận hành (production log), bạn thường xuyên bắt gặp đoạn thông báo dạng:

Plain Text
LibraryServiceException: Khong the thuc hien muon an pham ID: B-102
    at com.olhub.library.LibraryService.checkout(LibraryService.java:45)
    at com.olhub.library.LoanController.handleLoan(LoanController.java:28)
Caused by: java.sql.SQLException: connection refused: database-cluster:5432
    at org.postgresql.Driver.connect(Driver.java:456)

Chỉ mất chưa đầy 10 giây, một kỹ sư có kinh nghiệm sẽ nắm bắt trọn vẹn: lỗi nghiệp vụ xảy ra ở đâu (LibraryService.java:45), bắt nguồn từ tầng nào (LoanController), và quan trọng nhất: nguyên nhân kỹ thuật gốc rễ là máy chủ PostgreSQL từ chối kết nối (connection refused). Nếu không có Stack Trace và Cause Chain, việc chẩn đoán lỗi trong môi trường thực tế sẽ biến thành bài toán phỏng đoán tốn kém.

1. Stack trace hoạt động như thế nào và cách đọc nhanh trong thực tế?

Stack trace là bản ghi tuần tự các khung stack (stack frames) tại thời điểm ngoại lệ được khởi tạo:

Java
public class TraceDemo {
    public static void main(String[] args) {
        stepOne();
    }
    static void stepOne() { stepTwo(); }
    static void stepTwo() { stepThree(); }
    static void stepThree() {
        throw new RuntimeException("Su co tai stepThree");
    }
}

Khi thực thi, máy ảo in ra:

Plain Text
Exception in thread "main" java.lang.RuntimeException: Su co tai stepThree
    at TraceDemo.stepThree(TraceDemo.java:9)
    at TraceDemo.stepTwo(TraceDemo.java:7)
    at TraceDemo.stepOne(TraceDemo.java:6)
    at TraceDemo.main(TraceDemo.java:4)

Quy tắc đọc hiểu stack trace:

  1. Dòng tiêu đề: java.lang.RuntimeException: Su co tai stepThree cho biết Kiểu ngoại lệ và Thông điệp chi tiết.
  2. Dòng đầu tiên trong danh sách (at ...): Là điểm ném ngoại lệ chính xác (tại phương thức stepThree, file TraceDemo.java, dòng số 9).
  3. Các dòng tiếp theo: Chuỗi các hàm gọi ngược từ gần nhất tới xa nhất (stepTwo -> stepOne -> main).

Chiến lược 4 bước đọc log production trong 10 giây

  1. Đọc lướt dòng đầu tiên để phân loại vấn đề (Lỗi logic dữ liệu, NullPointer, hay mạng I/O).
  2. Quét nhanh xuống 3-5 dòng at đầu tiên thuộc package dự án của bạn (bỏ qua các package trung gian của framework như Spring, Tomcat, Netty).
  3. Tìm kiếm từ khóa Caused by: nằm ở cuối log. Nếu có, đây chính là nguyên nhân cốt lõi cần khắc phục trước tiên.

2. JVM tạo dựng Stack trace bằng cách nào trong bộ nhớ?

Một hiểu lầm rất phổ biến: Nhiều người nghĩ stack trace được tạo ra khi lệnh throw thực thi. Thực tế, JVM tạo dựng stack trace ngay tại thời điểm gọi hàm khởi tạo new RuntimeException(...).

Trong constructor của Throwable, máy ảo gọi hàm native:

Java
public synchronized Throwable fillInStackTrace() {
    if (stackTrace != null || backtrace != null) {
        fillInStackTrace(0);
        stackTrace = UNASSIGNED_STACK;
    }
    return this;
}

JVM sẽ quét ngược Call Stack của luồng hiện tại, trích xuất metadata từ bytecode:

  • Tên lớp (Class name) và Tên phương thức (Method name).
  • Tên file nguồn (lấy từ thuộc tính SourceFile trong bytecode class).
  • Dòng lệnh mã nguồn (lấy từ bảng LineNumberTable khi biên dịch với cờ -g).

Mỗi khung stack được lưu thành một đối tượng StackTraceElement. Thao tác duyệt stack này tốn từ 1 đến 10 microsecond cho mỗi ngoại lệ. Đối với luồng chạy bình thường, chi phí này hoàn toàn không đáng kể; nhưng đối với các vòng lặp xử lý hàng triệu phần tử mỗi giây, việc lạm dụng khởi tạo ngoại lệ sẽ trở thành điểm nghẽn hiệu năng.

3. Cause Chain: Giữ gìn nguyên nhân gốc rễ qua các tầng kiến trúc

Trong kiến trúc phân tầng (Controller -> Service -> Repository), mỗi tầng có một phạm vi trừu tượng riêng. Khi tầng cơ sở dữ liệu gặp sự cố SQLException, tầng LibraryService không nên ném thẳng SQLException ra ngoài mà cần bọc lại thành LibraryStorageException.

Tuy nhiên, bạn bắt buộc phải truyền ngoại lệ gốc vào tham số cause:

Java
public class LibraryRepository {
    public LibraryItem findById(String id) {
        try {
            return queryDatabase(id);
        } catch (SQLException e) {
            // Chuan muc: Truyen ca thong diep domain va ngoai le goc e vao cause
            throw new LibraryStorageException("Khong the truy van an pham: " + id, e);
        }
    }
}

Khi ghi log, JVM sẽ tự động xuất ra chuỗi nguyên nhân liên hoàn:

Plain Text
LibraryStorageException: Khong the truy van an pham: B-102
    at LibraryRepository.findById(LibraryRepository.java:25)
    at LibraryService.checkout(LibraryService.java:12)
Caused by: java.sql.SQLException: connection timeout to primary DB
    at org.postgresql.Driver.connect(Driver.java:456)

Sai lầm nghiêm trọng: Bỏ quên tham số Cause khi Wrap

Quan sát sự khác biệt khi lập trình viên quên truyền ngoại lệ gốc:

Java
// SAI LAM NGHIEP DU: Mat hoan toan dau vet loi goc
catch (SQLException e) {
    throw new LibraryStorageException("Khong the truy van an pham: " + id);
}

Hai stack trace của cùng một sự cố: bản quên cause chỉ có message và hai dòng at của tầng repository rồi dừng hẳn, bản giữ cause có thêm khối Caused by SQLException connection refused kèm dòng at của postgresql Driver

Toàn bộ thông tin về SQLException: connection timeout cùng dòng code chính xác trong thư viện JDBC bị xoá sạch hoàn toàn khỏi bộ nhớ log. Lập trình viên trực ca lúc đó sẽ không có cách nào biết hệ thống bị lỗi mạng, lỗi sai mật khẩu hay lỗi cú pháp SQL.

Quy tắc xây dựng Custom Exception hỗ trợ Cause Chain

Mọi lớp custom exception bạn tạo ra phải luôn cung cấp ít nhất 4 constructor chuẩn mực sau:

Java
public class LibraryStorageException extends RuntimeException {
    public LibraryStorageException(String message) {
        super(message);
    }

    public LibraryStorageException(String message, Throwable cause) {
        super(message, cause); // Truyen cause vao Throwable cha
    }

    public LibraryStorageException(Throwable cause) {
        super(cause);
    }
}

4. Suppressed Exceptions: Xử lý sự cố phụ khi dọn dẹp tài nguyên

Trước Java 7, nếu một ngoại lệ phát sinh trong khối try và sau đó một ngoại lệ khác tiếp tục phát sinh trong khối finally khi đóng tài nguyên:

Java
try {
    throw new LoanProcessingException("The ban doc khong hop le");
} finally {
    resource.close(); // Gia su close() nem IOException
}

Ngoại lệ LoanProcessingException sẽ bị biến mất hoàn toàn và bị ghi đè bởi IOException từ finally.

Java 7 bổ sung tính năng Suppressed Exception (Ngoại lệ bị nén). Cú pháp try-with-resources tự động ghim ngoại lệ phát sinh lúc đóng vào danh sách suppressed của ngoại lệ chính:

Plain Text
LoanProcessingException: The ban doc khong hop le
    at LoanService.process(LoanService.java:42)
    Suppressed: java.io.IOException: Failed to release file lock
        at CatalogFile.close(CatalogFile.java:88)

Bạn có thể truy xuất danh sách này bằng mã nguồn thông qua phương thức e.getSuppressed().

5. Những cạm bẫy thường gặp khi xử lý Stack trace

❌ Bẫy 1: Gọi printStackTrace() bừa bãi thay vì dùng Logger

Java
catch (Exception e) {
    e.printStackTrace(); // Anti-pattern
}

Lệnh này in ra System.err. Trong môi trường container hoặc cloud, luồng System.err thường không được định dạng JSON, không có timestamp, và không gửi được về hệ thống giám sát tập trung (như ElasticSearch hoặc CloudWatch). Luôn dùng: logger.error("Thao tac that bai", e).

❌ Bẫy 2: Ném ngoại lệ mới mà bỏ quên biến gốc Luôn nhớ nguyên tắc: Khi viết throw new MyException(...) bên trong khối catch (OtherException e), vị trí tham số cuối cùng của constructor luôn phải là biến e.

❌ Bẫy 3: Ghi log lỗi lặp đi lặp lại qua nhiều tầng (Log and Rethrow)

Java
// TANG REPOSITORY
catch (SQLException e) {
    logger.error("Error in repo", e);
    throw new ServiceException(e);
}
// TANG SERVICE
catch (ServiceException e) {
    logger.error("Error in service", e);
    throw e;
}

Mỗi lỗi bị nhân bản log thành 3-4 lần cho cùng một sự cố, làm tràn ngập màn hình theo dõi và gây nhiễu cho đội ngũ vận hành. Chỉ nên ghi log tại tầng biên giới xử lý cuối cùng (Boundary Layer), còn các tầng trung gian chỉ nên wrap và propagate.

6. Deep Dive Oracle & OpenJDK References

Tài liệu tham khảo đặc tả
  • Throwable Javadoc: Đặc tả kỹ thuật về các phương thức fillInStackTrace(), getCause(), initCause(), và addSuppressed().
  • JLS §11.3 (Run-Time Handling of an Exception): Cơ chế dịch chuyển điều khiển và khởi tạo bảng vết tích ngoại lệ của máy ảo Java.
  • Effective Java (Joshua Bloch) Item 75: "Include failure-capture information in detail messages" (Đưa toàn bộ thông tin ngữ cảnh dữ liệu lỗi vào thông điệp ngoại lệ).

7. Tự kiểm tra kiến thức

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Tại sao nói ngoại lệ Java ghi nhận vị trí Call Stack tại thời điểm gọi lệnh new thay vì thời điểm gọi lệnh throw?
  2. Q2
    Hậu quả nghiêm trọng nhất của việc quên truyền ngoại lệ gốc (cause) khi tạo ngoại lệ bọc mới là gì?
  3. Q3
    Làm thế nào để lấy được ngoại lệ ban đầu (Root Cause) từ một đối tượng ngoại lệ bị bọc qua nhiều tầng kiến trúc?
  4. Q4
    Cơ chế Suppressed Exception được kích hoạt tự động trong tình huống nào?
  5. Q5
    Tại sao anti-pattern 'Log and Rethrow' (vừa ghi log vừa ném tiếp) ở các tầng trung gian lại bị coi là thực hành xấu?

Bài tiếp theo: Exception table và cost của exception — throw rẻ, tạo đắt

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Lộ trình Junior Java Backend Developer

Chặng 4/21 · Exception handling — bài 2/7 của chặng

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Exception table và cost của exception — throw rẻ, tạo đắt