Khắc phục java.io.NotSerializableException: Hướng dẫn thực tế

beginner Java2026-03-30| Java SE (mọi phiên bản), Jakarta EE, Spring Boot (Session/Caching), Apache Spark, Hệ thống phân tán.

Error Message

java.io.NotSerializableException: com.example.dto.UserDTO
#java#tuần tự hóa#notserializableexception#backend

Thông báo lỗi

Bạn có thể đã gặp lỗi này khi cố gắng lưu một đối tượng vào HTTP session, lưu dữ liệu đệm (cache) trong Redis, hoặc truyền một DTO qua mạng. Nó trông như thế này:

java.io.NotSerializableException: com.example.dto.UserDTO
    at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1197)
    at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:354)

Nguyên nhân gốc rễ

Quá trình tuần tự hóa (serialization) trong Java chuyển đổi trạng thái của một đối tượng thành một luồng byte (byte stream). Để quá trình này hoạt động, lớp (class) của bạn phải "chấp thuận" bằng cách thực thi (implement) marker interface java.io.Serializable. Nếu JVM gặp một lớp chưa ký kết "hợp đồng" này — hoặc chứa một trường (field) không thể tuần tự hóa — nó sẽ dừng lại ngay lập tức và ném ra ngoại lệ này.

Bạn sẽ thường thấy lỗi này trong các môi trường sau:

- **Clustered Sessions:** Spring Boot hoặc Tomcat cố gắng sao chép một `HttpSession` qua nhiều nút (node).
- **Tính toán phân tán (Distributed Computing):** Apache Spark hoặc Flink di chuyển các tác vụ và dữ liệu giữa các worker node.
- **Stateful Caching:** Lưu trữ các đối tượng phức tạp trong Hazelcast, Ehcache, hoặc Redis bằng trình tuần tự hóa gốc của Java.
- **Legacy RMI:** Gửi các đối tượng giữa các JVM khác nhau bằng Remote Method Invocation.

Cách khắc phục java.io.NotSerializableException

1. Thực thi Interface Serializable

Cách khắc phục tiêu chuẩn là thêm implements Serializable vào lớp của bạn. Đây là một marker interface, vì vậy bạn không cần viết thêm bất kỳ phương thức mới nào. Tuy nhiên, bạn nên luôn định nghĩa một serialVersionUID.

package com.example.dto;

import java.io.Serializable;

public class UserDTO implements Serializable {
    // Việc thiết lập thủ công giá trị này giúp ngăn chặn InvalidClassExceptions khi cập nhật
    private static final long serialVersionUID = 1L;

    private String username;
    private String email;
}

Tại sao cần ID này? Nếu bạn không định nghĩa, Java sẽ tự động tạo nó dựa trên các thành phần của lớp. Nếu sau này bạn thêm dù chỉ một trường, ID sẽ thay đổi. Điều này sẽ khiến bạn không thể đọc được dữ liệu cũ đã lưu trước khi thay đổi.

2. Kiểm tra chuỗi đối tượng

Tuần tự hóa hoạt động giống như một chuỗi. Nếu UserDTO có thể tuần tự hóa nhưng chứa một đối tượng Profile không có khả năng này, quá trình vẫn sẽ thất bại. Mọi đối tượng lồng nhau cũng phải thực thi Serializable.

public class UserDTO implements Serializable {
    private String username;
    private Profile profile; // Profile cũng phải thực thi Serializable!
}

3. Sử dụng từ khóa 'transient' cho các trường hợp ngoại lệ

Một số thứ đơn giản là không thể tuần tự hóa, chẳng hạn như các kết nối cơ sở dữ liệu, các luồng (thread) đang hoạt động, hoặc các socket mạng. Hãy sử dụng từ khóa transient để yêu cầu Java bỏ qua các trường này trong quá trình thực hiện.

public class UserDTO implements Serializable {
    private String username;
    
    // Dữ liệu nhạy cảm về bảo mật hoặc trạng thái tạm thời không nên được tuần tự hóa
    private transient String sessionToken;
    
    // Logger gắn liền với phiên bản JVM cụ thể
    private transient Logger logger = Logger.getLogger(UserDTO.class.getName());
}

4. Xử lý các lớp bên thứ ba không thể tuần tự hóa

Bạn có thể sử dụng một lớp từ thư viện không hỗ trợ tuần tự hóa, chẳng hạn như java.util.Optional (một lớp nổi tiếng là không thể tuần tự hóa). Vì bạn không thể sửa đổi mã nguồn của thư viện, bạn phải đánh dấu trường đó là transient hoặc sử dụng một wrapper có thể tuần tự hóa. Trong các DTO, tốt hơn hết là sử dụng các trường thông thường và chỉ trả về Optional trong các phương thức getter.

5. Lớp nội (Inner Class) Static và Non-Static

Các lớp nội là một sai lầm phổ biến. Một lớp nội tiêu chuẩn giữ một tham chiếu ẩn đến lớp bên ngoài của nó. Nếu bạn tuần tự hóa lớp nội, Java cũng sẽ cố gắng tuần tự hóa lớp bên ngoài. Để khắc phục điều này, hãy làm cho lớp bên ngoài có thể tuần tự hóa hoặc — lý tưởng hơn — chuyển lớp nội thành static.

public class Outer {
    // Các lớp nội static không cần tham chiếu đến 'Outer'
    public static class Inner implements Serializable {
        private int data;
    }
}

Xác minh: Kiểm tra trước khi triển khai

Đừng đợi đến khi hệ thống gặp sự cố trên môi trường production mới tìm lỗi tuần tự hóa. Một unit test đơn giản có thể xác minh rằng đối tượng của bạn có thể chuyển đổi sang mảng byte và ngược lại một cách an toàn. Một UserDTO điển hình với một vài chuỗi ký tự sẽ tạo ra một mảng byte khoảng 150 đến 300 byte.

@Test
void verifySerialization() throws IOException {
    UserDTO dto = new UserDTO("dev_user", "test@example.com");
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);

    // Lệnh này sẽ ném ra ngoại lệ ngay lập tức nếu việc khắc phục thất bại
    oos.writeObject(dto);
    assertTrue(baos.toByteArray().length > 0);
}

Các thực hành tốt nhất cho ứng dụng hiện đại

- **Tự động hóa kiểm tra:** Cấu hình IDE của bạn (IntelliJ hoặc Eclipse) để cảnh báo bất kỳ lớp nào thực thi `Serializable` mà thiếu `serialVersionUID`.
- **Cân nhắc sử dụng JSON:** Quá trình tuần tự hóa gốc của Java thường dễ gãy (brittle) và có các lỗ hổng bảo mật đã biết. Nếu bạn đang xây dựng một hệ thống mới, hãy sử dụng JSON (thông qua Jackson) hoặc Protobuf. Chúng linh hoạt hơn và hoạt động tốt trên các ngôn ngữ lập trình khác nhau.
- **Giữ DTO đơn giản:** Giới hạn DTO ở các kiểu dữ liệu nguyên thủy (primitives), String và các Collection có thể tuần tự hóa khác.

Related Error Notes