Java Internals & Concurrency
42/75
Bài 42 / 75~14 phútI/O & NIOMiễn phí… lượt xem

Serialization — cơ chế, serialVersionUID và transient

Java native serialization hoạt động thế nào: marker interface + reflection, serialVersionUID làm version check, transient cho secret/resource, custom writeObject/readObject để encrypt và validate.

TL;DR: Java native serialization ghi object graph xuống byte stream qua ObjectOutputStream: class chỉ cần implement Serializable — marker interface không method, JVM dùng reflection đọc field. serialVersionUID là version check khi deserialize — không khai báo explicit thì JVM tự hash từ structure class, đổi javac/thêm method là UID đổi → InvalidClassException silent khi deploy. transient loại field khỏi serialize (secret, resource, cache); custom writeObject/readObject (private, JVM gọi qua reflection) cho encrypt/validate/rebuild. Record (Java 14+) deserialize qua canonical constructor — an toàn hơn class thường vốn bị bypass constructor.

Bạn cần lưu một object graph xuống disk để lần khởi động sau load lại, hoặc gửi object qua network giữa 2 JVM (RMI, distributed cache). Java có sẵn câu trả lời từ phiên bản 1.1: chỉ cần implements Serializable — không viết thêm dòng code nào — JVM tự ghi được cả object graph, kể cả reference lồng nhau, xuống byte stream.

"Không viết gì mà chạy được" luôn có giá của nó. Một interface rỗng làm sao ghi được object? Vì sao recompile xong file .ser cũ bỗng không load được? Vì sao field Connection làm cả class không serialize nổi? Bài này mổ cơ chế bên dưới: marker interface + reflection, serialVersionUID, transient, và custom writeObject/readObject.

Còn mặt tối của serialization — vì sao nó thành lỗ hổng security số 1 của Java năm 2015 và industry chuyển sang JSON/Protobuf — để dành trọn cho bài kế tiếp: Deserialization — lỗ hổng RCE và alternatives.

Nhớ lại — stream co dien

Ôn lại kiến thức nền tảng từ bài stream co dien trước khi đi sâu vào nội dung bài học này.

1. Java native serialization — cơ chế cơ bản

Ví dụ đơn giản

Java
import java.io.*;

class User implements Serializable {
    private static final long serialVersionUID = 1L;
    String name;
    int age;

    User(String n, int a) { name = n; age = a; }
}

// Serialize
try (ObjectOutputStream out = new ObjectOutputStream(
        new FileOutputStream("user.ser"))) {
    out.writeObject(new User("Alice", 30));
}

// Deserialize
try (ObjectInputStream in = new ObjectInputStream(
        new FileInputStream("user.ser"))) {
    User u = (User) in.readObject();
    System.out.println(u.name);   // Alice
}

3 yêu cầu:

  1. Class implement Serializable — marker interface (không có method), signal "an toàn serialize".
  2. Field không transient — sẽ được serialize. Field transient bỏ qua.
  3. serialVersionUID — version identifier.

Serializable chỉ là marker

Java
public interface Serializable {
    // Khong co method
}

Không có method nào — JVM dùng reflection để đọc field, serialize. Class không implement Serializable → throw NotSerializableException khi writeObject.

Marker interface là pattern có kể từ Java 1.1 — khác modern-style annotation (@Serializable như Kotlin). Lý do lịch sử: Java 1.1 không có annotation (thêm Java 5), dùng interface rỗng.

Field nào được serialize

  • Field không transient → serialize.
  • Field không static → serialize (static thuộc class, không instance).
  • Field transient → bỏ qua, deserialize trả default (null/0/false).
Java
class User implements Serializable {
    private static final long serialVersionUID = 1L;

    String name;              // serialize
    int age;                  // serialize
    transient String password;   // KHONG serialize
    transient Connection conn;   // KHONG serialize
    static int COUNT;         // KHONG serialize (static)
}

Deserialize sau, password và conn = null (default cho reference). Nếu cần value, override readObject (mục 4) để rebuild.

2. serialVersionUID — version của class

Java
class User implements Serializable {
    private static final long serialVersionUID = 1L;
    // ...
}

