Học Java

Tìm hiểu File nhị phân số hoá đối tượng trong Java (Phần 1)

Chắc hẳn mỗi lập trình viên cũng sẽ có đôi lần tiếp xúc với khái niệm số hóa, cụ thể hơn là số hoá một Object trong Java thành File nhị phân. Nhưng có bao giờ các bạn thắc mắc rằng file nhị phân kia nội dung gì không ? Trong phần đầu tiên của bài viết này, mình sẽ chia sẻ về thuật toán số hoá trong Java cũng như định dạng dữ liệu trong một file nhị phân ra sao. Trong phần 2, mình sẽ chia sẻ các lỗi thường gặp khi làm việc với số hoá đối tượng.

Phần 1 – Cấu trúc định dạng của 1 File nhị phân số hoá đối tượng

Số hoá là gì

Số hoá (hay còn gọi là Serialization) là quá trình lưu trữ trạng thái của một đối tượng dưới dạng một chuỗi các byte. Ngược lại với nó là quá trình chuyển đổi các byte thành trạng thái của một đối tượng (Deserialization).

Trong Java hỗ trợ nhiều API Serialization. Với phạm vi bài viết, mình chỉ đề cập tới interface Serializable và các class ObjectOutputStream, ObjectInputStream để minh hoạ cho chủ đề chính.

Định dạng của một file nhị phân

Để đơn giản hoá, trong bài viết này mình chỉ đề cập tới việc số hoá trên 1 Object của 1 lớp duy nhất. Độc giả có thể tìm hiểu thêm ở bài viết trong phần tham khảo.

Trước khi mổ xẻ một File nhị phân, chúng ta cần tạo ra File nhị phân trước.

Trong đoạn code dưới đây, mình tạo một class Person chứa 2 thuộc tính name và age. Class Person triển khai interface Serializable để cho phép số hoá các object của class Person. Trong hàm main, mình tạo Object person của class Person và truyền vào nó 2 giá trị nam 20 tương ứng cho tên và tuổi. Sau đó mình dùng ObjectOutputStream để lưu trữ trạng thái của Object person thành File nhị phân có tên là Object.dat.

Class Person

import java.io.Serializable;
 
public class Person implements Serializable {
        private static final long serialVersionUID = L;
	private String name;
	private Integer age;
 
	public Person(String name, int age) {
    	this.name = name;
    	this.age = age;
	}
}

 Main

import java.io.*;
 
public class Main {
	public static void main(String[] args) {
    	Person person = new Person("nam", 20);
    	File file = new File("Object.dat");
    	try {
            if (!file.exists()) {
            	if (file.createNewFile()) {
                	FileOutputStream fos = new FileOutputStream(file);
                	ObjectOutputStream oos = new ObjectOutputStream(fos);
                	oos.writeObject(person);
            	}
            }
  	  } catch (IOException e) {
        	e.printStackTrace();
          }
     }
}

Thực thi đoạn code trên, File nhị phân Object.dat được sinh ra trong directory của project

File nhị phân được lưu trữ dưới dạng các byte 8 bit nên nếu mở bằng các trình đọc văn bản thông thường sẽ không đọc được. Lý do bởi vì toàn bộ các byte trong đây không đại diện cho các ký tự trong mã ASCII. File nhị phân này phải được đọc dưới dạng các giá trị Hex. Ở đây mình dùng trình đọc thông dụng là HxD.

Lúc này File nhị phân đã có thể đọc được và có thể giải mã các giá trị byte (được chuyển đổi về giá trị Hex). Thoạt nhìn, chúng ta cũng có thể đoán được các giá trị được lưu trữ trong File nhị phân này.

Thuật toán số hoá trong Java

