Skip to content

14. تطبيق ويب MVC في بنية ثلاثية الطبقات – المثال 1

14.1. مقدمة

حتى هذه المرحلة، اقتصرنا على أمثلة مخصصة للأغراض التعليمية. ولهذا السبب، كان لا بد أن تكون بسيطة. نقدم الآن تطبيقًا أساسيًا، ولكنه مع ذلك أكثر ثراءً بالميزات من أي من التطبيقات التي تم عرضها حتى الآن. وسيكون فريدًا من نوعه من حيث أنه يستخدم الطبقات الثلاث لهندسة ثلاثية الطبقات:

Image

ننصح القراء بمراجعة مبادئ تطبيق الويب MVC في بنية ثلاثية الطبقات في القسم 4 إذا كانوا قد نسوها.

سيسمح لنا تطبيق الويب الذي سنقوم بكتابته بإدارة مجموعة من الأشخاص باستخدام أربع عمليات:

  • قائمة الأشخاص في المجموعة
  • إضافة شخص إلى المجموعة
  • تعديل شخص في المجموعة
  • إزالة شخص من المجموعة

هذه هي العمليات الأساسية الأربع على جدول قاعدة البيانات. سنكتب نسختين من هذا التطبيق:

  • في الإصدار 1، لن تستخدم طبقة [DAO] قاعدة بيانات. سيتم تخزين أعضاء المجموعة في كائن [ArrayList] بسيط تديره طبقة [DAO] داخليًا. سيسمح هذا للقارئ باختبار التطبيق دون قيود قاعدة البيانات.
  • في الإصدار 2، سنضع مجموعة الأشخاص في جدول قاعدة بيانات. سنوضح أنه يمكن القيام بذلك دون التأثير على طبقة الويب في الإصدار 1، والتي ستبقى دون تغيير.

تُظهر لقطات الشاشة التالية الصفحات التي يتبادلها التطبيق مع المستخدم.

Image

Image

Image

 

14.2. مشروع Eclipse

اسم مشروع التطبيق هو [people-01]:

Image

يغطي هذا المشروع الطبقات الثلاث لهيكل التطبيق ثلاثي الطبقات:

  • توجد طبقة [dao] في الحزمة [istia.st.mvc.personnes.dao]
  • توجد طبقة [business] أو [service] في الحزمة [istia.st.mvc.personnes.service]
  • توجد طبقة [web] أو [ui] في الحزمة [istia.st.mvc.personnes.web]
  • تحتوي الحزمة [istia.st.mvc.personnes.entities] على كائنات مشتركة بين الطبقات المختلفة
  • تحتوي الحزمة [istia.st.mvc.people.tests] على اختبارات JUnit لطبقات [DAO] و[service]

سنستكشف الطبقات الثلاث [dao] و [service] و [web] بالترتيب. ونظرًا لأن الكتابة قد تستغرق وقتًا طويلاً وقد تكون القراءة مملة، فقد ننتقل أحيانًا عبر التفسيرات بسرعة، إلا إذا كان الموضوع المقدم جديدًا.

14.3. تمثيل الشخص

يدير التطبيق مجموعة من الأشخاص. أظهرت لقطات الشاشة في القسم 14.1 بعض خصائص الشخص. رسميًا، يتم تمثيل هذه الخصائص بواسطة فئة [Person]:

Image

فئة [Person] هي كما يلي:

package istia.st.springmvc.personnes.entites;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Personne {

    // unique personal identifier
    private int id;
    // the current version
    private long version;
    // the name
    private String nom;
    // first name
    private String prenom;
    // date of birth
    private Date dateNaissance;
    // marital status
    private boolean marie = false;
    // number of children
    private int nbEnfants;

    // getters - setters
...

    // default builder
    public Personne() {

    }

    // constructor with initialization of person fields
    public Personne(int id, String prenom, String nom, Date dateNaissance,
            boolean marie, int nbEnfants) {
        setId(id);
        setNom(nom);
        setPrenom(prenom);
        setDateNaissance(dateNaissance);
        setMarie(marie);
        setNbEnfants(nbEnfants);
    }

    // builder of a person by copying another person
    public Personne(Personne p) {
        setId(p.getId());
        setVersion(p.getVersion());
        setNom(p.getNom());
        setPrenom(p.getPrenom());
        setDateNaissance(p.getDateNaissance());
        setMarie(p.getMarie());
        setNbEnfants(p.getNbEnfants());
    }


    // toString
    public String toString() {
        return "[" + id + "," + version + "," + prenom + "," + nom + ","
                + new SimpleDateFormat("dd/MM/yyyy").format(dateNaissance)
                + "," + marie + "," + nbEnfants + "]";
    }
}
  • يتم تحديد هوية الشخص من خلال المعلومات التالية:
    • id: معرف فريد للشخص
    • last_name: لقب الشخص
    • firstName: الاسم الأول
    • dateOfBirth: تاريخ ميلاده
    • maritalStatus: ما إذا كان متزوجًا أم لا
    • nbChildren: عدد الأطفال
  • السمة [version] هي سمة تمت إضافتها بشكل مصطنع لأغراض التطبيق. من منظور موجه للكائنات، كان من الأفضل على الأرجح إضافة هذه السمة إلى فئة مشتقة من [Person]. وتصبح ضرورتها واضحة عند النظر في حالات استخدام تطبيق الويب. وإحدى حالات الاستخدام هذه هي كما يلي:

في الوقت T1، يبدأ المستخدم U1 في تعديل شخص P. في هذه المرحلة، يكون عدد الأطفال 0. يغير U1 هذا الرقم إلى 1، ولكن قبل التحقق من صحة التغيير، يبدأ المستخدم U2 في تعديل نفس الشخص P. نظرًا لأن U1 لم يتحقق بعد من صحة التغيير، يرى U2 أن عدد الأطفال هو 0. يغير U2 اسم الشخص P إلى أحرف كبيرة. ثم يحفظ U1 و U2 تغييراتهما بهذا الترتيب. سيكون لتغيير U2 الأسبقية: سيكون الاسم بأحرف كبيرة وسيظل عدد الأطفال صفرًا، على الرغم من أن U1 يعتقد أنه غيره إلى 1.

يساعدنا مفهوم إصدار الشخص في حل هذه المشكلة. دعونا نعيد النظر في نفس حالة الاستخدام:

في الوقت T1، يبدأ المستخدم U1 في تعديل الشخص P. في هذه اللحظة، يكون عدد الأبناء 0 والإصدار هو V1. يقوم بتغيير عدد الأبناء إلى 1، ولكن قبل أن يلتزم بتعديله، يدخل المستخدم U2 في وضع التعديل لنفس الشخص P. نظرًا لأن U1 لم يلتزم بتعديله بعد، يرى U2 أن عدد الأبناء هو 0 والإصدار هو V1. يغير U2 اسم الشخص P إلى أحرف كبيرة. ثم يقوم U1 و U2 بتثبيت تعديلاتهما بهذا الترتيب. قبل تثبيت التغيير، نتحقق من أن المستخدم الذي يعدل الشخص P لديه نفس الإصدار مثل الإصدار المحفوظ حاليًا للشخص P. سيكون هذا هو الحال بالنسبة للمستخدم U1. وبالتالي يتم قبول تغييره، ثم نقوم بتغيير إصدار الشخص المعدل من V1 إلى V2 للإشارة إلى أن الشخص قد خضع لتغيير. عند التحقق من صحة تعديل U2، سنلاحظ أن لديه الإصدار V1 للشخص P، في حين أن الإصدار الحالي هو V2. يمكننا بعد ذلك إبلاغ المستخدم U2 بأن شخصًا آخر قد سبقه وأنه يجب عليه البدء بالإصدار الجديد للشخص P. وسيقوم بذلك، ويسترد الإصدار V2 للشخص P الذي أصبح لديه الآن طفل، ويكتب الاسم بأحرف كبيرة، ويقوم بالتحقق من صحة التعديل. سيتم قبول تعديله إذا كان الشخص P المسجل لا يزال يحمل الإصدار V2. في النهاية، سيتم أخذ التعديلات التي أجراها U1 و U2 في الاعتبار، بينما في حالة الاستخدام بدون إصدارات، فقد تم فقدان أحد التعديلات.

  • الأسطر 32–40: منشئ قادر على تهيئة حقول الشخص. تم حذف حقل [version].
  • الأسطر 43-51: منشئ يقوم بإنشاء نسخة من الشخص الذي تم تمريره إليه كمعلمة. لدينا الآن كائنان بمحتوى متطابق ولكن يشار إليهما بواسطة مؤشرين مختلفين.
  • السطر 55: يتم إعادة تعريف طريقة [toString] لتُرجع سلسلة تمثل حالة الشخص

14.4. طبقة [DAO]

تتكون طبقة [DAO] من الفئات والواجهات التالية:

Image

  • [IDao] هي الواجهة التي تقدمها طبقة [dao]
  • [DaoImpl] هي تطبيق لهذه الواجهة حيث يتم تغليف مجموعة الأشخاص في كائن [ArrayList]
  • [DaoException] هو نوع من الاستثناءات غير المحددة التي تطلقها طبقة [dao]

واجهة [ IDao] هي كما يلي:

package istia.st.springmvc.personnes.dao;

import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public interface IDao {
    // list of all persons
    Collection getAll();
    // get a specific person
    Personne getOne(int id);
    // add/modify a person
    void saveOne(Personne personne);
    // delete a person
    void deleteOne(int id);
}
  • تحتوي الواجهة على أربع طرق للعمليات الأربع التي نريد تنفيذها على مجموعة الأشخاص:
    • getAll: لاسترداد مجموعة من الأشخاص
    • getOne: لاسترداد شخص بمعرف محدد
    • saveOne: لإضافة شخص (id=-1) أو تعديل شخص موجود (id ≠ -1)
    • deleteOne: لحذف شخص برقم تعريف محدد

قد تطلق طبقة [DAO] استثناءات. وستكون هذه من النوع [ DaoException]:

package istia.st.springmvc.personnes.dao;

public class DaoException extends RuntimeException {

    // error code
    private int code;

    public int getCode() {
        return code;
    }

// manufacturer
    public DaoException(String message,int code) {
        super(message);
        this.code=code;
    }
}
  • السطر 3: فئة [DaoException]، التي تنحدر من [RuntimeException]، هي نوع استثناء غير معالج: لا يطلب منا المُجمِّع:
    • معالجة هذا النوع من الاستثناءات باستخدام كتلة try/catch عند استدعاء دالة قد ترمي هذا الاستثناء
    • تضمين الكلمة الرئيسية "throws DaoException" في توقيع الأسلوب الذي قد يرمي الاستثناء