Mỗi class Serializable nên có field serialVersionUID — static final long. Vai trò: version identifier.

Deserialize check

Khi deserialize user.ser:

  • JVM đọc UID từ byte stream.
  • So với UID class hiện tại.
  • Match → deserialize tiếp.
  • Khác → throw InvalidClassException.

Cơ chế: "file serialize version cũ không load được bằng class version mới khác UID".

Nếu không khai báo

JVM tính UID tự động bằng hash từ structure class: signature field, method, interface. Algorithm có trong spec nhưng không hoàn toàn deterministic giữa javac version, JDK version, OS.

Hậu quả:

  • Recompile với javac khác → UID đổi.
  • Thêm 1 method (kể cả private) → UID đổi.
  • Reorder method → UID đổi (có thể).

File .ser serialized với version A không deserialize được bằng version B. Bug silent — phát hiện khi deploy.

⚠️ Luôn khai báo serialVersionUID explicit

Không khai báo → fragile với rebuild. IDE có warning: "The serializable class does not declare a static final serialVersionUID field". Nên bật strict mode và fix. Ngoại lệ duy nhất: record — xem mục 5, record có cơ chế serialization riêng và spec nới lỏng yêu cầu match UID.

Khi tăng UID?

Tăng UID (1L → 2L) khi cố ý break compatibility — file cũ không load được với class mới. Ví dụ đổi schema field không tương thích, rename class.

Giữ UID khi thay đổi backward-compatible — thêm field mới (default), thêm method. Deserialize file cũ vẫn OK, field mới = default.

Thử ngẫmđội bạn thêm một method getDisplayName() không đổi field nào rồi build lại và deploy. Nếu class chưa khai báo serialVersionUID explicit, các file .ser cũ trên production còn đọc được không?

3. transient — bỏ field khỏi serialize

Java
class Session implements Serializable {
    private static final long serialVersionUID = 1L;

    String userId;
    transient String authToken;      // Secret - khong ghi disk
    transient Socket connection;      // Resource runtime
    transient Instant lastAccessed;   // Compute lai duoc
}

Deserialize → authToken null, connection null, lastAccessed null.

Dùng cho:

Secret / sensitive data

Password, API token, session token — không persist disk (security risk nếu file leak ra ngoài).

Resource runtime

Socket, Connection, InputStream, Thread — không có ý nghĩa khi deserialize ở máy khác hoặc lần khởi động khác. Phải rebuild.

Cache / computed field

Field tính từ field khác — rebuild trong readObject khi cần:

Java
class PriceCache implements Serializable {
    private static final long serialVersionUID = 1L;
    private final BigDecimal amount;
    private final String currency;
    private transient String displayPrice;   // "$100.00" - compute tu amount + currency

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        this.displayPrice = formatPrice(amount, currency);   // rebuild
    }
}

Large intermediate

Buffer, temporary structure — không cần persist, tạo lại khi cần.

4. Custom writeObject / readObject

Override cơ chế mặc định để control format:

Java
class User implements Serializable {
    private static final long serialVersionUID = 1L;
    String name;
    transient String passwordHash;

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();             // Serialize field binh thuong
        out.writeUTF(encrypt(passwordHash));   // Custom: encrypt password truoc khi ghi
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();                // Deserialize field binh thuong
        this.passwordHash = decrypt(in.readUTF());   // Custom: decrypt
    }
}

Method private nhưng JVM vẫn gọi được — dùng reflection bypass access check. Thiết kế hơi kỳ lạ, di sản từ Java 1.1.

Use case:

  • Encrypt sensitive field before write.
  • Validate invariant in readObject (phòng data corruption).
  • Rebuild transient field after deserialize.
  • Change storage format mà không đổi field (backward-compatible migration).

Thêm readObjectNoData() — gọi khi deserialize object của subclass từ stream cũ không có data parent class. Hiếm dùng.

Thử ngẫmreadObject của bạn gọi decrypt(in.readUTF()) nhưng quên gọi in.defaultReadObject() trước đó. Các field còn lại của object sẽ ở trạng thái nào sau khi deserialize?

5. Record serialization (Java 14+)

