Skip to main content

Command Palette

Search for a command to run...

Java Dev 5 - Serialization & Deserialization

Updated
6 min read
Java Dev 5 - Serialization & Deserialization
S

Contact: phmclong@gmail.com

Lời nói đầu

Bài này tôi sẽ note lại 1 số thứ về Serialization & Deserialization trong Java. Nếu muốn tìm hiểu chi tiết hơn thì bạn có thể tìm đọc các link tham khảo ở cuối bài.

Tổng quan

serialize-deserialize-java

  • Serialization là cơ chế chuyển đổi cấu trúc dữ liệu (data structure) hoặc trạng thái của object (object states) trở thành luồng byte (byte streams), byte streams có thể được lưu vào file, bộ nhớ đệm (memory buffering), database hoặc truyền đi qua network.

  • Deserialization là cơ chế ngược lại với Serialization, cho phép khôi phục lại object states thông qua việc phân tích byte streams được tạo ra sau khi Serialization.

  • Nhờ cơ chế này mà object states có thể được lưu trữ ở trong cùng 1 môi trường hay cả 2 môi trường khác nhau. Cơ chế này được sử dụng rộng rãi trên các công nghệ như EJB, RMI, Hessian.

Lý do cần Serialization

Các định dạng như JSON, XML chỉ cho phép lưu các kiểu dữ liệu cơ bản. Trong khi đó Serialization hỗ trợ việc lưu trữ và truyền tải các đối tượng phức tạp, bao gồm cả các quan hệ đối tượng, kế thừa, đa hình và các tính năng nâng cao khác, đặc biệt trong ngôn ngữ Java.

Serialization

Để hiểu cách Java serializes 1 object, ta xem xét ví dụ sau:

// Class SerializationDemo được implements từ interface Serializable.
public class SerializationDemo implements Serializable {
    private String stringField;
    private int intField;

    public SerializationDemo(String s, int i) {
        this.stringField = s;
        this.intField = i;
    }

    public static void main(String[] args) throws IOException {
        // Tạo đối tượng từ class SerializationDemo
        SerializationDemo object = new SerializationDemo("gyyyy", 97777);
        /* 
        Khởi tạo 1 object từ class ByteArrayOutputStream
        việc này giống như tạo ra 1 vùng để chứa các byte,
        vùng chứa này là 1 mảng byte (byte array)
        */
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        /* 
        Khởi tạo object từ class ObjectOutputStream, 
        ObjectOutputStream có các method dùng để serialize, 
        khi serialize thì dữ liệu sẽ được ghi vào 1 OutputStream.        
        Vậy nên ta truyền vào object từ class ByteArrayOutputStream 
        để làm vùng chứa
        */
        ObjectOutputStream out = new ObjectOutputStream(bout);
        // method writeObject() được dùng để serialize đối tượng 
        out.writeObject(object);
    }
}

Ở ví dụ trên, ta tiến hành serialize một object kiểu SerializationDemo.

Một vài lưu ý:

  • Chỉ các object của các class được implements từ interface Serializable mới có thể được đưa vào quá trình Serialization.

  • Nếu lớp cha đã được implements từ interface Serializable thì lớp con không cần implements vẫn có thể được đưa vào quá trình Serialization.

  • Bên trong class, các dữ liệu có non-access modifierstatictransient sẽ không được lưu vào object states khi serialize.

  • Bên trong class, các dữ liệu có non-access modifierfinal sẽ luôn được lưu vào object states khi serialize. Do đó việc 1 dữ liệu có cả 2 non-access modifierfinalstatic thì vẫn luôn được lưu vào object states. Với ví dụ transient final int age = 20 , sau khi serialize thì age vẫn có giá trị 20.

Sau khi serialize, sử dụng các công cụ hex-editor ta có thể thấy dữ liệu là dạng chuỗi nhị phân (binary string):

