공개 된 자바 직렬화 알고리즘

직렬화 는 객체의 상태를 일련의 바이트로 저장하는 프로세스입니다. 역 직렬화 는 해당 바이트를 라이브 개체로 재 구축하는 프로세스입니다. Java Serialization API는 개발자가 객체 직렬화를 처리 할 수있는 표준 메커니즘을 제공합니다. 이 팁에서는 객체를 직렬화하는 방법과 직렬화가 때때로 필요한 이유를 알아 봅니다. Java에서 사용되는 직렬화 알고리즘에 대해 배우고 객체의 직렬화 된 형식을 보여주는 예제를 볼 수 있습니다. 완료 할 때까지 직렬화 알고리즘이 작동하는 방식과 하위 수준에서 개체의 일부로 직렬화되는 엔티티에 대한 확실한 지식을 가지고 있어야합니다.

직렬화가 필요한 이유는 무엇입니까?

오늘날의 세계에서 일반적인 엔터프라이즈 애플리케이션은 여러 구성 요소를 가지며 다양한 시스템과 네트워크에 분산됩니다. Java에서는 모든 것이 객체로 표현됩니다. 두 Java 구성 요소가 서로 통신하려면 데이터를 교환하는 메커니즘이 필요합니다. 이를 달성하는 한 가지 방법은 자신의 프로토콜을 정의하고 객체를 전송하는 것입니다. 즉,받는 쪽은 보낸 사람이 개체를 다시 만드는 데 사용하는 프로토콜을 알아야하므로 타사 구성 요소와 통신하기가 매우 어렵습니다. 따라서 구성 요소간에 개체를 전송하려면 일반적이고 효율적인 프로토콜이 필요합니다. 이를 위해 직렬화가 정의되며 Java 구성 요소는이 프로토콜을 사용하여 객체를 전송합니다.

그림 1은 클라이언트 / 서버 통신에 대한 상위 수준보기를 보여줍니다. 여기서 객체는 직렬화를 통해 클라이언트에서 서버로 전송됩니다.

그림 1. 실행중인 직렬화의 상위 레벨보기 (확대하려면 클릭)

객체를 직렬화하는 방법

객체를 직렬화하려면 java.io.Serializable목록 1에 표시된대로 객체의 클래스가 인터페이스를 구현하는지 확인해야합니다 .

목록 1. 직렬화 가능 구현

 import java.io.Serializable; class TestSerial implements Serializable { public byte version = 100; public byte count = 0; } 

Listing 1에서 일반 클래스를 생성하는 것과 다르게해야 할 일은 java.io.Serializable인터페이스를 구현하는 것뿐입니다 . Serializable인터페이스 마커 인터페이스이고; 메서드를 전혀 선언하지 않습니다. 직렬화 메커니즘에 클래스를 직렬화 할 수 있음을 알려줍니다.

이제 클래스를 직렬화에 적합하게 만들었으므로 다음 단계는 실제로 객체를 직렬화하는 것입니다. 이는 목록 2에 표시된대로 클래스 의 writeObject()메소드를 호출하여 수행됩니다 java.io.ObjectOutputStream.

목록 2. writeObject () 호출

 public static void main(String args[]) throws IOException { FileOutputStream fos = new FileOutputStream("temp.out"); ObjectOutputStream oos = new ObjectOutputStream(fos); TestSerial ts = new TestSerial(); oos.writeObject(ts); oos.flush(); oos.close(); } 

목록 2는 TestSerial라는 파일에 객체 의 상태를 저장합니다 temp.out. oos.writeObject(ts);실제로 직렬화 알고리즘을 시작하여 객체를에 씁니다 temp.out.

영구 파일에서 객체를 다시 생성하려면 목록 3의 코드를 사용합니다.

Listing 3. 직렬화 된 객체 다시 만들기

 public static void main(String args[]) throws IOException { FileInputStream fis = new FileInputStream("temp.out"); ObjectInputStream oin = new ObjectInputStream(fis); TestSerial ts = (TestSerial) oin.readObject(); System.out.println("version="+ts.version); } 

목록 3에서 객체의 복원은 oin.readObject()메소드 호출로 발생합니다 . 이 메서드 호출은 이전에 유지 한 원시 바이트를 읽고 원래 개체 그래프의 정확한 복제 본인 라이브 개체를 만듭니다. 때문에 readObject()직렬화 가능 객체를 읽을 수있는 올바른 유형 캐스트가 필요합니다.

이 코드를 실행 version=100하면 표준 출력에 인쇄 됩니다.

객체의 직렬화 된 형식

