C, PHP, VB, .NET

Дневникът на Филип Петров


* Сериализация на обекти

Публикувано на 05 декември 2015 в раздел ПИК3 Java.

Java ни предлага механизъм, наречен сериализация на обекти. При него даден обект се представя като бинарна поредица съдържаща описание на класа на обекта и неговите конкретни данни (член-променливи). Така получената бинарна поредица може да бъде изпращана чрез ObjectOutputStream - например да бъде записана във файл, да бъде изпратена по мрежата до друг компютър и т.н. Четенето на обекта се осъществява чрез прозцес наречен "десериализация" - при него обекта се възстановява в паметта в оригиналния си вид. Извършва се чрез ObjectInputStream. Най-важното за процеса на сериализация-десериализация е, че те са платформено независими, т.е. няма проблем да е сериализиран на един компютър и да бъде десериализиран на друг, който е със съвсем различни характеристики (дори и различна Java виртуална машина).

ObjectOutputStream обхваща почти всички функционалности, които предлага DataOutputStream. С изключение на метод size(), който при ObjectOutputStream липсва, той притежава същите методи: void close(), void flush(), void write(byte[] b, int off, int len), void write(int b), void writeBoolean(boolean v), void writeByte(int v), void writeBytes(String s), void writeChar(int v), void writeChars(String s), void writeDouble(double v), void writeFloat(float v), void writeInt(int v), void writeLong(long v), void writeShort(int v) и void writeUTF(String str). Това дублиране е нормално, тъй като и ObjectOutputStream и DataOutputStream имплементират един и същи интерфейс - DataOutput. Допълнителните методи, които ObjectOutputStream добавя са няколко, но най-съществения от тях е void writeObject(Object obj). Чрез него даден обект се записва в потока. Именно той извършва процеса "сериализация".

Положението с ObjectInputStream е същото - той притежава методите на DataInputStream, но допълнително добавя няколко метода, най-съществения от които е Object readObject(). Чрез този метод ще бъде прочетен поредния обект от входния поток и ще бъде десериализиран. Забележете, че обекта ще бъде върнат като тип Object - ще трябва да го трансформирате до типа, който ви интересува, ръчно.

За да може даден обект да бъде сериализируем, неговия клас трябва да имплементира интерфейс java.io.Serializable. Този интерфейс не ви задължава да добавяте никакви допълнителни методи. Единственото изискване е да добавите следната член-променлива:

  • private static final long serialVersionUID = 1L

Не е задължително да имате private модификатор за достъп, но рядко има смисъл тази член-променлива да е видима. Задължително е да е статична и да е константа. Колкото до стойността ѝ - може да дефинирате каквото число си пожелаете. Важно условие е тогава, когато обекта ще бъде десериализиран, стойността да съвпада. С други думи ако вие запишете обект с например тази версия 1L, а по-късно промените класа на версия 2L, новата версия няма да работи със стария обект, та дори промените да са само козметични (или дори никакви) - грешката ще бъде "local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2".

Нека демонстрираме казаното дотук с прост пример. Създаваме обект от тип Person, записваме го във файл и после го прочитаме обратно:

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;

public class Tester {
  public static void main(String[] args) {
    Person p = new Person("Ivan", 18);
    
    // Сериализираме и записваме във файл
    try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("test.ser"))){
      out.writeObject(p);
      out.flush();
      out.close();
    }
    catch(IOException e){
      System.out.println("Проблем при сериализацията "+e.getMessage());
      return;
    }
    
    // Прочитаме, десериализираме обекта и го възстановяваме
    try(ObjectInputStream in = new ObjectInputStream(new FileInputStream("test.ser"))){
      Object o = in.readObject();
      if(!(o instanceof Person)){
        System.out.println("Прочетения обект не е Person");
        return;
      }
      Person restored = (Person)o;
      restored.print();
      in.close();
    }
    catch(IOException e1){
      System.out.println("Проблем при десериализацията "+e1.getMessage());
      return;
    }
    catch(ClassNotFoundException e2){
      System.out.println("Прочетен е непознат обект "+e2.getMessage());
      return;      
    }
  }
}

class Person implements Serializable{
  private static final long serialVersionUID = 1L;
  String name;
  int age;
  Person(String name, int age){
    this.name = name;
    this.age = age;
  }
  void print(){
    System.out.println(this.name+" is "+this.age+" old");
  }
}

Когато разработвате клиент-сървър приложения, неминуемо ще се сблъскате с един проблем - обектите, които се сериализират и десериализират при пренасяне от един хост на друг, трябва освен да са идентични като функционалност и версия, да са също така от един и същи пакет (package). Не е невъзможно сървърното и клиентското приолжение да работят в различни package с едно и също име (все пак сървъра работи на една машина, а клиента на съвсем друга). Ако трябва и двете да се използват на една машина, както е в developer варианта, при който работим с връзка по localhost, може просто да ги сложите на различни места във файловата структура (например ако пакета се казва "mypackage", сървъра да е в директория "./server/mypackage", а клиента в "./client/mypackage"). При такъв вариант не трябва да слагате едновременно сървъра и клиента в ClassPath. Най-добре е обаче да изолирате сериализируемите класове в свой собствен отделен пакет, да го пакетирате в Jar файл и да го включвате като допълнителна библиотека веднъж в сървърното и веднъж в клиентското прилжение.