Nhìn chung, thuật toán số hoá trong Java thực hiện các công việc sau:

  • Ghi metadata (siêu dữ liệu) của lớp mà đối tượng được sinh ra. Các metadata này bao gồm các dữ liệu liên quan tới thuộc tính, phương thức của đối tượng và các dữ liệu khác.
  • Liên tục tìm và ghi các metadata của các lớp cha liên quan tới đối tượng và dừng lại tới khi tìm thấy lớp java.lang.object (lớp tổ tiên của tất cả các đối tượng trong Java)
  • Sau khi hoàn thành việc ghi các metadata, thuật toán bắt đầu ghi các giá trị liên quan tới đối tượng được ghi. Việc ghi này bắt đầu từ lớp cha trên cùng.
  • Liên tục tìm và ghi các giá trị gắn với đối tượng từ lớp cha trên cùng, qua từng lớp con và dừng lại ở lớp con dưới cùng.

Minh họa thuật toán số hoá

Giải mã các giá trị Hex

Bắt đầu file nhị phân là các giá trị thông báo thông tin của giao thức số hoá.

AC ED 00 05 73 72 00 06 50 65 72 73 6F 6E 98 2F A2 3D 66 E6 
D7 9D 02 00 02 4C 00 03 61 67 65 74 00 13 4C 6A 61 76 61 2F 
6C 61 6E 67 2F 49 6E 74 65 67 65 72 3B 4C 00 04 6E 61 6D 65 
74 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 
3B 78 70 73 72 00 11 6A 61 76 61 2E 6C 61 6E 67 2E 49 6E 74 
65 67 65 72 12 E2 A0 A4 F7 81 87 38 02 00 01 49 00 05 76 61 
6C 75 65 78 72 00 10 6A 61 76 61 2E 6C 61 6E 67 2E 4E 75 6D 
62 65 72 86 AC 95 1D 0B 94 E0 8B 02 00 00 78 70 00 00 00 14 
74 00 03 6E 61 6D
  • AC ED: STREAM_MAGIC. Chỉ ra rằng đây là một giao thức số hoá. Tất cả các File Object sẽ bắt đầu với giá trị này và nó LUÔN được viết ở vị trí đầu tiên của chuỗi. Có thể thấy giá trị này gần giống với File Signature. Ví dụ File JPEG luôn bắt đầu với chuỗi FF D8 FF DB hoặc FF D8 FF EE.

00 05: STREAM_VERSION. Phiên bản của giao thức số hoá. Phiên bản số hoá đang sử dụng là 5

Thuật toán số hoá tiếp tục ghi tiếp các thông tin về class gắn với object được ghi. Đầu tiên là class Person.