객체의 직렬화 된 버전은 어떻게 생겼습니까? 이전 섹션의 샘플 코드는 TestSerial객체 의 직렬화 된 버전을 파일에 저장했습니다 temp.out. 목록 4는 temp.out16 진수로 표시된 의 내용을 보여줍니다 . (16 진수 형식으로 출력을 보려면 16 진수 편집기가 필요합니다.)

Listing 4. 16 진 형식의 TestSerial

 AC ED 00 05 73 72 00 0A 53 65 72 69 61 6C 54 65 73 74 A0 0C 34 00 FE B1 DD F9 02 00 02 42 00 05 63 6F 75 6E 74 42 00 07 76 65 72 73 69 6F 6E 78 70 00 64 

실제 TestSerial객체를 다시 살펴보면 목록 5에 표시된 것처럼 두 바이트 멤버 만 있음을 알 수 있습니다.

Listing 5. TestSerial의 바이트 멤버

 public byte version = 100; public byte count = 0; 

바이트 변수의 크기는 1 바이트이므로 헤더가없는 개체의 총 크기는 2 바이트입니다. 그러나 목록 4에서 직렬화 된 객체의 크기를 보면 51 바이트가 표시됩니다. 놀라다! 여분의 바이트는 어디에서 왔으며 그 의미는 무엇입니까? 직렬화 알고리즘에 의해 도입되며 개체를 다시 만드는 데 필요합니다. 다음 섹션에서는이 알고리즘을 자세히 살펴 보겠습니다.

자바의 직렬화 알고리즘

이제 객체를 직렬화하는 방법에 대해 꽤 잘 알고 있어야합니다. 그러나 프로세스는 어떻게 작동합니까? 일반적으로 직렬화 알고리즘은 다음을 수행합니다.

  • 인스턴스와 관련된 클래스의 메타 데이터를 작성합니다.
  • 를 찾을 때까지 수퍼 클래스에 대한 설명을 재귀 적으로 작성합니다 java.lang.object.
  • 메타 데이터 정보 쓰기가 완료되면 인스턴스와 연결된 실제 데이터로 시작됩니다. 그러나 이번에는 최상위 수퍼 클래스에서 시작합니다.
  • 인스턴스와 관련된 데이터를 재귀 적으로 작성합니다. 가장 적은 수퍼 클래스부터 가장 많이 파생 된 클래스까지입니다.

가능한 모든 경우를 다루는이 섹션에 대한 다른 예제 개체를 작성했습니다. 직렬화 할 새 샘플 객체는 목록 6에 나와 있습니다.

Listing 6. 직렬화 된 샘플 객체

 class parent implements Serializable { int parentVersion = 10; } class contain implements Serializable{ int containVersion = 11; } public class SerialTest extends parent implements Serializable { int version = 66; contain con = new contain(); public int getVersion() { return version; } public static void main(String args[]) throws IOException { FileOutputStream fos = new FileOutputStream("temp.out"); ObjectOutputStream oos = new ObjectOutputStream(fos); SerialTest st = new SerialTest(); oos.writeObject(st); oos.flush(); oos.close(); } } 

This example is a straightforward one. It serializes an object of type SerialTest, which is derived from parent and has a container object, contain. The serialized format of this object is shown in Listing 7.

Listing 7. Serialized form of sample object

 AC ED 00 05 73 72 00 0A 53 65 72 69 61 6C 54 65 73 74 05 52 81 5A AC 66 02 F6 02 00 02 49 00 07 76 65 72 73 69 6F 6E 4C 00 03 63 6F 6E 74 00 09 4C 63 6F 6E 74 61 69 6E 3B 78 72 00 06 70 61 72 65 6E 74 0E DB D2 BD 85 EE 63 7A 02 00 01 49 00 0D 70 61 72 65 6E 74 56 65 72 73 69 6F 6E 78 70 00 00 00 0A 00 00 00 42 73 72 00 07 63 6F 6E 74 61 69 6E FC BB E6 0E FB CB 60 C7 02 00 01 49 00 0E 63 6F 6E 74 61 69 6E 56 65 72 73 69 6F 6E 78 70 00 00 00 0B 

Figure 2 offers a high-level look at the serialization algorithm for this scenario.

Figure 2. An outline of the serialization algorithm

Let's go through the serialized format of the object in detail and see what each byte represents. Begin with the serialization protocol information:

  • AC ED: STREAM_MAGIC. Specifies that this is a serialization protocol.
  • 00 05: STREAM_VERSION. The serialization version.
  • 0x73: TC_OBJECT. Specifies that this is a new Object.

The first step of the serialization algorithm is to write the description of the class associated with an instance. The example serializes an object of type SerialTest, so the algorithm starts by writing the description of the SerialTest class.

  • 0x72: TC_CLASSDESC. Specifies that this is a new class.
  • 00 0A: Length of the class name.
  • 53 65 72 69 61 6c 54 65 73 74: SerialTest, the name of the class.
  • 05 52 81 5A AC 66 02 F6: SerialVersionUID, the serial version identifier of this class.
  • 0x02: Various flags. This particular flag says that the object supports serialization.
  • 00 02: Number of fields in this class.