تمنعنا هذه التقنية من الاضطرار إلى توقيع طرق واجهة [IDao] باستثناءات من نوع معين. وبالتالي، سيكون أي تنفيذ يرمي استثناءات غير محددة مقبولاً، مما يضفي مرونة على البنية.

  • السطر 6: رمز خطأ. ستقوم طبقة [dao] بإلقاء استثناءات متنوعة يتم تحديدها برموز أخطاء مختلفة. سيسمح هذا للطبقة المسؤولة عن معالجة الاستثناء بتحديد المصدر الدقيق للخطأ واتخاذ الإجراء المناسب. هناك طرق أخرى لتحقيق نفس النتيجة. إحدى هذه الطرق هي إنشاء نوع استثناء لكل نوع خطأ محتمل، على سبيل المثال MissingLastNameException، MissingFirstNameException، IncorrectAgeException، ...
  • الأسطر 13–16: المنشئ الذي يسمح لك بإنشاء استثناء يتم تحديده بواسطة رمز خطأ ورسالة خطأ.
  • الأسطر 8–10: الطريقة التي تسمح لمعالج الاستثناء باسترداد رمز الخطأ.

تنفذ الفئة [ DaoImpl] واجهة [IDao]:

package istia.st.springmvc.personnes.dao;

import istia.st.springmvc.personnes.entites.Personne;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;

public class DaoImpl implements IDao {

    // a list of people
    private ArrayList personnes = new ArrayList();

    // next person's no
    private int id = 0;

    // initializations
    public void init() {
        try {
            Personne p1 = new Personne(-1, "Joachim", "Major",
                    new SimpleDateFormat("dd/MM/yyyy").parse("13/11/1984"),
                    true, 2);
            saveOne(p1);
            Personne p2 = new Personne(-1, "Mélanie", "Humbort",
                    new SimpleDateFormat("dd/MM/yyyy").parse("12/02/1985"),
                    false, 1);
            saveOne(p2);
            Personne p3 = new Personne(-1, "Charles", "Lemarchand",
                    new SimpleDateFormat("dd/MM/yyyy").parse("01/03/1986"),
                    false, 0);
            saveOne(p3);
        } catch (ParseException ex) {
            throw new DaoException(
                    "Erreur d'initialisation de la couche [dao] : "
                            + ex.toString(), 1);
        }
    }

    // list of persons
    public Collection getAll() {
        return personnes;
    }

    // get a specific person
    public Personne getOne(int id) {
        // we're looking for the person
        int i = getPosition(id);
        // have we found?
        if (i != -1) {
            return new Personne(((Personne) personnes.get(i)));
        } else {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        }
    }

    // add or modify a person
    public void saveOne(Personne personne) {
        // is the person parameter valid?
        check(personne);
        // addition or modification?
        if (personne.getId() == -1) {
            // add
            personne.setId(getNextId());
            personne.setVersion(1);
            personnes.add(personne);
            return;
        }
        // modification - we're looking for the person
        int i = getPosition(personne.getId());
        // have we found?
        if (i == -1) {
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] qu'on veut modifier n'existe pas", 2);
        }
        // do we have the right version of the original?
        Personne original = (Personne) personnes.get(i);
        if (original.getVersion() != personne.getVersion()) {
            throw new DaoException("L'original de la personne [" + personne
                    + "] a changé depuis sa lecture initiale", 3);
        }
        // wait 10 ms
        //wait(10);
        // that's it - make the change
        original.setVersion(original.getVersion()+1);
        original.setNom(personne.getNom());
        original.setPrenom(personne.getPrenom());
        original.setDateNaissance((personne.getDateNaissance()));
        original.setMarie(personne.getMarie());
        original.setNbEnfants(personne.getNbEnfants());
    }

    // deleting a person
    public void deleteOne(int id) {
        // we're looking for the person
        int i = getPosition(id);
        // have we found?
        if (i == -1) {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        } else {
            // we delete the person
            personnes.remove(i);
        }
    }

    // id generator
    private int getNextId() {
        id++;
        return id;
    }

    // find a person
    private int getPosition(int id) {
        int i = 0;
        boolean trouvé = false;
        // browse the list of people
        while (i < personnes.size() && !trouvé) {
            if (id == ((Personne) personnes.get(i)).getId()) {
                trouvé = true;
            } else {
                i++;
            }
        }
        // result?
        return trouvé ? i : -1;
    }

    // person verification
    private void check(Personne p) {
        // person p
        if (p == null) {
            throw new DaoException("Personne null", 10);
        }
        // id
        if (p.getId() != -1 && p.getId() < 0) {
            throw new DaoException("Id [" + p.getId() + "] invalide", 11);
        }
        // date of birth
        if (p.getDateNaissance() == null) {
            throw new DaoException("Date de naissance manquante", 12);
        }
        // number of children
        if (p.getNbEnfants() < 0) {
            throw new DaoException("Nombre d'enfants [" + p.getNbEnfants()
                    + "] invalide", 13);
        }
        // name
        if (p.getNom() == null || p.getNom().trim().length() == 0) {
            throw new DaoException("Nom manquant", 14);
        }
        // first name
        if (p.getPrenom() == null || p.getPrenom().trim().length() == 0) {
            throw new DaoException("Prénom manquant", 15);
        }
    }

    // waiting
    private void wait(int N) {
        // we wait for N ms
        try {
            Thread.sleep(N);
        } catch (InterruptedException e) {
            // display the exception trace
            e.printStackTrace();
            return;
        }
    }
}

سنكتفي بشرح هذا الكود بشكل عام. ومع ذلك، سنخصص بعض الوقت للأجزاء الأكثر تعقيدًا.

  • السطر 13: كائن [ArrayList] الذي سيحتوي على مجموعة الأشخاص
  • السطر 16: معرف آخر شخص تمت إضافته. في كل مرة يتم فيها إضافة شخص جديد، سيتم زيادة هذا المعرف بمقدار 1.

سيتم إنشاء مثيل لفئة [DaoImpl] كمثيل واحد. وهذا ما يُعرف باسم singleton. يقدم تطبيق الويب خدماته لمستخدميه في وقت واحد. في أي وقت معين، هناك عدة خيوط تعمل على خادم الويب. تتشارك هذه الخيوط في singletons:

  • الخيط من طبقة [dao]
  • الخيط الموجود في طبقة [service]
  • تلك الخاصة بمختلف وحدات التحكم ومدققي البيانات وما إلى ذلك، في طبقة الويب

إذا كان للسينجلتون حقول خاصة، فيجب أن تسأل نفسك على الفور عن سبب وجودها. هل هناك ما يبرر وجودها؟ في الواقع، سيتم مشاركتها بين خيوط مختلفة. إذا كانت للقراءة فقط، فلن يكون ذلك مشكلة إذا كان من الممكن تهيئتها في وقت تكون فيه متأكدًا من وجود خيط نشط واحد فقط. نحن نعرف عمومًا كيفية تحديد هذه اللحظة. وهي عندما يبدأ تشغيل تطبيق الويب ولكنه لم يبدأ بعد في خدمة العملاء. إذا كانت هذه الحقول للقراءة/الكتابة، فيجب تنفيذ مزامنة الوصول إلى الحقول؛ وإلا، فإن الكارثة حتمية. سنوضح هذه المشكلة عندما نختبر طبقة [dao].

  • لا تحتوي فئة [DaoImpl] على منشئ. لذلك، سيتم استخدام منشئها الافتراضي.
  • الأسطر 19-38: سيتم استدعاء طريقة [init] عند إنشاء مثيل للطبقة [dao] الفردية. وهي تنشئ قائمة تضم ثلاثة أشخاص.
  • الأسطر 41-43: تنفذ طريقة [getAll] لواجهة [IDao]. وهي تُرجع مرجعًا إلى قائمة الأشخاص.
  • الأسطر 46–55: تنفذ طريقة [getOne] لواجهة [IDao]. معلمتها هي معرف الشخص الذي يتم البحث عنه.

لاسترداده، نستدعي طريقة خاصة [getPosition] في الأسطر 113–126. تُرجع هذه الطريقة الموضع في قائمة الشخص الذي يتم البحث عنه، أو -1 إذا لم يتم العثور على الشخص.

إذا تم العثور على الشخص، فإن طريقة [getOne] ترجع مرجعًا (السطر 51) إلى نسخة من ذلك الشخص، وليس إلى الشخص نفسه. في الواقع، عندما يرغب المستخدم في تعديل شخص ما، يتم طلب المعلومات المتعلقة بهذا الشخص من طبقة [dao] وتمريرها إلى طبقة [web] للتعديل، في شكل مرجع إلى كائن [Person]. يعمل هذا المرجع كحاوية إدخال في نموذج التعديل. عندما يرسل المستخدم تغييراته في طبقة الويب، سيتم تعديل محتويات حاوية الإدخال. إذا كان الحاوية عبارة عن مرجع إلى الشخص الفعلي في [ArrayList] في طبقة [dao]، فسيتم تعديل ذلك الشخص حتى لو لم يتم تقديم التغييرات إلى طبقتي [service] و [dao]. هذه الأخيرة هي الطبقة الوحيدة المصرح لها بإدارة قائمة الأشخاص. لذلك، يجب أن تعمل طبقة الويب على نسخة من الشخص المراد تعديله. هنا، توفر طبقة [dao] هذه النسخة.

إذا لم يتم العثور على الشخص الذي يتم البحث عنه، يتم إلقاء [DaoException] مع رمز الخطأ 2 (السطر 53).

  • الأسطر 94–104: تنفذ طريقة [deleteOne] لواجهة [IDao]. معلمتها هي معرف الشخص المراد حذفه. إذا كان الشخص المراد حذفه غير موجود، يتم إلقاء استثناء [DaoException] برمز الخطأ 2.
  • الأسطر 58–91: تنفذ طريقة [saveOne] الخاصة بواجهة [IDao]. معلمتها هي كائن [Person]. إذا كان معرف هذا الكائن هو -1، فهذا يعني أنه شخص جديد يتم إضافته. وإلا، فإنها تعدل الشخص الموجود في القائمة الذي يحمل هذا المعرف باستخدام القيم الموجودة في المعلمة.
    • السطر 60: يتم التحقق من صحة المعلمة [Person] بواسطة طريقة خاصة [check] محددة في الأسطر 129–155. تقوم هذه الطريقة بإجراء فحوصات أساسية على قيم الحقول المختلفة لـ [Person]. عندما يتم اكتشاف أي شذوذ، يتم إلقاء استثناء [DaoException] مع رمز خطأ محدد. نظرًا لأن طريقة [saveOne] لا تتعامل مع هذا الاستثناء، فسيتم تمريره إلى الطريقة المستدعية.
    • السطر 62: إذا كان المعلمة [Person] تحتوي على معرف يساوي -1، فهذا يعني أنها عملية إضافة. تتم إضافة كائن [Person] إلى القائمة الداخلية للأشخاص (السطر 66)، مع أول معرف متاح (السطر 64)، ورقم إصدار يساوي 1 (السطر 65).
    • إذا كان للمعلمة [Person] معرف [id] غير -1، فإن هذا ينطوي على تعديل الشخص الموجود في القائمة الداخلية بهذا [id]. أولاً، نتحقق (الأسطر 70–75) من وجود الشخص المراد تعديله. إذا لم يكن الأمر كذلك، فإننا نطلق استثناء [DaoException] برمز الخطأ 2.
    • إذا كان الشخص موجودًا بالفعل، فإننا نتحقق من أن إصداره الحالي يطابق إصدار المعلمة [Person]، التي تحتوي على التغييرات المطلوب تطبيقها على الأصل. إذا لم يكن الأمر كذلك، فهذا يعني أن المستخدم الذي يحاول تعديل الشخص لا يمتلك أحدث إصدار. نبلغه بذلك عن طريق إلقاء استثناء [DaoException] برمز الخطأ 3 (السطور 79–80).
    • إذا سارت الأمور على ما يرام، يتم إجراء التغييرات على سجل الشخص الأصلي (الأسطر 85–90)