Java
record Point(int x, int y) implements Serializable {
}

Record có cơ chế serialization riêng, không đi qua đường reflection set-field như class thường. Khi deserialize:

  • JVM đọc các component từ stream, rồi gọi canonical constructor — validate input qua compact constructor nếu có.
Java
record Age(int value) implements Serializable {

    public Age {
        if (value < 0) throw new IllegalArgumentException();
    }
}

Deserialize file với value = -1 → canonical constructor chạy → throw → reject. An toàn hơn class thường — class thường bypass constructor khi deserialize, compact constructor của record không bị bypass.

Về serialVersionUID: spec serialization nới lỏng yêu cầu match UID cho record — record không khai báo UID có default 0L, và mismatch UID không chặn deserialize record. Vì vậy lời khuyên "luôn khai báo UID" ở mục 2 áp cho class thường; với record, khai báo hay không chủ yếu là chuyện convention nhất quán trong codebase.

Record vẫn có thể là gadget trong chain nếu implement readObject custom — nhưng mặc định an toàn hơn (chi tiết về gadget chain ở bài kế tiếp).

6. Pitfall tổng hợp

❌ Nhầm 1: Không khai báo serialVersionUID (class thường).

Java
class User implements Serializable { String name; }   // UID tu compute

✅ private static final long serialVersionUID = 1L;

❌ Nhầm 2: Serialize resource.

Java
class Session implements Serializable {
    private Socket socket;   // Fail hoac corrupt khi deserialize
}

✅ Mark transient, rebuild trong readObject:

Java
private transient Socket socket;

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    this.socket = createSocket();   // rebuild
}

❌ Nhầm 3: Quên transient cho secret.

Java
class Account implements Serializable {
    String username;
    String password;   // Ghi plaintext xuong disk!
}

✅ transient String password; — hoặc tốt hơn, đừng giữ secret trong object cần persist.

❌ Nhầm 4: Dùng .ser làm config persistent.

Java
// Format coupled voi Java version, class version

✅ YAML / JSON — stable, editable, version-independent.

7. 📚 Deep Dive Oracle

📚 Deep Dive Oracle

Spec / reference chính thức:

Ghi chú: Spec serialization là tài liệu hiếm hoi mô tả chính xác từng byte trong file .ser (magic number ACED, stream version, class descriptor). Đọc chương 6 "Object Serialization Stream Protocol" nếu cần debug file serialize bằng hex editor — kỹ năng hữu ích khi gặp InvalidClassException khó hiểu trong production.

8. Tóm tắt

  • Serializable marker interface, không method — JVM dùng reflection serialize field qua ObjectOutputStream.
  • Luôn khai báo serialVersionUID cho class thường — không khai báo = fragile với rebuild (UID tự hash từ structure class).
  • Tăng UID khi cố ý break compatibility; giữ UID khi thay đổi backward-compatible.
  • transient bỏ field khỏi serialize — dùng cho secret, resource, cache, computed.
  • Custom writeObject/readObject (private method, JVM gọi qua reflection) override mặc định — dùng cho encrypt, validate, rebuild.
  • Record (Java 14+) serialize qua canonical constructor — an toàn hơn class thường (compact constructor validate, không bypass).
  • Record có cơ chế serialization riêng — spec nới lỏng yêu cầu match serialVersionUID.
  • Mặt security của deserialization (gadget chain, RCE, alternatives) — bài kế tiếp.

9. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì sao serialVersionUID phải khai báo explicit?
  2. Q2
    Vì sao readObject/writeObject khai báo private mà JVM vẫn gọi được? Thiết kế này nói gì về cơ chế serialization?
  3. Q3
    Khi nào dùng transient?
  4. Q4
    Đoạn sau có vấn đề gì? class Order implements Serializable { int id; String name; Connection conn; }
  5. Q5
    Record (Java 14+) có an toàn hơn class thường khi deserialize không? Vì sao?

Bài tiếp theo: Deserialization — lỗ hổng RCE và alternatives JSON/Protobuf

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.

Thảo luận & góp ý

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

Bài tiếp theo

Deserialization — lỗ hổng RCE và alternatives JSON/Protobuf