AC ED 00 05 73 72 00 06 50 65 72 73 6F 6E 98 2F A2 3D 66 E6 
D7 9D 02 00 02 4C 00 03 61 67 65 74 00 13 4C 6A 61 76 61 2F 
6C 61 6E 67 2F 49 6E 74 65 67 65 72 3B 4C 00 04 6E 61 6D 65 
74 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 
3B 78 70 73 72 00 11 6A 61 76 61 2E 6C 61 6E 67 2E 49 6E 74 
65 67 65 72 12 E2 A0 A4 F7 81 87 38 02 00 01 49 00 05 76 61 
6C 75 65 78 72 00 10 6A 61 76 61 2E 6C 61 6E 67 2E 4E 75 6D 
62 65 72 86 AC 95 1D 0B 94 E0 8B 02 00 00 78 70 00 00 00 14 
74 00 03 6E 61 6D
  • 0x73: TC_OBJECT. Chỉ ra rằng file số hoá này lưu trữ một Object mới
  • 0x72: TC_CLASSDESC. Chỉ ra rằng bắt đầu từ đây lưu trữ một Class mới
  • 00 06: 6. Độ dài tên class Person
  • 50 65 72 73 6F 6E: Person. Tên của class Person trong bảng mã ASCII.
  • 98 2F A2 3D 66 E6 D7 9D: SerialVersionUID. Serial Identifier của class. Giá trị này dùng để đánh giá object trong File nhị phân có đúng với object của class đó không.Khi deserialize sẽ được chuyển về kiểu dữ liệu Long và được so sánh bằng với giá trị được khai báo trong class. Nếu không khai báo, thuật toán sẽ tự gán một giá trị mặc định cho File nhị phân. Do đó, nếu class bị chỉnh sửa, giá trị mặc định này sẽ thay đổi ở phía class.
  • 0x02: Flags đặc thù. Flag này nói rằng object hỗ trợ số hoá
  • 00 02: 2. Class được số hoá có 2 thuộc tính.
  • 4C: L. TypeCode L trong bảng mã ASCII. Theo tài liệu của Oracle, TypeCode L là Object
  • 00 03: 3. Độ dài tên thuộc tính age
  • 61 67 65: age. Tên của thuộc tính age trong bảng mã ASCII
  • 00 13: Độ dài của String
  • 4C: L. Signature L trong Java Vm Type Signature. Báo hiệu đây là tên class đầy đủ. (fully-qualified-class)
  • 6A 61 76 61 2F 6C 61 6E 67 2F 49 6E 74 65 67 65 72: java/lang/Integer. Tên đầy đủ của Wrapper class gắn với thuộc tính age
  • 3B: Dấu ;
  • 4C: L. TypeCode L trong bảng mã ASCII. Theo tài liệu của Oracle, TypeCode L là Object
  • 6E 61 6D 65: name. Tên thuộc tính name trong bảng mã ASCII
  • 74: TC_STRING. Báo hiệu sắp tới ghi một String
  • 00 12: Độ dài của String
  • 4C: L. Signature L trong Java Vm Type Signature
  • 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67: java/lang/String. Tên đầy đủ của Wrapper class gắn với thuộc tính name
  • 3B: ;. Dấu ;
  • 0x78: TC_ENDBLOCKDATA. Đánh dấu block dữ liệu cuối cùng của class Person
  • 0x70: TC_NULL. Đánh dấu rằng không tìm thấy các class cha nữa.

Sau khi ghi xong class Person, thuật toán bắt đầu ghi các metadata gắn với giá trị của object person. Ở đây bắt đầu ghi metadata của class Integer

AC ED 00 05 73 72 00 06 50 65 72 73 6F 6E 98 2F A2 3D 66 E6 
D7 9D 02 00 02 4C 00 03 61 67 65 74 00 13 4C 6A 61 76 61 2F 
6C 61 6E 67 2F 49 6E 74 65 67 65 72 3B 4C 00 04 6E 61 6D 65 
74 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 
3B 78 70 73 72 00 11 6A 61 76 61 2E 6C 61 6E 67 2E 49 6E 74 
65 67 65 72 12 E2 A0 A4 F7 81 87 38 02 00 01 49 00 05 76 61 
6C 75 65 78 72 00 10 6A 61 76 61 2E 6C 61 6E 67 2E 4E 75 6D 
62 65 72 86 AC 95 1D 0B 94 E0 8B 02 00 00 78 70 00 00 00 14 
74 00 03 6E 61 6D
  • 0x73: TC_OBJECT. Chỉ ra rằng file số hoá này lưu trữ một Object mới
  • 0x72: TC_CLASSDESC. Chỉ ra rằng bắt đầu từ đây lưu trữ một Class mới
  • 00 11: Độ dài của chuỗi.
  • 6A 61 76 61 2E 6C 61 6E 67 2E 49 6E 74 65 67 65 72: java.lang.Integer. Tên đầy đủ của class trong bảng mã ASCII
  • 12 E2 A0 A4 F7 81 87 38: SerialVersionUID.
  • 0x02: Flags đặc thù. Flag này nói rằng object hỗ trợ số hoá
  • 00 01: 1. Class được số hoá có 1 thuộc tính.
  • 49: I. TypeCode I đại diện cho integer
  • 00 05: 5. Độ dài của string value
  • 76 61 6C 75 65: value. Trường value trong bảng mã ASCII
  • 0x78: TC_ENDBLOCKDATA. Đánh dấu block dữ liệu cuối cùng của class Integer