من الواضح أن هذه الطريقة يجب أن تكون متزامنة. على سبيل المثال، بين اللحظة التي نتحقق فيها من أن الشخص المراد تعديله موجود بالفعل واللحظة التي يتم فيها إجراء التعديل، قد يكون شخص آخر قد أزال هذا الشخص من القائمة. لذلك يجب إعلان الطريقة على أنها [synchronized] لضمان أن يقوم مؤشر ترابط واحد فقط بتنفيذها في كل مرة. وينطبق الأمر نفسه على الطرق الأخرى لواجهة [IDao]. نحن لا نفعل ذلك، ونفضل نقل هذه المزامنة إلى طبقة [service]. لتسليط الضوء على مشكلات المزامنة، أثناء اختبار طبقة [dao]، سنوقف تنفيذ [saveOne] لمدة 10 مللي ثانية (السطر 83) بين اللحظة التي نعلم فيها أنه يمكننا إجراء التعديل واللحظة التي نقوم فيها بإجرائه فعليًا. سيفقد الخيط الذي ينفذ [saveOne] بعد ذلك وحدة المعالجة المركزية (CPU) لصالح خيط آخر. وهذا يزيد من فرص ظهور تعارضات في الوصول إلى قائمة الأشخاص.

14.5. اختبارات طبقة [DAO]

يتم كتابة اختبار JUnit لطبقة [dao]:

[TestDao] هو اختبار JUnit. لتسليط الضوء على مشكلات الوصول المتزامن إلى قائمة الأشخاص، يتم إنشاء مؤشرات ترابط من النوع [ThreadDaoMajEnfants]. وهي مسؤولة عن زيادة عدد أطفال شخص معين بمقدار 1.

يحتوي [TestDao] على خمسة اختبارات، من [test1] إلى [test5]. نقدم هنا اثنين منها فقط؛ وندعو القراء لاستكشاف الاختبارات الأخرى في شفرة المصدر المرتبطة بهذا المقال.

package istia.st.springmvc.personnes.tests;

import java.text.ParseException;
...

public class TestDao extends TestCase {

    // layer [dao]
    private DaoImpl dao;

    // manufacturer
    public TestDao() {
        dao = new DaoImpl();
        dao.init();
    }

    // list of persons
    private void doListe(Collection personnes) {
        Iterator iter = personnes.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
    }

    // test1
    public void test1() throws ParseException {
...
    }

    // modification-deletion of a non-existent element
    public void test2() throws ParseException {
...
    }

    // person version management
    public void test3() throws ParseException, InterruptedException {
...
    }

    // optimistic locking - multi-threaded access
    public void test4() throws Exception {
...
    }

    // validity tests for saveOne
    public void test5() throws ParseException {
    ...
}
  • السطر 9: إشارة إلى تنفيذ طبقة [dao] قيد الاختبار
  • الأسطر 12–15: منشئ اختبار JUnit. يقوم بإنشاء مثيل من النوع [DaoImpl] من طبقة [dao] المراد اختبارها وتهيئته.

تختبر الطريقة [test1] الطرق الأربع لواجهة [IDao] على النحو التالي:

    public void test1() throws ParseException {
        // current list
        Collection personnes = dao.getAll();
        int nbPersonnes = personnes.size();
        // display
        doListe(personnes);
        // add a person
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        dao.saveOne(p1);
        int id1 = p1.getId();
        // verification - a crash will occur if the person is not found
        p1 = dao.getOne(id1);
        assertEquals("X", p1.getNom());
        // modification
        p1.setNom("Y");
        dao.saveOne(p1);
        // verification - a crash will occur if the person is not found
        p1 = dao.getOne(id1);
        assertEquals("Y", p1.getNom());
        // delete
        dao.deleteOne(id1);
        // check
        int codeErreur = 0;
        boolean erreur = false;
        try {
            p1 = dao.getOne(id1);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
        // we must have a code 2 error
        assertTrue(erreur);
        assertEquals(2, codeErreur);
        // list of persons
        personnes = dao.getAll();
        assertEquals(nbPersonnes, personnes.size());
    }
  • السطر 3: طلب قائمة الأشخاص
  • السطر 6: نعرضها
[1,1,Joachim,Major,13/01/1984,true,2]
[2,1,Mélanie,Humbort,12/01/1985,false,1]
[3,1,Charles,Lemarchand,01/01/1986,false,0]

ثم يقوم الاختبار بإضافة شخص، وتعديله، وحذفه. وبالتالي، يتم استخدام الطرق الأربع لواجهة [IDao].

  • الأسطر 8–10: تتم إضافة شخص جديد (id=-1).
  • السطر 11: نسترد معرّف الشخص المضاف لأن عملية الإضافة خصصت له معرّفًا. قبل ذلك، لم يكن لديه معرّف.
  • الأسطر 13-14: نطلب من طبقة [dao] نسخة من الشخص الذي تمت إضافته للتو. ضع في اعتبارك أنه إذا لم يتم العثور على الشخص المطلوب، فإن طبقة [dao] ترمي استثناءً. سيؤدي هذا إلى تعطل في السطر 13. كان بإمكاننا التعامل مع هذه الحالة بشكل أنظف. في السطر 14، نتحقق من اسم الشخص الذي تم استرداده.
  • السطران 16-17: نقوم بتعديل هذا الاسم ونطلب من طبقة [DAO] حفظ التغييرات.
  • السطران 19-20: نطلب من طبقة [DAO] نسخة من الشخص الذي تمت إضافته للتو ونتحقق من اسمه الجديد.
  • السطر 22: حذف الشخص الذي تمت إضافته في بداية الاختبار.
  • الأسطر 23-34: نطلب نسخة من الشخص الذي تم حذفه للتو من طبقة [dao]. يجب أن تتلقى [DaoException] برمز 2.
  • السطران 36-37: يتم طلب قائمة الأشخاص مرة أخرى. يجب أن نحصل على نفس القائمة الموجودة في بداية الاختبار.

تهدف طريقة [test4] إلى تسليط الضوء على المشكلات المتعلقة بالوصول المتزامن إلى طرق طبقة [dao]. تذكر أن هذه الطرق لم تتم مزامنتها. فيما يلي كود الاختبار:

    public void test4() throws Exception {
        // add a person
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        dao.saveOne(p1);
        int id1 = p1.getId();
        // creation of N threads for updating the number of children
        final int N = 10;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoMajEnfants("thread n° " + i, dao, id1);
            taches[i].start();
        }
        // we wait for the end of threads
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
        // we pick up the person
        p1 = dao.getOne(id1);
        // she must have N children
        assertEquals(N, p1.getNbEnfants());
        // delete person p1
        dao.deleteOne(p1.getId());
        // check
        boolean erreur = false;
        int codeErreur = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
        // we must have a code 2 error
        assertTrue(erreur);
        assertEquals(2, codeErreur);
    }
  • الأسطر 3–6: نضيف شخصًا P بدون أطفال إلى القائمة. نسجل [id] الخاص به (السطر 6).
  • الأسطر 7–13: نطلق N خيوط. سيقوم كل منها بزيادة عدد أطفال الشخص P بمقدار 1. في النهاية، يجب أن يكون لدى الشخص P N أطفال.
  • الأسطر 15–17: تنتظر طريقة [test4] التي أطلقت الخيوط N حتى تنتهي من عملها قبل التحقق من العدد الجديد لأطفال الشخص P.
  • الأسطر 18–21: نسترجع الشخص P ونتحقق من أن عدد أطفاله هو N.
  • الأسطر 22–35: يتم إزالة الشخص P، ونتحقق من أنه لم يعد موجودًا في القائمة.

في السطر 11، نرى أن الخيوط من النوع [ThreadDaoMajEnfants]. يحتوي منشئ هذا النوع على ثلاثة معلمات:

  1. الاسم الممنوح للخيط، ويُستخدم لتتبعه عبر السجلات
  2. إشارة إلى طبقة [dao] حتى يتمكن الخيط من الوصول إليها
  3. معرف الشخص الذي من المفترض أن يعمل عليه الخيط

نوع [ThreadDaoMajEnfants] هو كما يلي:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;

public class ThreadDaoMajEnfants extends Thread {
    // thread name
    private String name;
    // reference on the [dao] layer
    private IDao dao;
    // the id of the person we're going to work on
    private int idPersonne;

    // manufacturer
    public ThreadDaoMajEnfants(String name, IDao dao, int idPersonne) {
        this.name = name;
        this.dao = dao;
        this.idPersonne = idPersonne;
    }

    // thread core
    public void run() {
        // follow-up
        suivi("lancé");
        // we loop until we have succeeded in incrementing by 1
        // person's number of children idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
            // a copy of the idPersonne person is retrieved
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
            // follow-up
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version "+personne.getVersion());
            // 10 ms wait to abandon processor
            try {
                // follow-up
                suivi("début attente");
                // we pause to let the processor
                Thread.sleep(10);
                // follow-up
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
            // waiting complete - try to validate the copy
            // in the meantime, other threads may have modified the original
            int codeErreur = 0;
            try {
                // increments by 1 the number of children in this copy
                personne.setNbEnfants(nbEnfants + 1);
                // we try to modify the original
                dao.saveOne(personne);
                // we passed - the original has been modified
                fini = true;
            } catch (DaoException ex) {
                // we retrieve the error code
                codeErreur = ex.getCode();
                // must be a version 3 error - otherwise it will be re-run
                // the exception
                if (codeErreur != 3) {
                    throw ex;
                } else {
                    // follow-up
                    suivi(ex.getMessage());
                }
                // the original has changed - start all over again
            }
        }
        // follow-up
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

    // follow-up
    private void suivi(String message) {
        System.out
                .println(name + " [" + new Date().getTime()+ "] : " + message);
    }
}
  • السطر 9: [ThreadDaoMajEnfants] هو بالفعل مؤشر ترابط
  • الأسطر 18–22: المنشئ الذي يقوم بتهيئة الخيط بثلاث معلومات
    1. الاسم [name] الممنوح للخيط
    2. مرجع [dao] إلى طبقة [dao]. لاحظ مرة أخرى أننا نعمل مع نوع الواجهة [IDao] وليس نوع التنفيذ [DaoImpl].
    3. المعرف [id] للشخص الذي سيعمل عليه الخيط

عندما يقوم [test4] بتشغيل مؤشر ترابط [ThreadDaoMajEnfants] (السطر 12 من test4)، يتم تنفيذ طريقة [run] الخاصة به (السطر 25):