ac ed 00 05 73 72 00 11  53 65 72 69 61 6c 69 7a    ....sr.. Serializ
61 74 69 6f 6e 44 65 6d  6f d9 35 3c f7 d6 0a c6    ationDem o.5<....
d5 02 00 02 49 00 08 69  6e 74 46 69 65 6c 64 4c    ....I..i ntFieldL
00 0b 73 74 72 69 6e 67  46 69 65 6c 64 74 00 12    ..string Fieldt..
4c 6a 61 76 61 2f 6c 61  6e 67 2f 53 74 72 69 6e    Ljava/la ng/Strin
67 3b 78 70 00 01 7d f1  74 00 05 67 79 79 79 79    g;xp..}. t..gyyyy

0xaced: STREAM_MAGIC. Chỉ ra đây là dữ liệu dạng serialize.

Thật ra tôi định phân tích nốt các giá trị hex còn lại, nhưng nó khá dài (tương lai tôi có thể sẽ update phần này). Chi tiết tham khảo tại đây: https://docs.oracle.com/javase/8/docs/api/java/io/ObjectStreamConstants.html

Thông tin thêm: 1 dấu hiệu nữa của 1 serialized data trong Java là khi được chuyển sang dạng Base64 thì sẽ có phần đầu chuỗi là rO0AB

Deserialization

Xem xét ví dụ sau, đây là phần nối tiếp với ví dụ trước:

public static void main(String[] args) throws ClassNotFoundException {
    // khai báo 1 mảng byte, giả sử data này là output từ việc serialize
    // trong ví dụ trước
    byte[] data; 
    /* 
    Ở trên mình đã nói khi serialize thì data sẽ được ghi vào 1 OutputStream.
    ByteArrayInputStream chứa Internal buffer chứa các byte 
    có thể được đọc từ stream. 
    Internal buffer theo dõi byte tiếp theo được cung cấp từ method readObject()
    */
    ByteArrayInputStream bin = new ByteArrayInputStream(data);
    /* 
    Khởi tạo object từ class ObjectInputStream,
    ObjectInputStream có các method dùng để deserialize
    */
    ObjectInputStream in = new ObjectInputStream(bin);
    // method readObject() được dùng để deserialize đối tượng
    SerializationDemo demo = (SerializationDemo) in.readObject();
}

Ở ví dụ vừa xong, ta tiến hành deserialize một object kiểu SerializationDemo.

Một số lưu ý:

  • Khi 1 object được deserialized, constructor của object đó sẽ không bao giờ được gọi.

SerialVersionUID

Serialization runtime liên kết với mỗi class Serializable một số phiên bản (version number), version number này còn gọi SerialVersionUID hay viết tắt là SUID.

SUID chiếm 1 vị trí rất quan trọng trong quá trình Serialization. Nó tương đương với thông tin dấu vân tay (fingerprint) của một object và có thể trực tiếp xác định sự thành công của quá trình Serialize.

Nếu bên nhận (receiver) đã load một class (giả sử class A) cho object có SUID khác với class tương ứng (cũng là class A) của bên gửi (sender) thì khi deserialize sẽ dẫn đến InvalidClassException.

Một vài lưu ý:

  • Một class có thể Serialize (Serializable) có thể khai báo rõ ràng UID của chính nó bằng cách khai báo tên trường (Field name)serialVersionUID. Field này phải là static và có kiểu dữ liệu long.
    Ví dụ: <Access Modifier bất kì> static long serialVersionUID=42L;

  • Nếu một class (Serializable) không khai báo serialVersionUID một cách rõ ràng, thì Serialization runtime tự sẽ tính toán một giá trị mặc định cho class đó dựa trên các khía cạnh khác nhau của class.

Kết bài

Bài này có vẻ khá ngắn nhưng cũng làm tôi mất khá nhiều thời gian để viết. Sang bài sau tôi sẽ giới thiệu về Reflection API. Việc hiểu cơ chế Serialization/Deserialization và Reflection API sẽ là bước đệm cho việc hiểu và phân tích lỗ hổng Insecure Deserialization trong Java.

Tham khảo:

104 views

More from this blog

Sheon

18 posts

  • Another blog: https://phmclong.github.io/myblog
  • Email: phmclong@gmail.com