Sau khi ghi xong class Integer, thuật toán bắt đầu ghi các metadata gắn với class cha của class Integer. Ở đây thuật toán ghi các metadata của class Number.


AC ED 00 05 73 72 00 06 50 65 72 73 6F 6E 98 2F A2 3D 66 E6 
D7 9D 02 00 02 4C 00 03 61 67 65 74 00 13 4C 6A 61 76 61 2F 
6C 61 6E 67 2F 49 6E 74 65 67 65 72 3B 4C 00 04 6E 61 6D 65 
74 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 
3B 78 70 73 72 00 11 6A 61 76 61 2E 6C 61 6E 67 2E 49 6E 74 
65 67 65 72 12 E2 A0 A4 F7 81 87 38 02 00 01 49 00 05 76 61 
6C 75 65 78 72 00 10 6A 61 76 61 2E 6C 61 6E 67 2E 4E 75 6D 
62 65 72 86 AC 95 1D 0B 94 E0 8B 02 00 00 78 70 00 00 00 14 
74 00 03 6E 61 6D
  • 0x72: TC_CLASSDESC. Chỉ ra rằng bắt đầu từ đây lưu trữ một Class mới
  • 00 10: Độ dài của chuỗi
  • 6A 61 76 61 2E 6C 61 6E 67 2E 4E 75 6D 62 65 72: java.lang.Number. Tên đầy đủ của class trong bảng mã ASCII
  • 86 AC 95 1D 0B 94 E0 8B: SerialVersionUID.
  • 0x02: Flags đặc thù. Flag này nói rằng object hỗ trợ số hoá
  • 00 00: 0. Class được số hoá không có thuộc tính.
  • 0x78: TC_ENDBLOCKDATA. Đánh dấu block dữ liệu cuối cùng của class Number
  • 0x70: TC_NULL. Đánh dấu rằng không tìm thấy các class cha nữa.

Kết thúc việc ghi các metadata của class gắn với object person. Kể từ đây, class bắt đầu ghi các dữ liệu của object person.


AC ED 00 05 73 72 00 06 50 65 72 73 6F 6E 98 2F A2 3D 66 E6 
D7 9D 02 00 02 4C 00 03 61 67 65 74 00 13 4C 6A 61 76 61 2F 
6C 61 6E 67 2F 49 6E 74 65 67 65 72 3B 4C 00 04 6E 61 6D 65 
74 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 
3B 78 70 73 72 00 11 6A 61 76 61 2E 6C 61 6E 67 2E 49 6E 74 
65 67 65 72 12 E2 A0 A4 F7 81 87 38 02 00 01 49 00 05 76 61 
6C 75 65 78 72 00 10 6A 61 76 61 2E 6C 61 6E 67 2E 4E 75 6D 
62 65 72 86 AC 95 1D 0B 94 E0 8B 02 00 00 78 70 00 00 00 14 
74 00 03 6E 61 6D
  • 00 00 00 14: 20. Giá trị thập phân của thuộc tính age
  • 0x74: TC_STRING. Báo hiệu sắp tới sẽ ghi 1 String
  • 00 03: 3. Độ dài của giá trị nam
  • 6E 61 6D: nam. Giá trị của thuộc tính name

Qua bài viết trên, mình hy vọng có thể mang tới một cái nhìn tổng quan và đơn giản về những gì xảy ra khi Java ghi một đối tượng dưới dạng File nhị phân. Trong phần tiếp theo của series bài viết, mình sẽ viết về những lỗi mình và bạn mình hay gặp phải khi làm việc trên File nhị phân này. Mong các bạn đón đọc.

Bài viết liên quan

Leave a Reply

Your email address will not be published.