  • الأسطر 78-81: تسمح الطريقة الخاصة [suivi] بتسجيل الشاشة. تستخدمها طريقة [run] لتتبع تنفيذ الخيط.
  • يحاول الخيط زيادة عدد أطفال الشخص P ذي المعرف [id] بمقدار 1. قد يتطلب هذا التحديث عدة محاولات. لنفكر في خيطين [TH1] و [TH2]. يطلب [TH1] نسخة من الشخص P من طبقة [dao]. يحصل عليها ويلاحظ أن إصدارها هو V1. يتم مقاطعة [TH1]. يقوم [TH2]، الذي كان يتبعه، بنفس الشيء ويحصل على نفس الإصدار V1 للشخص P. يتم مقاطعة [TH2]. يستأنف [TH2] التحكم، ويزيد عدد الأبناء لـ P، ويحفظ تغييراته. نعلم أن هذه التغييرات محفوظة الآن وأن إصدار P سيتغير إلى V2. انتهى [TH1] من عمله. يستأنف [TH2] التحكم ويفعل الشيء نفسه. سيتم رفض تحديثه لـ P لأنه يحتفظ بنسخة من P في الإصدار V1، في حين أن P الأصلي أصبح الآن في الإصدار V2. يجب على [TH2] بعد ذلك تكرار الدورة بأكملها [قراءة -> تحديث -> حفظ]. هذا هو سبب وجود الحلقة في الأسطر 32–72. في هذه الحلقة، يقوم الخيط بما يلي:
  • يطلب نسخة من الشخص P لتعديلها (السطر 34)
  • ينتظر 10 مللي ثانية (السطر 43). هذا أمر مصطنع ويهدف إلى مقاطعة الخيط بين قراءة الشخص P وتحديثه فعليًا في قائمة الأشخاص من أجل زيادة احتمالية حدوث تعارضات.
  • يزيد عدد أبناء P (السطر 54) ويحفظ P (السطر 56). إذا لم يكن لدى الخيط الإصدار الصحيح من P، فسيتم إلقاء استثناء بواسطة طبقة [dao]. ثم نسترد رمز الاستثناء (السطر 61) للتحقق من أنه هو بالفعل الرمز 3 (إصدار غير صحيح من P). إذا لم يكن الأمر كذلك، يتم إعادة إلقاء الاستثناء إلى الطريقة المستدعية، وهي في النهاية طريقة الاختبار [test4]. إذا كان لدينا استثناء الرمز 3، فإننا نعيد تشغيل الدورة [قراءة -> تحديث -> حفظ]. إذا لم يكن هناك استثناء، فإن التحديث قد اكتمل وانتهى عمل الخيط.

ماذا تظهر الاختبارات؟

في التكوين الأول الذي تم اختباره:

  • نقوم بتعليق عبارة الانتظار في طريقة [saveOne] في [DaoImpl] (السطر 83، القسم 14.4).
        // on attend 10 ms
        //wait(10);
  • تقوم طريقة [test4] بإنشاء 100 مؤشر ترابط (السطر 8، القسم 14.5).
        // création de N threads de mise à jour du nombre d'enfants
        final int N = 100;

تم الحصول على النتائج التالية:

Image

نجحت جميع الاختبارات الخمسة.

في التكوين الثاني الذي تم اختباره:

  • تم إلغاء تعليق الأمر "wait" في طريقة [saveOne] في [DaoImpl] (السطر 83، القسم 14.4).
        // on attend 10 ms
        wait(10);
  • تقوم طريقة [test4] بإنشاء مؤشرين (السطر 8، القسم 14.5).
        // création de N threads de mise à jour du nombre d'enfants
        final int N = 2;

تم الحصول على النتائج التالية:

فشل اختبار [test4]. أنشأنا خيطين، كل منهما مكلف بزيادة عدد أبناء الشخص P، الذي كان لديه في البداية 0، بمقدار 1. لذلك توقعنا أن يكون هناك طفلان بعد تشغيل الخيطين، لكن لدينا طفل واحد فقط.

دعونا نفحص سجلات الشاشة من [test4] لفهم ما حدث:

thread n° 0 [1145536368171] : lancé
thread n° 0 [1145536368171] : 0 -> 1 pour la version 1
thread n° 0 [1145536368171] : début attente
thread n° 1 [1145536368171] : lancé
thread n° 1 [1145536368171] : 0 -> 1 pour la version 1
thread n° 1 [1145536368171] : début attente
thread n° 0 [1145536368187] : fin attente
thread n° 1 [1145536368187] : fin attente
thread n° 0 [1145536368187] : a terminé et passé le nombre d'enfants à 1
thread n° 1 [1145536368187] : a terminé et passé le nombre d'enfants à 1
  • السطر 1: يبدأ الخيط رقم 0 عمله
  • السطر 2: استرد نسخة من الشخص P ووجد أن عدد الأبناء هو 0
  • السطر 3: يصادف [Thread.sleep(10)] في طريقة [run] الخاصة به، وبالتالي يتوقف مؤقتًا عند الوقت [1145536368171] (مللي ثانية)
  • السطر 4: ثم يستحوذ الخيط رقم 1 على المعالج ويبدأ عمله
  • السطر 5: استرد نسخة من الشخص P ووجد أن عدد الأطفال هو 0
  • السطر 6: يصادف [Thread.sleep(10)] في طريقة [run] الخاصة به، وبالتالي يتوقف مؤقتًا
  • السطر 7: يستعيد الخيط 0 وحدة المعالجة المركزية في الوقت [1145536368187] (مللي ثانية)، أي بعد 16 مللي ثانية من فقدانها.
  • السطر 8: نفس الشيء بالنسبة للخيط رقم 1
  • السطر 9: قام الخيط رقم 0 بتحديث نفسه وتعيين عدد الأبناء إلى 1
  • السطر 10: قام الخيط رقم 1 بنفس الشيء

السؤال هو: لماذا تمكن الخيط رقم 1 من إجراء التحديث في حين أنه، في العادة، لم يعد يمتلك النسخة الصحيحة من الشخص P، التي تم تحديثها للتو بواسطة الخيط رقم 0؟

أولاً، يمكننا ملاحظة وجود شذوذ بين السطرين 7 و 8: يبدو أن الخيط رقم 0 فقد وحدة المعالجة المركزية (CPU) بين هذين السطرين لصالح الخيط رقم 1. ماذا كان يفعل في تلك اللحظة؟ كان ينفذ طريقة [saveOne] الخاصة بطبقة [dao]. تحتوي هذه الطريقة على الهيكل التالي (انظر القسم 14.4):

    public void saveOne(Personne personne) {
...
        // modification - we're looking for the person
....
        // do we have the right version of the original?
...
        // wait 10 ms
        wait(10);
        // that's it - make the change
    ...
}
  • نفذ الخيط #0 [saveOne] وانتقل إلى السطر 8، حيث اضطر إلى التخلي عن وحدة المعالجة المركزية. في غضون ذلك، قرأ نسخة الشخص P، التي كانت 1 لأن الشخص P لم يتم تحديثه بعد.
  • وبما أن وحدة المعالجة المركزية (CPU) أصبحت متاحة، تولى الخيط رقم 1 زمام الأمور. وقام بدوره بتنفيذ [saveOne] ووصل إلى السطر 8، حيث اضطر إلى تحرير وحدة المعالجة المركزية. وفي غضون ذلك، قرأ إصدار الشخص P، الذي كان 1 لأن الشخص P لم يتم تحديثه بعد.
  • نظرًا لأن المعالج أصبح متاحًا، استحوذ عليه الخيط رقم 0. بدءًا من السطر 9، قام بإجراء التحديث الخاص به وقام بتعيين عدد الأبناء إلى 1. ثم انتهت طريقة [run] للخيط رقم 0، وعرض الخيط السجل الذي يفيد بأنه قام بتعيين عدد الأبناء إلى 1 (السطر 9).
  • بما أن المعالج أصبح متاحًا، فقد ورثه الخيط رقم 1. بدءًا من السطر 9، قام بإجراء التحديث الخاص به وقام بتعيين عدد الأبناء إلى 1. لماذا 1؟ لأنه يحتفظ بنسخة من P مع تعيين عدد الأبناء إلى 0. وهذا ما يشير إليه السجل (السطر 5). ثم انتهت طريقة [run] للخيط رقم 1، وعرض الخيط السجل الذي يفيد بأنه قام بتعيين عدد الأبناء إلى 1 (السطر 10).

من أين تأتي المشكلة؟ تنبع من حقيقة أن الخيط رقم 0 لم يكن لديه الوقت لتثبيت تغييره وبالتالي تحديث إصدار الشخص P قبل أن يحاول الخيط رقم 1 قراءة ذلك الإصدار للتحقق مما إذا كان الشخص P قد تغير. هذا السيناريو غير محتمل ولكنه ليس مستحيلاً. اضطررنا إلى إجبار الخيط رقم 0 على فقدان وحدة المعالجة المركزية (CPU) لجعله يظهر مع خيطين فقط. بدون هذا الحل البديل، فشلت التهيئة السابقة في إعادة إنتاج هذا السيناريو نفسه مع 100 خيط. كان اختبار [test4] ناجحًا.

ما هو الحل؟ لا شك أن هناك عدة حلول. أحدها، وهو سهل التنفيذ، هو مزامنة طريقة [saveOne]:


    public synchronized void saveOne(Personne personne)

تضمن الكلمة الرئيسية [synchronized] أن يتم تنفيذ الطريقة بواسطة مؤشر ترابط واحد فقط في كل مرة. وبالتالي، لن يُسمح للمؤشر الترابط رقم 1 بتنفيذ [saveOne] إلا بعد خروج المؤشر الترابط رقم 0 منها. يمكننا عندئذ التأكد من أن نسخة الشخص P ستكون قد تغيرت بحلول الوقت الذي يدخل فيه المؤشر الترابط رقم 1 إلى [saveOne]. سيتم عندئذ رفض تحديثه لأنه لن يكون لديه النسخة الصحيحة من P.

هذه هي الطرق الأربع لطبقة [dao] التي تحتاج إلى التزامن. ومع ذلك، قررنا الإبقاء على هذه الطبقة كما هي ونقل التزامن إلى طبقة [service]. وهناك عدة أسباب لذلك:

  • نفترض أن الوصول إلى طبقة [dao] يحدث دائمًا من خلال طبقة [service]. وهذا هو الحال في تطبيق الويب الخاص بنا.
  • قد يكون من الضروري أيضًا مزامنة الوصول إلى طرق طبقة [service] لأسباب أخرى غير تلك التي تدفعنا إلى مزامنة طرق طبقة [dao]. في هذه الحالة، لا توجد حاجة لمزامنة طرق طبقة [dao]. إذا كنا متأكدين من أن:
  • أن كل الوصول إلى طبقة [DAO] يمر عبر طبقة [service]
  • يستخدم خيط واحد فقط في كل مرة طبقة [الخدمة]

فإننا يمكننا أن نكون على يقين من أن أساليب طبقة [DAO] لن يتم تنفيذها بواسطة خيطين في نفس الوقت.

سنستكشف الآن طبقة [service].

14.6. طبقة [service]

تتكون طبقة [service] من الفئات والواجهات التالية:

Image

