السلام عليكم و رحمة الله و بركاته
الفهرس :
- ما هي عملية الـ Serialization ؟
- كيف نقوم بها ؟
- مثال تطبيقي
- متى نستخدم transient ؟
- الواجهة البديلة Externalizable
- اختبر قدراتك في الـ Serialization
1. ما هي عملية الـ Serialization ؟
الـ Serialization عبارة عن آلية تسمح بحفظ الكائنات في storage medium أي وسيلة تخزين مثل الملفات أو ذاكرات التخزين المؤقت (memory buffer) كما تُمكن أيضا من نقل الكائنات عبر شبكات الاتصال, تم دعمها في الجافا ابتداء من النسخة 1.1 من الـ JDK, تسمح هذه الآلية بالحفاظ على الـ Object Persistence حيث تُنقل الكائنات عبر binary support على شكل مجموعة من الـ bytes أو على شكل قابل للقراءة مثل ملفات XML. الـ format المستخدم لحفظ الكائنات لا علاقة له بنظام التشغيل حيث يُمكن إعادة بناء الكائن (تُسمى هذه العملية بـ deserialization) في نظام تشغيل مختلف عن الذي تمت فيه عملية الـ serialization.
أثناء هذه العملية, يُمكن تخزين الكائنات على القرص الصلب أو في قاعدة بيانات كما يُمكن أيضاً إعادة بنائها في جهاز آخر في الشبكة حيث تتم إعادة إنشائها باستخدام الـ JVM الموجودة في الجهاز المستقبِل. (كما تفعل RMI مثلا)
في هذه المقالة, سنشرح كيفية عمل الـ binary serialization مع العلم أنه يُمكن عمل الـ XML serialization في الجافا باستخدام الفئتين XMLEncoder و XMLDecoder حيث يتم تخزين البيانات بصيغة XML.
2. كيف نقوم بالـ Serialization
تعتمد الـ serialization على الـ streams لذا يجب أن تكون لديك معرفة مُسبقة بمعظم الفئات الموجودة في الحزمة java.io و كيفية التعامل معها. تُوفر الـ Java API الأدوات اللازمة لعمل الـ serialization و هي :
- الواجهة Serializable
- الفئتين ObjectOutputStream و ObjectInputStream
الواجهة Serializable تسمح بتحديد الفئة التي نُريد عمل serialization لها, الفئتين ObjectOutputStream و ObjectInputStream يُمكّنان (على التوالي) من تحديد كيف ستتم عملية الــ serialization و الـ deserialization.
2.1 – الواجهة Serializable
لعمل serialization لكائن من فئة معينة, يجب على هذه الفئة أن تقوم بعمل implements للواجهة Serializable أو ترث فئة قامت مُسبقاً بعمل implements للواجهة Serializable.
لا تحتوي الواجهة Serializable على أي دالة حيث يقتصر دورها على تحديد فئة معينة كــ serializable.
يتم عمل الـ serialization لكافة حقول الكائن باستثناء تلك الغير serializable أو تلك التي تم الإعلان عنها باستخدام الكلمة static أو transient.
ملاحظات :
- جميع الأنواع الأساسية (مثل int, long, float, …) يُمكن عمل serialization لها.
- ليست كل الكائنات serializable فقد تكون مرتبطة بنظام التشغيل أو مرتبطة بسياق التنفيذ في الذاكرة مثل الـ Threads.
- أصبحت File فئة serializable في النسخ الحديثة من جافا.
- إذا حاولت عمل serialization لكائن من فئة لم تقم بعمل implements للواجهة Serializable, سيتم إصدار استثناء من نوع NotSerializableException.
- استخدم الأداة Serialver من سطر الأوامر لتحديد هل الفئة تدعم الـ serialization أم لا, أمثلة :
serialver java.awt.Graphics Class java.awt.Graphics is not Serializable. ---- serialver java.io.File java.io.File: static final long serialVersionUID = 301077366599181567L;
- الـ serialVersionUID عبارة عن رقم النسخة حيث يوجد في كل فئة قامت بعمل implements للواجهة Serializable, الهدف منه هو ضمان تطابق الفئة عند عمل الـ deserialization, وإذا تغيرت الفئة بين عمليتي الـ serialization و deserialization, يتم إصدار استثناء من نوع InvalidClassException.
- كل فئة serializable يجب أن تُعلن بشكل صريح عن المتغير serialVersionUID حيث يجب أن يكون final, static و من نوع long أيضاً, مثال :
private static final long serialVersionUID = 42L;
- إذا لم تقم الفئة بالإعلان عن الحقل serialVersionUID, تتولى آلية الـ serialization توليد رقم تسلسلي للفئة بشكل تلقائي.
2.2 – الفئتان ObjectOutputStream و ObjectInputStream
ورثت الواجهتان ObjectOutput و ObjectInput خصائص الواجهتين DataOutput و DataInput و أضافتا إمكانية كتابة/قراءة الكائنات, تحتوي كل من DataOutput و DataInput على دوال تُمكن من كتابة/قراءة الأنواع الأولية (مثل char, long, double, ..) حيث أضافت ObjectOutput و ObjectInput إمكانية كتابة/قراءة المصفوفات و الكائنات. قامت كل من ObjectOutputStream و ObjectInputStream على التوالي بعمل implements لـ ObjectOutput|ObjectInput.
كلا الفئتين ObjectOutputStream و ObjectInputStream عبارة عن stream object, حيث يسمحان بعمل serialization/deserialization لكائن معين, على التوالي باستخدام إحدى الدالتين writeObject/readObject.
3. مثال تطبيقي
نبدأ مع الفئة التي سيتم عمل serialization لها :
public class User implements java.io.Serializable { private String login; private String password; public User() { this("anonymous", ""); } public User(String login, String password) { this.login = login; this.password = password; } public String getLogin() { return login; } public void setLogin(String login) { this.login = login; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "User -> Login : " + this.getLogin() + "\t Password : " + (password.equals("") ? "(nothing)" : this.getPassword()); } public static void main(String[] args) { User user = new User("Snack3r", "#_#Sn@CkeRC00L"); System.out.println(user.toString()); User anonym = new User(); System.out.println(anonym.toString()); } }
و هذه الفئة المسئولة عن الـ Serialization :
import java.io.*; public class Serialization { public File serializeUser(User u) { try { System.out.println("Création de : " + u.toString()); File fichier = new File("C:\\Users\\Snacker\\Desktop\\user.ser"); FileOutputStream fos = new FileOutputStream(fichier); ObjectOutputStream oos = new ObjectOutputStream(fos); try { oos.writeObject(u); oos.flush(); System.out.println(u + " a été sérialisé"); return fichier; } finally { try { oos.close(); } finally { fos.close(); } } } catch (IOException ioe) { System.err.println("Une exception de type Input/Output est declanchée"); return null; } } public static void main(String... args) { User utilisateur = new User("Mohamed AHMED", "123456"); new Serialization().serializeUser(utilisateur); } }
تستقبل الدالة serializeUser كائناً من نوع User و تُعيد كائناً من نوع File, يُمثل الملف الذي تمت فيه عملية الـ serialization.
في البداية, قمنا بإنشاء الملف الذي سيحوي الـ serialized object (في الحقيقة, الملف سيُخزن فقط حقول الكائن) و باستخدام الفئة FileOutputStream, قمنا بإرفاق stream writing للكائن fichier, ثم قمنا بكتابة بيانات المستخدم u داخل كائن الـ output stream باستخدام الدالة writeObject لنقوم بعد ذلك بإفراغ محتوى الـ oos باستخدام الدالة flush.
مُخرجات الكود :
Création de : User -> Login : Mohamed AHMED Password : 123456 User -> Login : Mohamed AHMED Password : 123456 a été sérialisé
نأتي الآن إلى الفئة المسئولة عن الـ Deserialization :
import java.io.FileInputStream; import java.io.IOException; import java.io.ObjectInputStream; public class Deserialization { public void deserializeUser(String chemin) { User u = null; try { FileInputStream fis = new FileInputStream(chemin); ObjectInputStream ois = new ObjectInputStream(fis); u = (User) ois.readObject(); System.out.println("~OoO~ -------------------------------------- ~OoO~"); System.out.println("Les données de l'objet qui était sérialisé sont :"); System.out.println(u.toString()); System.out.println("~OoO~ -------------------------------------- ~OoO~\n"); } catch (IOException ioe) { System.err.println("Une exception de type Input/Output est déclanchée"); } catch (ClassNotFoundException cnfe) { System.err.println("Une exception de type ClassNotFoundException est déclanchée"); } if (u != null) { System.out.println("[ " + u + " ] a été dé-sérialisé"); } } public static void main(String... args) { new Deserialization().deserializeUser("C:\\Users\\Snacker\\Desktop\\user.ser"); } }
في البداية, قمنا بإنشاء كائن من FileInputStream يُمثل stream reading للملف الموجود في المسار chemin ثم قمنا بالإعلان عن كائن من ObjectInputStream من أجل إعادة بناء الكائن u باستخدام الدالة readObject. لاحظ أننا قمنا بعمل casting للحصول على النوع الحقيقي للـ serialized object. بطبيعة الحال, يجب أن تتم الـ deserialization بنفس الفئة التي تمت بها عملية الـ serialization. (بغض النظر عن المفاهيم البرمجية, يُمكننا استنتاج هذه الملاحظة اعتماداً على المنطق السليم).
يتم إصدار استثناء من نوع StreamCorruptedException إذا تم تغيير محتوى الملف من خلال editor مثلا. يُمكن أيضاً إصدار استثناء من نوع ClassNotFoundException إذا تم تحويل الكائن إلى فئة لم يتم العثور عليها أثناء التنفيذ.
مُخرجات الكود :
~OoO~ -------------------------------------- ~OoO~ Les données de l'objet qui était sérialisé sont : User -> Login : Mohamed AHMED Password : 123456 ~OoO~ -------------------------------------- ~OoO~ [ User -> Login : Mohamed AHMED Password : 123456 ] a été dé-sérialisé
4. متى نستخدم transient ؟
يُمكن رؤية محتوى المتغيرات من خلال الـ stream الذي تمت عملية الـ serialization بداخله و بالتالي يُمكن لأي شخص يستطيع الوصول إلى الـ stream, رؤية محتوى مختلف المتغيرات حتى لو كانت private و هذا يُمكن أن يُؤدي إلى وجود العديد من المشاكل التي تُهدد حماية البيانات, خصوصاً عند نقل معلومات حساسة عن طريق الشبكة. لهذا السبب قامت Java بتوفير الكلمة المحجوز transient التي تعني أن المتغير المرافق لها ينبغي عدم إدراجه في عملية الـ serialization و بالتالي الـ deserialization.
للتأكد, يُمكنك تجربة transient مع المتغير الخاص بكلمة المرور في الفئة User.
أثناء عملية الـ deserialization يتم إسناد القيمة null للمتغيرات التي تم الإعلان عنها باستخدام transient و هذا جيد من ناحية لكن في نفس الوقت قد يؤثر على عمل الدوال التي تستخدم تلك المتغيرات حيث سيتم إصدار استثناء من نوع NullPointerException.
5. الواجهة البديلة Externalizable
رأينا سابقاً كيفية عمل serialization من خلال كتابة كائن بمجمله في stream و إعادة بنائه من جديد. قد نحتاج في بعض الأحيان إلى عمل الـ serialization لمتغيرات معدودة لكائن معين لذا توفر جافا الواجهة البديلة Externalizable التي تُمكن من السيطرة الكاملة على عملية الـ serialization. (مع العلم أن الواجهة Externalizable قامت بعمل implements للواجهة Serializable)
تحتوي الواجهة البديلة على دالتين هما writeExternal و readExternal, من خلال إعادة تعريفهما سنحدد المتغيرات التي ستدخل في عملية الـ serialization.
وفقاً للحالة الافتراضية للواجهة Externalizable, لا تأخذ عملية الـ serialization في الاعتبار أي من متغيرات الكائن لذا لا فائدة من استخدام transient في هذه الحالة.
عند إعادة تعريف الدالة readExternal ستحتاج إلى استخدام دوال DataInput للأنواع الأولية (مثل char, long, double, ..) و readObject للكائنات (مثل String, Object, Arrays ..). نفس الشيء بالنسبة للدالة writeObject و الواجهة DataOutput مع writeExternal.
لنأخذ الفئة User كمثال :
import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; public class User implements Externalizable { private int id; private String login; private String password; public User() { this(-1, "anonymous", ""); } public User(int id, String login, String password) { this.id = id; this.login = login; this.password = password; } public int getID() { return id; } public void setID(int id) { this.id = id; } public String getLogin() { return login; } public void setLogin(String login) { this.login = login; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "User -> Login : " + this.getLogin() + "\t Password : " + (password.equals("") ? "(nothing)" : this.getPassword()); } @Override public void writeExternal(ObjectOutput e) { try { System.out.println("-> starting writing..."); e.writeInt(this.getID()); e.writeObject(this.getLogin()); e.writeObject(this.getPassword()); e.flush(); e.close(); System.out.println("-> finishing writing !\n"); } catch (IOException ioe) { System.out.println("Erreur reading..."); } } @Override public void readExternal(ObjectInput e) { try { System.out.println("-> starting reading..."); System.out.println("ID : " + e.readInt()); System.out.println("Login : " + e.readObject()); System.out.println("Password : " + e.readObject()); System.out.println("-> finishing reading !\n"); } catch (IOException | ClassNotFoundException ex) { System.out.println("Erreur reading..."); } } }
و هذه الفئة المسئولة عن الـ serialization/deserialization :
import java.io.*; public class Test { public static void main(String args[]) { User u = new User(4507, "Snack3r", "#_#Sn@CkeRC00L"); try { File fichier = new File("C:\\Users\\Snacker\\Desktop\\user.ser"); FileOutputStream fos = new FileOutputStream(fichier); ObjectOutput oos = new ObjectOutputStream(fos); u.writeExternal(oos); FileInputStream fis = new FileInputStream(fichier); ObjectInputStream ois = new ObjectInputStream(fis); u.readExternal(ois); } catch (IOException ioe) { System.out.println("Erreur main"); } } }
مُخرجات الكود :
-> starting writing... -> finishing writing ! -> starting reading... ID : 4507 Login : Snack3r Password : #_#Sn@CkeRC00L -> finishing reading !
6. اختبر قدراتك في الـ Serialization
قم بإنشاء فئة باسم CompteBank تُمكن من التعامل مع حسابات بنكية مع الأخذ بعين الاعتبار حساسية المبالغ المالية للزبناء. يتم تعريف كل حساب بالرقم التسلسلي, الاسم الكامل لصاحب الحساب, رصيد الحساب, بالإضافة إلى الضمان المالي.
تتم عملية الـ serialization في ملف بامتداد ser يُخزن على سطح المكتب. قم بعمل الـ deserialization و أظهر أسماء أصحاب الحسابات.
قم بإنشاء فئة أخرى باسم Client, يتم تعريف كل زبون برقم بطاقة الهوية (IC) بالإضافة إلى الاسم الكامل.
قم بعمل الـ serialization في نفس الملف السابق الذي تم تخزينه على سطح المكتب ثم قم بعمل deserialization لجميع الكائنات الموجودة في الملف و أظهر أسماء الزبناء بجانب الضمان المالي لكل حساب.
تحياتي.