Next, the algorithm writes the field int version = 66;.

  • 0x49: Field type code. 49 represents "I", which stands for Int.
  • 00 07: Length of the field name.
  • 76 65 72 73 69 6F 6E: version, the name of the field.

And then the algorithm writes the next field, contain con = new contain();. This is an object, so it will write the canonical JVM signature of this field.

  • 0x74: TC_STRING. Represents a new string.
  • 00 09: Length of the string.
  • 4C 63 6F 6E 74 61 69 6E 3B: Lcontain;, the canonical JVM signature.
  • 0x78: TC_ENDBLOCKDATA, the end of the optional block data for an object.

The next step of the algorithm is to write the description of the parent class, which is the immediate superclass of SerialTest.

  • 0x72: TC_CLASSDESC. Specifies that this is a new class.
  • 00 06: Length of the class name.
  • 70 61 72 65 6E 74: SerialTest, the name of the class
  • 0E DB D2 BD 85 EE 63 7A: SerialVersionUID, the serial version identifier of this class.
  • 0x02: Various flags. This flag notes that the object supports serialization.
  • 00 01: Number of fields in this class.

Now the algorithm will write the field description for the parent class. parent has one field, int parentVersion = 100;.

  • 0x49: Field type code. 49 represents "I", which stands for Int.
  • 00 0D: Length of the field name.
  • 70 61 72 65 6E 74 56 65 72 73 69 6F 6E: parentVersion, the name of the field.
  • 0x78: TC_ENDBLOCKDATA, the end of block data for this object.
  • 0x70: TC_NULL, which represents the fact that there are no more superclasses because we have reached the top of the class hierarchy.

So far, the serialization algorithm has written the description of the class associated with the instance and all its superclasses. Next, it will write the actual data associated with the instance. It writes the parent class members first:

  • 00 00 00 0A: 10, the value of parentVersion.

Then it moves on to SerialTest.

  • 00 00 00 42: 66, the value of version.

The next few bytes are interesting. The algorithm needs to write the information about the contain object, shown in Listing 8.

Listing 8. The contain object

 contain con = new contain(); 

Remember, the serialization algorithm hasn't written the class description for the contain class yet. This is the opportunity to write this description.

  • 0x73: TC_OBJECT, designating a new object.
  • 0x72: TC_CLASSDESC.
  • 00 07: Length of the class name.
  • 63 6F 6E 74 61 69 6E: contain, the name of the class.
  • FC BB E6 0E FB CB 60 C7: SerialVersionUID, the serial version identifier of this class.
  • 0x02: Various flags. This flag indicates that this class supports serialization.
  • 00 01: Number of fields in this class.

Next, the algorithm must write the description for contain's only field, int containVersion = 11;.

  • 0x49: Field type code. 49 represents "I", which stands for Int.
  • 00 0E: Length of the field name.
  • 63 6F 6E 74 61 69 6E 56 65 72 73 69 6F 6E: containVersion, the name of the field.
  • 0x78: TC_ENDBLOCKDATA.

Next, the serialization algorithm checks to see if contain has any parent classes. If it did, the algorithm would start writing that class; but in this case there is no superclass for contain, so the algorithm writes TC_NULL.

  • 0x70: TC_NULL.

Finally, the algorithm writes the actual data associated with contain.

  • 00 00 00 0B: 11, the value of containVersion.

Conclusion

In this tip, you have seen how to serialize an object, and learned how the serialization algorithm works in detail. I hope this article gives you more detail on what happens when you actually serialize an object.

About the author

Sathiskumar Palaniappan은 IT 업계에서 4 년 이상의 경험을 보유하고 있으며 3 년 이상 Java 관련 기술로 작업 해 왔습니다. 현재 그는 IBM Labs의 Java Technology Center에서 시스템 소프트웨어 엔지니어로 일하고 있습니다. 그는 또한 통신 업계에서 경험이 있습니다.

자원

  • Java 객체 직렬화 사양을 읽으십시오. (사양은 PDF입니다.)
  • "개체 정리 : Java 직렬화 API의 비밀 발견"(Todd M. Greanier, JavaWorld, 2000 년 7 월)은 직렬화 프로세스의 기본 사항을 살펴 봅니다.
  • Java RMI의 10 장 (William Grosso, O'Reilly, 2001 년 10 월)도 유용한 참고 자료입니다.

이 이야기 "The Java serialization algorithm reveal"는 원래 JavaWorld에 의해 출판되었습니다.