  • [IService] هي الواجهة التي تعرضها طبقة [dao]
  • [ServiceImpl] هي تنفيذ لهذه الواجهة

واجهة [IService] هي كما يلي:

package istia.st.springmvc.personnes.service;

import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public interface IService {
    // list of all persons
    Collection getAll();
    // find a specific person
    Personne getOne(int id);
    // add/modify a person
    void saveOne(Personne personne);
    // delete a person
    void deleteOne(int id);
}

وهي مطابقة لواجهة [IDao].

فيما يلي تنفيذ [ServiceImpl] لواجهة [IService]:

package istia.st.springmvc.personnes.service;

import istia.st.springmvc.personnes.dao.IDao;
import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public class ServiceImpl implements IService {

    // the [dao] layer
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

    // list of persons
    public synchronized Collection getAll() {
        return dao.getAll();
    }

    // get a specific person
    public synchronized Personne getOne(int id) {
        return dao.getOne(id);
    }

    // add or modify a person
    public synchronized void saveOne(Personne personne) {
        dao.saveOne(personne);
    }

    // deleting a person
    public synchronized void deleteOne(int id) {
        dao.deleteOne(id);
    }
}
  • الأسطر 10–19: السمة [IDao dao] هي مرجع إلى طبقة [dao]. سيتم تهيئتها بواسطة Spring IoC.
  • الأسطر 22–24: تنفيذ طريقة [getAll] لواجهة [IService]. تقوم الطريقة ببساطة بتفويض الطلب إلى طبقة [dao].
  • الأسطر 27–29: تنفيذ طريقة [getOne] لواجهة [IService]. تقوم الطريقة ببساطة بتفويض الطلب إلى طبقة [dao].
  • الأسطر 32–34: تنفيذ طريقة [saveOne] لواجهة [IService]. تقوم الطريقة ببساطة بتفويض الطلب إلى طبقة [dao].
  • الأسطر 37-39: تنفيذ طريقة [deleteOne] لواجهة [IService]. تقوم الطريقة ببساطة بتفويض الطلب إلى طبقة [dao].
  • يتم مزامنة جميع الطرق (باستخدام الكلمة الرئيسية `synchronized`)، مما يضمن أن خيطًا واحدًا فقط في كل مرة يمكنه استخدام طبقة [service] وبالتالي طبقة [dao].

14.7. اختبارات لطبقة [service]

يتم كتابة اختبار JUnit لطبقة [service]:

[TestService] هو اختبار JUnit. الاختبارات التي يتم إجراؤها هي نفسها تمامًا تلك التي يتم إجراؤها لطبقة [dao]. هيكل [TestService] هو كما يلي:

package istia.st.springmvc.personnes.tests;

...

public class TestService extends TestCase {

    // service] layer
    private ServiceImpl service;

    // manufacturer
    public TestService() {
        service = new ServiceImpl();
        DaoImpl dao=new DaoImpl();
        service.setDao(dao);
    }

    // list of persons
    private void doListe(Collection personnes) {
...
    }

    // test1
    public void test1() throws ParseException {
        // current list
        Collection personnes = service.getAll();
        int nbPersonnes = personnes.size();
        // display
        doListe(personnes);
        // add a person
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        service.saveOne(p1);
        int id1 = p1.getId();
        // verification - a crash will occur if the person is not found
        p1 = service.getOne(id1);
        assertEquals("X", p1.getNom());
...
    }

    // modification-deletion of a non-existent element
    public void test2() throws ParseException {
...
    }

    // person version management
    public void test3() throws ParseException, InterruptedException {
...
    }

    // optimistic locking - multi-threaded access
    public void test4() throws Exception {
        // add a person
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        service.saveOne(p1);
        int id1 = p1.getId();
        // creation of N child update threads
        final int N = 100;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadServiceMajEnfants("thread n° " + i, service,
                    id1);
            taches[i].start();
        }
...
    }

    // validity tests for saveOne
    public void test5() throws ParseException {
    ...
    }
}
  • السطر 9: طبقة [service] التي يتم اختبارها هي من النوع [ServiceImpl].
  • الأسطر 11–15: يقوم منشئ اختبار JUnit بإنشاء مثيل لطبقة [service] المراد اختبارها (السطر 12)، وإنشاء مثيل لطبقة [dao] (السطر 13)، وإصدار تعليمات لطبقة [service] باستخدام طبقة [dao] هذه (السطر 14).

تختبر طريقة [test1] الطرق الأربع لواجهة [IService] بنفس طريقة طريقة الاختبار في طبقة [dao] التي تحمل الاسم نفسه. والفرق الوحيد هو أنها تصل إلى طبقة [service] (الأسطر 25 و32 و35) بدلاً من طبقة [dao].

تهدف طريقة [test4] إلى تسليط الضوء على المشكلات المتعلقة بالوصول المتزامن إلى طرق طبقة [service]. وهي، مرة أخرى، مطابقة لطريقة الاختبار [test4] لطبقة [dao]. ومع ذلك، هناك بعض التفاصيل التي تختلف:

  • نحن نتعامل مع طبقة [service] بدلاً من طبقة [dao] (السطر 55)
  • نمرر مرجعًا إلى طبقة [service] إلى الخيوط بدلاً من طبقة [dao] (السطر 61)

كما أن نوع [ThreadServiceMajEnfants] مطابق تقريبًا لنوع [ThreadDaoMajEnfants]، باستثناء أنه يعمل مع طبقة [service] بدلاً من طبقة [dao]:

package istia.st.springmvc.personnes.tests;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.entites.Personne;
import istia.st.mvc.personnes.service.IService;

public class ThreadServiceMajEnfants extends Thread {

    // thread name
    private String name;
    // reference on the [service] layer
    private IService service;
    // the id of the person we're going to work on
    private int idPersonne;

    public ThreadServiceMajEnfants(String name, IService service, int idPersonne) {
        this.name = name;
        this.service = service;
        this.idPersonne = idPersonne;
    }

    public void run() {
...
    }

    // follow-up
    private void suivi(String message) {
        System.out.println(name + " : " + message);
    }

}
  • السطر 12: يعمل الخيط مع طبقة [service]

نقوم بتشغيل الاختبارات باستخدام التكوين الذي تسبب في حدوث مشكلات في طبقة [dao]:

  • نقوم بإلغاء تعليق عبارة الانتظار في طريقة [saveOne] في [DaoImpl] (السطر 83، القسم 14.4).
        // on attend 10 ms
        wait(10);
  • تقوم طريقة [test4] بإنشاء 100 مؤشر ترابط (السطر 65، القسم 14.7).
        // création de N threads de mise à jour du nombre d'enfants
        final int N = 100;

النتائج التي تم الحصول عليها هي كما يلي:

كان تزامن الأساليب في طبقة [service] هو ما مكن من نجاح اختبار [test4].

14.8. طبقة [الويب]

دعونا نستعرض بنية التطبيق المكونة من 3 طبقات:

ستوفر طبقة [الويب] شاشات للمستخدم لتمكينه من إدارة مجموعة الأشخاص:

  • قائمة الأشخاص في المجموعة
  • إضافة شخص إلى المجموعة
  • تحرير معلومات شخص في المجموعة
  • إزالة شخص من المجموعة

للقيام بذلك، سيعتمد على طبقة [service]، والتي بدورها ستستدعي طبقة [DAO]. لقد قدمنا بالفعل الشاشات التي تديرها طبقة [web] (القسم 14.1). لوصف طبقة الويب، سنقدم ما يلي بالترتيب:

  • تكوينها
  • طرق العرض الخاصة بها
  • وحدة التحكم
  • بعض الاختبارات

14.8.1. تكوين تطبيق الويب

مشروع Eclipse للتطبيق هو كما يلي:

Image