Ще демонстрираме това подробно с пример. Ще направим клиент-сървър приложение, което изпраща дефинирания по-горе клас Person от един хост на друг. Ще направим демонстрацията с DrJava, но по аналогичен начин ще се получи с всяка друга среда.

Първо искаме да създадем самостоятелен Project за клас Person.

1

Задайте му име "MyDemoClientServerLib". Запишете го в някоя директория - в моя пример ще е в директория "demo" на Desktop. Вътре в тази директория създайте поддиректория MyDemoClientServerLib. Нея трябва да я зададете като ProjectRoot и Working Directory. Сладващият диалогов прозорец ще изглежда подобно на това:

2

Натиснете OK. Отидете на File > New Java Class и добавете клас с име "Person".

3

Заменете автоматично генерираното съдържание със следното:

package MyDemoClientServerLib;
import java.io.Serializable;

public class Person implements Serializable{
   private static final long serialVersionUID = 1L;
   String name;
   int age;
   public Person(String name, int age){
      this.name = name;
      this.age = age;
   }
   public void print(){
      System.out.println(this.name+" is "+this.age+" old");
   }
}

Запаметете файла в директория MyDemoClientServerLib. Компилирайте проекта. След това отидете на Project > Create Jar File From Porject:

 

4

и изберете име за Jar файла MyDemoClientServerLib.jar:

5

Готово - нашата библиотека с въпросния клас Person е съхранена (в примера във файлче на моя Desktop). Jar файловете са просто Zip архиви, съдържащи класовете на проекта и допълнителен manifest файл, в който може да се добавят допълнителни характеристики да дадената библиотека или приложение.

Сега затворете този проект и по аналогичен начин създайте нов проект, който ще бъде с име MyDemoServer. При създаването на проекта, добавете вече създадения Jar файл в полето "Extra ClassPath". За удобство преди това можете да го копирате директно в директорията на проекта:

6

След това в проекта добавете клас MyDemoServer, който да има следното съдържание:

package MyDemoServer;
import MyDemoClientServerLib.Person;
import java.io.ObjectInputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.ServerSocket;

public class MyDemoServer{
  public static void main(String[] args){
    ServerSocket ss = null;
    try{
      ss = new ServerSocket(5000);
    }
    catch(IOException e){
      System.out.println("Не мога да стартирам ServerSocket");
      return;
    }
    System.out.println("Сървърът е стартиран");
    
    try(Socket sock = ss.accept()){
      ObjectInputStream in = new ObjectInputStream(sock.getInputStream());
      Object o = in.readObject();
      if(!(o instanceof Person)){
        System.out.println("Получих грешен обект");
        return;
      }
      Person p = (Person)o;
      p.print();
      in.close();
      sock.close();
    }
    catch(IOException e){
      System.out.println("Проблем с получаването на данни");
      return;
    }
    catch(ClassNotFoundException e2){
      System.out.println("Изпратиха ни непознат обект");
      return;      
    }
  }  
}

Компирилайте проекта и стартирайте сървъра. Отворете нова инстанция на DrJava и по абсолютно същият начин създайте нов проект с име MyDemoClient, добавете в него MyClientServerLib.jar и създайте клас MyDemoClient.java със следното съдържание:

package MyDemoClient;

import MyDemoClientServerLib.Person;
import java.io.ObjectOutputStream;
import java.io.IOException;
import java.net.Socket;

public class MyDemoClient{
  public static void main(String[] args){
    Person p = new Person("Ivan", 18);

    System.out.println("Свързвам се...");
    try(Socket sock = new Socket("localhost", 5000)){
      ObjectOutputStream out = new ObjectOutputStream(sock.getOutputStream());
      out.writeObject(p);
      out.flush();
      out.close();
      sock.close();
      System.out.println("Данните са изпратени");
    }
    catch(IOException e){
      System.out.println("Проблем с изпращането на данни");
      return;
    }
  }
}

С това примерът е завършен. Ако искате, можете допълнително да пакетирате сървъра и клиента като самостоятелни Jar файлове. В Project Properties задайте MyDemoClient.MyDemoClient като Main клас за клиента и аналогично MyDemoServer.MyDemoServer като Main клас за сървъра. След това и на двата проекта дайте Project > Create Jar File from Project. Отбележете, че са "Executable" и ги създайте - например като Client.jar и Server.jar. Ще можете да стартирате програмите под Command Prompt на Windows по следния начин:

  • java -jar Server.jar
  • java -jar Client.jar

 



Добави коментар

Адресът на електронната поща няма да се публикува


*