Học Java

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

Ở phần trước, mình đã đề cập tới thuật toán số hoá cũng như định dạng một File nhị phân số hoá đối tượng trong Java. Trong bài viết này mình sẽ chia sẻ về những lỗi thường gặp khi thao tác với các File nhị phân này và những hướng khắc phục.

Tình huống 1: Tạo ObjectInputStream trong vòng lặp

Khi mới bắt đầu làm việc với cơ chế số hoá đối tượng, mình có một suy nghĩ mỗi một ObjectInputStream sẽ ghi ra một Object. Do vậy để ghi ra nhiều Object mình phải tạo nhiều ObjectOutputStream. Với lối suy nghĩ đó, mình đã code một đoạn sau:

Class Person

import java.io.Serializable;

public class Person implements Serializable {
   private String name;
   private Integer age;

   public Person(String name, int age) {
       this.name = name;
       this.age = age;
   }

MainOutput

import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class MainOutput {
   public static void main(String[] args) {
       Person person1 = new Person("Nam", 20);
       Person person2 = new Person("Duc", 28);
       Person person3 = new Person("Binh", 30);
       List<Person> personList = new ArrayList<>();
       personList.add(person1);
       personList.add(person2);
       personList.add(person3);

       File file = new File("person.dat");
       if (!file.exists()) {
           try {
               file.createNewFile();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
       try {
           OutputStream out = new FileOutputStream(file, true);
           for (Person person : personList) {
               ObjectOutputStream oos = new ObjectOutputStream(out);
               oos.writeObject(person);
               System.out.println("saved");
           }
       } catch (IOException e) {
           e.printStackTrace();
       }
   }
}

Đoạn code trên chạy vẫn bình thường nhưng khi thực hiện đọc Object từ File, chương trình throw ra một Exception như sau:

Exception trên được quăng ra khi khi dòng đọc bị và File bị corrupt.

Để kiểm tra, mình sẽ đọc File nhị phân person.dat dưới dạng hex như sau:

AC ED 00 05 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 4E 61 6D 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 1C 74 00 03 44 75 63 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 1E 74 00 04 42 69 6E 68

Có thể thấy, giá trị STREAM_MAGIC đã bị ghi tới 3 lần. Giá trị này chỉ được xuất hiện ở những block đầu tiên của File nhị phân. Do đó chương trình quăng ra một Exception như trong source code có đề cập

Để xử lý tình huống này, mình sẽ cài đặt sao cho mỗi lần ghi Stream Header sẽ reset() và không tạo ra giá trị header mới như sau:

OutputStream out = new FileOutputStream(file, true);
ObjectOutputStream oos = file.exists() && file.length() > 0 ? new   ObjectOutputStream(out) {
       @Override
       protected void writeStreamHeader() throws IOException {
          reset();
      }
   } : new ObjectOutputStream(out);
for (Person person : personList) {
      objectOutputStream.writeObject(person);
      System.out.println("saved");
}

Thực thi đoạn code trên, chương trình không throw ra Exception trước nhưng throw ra 1 Exception mới như tình huống dưới đây:

Tình huống thứ 2: Deserialize Object null

Exception trên throw ra khi input stream đã đọc đến cuối File và không tìm thấy Object nào

Đoạn code của Stream Input mình viết với lối suy nghĩ chương trình sẽ đọc lần lượt từng object và dừng lại khi không đọc được object nào nữa (hay đọc ra đối tượng có giá trị null). Vậy tại sao Exception vẫn được throw ra ? Khi kiểm tra mã nguồn của method readObject(), mình nhận thấy rằng kết quả của method này không trả về null. Do đó việc mình áp dụng lối suy nghĩ trên là hoàn toàn sai lầm.

Để khắc phục, mình sẽ sửa lại đoạn code ở phần ObjectInputStream như sau

while(in.available() != 0) {
   person = (Person) ois.readObject();
   personRead.add(person);
}

Chương trình chạy thành công và không quăng ra bất kì Exception nào.

Tuy nhiên, cách trên đây đã bắt buộc phải can thiệp vào Class và ghi đè lại phương thức. Cách này sẽ không hiệu quả và có thể gây lỗi khi sang một hệ thống khác. Để giải khắc phục triệt để, mình sẽ làm như sau:

  1. Đưa Object vào 1 List
  2. Số hoá List trên
  3. Đọc File nhị phân và cast về List.

MainOutput

import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class MainOutput {
   public static void main(String[] args) {
       Person person1 = new Person("Nam", 20);
       Person person2 = new Person("Duc", 28);
       Person person3 = new Person("Binh", 30);
       List<Person> personList = new ArrayList<>();
       personList.add(person1);
       personList.add(person2);
       personList.add(person3);

       File file = new File("personList.dat");
       if (!file.exists()) {
           try {
               file.createNewFile();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
       try {
           OutputStream out = new FileOutputStream(file);
           ObjectOutputStream oos = new ObjectOutputStream(out);
           objectOutputStream.writeObject(personList);
       } catch (IOException e) {
           e.printStackTrace();
       }
   }
}

MainInput

import java.io.*;
import java.util.List;

public class MainInput {
   public static void main(String[] args) {
       File file = new File("personList.dat");
       List<Person> personRead;
       try {
           InputStream in = new FileInputStream(file);
           ObjectInputStream ois = new ObjectInputStream(in);
           personRead = (List<Person>) objectInputStream.readObject();
           System.out.println(personRead.toString());
       } catch (IOException | ClassNotFoundException e) {
           e.printStackTrace();
       }
   }
}

Cách này Code dễ đọc hơn và không cần phải ghi đè các phương thức có sẵn. 

Tình huống 3: Thêm phương thức và thuộc tính cho Class sau khi đã số hoá Object

Vấn đề xảy ra khi mình viết thêm phương thức toString như sau

import java.io.Serializable;

public class Person implements Serializable {
   private String name;
   private Integer age;

   public Person(String name, int age) {
       this.name = name;
       this.age = age;
   }

   @Override
   public String toString() {
       return "Person{" +
               "name='" + name + '\'' +
               ", age=" + age +
               '}';
   }
}

Mình tiến hành đọc File nhị phân để lấy ra các đối tượng theo đoạn code trên. Chương trình quăng ra Exception:

Việc sửa Class gốc đã làm thay đổi giá trị serialVersionUID bên phía Class. Do đó khi đọc File, giá trị serialVersionUID trong File không khớp với giá trị serialVersionUID bên phía Class, dẫn đến Exception trên.

Để giải quyết tình huống này mình sẽ cài đặt giá trị serialVersionUID trong Class Person như sau:

private static final long serialVersionUID = 1L;

Việc cài đặt này giúp cho serialVersionUID ở phía Class và trong File nhị phân luôn đồng nhất kể cả khi Class bị chỉnh sửa. Tuy nhiên việc này cũng làm cho không kiểm soát được những thay đổi dẫn đến việc thất thoát dữ liệu.

Trên đây là những tình huống phát sinh lỗi mình thường gặp khi xử lý các File nhị phân. Hy vọng qua bài viết này, mình có thể giúp các bạn hiểu rõ được cách thức hoạt động của các phương thức đọc và ghi đối tượng, cũng như giải quyết được các lỗi phát sinh khi bắt gặp những tình huống trên.

Bài viết liên quan

Leave a Reply

Your email address will not be published.