  • في الحزمة [istia.st.mvc.personnes.web]، ستجد وحدة التحكم [Application].
  • توجد صفحات JSP/JSTL في [WEB-INF/views].
  • يحتوي المجلد [lib] على المكتبات الخارجية التي يتطلبها التطبيق. وهي مرئية في المجلد [Web App Libraries].

[web.xml]


ملف [web.xml] هو الملف الذي يستخدمه خادم الويب لتحميل التطبيق. ومحتواه كما يلي:


<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4"
    xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <display-name>mvc-personnes-01</display-name>
    <!--  ServletPersonne -->
    <servlet>
        <servlet-name>personnes</servlet-name>
        <servlet-class>
            istia.st.mvc.personnes.web.Application
        </servlet-class>
        <init-param>
            <param-name>urlEdit</param-name>
            <param-value>/WEB-INF/vues/edit.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlErreurs</param-name>
            <param-value>/WEB-INF/vues/erreurs.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlList</param-name>
            <param-value>/WEB-INF/vues/list.jsp</param-value>
        </init-param>
    </servlet>
    <!--  Mapping ServletPersonne-->
    <servlet-mapping>
        <servlet-name>personnes</servlet-name>
        <url-pattern>/do/*</url-pattern>
    </servlet-mapping>
    <!--  welcome files -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <!--  Unexpected error page -->
    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/WEB-INF/vues/exception.jsp</location>
    </error-page>
</web-app>
  • الأسطر 27-30: سيتم معالجة عناوين URL [/do/*] بواسطة سيرفلت [people]
  • الأسطر 9-12: خدمة [personnes] هي مثيل لفئة [Application]، وهي فئة سنقوم بإنشائها.
  • الأسطر 13-24: تعريف ثلاثة معلمات [urlList، urlEdit، urlErrors] تحدد عناوين URL لصفحات JSP الخاصة بعروض [list، edit، errors].
  • الأسطر 32–34: يحتوي التطبيق على صفحة دخول افتراضية [index.jsp] موجودة في جذر مجلد تطبيق الويب.
  • الأسطر 36–39: يحتوي التطبيق على صفحة خطأ افتراضية يتم عرضها عندما يواجه خادم الويب استثناءً لا يعالجه التطبيق.
    • السطر 37: تحدد العلامة <exception-type> نوع الاستثناء الذي تعالجه توجيهات <error-page>؛ وهنا، يكون النوع هو [java.lang.Exception] وأنواعه الفرعية، مما يعني جميع الاستثناءات.
    • السطر 38: تحدد العلامة <location> صفحة JSP التي سيتم عرضها عند حدوث استثناء من النوع المحدد بواسطة <exception-type>. يتوفر الاستثناء الذي حدث في هذه الصفحة في كائن باسم exception إذا كانت الصفحة تحتوي على التوجيه:

<%@ page isErrorPage="true" %>
  • (تابع)
    • إذا حددت <exception-type> نوعًا T1 وتم نشر استثناء من النوع T2 (غير مشتق من T1) إلى خادم الويب، فإن الخادم يرسل إلى العميل صفحة استثناء خاصة، والتي عادةً ما تكون غير سهلة الاستخدام. ومن هنا تأتي أهمية علامة <error-page> في ملف [web.xml].

[index.jsp]


يتم عرض هذه الصفحة إذا طلب المستخدم سياق التطبيق مباشرةً دون تحديد عنوان URL، أي هنا [/personnes-01]. ومحتواها كما يلي:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
 
<c:redirect url="/do/list"/>

[index.jsp] يعيد توجيه العميل إلى عنوان URL [/do/list]. يعرض عنوان URL هذا قائمة بالأشخاص الموجودين في المجموعة.

14.8.2. صفحات JSP/JSTL الخاصة بالتطبيق


عرض [ list.jsp]


يُستخدم لعرض قائمة الأشخاص:

Image

وإليك الكود الخاص بها:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>
 
<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
        <h2>Liste des personnes</h2>
        <table border="1">
            <tr>
                <th>Id</th>
                <th>Version</th>
                <th>Pr&eacute;nom</th>
                <th>Nom</th>
                <th>Date de naissance</th>
                <th>Mari&eacute;</th>
                <th>Nombre d'enfants</th>
                <th></th>
            </tr>
            <c:forEach var="personne" items="${personnes}">
                <tr>
                    <td><c:out value="${personne.id}"/></td>
                    <td><c:out value="${personne.version}"/></td>
                    <td><c:out value="${personne.prenom}"/></td>
                    <td><c:out value="${personne.nom}"/></td>
                    <td><dt:format pattern="dd/MM/yyyy">${personne.dateNaissance.time}</dt:format></td>
                    <td><c:out value="${personne.marie}"/></td>
                    <td><c:out value="${personne.nbEnfants}"/></td>
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>
 
  • تستقبل هذه الطريقة عنصرًا في قالبها:
  • عنصر [people] المرتبط بـ [ArrayList] من كائنات [Person]
  • الأسطر 22–34: نقوم بالتكرار عبر قائمة ${people} لعرض جدول HTML يحتوي على الأشخاص في المجموعة.
  • السطر 31: يتم تعيين عنوان URL الذي يشير إليه رابط [Edit] باستخدام حقل [id] للشخص الحالي بحيث يعرف وحدة التحكم المرتبطة بعنوان URL [/do/edit] الشخص الذي يجب تعديله.
  • السطر 32: يتم إجراء الأمر نفسه بالنسبة لرابط [Delete].
  • السطر 28: لعرض تاريخ ميلاد الشخص بتنسيق DD/MM/YYYY، نستخدم العلامة <dt> من مكتبة علامات [DateTime] التابعة لمشروع Apache [Jakarta Taglibs]:

Image

يتم تعريف ملف الوصف لمكتبة العلامات هذه في السطر 3.

  • السطر 37: يستهدف رابط [Add] لإضافة شخص جديد عنوان URL [/do/edit]، تمامًا مثل رابط [Edit] في السطر 31. تشير القيمة -1 للمعلمة [id] إلى أن هذه عملية إضافة وليست عملية تعديل.

عرض [ edit.jsp]


يُستخدم لعرض النموذج الخاص بإضافة شخص جديد أو تعديل شخص موجود:

فيما يلي كود عرض [edit.jsp]:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>
 
<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="../ressources/standard.jpg">
        <h2>Ajout/Modification d'une personne</h2>
        <c:if test="${erreurEdit != ''}">
            <h3>Echec de la mise à jour :</h3>
          L'erreur suivante s'est produite : ${erreurEdit}
            <hr>
        </c:if>
        <form method="post" action="<c:url value="/do/validate"/>">
            <table border="1">
                <tr>
                    <td>Id</td>
                    <td>${id}</td>
                </tr>
                <tr>
                    <td>Version</td>
                    <td>${version}</td>
                </tr>
                <tr>
                    <td>Pr&eacute;nom</td>
                    <td>
                        <input type="text" value="${prenom}" name="prenom" size="20">
                    </td>
                    <td>${erreurPrenom}</td>
                </tr>
                <tr>
                    <td>Nom</td>
                    <td>
                        <input type="text" value="${nom}" name="nom" size="20">
                    </td>
                    <td>${erreurNom}</td>
                </tr>
                <tr>
                <td>Date de naissance (JJ/MM/AAAA)</td>
                    <td>
                        <input type="text" value="${dateNaissance}" name="dateNaissance">
                    </td>
                    <td>${erreurDateNaissance}</td>
                </tr>
                <tr>
                    <td>Mari&eacute;</td>
                    <td>
                        <c:choose>
                            <c:when test="${marie}">
                                <input type="radio" name="marie" value="true" checked>Oui
                                <input type="radio" name="marie" value="false">Non
                            </c:when>
                            <c:otherwise>
                                <input type="radio" name="marie" value="true">Oui
                                <input type="radio" name="marie" value="false" checked>Non
                            </c:otherwise>
                        </c:choose>
                    </td>
                </tr>
                <tr>
                    <td>Nombre d'enfants</td>
                    <td>
                        <input type="text" value="${nbEnfants}" name="nbEnfants">
                    </td>
                    <td>${erreurNbEnfants}</td>
                </tr>
            </table>
            <br>
            <input type="hidden" value="${id}" name="id">
      <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
            <a href="<c:url value="/do/list"/>">Annuler</a>
        </form>
    </body>
</html>

تعرض هذه الصفحة نموذجًا لإضافة شخص جديد أو تحديث شخص موجود. من الآن فصاعدًا، لتبسيط النص، سنستخدم مصطلحًا واحدًا هو [تحديث]. يؤدي زر [Submit] (السطر 73) إلى إرسال طلب POST إلى عنوان URL [/do/validate] (السطر 16). إذا فشل طلب POST، يتم إعادة عرض طريقة العرض [edit.jsp] مع الأخطاء التي حدثت؛ وإلا، يتم عرض طريقة العرض [list.jsp].

  • تستقبل طريقة العرض [edit.jsp]، التي يتم عرضها في كل من طلب GET وطلب POST الفاشل، العناصر التالية في نموذجها:
السمة
GET
POST
id
معرف الشخص الذي يتم تحديثه
نفس
الإصدار
إصداره
نفس
الاسم الأول
الاسم الأول
الاسم الأول الذي تم إدخاله
اللقب
اللقب
تم إدخال اسم العائلة
تاريخ الميلاد
تاريخ ميلاده/ميلادها
تم إدخال تاريخ الميلاد
متزوج
الحالة الاجتماعية
تم إدخال الحالة الاجتماعية
عدد الأطفال
عدد الأطفال
تم إدخال عدد الأطفال
خطأ تحرير
فارغ
رسالة خطأ تشير إلى فشل الإضافة أو التعديل أثناء عملية POST التي تم تشغيلها بواسطة زر [إرسال]. فارغ في حالة عدم وجود خطأ.
errorFirstName
فارغ
يشير إلى اسم أول غير صحيح – فارغ في الحالات الأخرى
errorName
فارغ
يبلغ عن اسم غير صحيح – فارغ في الحالات الأخرى
خطأ_تاريخ_الميلاد
فارغ
يشير إلى تاريخ ميلاد غير صحيح – فارغ في الحالات الأخرى
خطأ_عدد_الأطفال
فارغ
يشير إلى عدد غير صحيح للأطفال – فارغ في الحالات الأخرى
  • الأسطر 11-15: إذا فشل إرسال النموذج (POST)، فسيتم إرجاع [errorEdit!=''] وسيتم عرض رسالة خطأ.
  • السطر 16: سيتم إرسال النموذج إلى عنوان URL [/do/validate]
  • السطر 20: يتم عرض عنصر [id] في القالب
  • السطر 24: يتم عرض عنصر [version] من القالب
  • الأسطر 26-32: إدخال الاسم الأول للشخص:
    • عند عرض النموذج لأول مرة (GET)، يعرض ${firstName} القيمة الحالية لحقل [firstName] في كائن [Person] المحدث، ويكون ${firstNameError} فارغًا.
    • في حالة حدوث خطأ بعد POST، يتم عرض القيمة المدخلة ${firstName} مرة أخرى، إلى جانب أي رسالة خطأ ${firstNameError}
  • الأسطر 33-39: إدخال اسم العائلة للشخص
  • الأسطر 40–46: إدخال تاريخ ميلاد الشخص
  • الأسطر 47–61: إدخال الحالة الاجتماعية للشخص باستخدام زر اختيار. نستخدم قيمة حقل [married] الخاص بكائن [Person] لتحديد أي من زري الاختيار يجب تحديده.
  • الأسطر 62-68: إدخال عدد أطفال الشخص
  • السطر 71: حقل HTML مخفي باسم [id] بقيمة تساوي حقل [id] للشخص الذي يتم تحديثه، -1 للإضافة، أو قيمة أخرى للتعديل.
  • السطر 72: حقل HTML مخفي باسم [version] بقيمة تساوي حقل [id] للشخص الذي يتم تحديثه.
  • السطر 73: زر [إرسال] الخاص بالنموذج
  • السطر 74: رابط للعودة إلى قائمة الأشخاص. يحمل اسم [Cancel] لأنه يسمح للمستخدم بالخروج من النموذج دون إرساله.

عرض [ exception.jsp]


يُستخدم لعرض صفحة تشير إلى حدوث استثناء لم يتم التعامل معه من قبل التطبيق وتم تمريره إلى خادم الويب.

على سبيل المثال، دعونا نحذف شخصًا غير موجود في المجموعة:

فيما يلي كود عرض [exception.jsp]:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ page isErrorPage="true" %>
 
<%
  response.setStatus(200);
%>
 
<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
        <h2>MVC - personnes</h2>
        L'exception suivante s'est produite :
        <%= exception.getMessage()%>
        <br><br>
        <a href="<c:url value="/do/list"/>">Retour &agrave; la liste</a>
    </body>
</html>
 
  • تتلقى هذه العرضة مفتاحًا في قالبها، وهو عنصر [exception]، الذي يمثل الاستثناء الذي اعترضه خادم الويب. لكي يقوم خادم الويب بتضمين هذا العنصر في قالب صفحة JSP، يجب أن تكون الصفحة قد عرّفت العلامة في السطر 3.
  • السطر 6: نضبط رمز حالة HTTP للاستجابة على 200. هذا هو رأس HTTP الأول للاستجابة. يشير رمز الحالة 200 إلى العميل بأن طلبه قد نجح. عادةً ما يتم تضمين مستند HTML في استجابة الخادم. وهذا هو الحال هنا. إذا لم يتم ضبط رمز حالة HTTP للاستجابة على 200، فسيكون له القيمة 500، مما يعني حدوث خطأ. في الواقع، عندما يعترض خادم الويب استثناءً لم يتم التعامل معه، فإنه يعتبر هذه الحالة غير طبيعية ويشير إليها برمز 500. يختلف الرد على رمز HTTP 500 باختلاف المتصفح: يعرض Firefox مستند HTML الذي قد يصاحب هذا الرد، بينما يتجاهل IE هذا المستند ويعرض صفحته الخاصة. لهذا السبب استبدلنا الرمز 500 بالرمز 200.
  • السطر 16: يتم عرض نص الاستثناء
  • السطر 18: يُعرض على المستخدم رابط للعودة إلى قائمة الأشخاص

عرض [ -errors .jsp]


يُستخدم لعرض صفحة تُبلغ عن أخطاء تهيئة التطبيق، أي الأخطاء التي يتم اكتشافها أثناء تنفيذ طريقة [init] في سيرفلت وحدة التحكم. وقد يكون ذلك، على سبيل المثال، عدم وجود معلمة في ملف [web.xml]، كما هو موضح في المثال التالي:

Image

فيما يلي كود صفحة [errors.jsp]:


<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
 
<html>
    <head>
      <title>MVC - Personnes</title>
  </head>
  <body>
      <h2>Les erreurs suivantes se sont produites</h2>
    <ul>
            <c:forEach var="erreur" items="${erreurs}">
                <li>${erreur}</li>
            </c:forEach>
    </ul>
  </body>
</html>

تستقبل الصفحة عنصر [errors] في قالبها، وهو عبارة عن [ArrayList] من كائنات [String]؛ وهذه هي رسائل الخطأ. يتم عرضها بواسطة الحلقة في الأسطر 13–15.

14.8.3. وحدة التحكم في التطبيق

يتم تعريف وحدة التحكم [Application] في الحزمة [istia.st.mvc.personnes.web]:

Image


هيكل وحدة التحكم وتهيئتها


هيكل وحدة التحكم [Application] هو كما يلي:

package istia.st.mvc.personnes.web;

import istia.st.mvc.personnes.dao.DaoException;
...

@SuppressWarnings("serial")
public class Application extends HttpServlet {
    // instance parameters
    private String urlErreurs = null;
    private ArrayList erreursInitialisation = new ArrayList<String>();
    private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
    private Map params = new HashMap<String, String>();

    // service
    ServiceImpl service=null;

    // init
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        // retrieve servlet initialization parameters
        ServletConfig config = getServletConfig();
        // other initialization parameters are processed
        String valeur = null;
        for (int i = 0; i < paramètres.length; i++) {
            // parameter value
            valeur = config.getInitParameter(paramètres[i]);
            // present parameter?
            if (valeur == null) {
                // we note the error
                erreursInitialisation.add("Le paramètre [" + paramètres[i]
                        + "] n'a pas été initialisé");
            } else {
                // parameter value is stored
                params.put(paramètres[i], valeur);
            }
        }
        // the [errors] view url has a special treatment
        urlErreurs = config.getInitParameter("urlErreurs");
        if (urlErreurs == null)
            throw new ServletException(
                    "Le paramètre [urlErreurs] n'a pas été initialisé");
        // dao] layer instantiation
        DaoImpl dao = new DaoImpl();
        dao.init();
        // instantiation of the [service] layer
        service = new ServiceImpl();
        service.setDao(dao);
    }

    // GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
....
    }

    // display list of persons
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

    // modify / add a person
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

    // validation modification / addition of a person
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

    // validation modification / addition of a person
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

    // display pre-filled form
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit) throws ServletException, IOException{
...
    }

    // post
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        // we hand over to GET
        doGet(request, response);    }
}
  • الأسطر 20–36: استرداد المعلمات المحددة في ملف [web.xml].
  • الأسطر 39–41: يجب أن تكون المعلمة [urlErrors] موجودة لأنها تحدد عنوان URL لعرض [errors]، الذي يعرض أي أخطاء في التهيئة. إذا لم تكن موجودة، يتم إنهاء التطبيق عن طريق إلقاء استثناء [ServletException] (السطر 40). سيتم نشر هذا الاستثناء إلى خادم الويب ومعالجته بواسطة العلامة <error-page> في ملف [web.xml]. وبالتالي يتم عرض عرض [exception.jsp]:

Image

الرابط [Back to list] أعلاه غير نشط. يؤدي النقر عليه إلى إرجاع نفس الاستجابة طالما لم يتم تعديل التطبيق وإعادة تحميله. وهو مفيد لأنواع أخرى من الاستثناءات، كما رأينا سابقًا.

  • السطر 43: ينشئ مثيل [DaoImpl] الذي ينفذ طبقة [dao]
  • السطر 44: تهيئة هذه المثيل (إنشاء قائمة أولية تضم ثلاثة أشخاص)
  • السطر 46: ينشئ مثيلًا لـ [ServiceImpl] ينفذ طبقة [service]
  • السطر 47: تهيئة طبقة [service] من خلال تزويدها بإشارة إلى طبقة [dao]

بعد تهيئة وحدة التحكم، تحتوي طرقها على مرجع [service] إلى طبقة [service] (السطر 15) الذي ستستخدمه لتنفيذ الإجراءات التي يطلبها المستخدم. سيتم اعتراض هذه الإجراءات بواسطة الطريقة [doGet]، والتي ستقوم بمعالجتها بواسطة طريقة محددة في وحدة التحكم:

Url
طريقة HTTP
طريقة وحدة التحكم
/do/list
GET
doListPeople
/do/edit
GET
doEditPerson
/do/validate
POST
doValidatePerson
/do/delete
GET
doDeletePerson

طريقة [doGet]


الغرض من هذه الطريقة هو توجيه معالجة الإجراءات التي يطلبها المستخدم إلى الطريقة الصحيحة. وفيما يلي كودها:

// GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {

        // check how the servlet was initialized
        if (erreursInitialisation.size() != 0) {
            // we hand over to the error page
            request.setAttribute("erreurs", erreursInitialisation);
            getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
            // end
            return;
        }
        // retrieve the request sending method
        String méthode = request.getMethod().toLowerCase();
        // retrieve the action to be executed
        String action = request.getPathInfo();
        // action?
        if (action == null) {
            action = "/list";
        }
        // execution action
        if (méthode.equals("get") && action.equals("/list")) {
            // list of persons
            doListPersonnes(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/delete")) {
            // deleting a person
            doDeletePersonne(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/edit")) {
            // presentation form add / modify a person
            doEditPersonne(request, response);
            return;
        }
        if (méthode.equals("post") && action.equals("/validate")) {
            // validation form add / modify a person
            doValidatePersonne(request, response);
            return;
        }
        // other cases
        doListPersonnes(request, response);
    }
  • الأسطر 7–13: نتحقق من أن قائمة أخطاء التهيئة فارغة. إذا لم تكن كذلك، نعرض طريقة العرض [errors(errors)]، والتي ستبلغ عن الخطأ (الأخطاء).
  • السطر 15: نسترد طريقة [get] أو [post] التي استخدمها العميل لإجراء الطلب.
  • السطر 17: نسترد قيمة المعلمة [action] من الطلب.
  • الأسطر 23–27: معالجة طلب [GET /do/list]، الذي يطلب قائمة الأشخاص.
  • الأسطر 28–32: معالجة طلب [GET /do/delete]، الذي يطلب حذف شخص.
  • الأسطر 33-37: معالجة طلب [GET /do/edit]، الذي يطلب النموذج لتحديث شخص.
  • الأسطر 38–42: معالجة طلب [POST /do/validate]، الذي يطلب التحقق من صحة الشخص الذي تم تحديثه.
  • السطر 44: إذا لم يكن الإجراء المطلوب أحد الإجراءات الخمسة السابقة، فإننا نتعامل معه كما لو كان [GET /do/list].

طريقة [doListPersonnes]


تتعامل هذه الطريقة مع طلب [GET /do/list]، الذي يطلب قائمة الأشخاص:

Image

وإليك كودها:

1
2
3
4
5
6
7
8
9
    // display list of persons
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // the [list] view model
        request.setAttribute("personnes", service.getAll());
        // list] view display
        getServletContext()
                .getRequestDispatcher((String) params.get("urlList")).forward(request, response);
    }
  • السطر 5: نطلب قائمة الأشخاص في المجموعة من طبقة [service] ونخزنها في النموذج تحت المفتاح "people".
  • السطر 7: يتم عرض طريقة العرض [list.jsp] الموضحة في القسم 14.8.2.

طريقة [doDeletePerson]


تتعامل هذه الطريقة مع طلب [GET /do/delete?id=XX]، الذي يطلب حذف الشخص ذي المعرف id=XX. عنوان URL [/do/delete?id=XX] هو عنوان روابط [Delete] في عرض [list.jsp]:

Image

والذي يكون كوده كما يلي:

...
<html>
    <head>
        <title>MVC - personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

يُظهر السطر 12 عنوان URL [/do/delete?id=XX] للرابط [حذف]. يجب أن تقوم الطريقة [doDeletePerson]، التي تتعامل مع عنوان URL هذا، بحذف الشخص الذي يحمل المعرف id=XX ثم عرض القائمة المحدثة للأشخاص في المجموعة. وفيما يلي كودها:

    // validation modification / addition of a person
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // retrieve the person's id
        int id = Integer.parseInt(request.getParameter("id"));
        // we delete the person
        service.deleteOne(id);
        // redirects to the list of persons
        response.sendRedirect("list");
    }
  • السطر 5: عنوان URL الذي تتم معالجته هو [/do/delete?id=XX]. نسترد القيمة [XX] من المعلمة [id].
  • السطر 7: نطلب من طبقة [service] حذف الشخص الذي يحمل المعرف الذي تم الحصول عليه. لا نقوم بأي عملية تحقق من الصحة. إذا كان الشخص الذي نحاول حذفه غير موجود، فإن طبقة [dao] ترمي استثناءً ينتقل إلى طبقة [service]. ولا نتعامل معه هنا في وحدة التحكم أيضًا. وبالتالي سينتقل إلى خادم الويب، والذي، حسب التكوين، سيعرض صفحة [exception.jsp]، الموصوفة في القسم 14.8.2:

Image

  • السطر 9: إذا نجح الحذف (لم تحدث استثناءات)، يتم إعادة توجيه العميل إلى عنوان URL النسبي [list]. وبما أن عنوان URL الذي تمت معالجته للتو كان [/do/delete]، فإن عنوان URL لإعادة التوجيه سيكون [/do/list]. وبالتالي، سيقوم المتصفح بتنفيذ طلب [GET /do/list]، والذي سيعرض قائمة الأشخاص.

طريقة [doEditPerson]


تتعامل هذه الطريقة مع طلب [GET /do/edit?id=XX]، الذي يطلب من النموذج تحديث الشخص ذي المعرف id=XX. عنوان URL [/do/edit?id=XX] هو العنوان المستخدم لروابط [Edit] و [Add] في عرض [list.jsp]:

Image

والذي يكون كوده كما يلي:

...
<html>
    <head>
        <title>MVC - personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

في السطر 11، نرى عنوان URL [/do/edit?id=XX] لرابط [تحرير]، وفي السطر 17، عنوان URL [/do/edit?id=-1] لرابط [إضافة]. يجب أن تعرض طريقة [doEditPersonne] نموذج التحرير للشخص الذي يحمل المعرف id=XX، أو إذا كانت عملية إضافة، فيجب أن تعرض نموذجًا فارغًا.

فيما يلي كود طريقة [doEditPerson]:

    // modify / add a person
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // retrieve the person's id
        int id = Integer.parseInt(request.getParameter("id"));
        // addition or modification?
        Personne personne = null;
        if (id != -1) {
            // modification - the person to be modified is retrieved
            personne = service.getOne(id);
        } else {
            // add - create an empty person
            personne = new Personne();
            personne.setId(-1);
        }
        // we put the [Person] object in the [edit] view model
        request.setAttribute("erreurEdit", "");
        request.setAttribute("id", personne.getId());
        request.setAttribute("version", personne.getVersion());
        request.setAttribute("prenom", personne.getPrenom());
        request.setAttribute("nom", personne.getNom());
        Date dateNaissance = personne.getDateNaissance();
        if (dateNaissance != null) {
            request.setAttribute("dateNaissance", new SimpleDateFormat(
                    "dd/MM/yyyy").format(dateNaissance));
        } else {
            request.setAttribute("dateNaissance", "");
        }
        request.setAttribute("marie", personne.getMarie());
        request.setAttribute("nbEnfants", personne.getNbEnfants());
        // view display [edit]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • يستهدف طلب GET عنوان URL بالشكل [/do/edit?id=XX]. في السطر 5، نسترد قيمة [id]. ثم هناك حالتان:
  1. إذا لم تكن قيمة id تساوي -1، فهذا يعني أنه تحديث، وعلينا عرض نموذج مملوء مسبقًا بمعلومات الشخص المراد تعديله. في السطر 10، يتم طلب هذا الشخص من طبقة [service].
  2. إذا كانت id تساوي -1، فهذا يعد إضافة، ويجب عرض نموذج فارغ. للقيام بذلك، يتم إنشاء شخص فارغ في السطرين 13-14.
  • يتم وضع كائن [Person] في قالب الصفحة [edit.jsp] الموصوف في القسم 14.8.2. يتضمن هذا القالب العناصر التالية: [errorEdit, id, version, firstName, errorFirstName, lastName, errorLastName, dateOfBirth, errorDateOfBirth, spouse, numberOfChildren, errorNumberOfChildren]. يتم تهيئة هذه العناصر في الأسطر 17-30، باستثناء تلك التي تكون قيمتها سلسلة فارغة [firstNameError، lastNameError، birthDateError، childrenCountError]. ونعلم أنه في حالة عدم وجودها في القالب، ستعرض مكتبة JSTL سلسلة فارغة كقيمة لها. وعلى الرغم من أن عنصر [errorEdit] يحتوي أيضًا على سلسلة فارغة كقيمة له، إلا أنه يتم تهيئته لأن يتم إجراء فحص لقيمته في صفحة [edit.jsp].
  • بمجرد أن يصبح النموذج جاهزًا، يتم تمرير التحكم إلى صفحة [edit.jsp]، في السطرين 32 و33، والتي ستقوم بإنشاء عرض [edit].

طريقة [doValidatePersonne]


تتعامل هذه الطريقة مع طلب [POST /do/validate]، الذي يتحقق من صحة نموذج التحديث. يتم تشغيل هذا الطلب POST بواسطة زر [Validate]:

Image

دعونا نستعرض عناصر الإدخال في نموذج HTML في العرض أعلاه:

<form method="post" action="<c:url value="/do/validate"/>">
....
        <input type="text" value="${prenom}" name="prenom" size="20">
....
        <input type="text" value="${nom}" name="nom" size="20">
....
        <input type="text" value="${dateNaissance}" name="dateNaissance">
...
        <input type="radio" name="marie" value="true" checked>Oui
....
        <input type="text" value="${nbEnfants}" name="nbEnfants">
....
            <input type="hidden" value="${id}" name="id">
     <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
</form>

يحتوي طلب POST على المعلمات [firstName, lastName, dateOfBirth, spouse, numberOfChildren, id, version] ويتم إرساله إلى عنوان URL [/do/validate] (السطر 1). تتم معالجته بواسطة الطريقة [doValidatePerson] التالية:

// validation modification / addition of a person
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // retrieve posted items
        boolean formulaireErroné = false;
        boolean erreur;
        // first name
        String prenom = request.getParameter("prenom").trim();
        // valid first name?
        if (prenom.length() == 0) {
            // we note the error
            request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
            formulaireErroné = true;
        }
        // the name
        String nom = request.getParameter("nom").trim();
        // valid first name?
        if (nom.length() == 0) {
            // we note the error
            request.setAttribute("erreurNom", "Le nom est obligatoire");
            formulaireErroné = true;
        }
        // date of birth
        Date dateNaissance = null;
        try {
            dateNaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request
                    .getParameter("dateNaissance").trim());
        } catch (ParseException e) {
            // we note the error
            request.setAttribute("erreurDateNaissance", "Date incorrecte");
            formulaireErroné = true;
        }
        // marital status
        boolean marie = Boolean.parseBoolean(request.getParameter("marie"));
        // number of children
        int nbEnfants = 0;
        erreur = false;
        try {
            nbEnfants = Integer.parseInt(request.getParameter("nbEnfants")
                    .trim());
            if (nbEnfants < 0) {
                erreur = true;
            }
        } catch (NumberFormatException ex) {
            // we note the error
            erreur = true;
        }
        // wrong number of children?
        if (erreur) {
            // we report the error
            request.setAttribute("erreurNbEnfants",
                    "Nombre d'enfants incorrect");
            formulaireErroné = true;
        }
        // pERSON ID
        int id = Integer.parseInt(request.getParameter("id"));
        // version
        long version = Long.parseLong(request.getParameter("version"));
        // is the form incorrect?
        if (formulaireErroné) {
            // redisplay the form with error messages
            showFormulaire(request, response, "");
            // finish
            return;
        }
        // the form is correct - the person is registered
        Personne personne = new Personne(id, prenom, nom, dateNaissance, marie,
                nbEnfants);
        personne.setVersion(version);
        try {
            // registration
            service.saveOne(personne);
        } catch (DaoException ex) {
            // redisplay the form with the error message
            showFormulaire(request, response, ex.getMessage());
            // finish
            return;
        }
        // redirects to the list of persons
        response.sendRedirect("list");
    }

    // display pre-filled form
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit)
            throws ServletException, IOException {
        // prepare the view model [edit]
        request.setAttribute("erreurEdit", erreurEdit);
        request.setAttribute("id", request.getParameter("id"));
        request.setAttribute("version", request.getParameter("version"));
        request.setAttribute("prenom", request.getParameter("prenom").trim());
        request.setAttribute("nom", request.getParameter("nom").trim());
        request.setAttribute("dateNaissance", request.getParameter(
                "dateNaissance").trim());
        request.setAttribute("marie", request.getParameter("marie"));
        request.setAttribute("nbEnfants", request.getParameter("nbEnfants")
                .trim());
        // view display [edit]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • الأسطر 8-14: يتم استرداد المعلمة [firstName] من طلب POST والتحقق من صحتها. إذا كانت غير صحيحة، يتم تهيئة العنصر [firstNameError] برسالة خطأ ووضعه في سمات الطلب.
  • الأسطر 16–22: يتم اتباع نفس العملية لمعلمة [lastName]
  • الأسطر 24-32: يتم تطبيق نفس العملية على المعلمة [dateOfBirth]
  • السطر 34: يتم استرداد المعلمة [spouse]. لا نتحقق من صحتها لأنها، من حيث المبدأ، تأتي من قيمة زر اختيار. ومع ذلك، لا شيء يمنع برنامجًا من إرسال طلب [POST /people-01/do/validate] مصحوبًا بمعلمة [spouse] وهمية. لذلك يجب أن نختبر صحة هذه المعلمة. هنا، نعتمد على معالجة الاستثناءات لدينا، والتي تؤدي إلى عرض صفحة [exception.jsp] إذا لم يتعامل وحدة التحكم مع الاستثناء بنفسها. لذا، إذا فشل تحويل المعلمة [marie] إلى قيمة منطقية في السطر 34، فسيتم إلقاء استثناء، مما يؤدي إلى إرسال صفحة [exception.jsp] إلى العميل. هذا السلوك يناسبنا.
  • الأسطر 34-54: نسترد المعلمة [nbEnfants] ونتحقق من قيمتها.
  • السطر 56: نسترد المعلمة [id] دون التحقق من قيمتها
  • السطر 58: نفعل الشيء نفسه مع المعلمة [version]
  • الأسطر 60-65: إذا كان النموذج غير صالح، يتم إعادة عرضه مع رسائل الخطأ التي تم إنشاؤها مسبقًا
  • الأسطر 67-69: إذا كان صالحًا، نقوم بإنشاء كائن [Person] جديد باستخدام حقول النموذج
  • الأسطر 70-78: يتم حفظ الشخص. قد تفشل عملية الحفظ. في بيئة متعددة المستخدمين، قد يكون الشخص المراد تعديله قد تم حذفه أو تعديله بالفعل بواسطة شخص آخر. في هذه الحالة، ستقوم طبقة [dao] بإلقاء استثناء، والذي نتعامل معه هنا.
  • السطر 80: إذا لم تحدث أي استثناءات، يتم إعادة توجيه العميل إلى عنوان URL [/do/list] لعرض الحالة الجديدة للمجموعة.
  • السطر 75: إذا حدث استثناء أثناء الحفظ، نطلب إعادة عرض النموذج الأولي، مع تمرير رسالة خطأ الاستثناء إليه (المعلمة الثالثة).

تقوم طريقة [showFormulaire] (الأسطر 84–101) بإنشاء القالب المطلوب لصفحة [edit.jsp] باستخدام القيم المدخلة (request.getParameter(" ... ")). تذكر أن رسائل الخطأ قد تمت إضافتها بالفعل إلى القالب بواسطة طريقة [doValidatePersonne]. يتم عرض صفحة [edit.jsp] في الأسطر 99–100.

14.9. اختبار تطبيق الويب

تم عرض عدد من الاختبارات في القسم 14.1. ندعو القارئ إلى إجرائها مرة أخرى. نعرض هنا لقطات شاشة إضافية توضح حالات تعارض الوصول إلى البيانات في بيئة متعددة المستخدمين:

سيكون [Firefox] هو متصفح المستخدم U1. يطلب المستخدم U1 عنوان URL [http://localhost:8080/personnes-01]:

Image

سيكون [IE] متصفح المستخدم U2. يطلب المستخدم U2 نفس عنوان URL:

Image

يبدأ المستخدم U1 في تعديل السجل الخاص بـ [Lemarchand]:

Image

يقوم المستخدم U2 بنفس الشيء:

Image

يقوم المستخدم U1 بإجراء تغييرات وحفظها:

يقوم المستخدم U2 بنفس الشيء:

يعود المستخدم U2 إلى قائمة الأشخاص الذين يستخدمون رابط [إلغاء] في النموذج:

Image

ويجد الشخص [Lemarchand] كما عدّله U1. والآن يقوم U2 بحذف [Lemarchand]:

لا يزال لدى U1 قائمته الخاصة ويريد تعديل [Lemarchand] مرة أخرى:

يستخدم U1 رابط [العودة إلى القائمة] لمعرفة ما يجري:

Image

يكتشف أن [Lemarchand] لم يعد موجودًا بالفعل في القائمة...

14.10. الخلاصة

لقد قمنا بتطبيق بنية MVC ضمن بنية ثلاثية المستويات [الويب، منطق الأعمال، DAO] باستخدام مثال بسيط لإدارة قائمة بالأشخاص. وقد سمح لنا ذلك بتطبيق المفاهيم التي تم عرضها في الأقسام السابقة. في النسخة التي قمنا بفحصها، تم الاحتفاظ بقائمة الأشخاص في الذاكرة. وسنستكشف قريبًا نسخًا يتم فيها تخزين هذه القائمة في جدول قاعدة بيانات.

ولكن أولاً، سنقدم أداة تسمى Spring IoC، والتي تسهل تكامل الطبقات المختلفة لتطبيق متعدد الطبقات.