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

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



![]() |
![]() |
14.2. مشروع Eclipse
اسم مشروع التطبيق هو [people-01]:

يغطي هذا المشروع الطبقات الثلاث لهيكل التطبيق ثلاثي الطبقات:
![]() |
- توجد طبقة [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]:
![]()
فئة [Person] هي كما يلي:
- يتم تحديد هوية الشخص من خلال المعلومات التالية:
- 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] من الفئات والواجهات التالية:
![]()
- [IDao] هي الواجهة التي تقدمها طبقة [dao]
- [DaoImpl] هي تطبيق لهذه الواجهة حيث يتم تغليف مجموعة الأشخاص في كائن [ArrayList]
- [DaoException] هو نوع من الاستثناءات غير المحددة التي تطلقها طبقة [dao]
- تحتوي الواجهة على أربع طرق للعمليات الأربع التي نريد تنفيذها على مجموعة الأشخاص:
- getAll: لاسترداد مجموعة من الأشخاص
- getOne: لاسترداد شخص بمعرف محدد
- saveOne: لإضافة شخص (id=-1) أو تعديل شخص موجود (id ≠ -1)
- deleteOne: لحذف شخص برقم تعريف محدد
قد تطلق طبقة [DAO] استثناءات. وستكون هذه من النوع [ DaoException]:
- السطر 3: فئة [DaoException]، التي تنحدر من [RuntimeException]، هي نوع استثناء غير معالج: لا يطلب منا المُجمِّع:
- معالجة هذا النوع من الاستثناءات باستخدام كتلة try/catch عند استدعاء دالة قد ترمي هذا الاستثناء
- تضمين الكلمة الرئيسية "throws DaoException" في توقيع الأسلوب الذي قد يرمي الاستثناء
تمنعنا هذه التقنية من الاضطرار إلى توقيع طرق واجهة [IDao] باستثناءات من نوع معين. وبالتالي، سيكون أي تنفيذ يرمي استثناءات غير محددة مقبولاً، مما يضفي مرونة على البنية.
- السطر 6: رمز خطأ. ستقوم طبقة [dao] بإلقاء استثناءات متنوعة يتم تحديدها برموز أخطاء مختلفة. سيسمح هذا للطبقة المسؤولة عن معالجة الاستثناء بتحديد المصدر الدقيق للخطأ واتخاذ الإجراء المناسب. هناك طرق أخرى لتحقيق نفس النتيجة. إحدى هذه الطرق هي إنشاء نوع استثناء لكل نوع خطأ محتمل، على سبيل المثال MissingLastNameException، MissingFirstNameException، IncorrectAgeException، ...
- الأسطر 13–16: المنشئ الذي يسمح لك بإنشاء استثناء يتم تحديده بواسطة رمز خطأ ورسالة خطأ.
- الأسطر 8–10: الطريقة التي تسمح لمعالج الاستثناء باسترداد رمز الخطأ.
تنفذ الفئة [ DaoImpl] واجهة [IDao]:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | |
سنكتفي بشرح هذا الكود بشكل عام. ومع ذلك، سنخصص بعض الوقت للأجزاء الأكثر تعقيدًا.
- السطر 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]. نقدم هنا اثنين منها فقط؛ وندعو القراء لاستكشاف الاختبارات الأخرى في شفرة المصدر المرتبطة بهذا المقال.
- السطر 9: إشارة إلى تنفيذ طبقة [dao] قيد الاختبار
- الأسطر 12–15: منشئ اختبار JUnit. يقوم بإنشاء مثيل من النوع [DaoImpl] من طبقة [dao] المراد اختبارها وتهيئته.
تختبر الطريقة [test1] الطرق الأربع لواجهة [IDao] على النحو التالي:
- السطر 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]. تذكر أن هذه الطرق لم تتم مزامنتها. فيما يلي كود الاختبار:
- الأسطر 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]. يحتوي منشئ هذا النوع على ثلاثة معلمات:
- الاسم الممنوح للخيط، ويُستخدم لتتبعه عبر السجلات
- إشارة إلى طبقة [dao] حتى يتمكن الخيط من الوصول إليها
- معرف الشخص الذي من المفترض أن يعمل عليه الخيط
نوع [ThreadDaoMajEnfants] هو كما يلي:
- السطر 9: [ThreadDaoMajEnfants] هو بالفعل مؤشر ترابط
- الأسطر 18–22: المنشئ الذي يقوم بتهيئة الخيط بثلاث معلومات
- الاسم [name] الممنوح للخيط
- مرجع [dao] إلى طبقة [dao]. لاحظ مرة أخرى أننا نعمل مع نوع الواجهة [IDao] وليس نوع التنفيذ [DaoImpl].
- المعرف [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).
- تقوم طريقة [test4] بإنشاء 100 مؤشر ترابط (السطر 8، القسم 14.5).
تم الحصول على النتائج التالية:

نجحت جميع الاختبارات الخمسة.
في التكوين الثاني الذي تم اختباره:
- تم إلغاء تعليق الأمر "wait" في طريقة [saveOne] في [DaoImpl] (السطر 83، القسم 14.4).
- تقوم طريقة [test4] بإنشاء مؤشرين (السطر 8، القسم 14.5).
تم الحصول على النتائج التالية:
![]() | ![]() |
فشل اختبار [test4]. أنشأنا خيطين، كل منهما مكلف بزيادة عدد أبناء الشخص P، الذي كان لديه في البداية 0، بمقدار 1. لذلك توقعنا أن يكون هناك طفلان بعد تشغيل الخيطين، لكن لدينا طفل واحد فقط.
دعونا نفحص سجلات الشاشة من [test4] لفهم ما حدث:
- السطر 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):
- نفذ الخيط #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] من الفئات والواجهات التالية:
![]()
- [IService] هي الواجهة التي تعرضها طبقة [dao]
- [ServiceImpl] هي تنفيذ لهذه الواجهة
واجهة [IService] هي كما يلي:
وهي مطابقة لواجهة [IDao].
فيما يلي تنفيذ [ServiceImpl] لواجهة [IService]:
- الأسطر 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] هو كما يلي:
- السطر 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]:
- السطر 12: يعمل الخيط مع طبقة [service]
نقوم بتشغيل الاختبارات باستخدام التكوين الذي تسبب في حدوث مشكلات في طبقة [dao]:
- نقوم بإلغاء تعليق عبارة الانتظار في طريقة [saveOne] في [DaoImpl] (السطر 83، القسم 14.4).
- تقوم طريقة [test4] بإنشاء 100 مؤشر ترابط (السطر 65، القسم 14.7).
النتائج التي تم الحصول عليها هي كما يلي:
![]() |
كان تزامن الأساليب في طبقة [service] هو ما مكن من نجاح اختبار [test4].
14.8. طبقة [الويب]
دعونا نستعرض بنية التطبيق المكونة من 3 طبقات:
![]() |
ستوفر طبقة [الويب] شاشات للمستخدم لتمكينه من إدارة مجموعة الأشخاص:
- قائمة الأشخاص في المجموعة
- إضافة شخص إلى المجموعة
- تحرير معلومات شخص في المجموعة
- إزالة شخص من المجموعة
للقيام بذلك، سيعتمد على طبقة [service]، والتي بدورها ستستدعي طبقة [DAO]. لقد قدمنا بالفعل الشاشات التي تديرها طبقة [web] (القسم 14.1). لوصف طبقة الويب، سنقدم ما يلي بالترتيب:
- تكوينها
- طرق العرض الخاصة بها
- وحدة التحكم
- بعض الاختبارات
14.8.1. تكوين تطبيق الويب
مشروع Eclipse للتطبيق هو كما يلي:

- في الحزمة [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 الخاصة بالتطبيق
يُستخدم لعرض قائمة الأشخاص:

وإليك الكود الخاص بها:
<%@ 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énom</th>
<th>Nom</th>
<th>Date de naissance</th>
<th>Marié</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]:

يتم تعريف ملف الوصف لمكتبة العلامات هذه في السطر 3.
- السطر 37: يستهدف رابط [Add] لإضافة شخص جديد عنوان URL [/do/edit]، تمامًا مثل رابط [Edit] في السطر 31. تشير القيمة -1 للمعلمة [id] إلى أن هذه عملية إضافة وليست عملية تعديل.
يُستخدم لعرض النموذج الخاص بإضافة شخص جديد أو تعديل شخص موجود:
![]() |
فيما يلي كود عرض [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é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é</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 |
معرف الشخص الذي يتم تحديثه | نفس | |
إصداره | نفس | |
الاسم الأول | الاسم الأول الذي تم إدخاله | |
اللقب | تم إدخال اسم العائلة | |
تاريخ ميلاده/ميلادها | تم إدخال تاريخ الميلاد | |
الحالة الاجتماعية | تم إدخال الحالة الاجتماعية | |
عدد الأطفال | تم إدخال عدد الأطفال | |
فارغ | رسالة خطأ تشير إلى فشل الإضافة أو التعديل أثناء عملية POST التي تم تشغيلها بواسطة زر [إرسال]. فارغ في حالة عدم وجود خطأ. | |
فارغ | يشير إلى اسم أول غير صحيح – فارغ في الحالات الأخرى | |
فارغ | يبلغ عن اسم غير صحيح – فارغ في الحالات الأخرى | |
فارغ | يشير إلى تاريخ ميلاد غير صحيح – فارغ في الحالات الأخرى | |
فارغ | يشير إلى عدد غير صحيح للأطفال – فارغ في الحالات الأخرى |
- الأسطر 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]:
<%@ 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 à 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: يُعرض على المستخدم رابط للعودة إلى قائمة الأشخاص
يُستخدم لعرض صفحة تُبلغ عن أخطاء تهيئة التطبيق، أي الأخطاء التي يتم اكتشافها أثناء تنفيذ طريقة [init] في سيرفلت وحدة التحكم. وقد يكون ذلك، على سبيل المثال، عدم وجود معلمة في ملف [web.xml]، كما هو موضح في المثال التالي:

فيما يلي كود صفحة [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]:
![]()
هيكل وحدة التحكم [Application] هو كما يلي:
- الأسطر 20–36: استرداد المعلمات المحددة في ملف [web.xml].
- الأسطر 39–41: يجب أن تكون المعلمة [urlErrors] موجودة لأنها تحدد عنوان URL لعرض [errors]، الذي يعرض أي أخطاء في التهيئة. إذا لم تكن موجودة، يتم إنهاء التطبيق عن طريق إلقاء استثناء [ServletException] (السطر 40). سيتم نشر هذا الاستثناء إلى خادم الويب ومعالجته بواسطة العلامة <error-page> في ملف [web.xml]. وبالتالي يتم عرض عرض [exception.jsp]:

الرابط [Back to list] أعلاه غير نشط. يؤدي النقر عليه إلى إرجاع نفس الاستجابة طالما لم يتم تعديل التطبيق وإعادة تحميله. وهو مفيد لأنواع أخرى من الاستثناءات، كما رأينا سابقًا.
- السطر 43: ينشئ مثيل [DaoImpl] الذي ينفذ طبقة [dao]
- السطر 44: تهيئة هذه المثيل (إنشاء قائمة أولية تضم ثلاثة أشخاص)
- السطر 46: ينشئ مثيلًا لـ [ServiceImpl] ينفذ طبقة [service]
- السطر 47: تهيئة طبقة [service] من خلال تزويدها بإشارة إلى طبقة [dao]
بعد تهيئة وحدة التحكم، تحتوي طرقها على مرجع [service] إلى طبقة [service] (السطر 15) الذي ستستخدمه لتنفيذ الإجراءات التي يطلبها المستخدم. سيتم اعتراض هذه الإجراءات بواسطة الطريقة [doGet]، والتي ستقوم بمعالجتها بواسطة طريقة محددة في وحدة التحكم:
Url | طريقة HTTP | طريقة وحدة التحكم |
GET | doListPeople | |
GET | doEditPerson | |
POST | doValidatePerson | |
GET | doDeletePerson |
طريقة [doGet]
الغرض من هذه الطريقة هو توجيه معالجة الإجراءات التي يطلبها المستخدم إلى الطريقة الصحيحة. وفيما يلي كودها:
- الأسطر 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]، الذي يطلب قائمة الأشخاص:

وإليك كودها:
- السطر 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]:

والذي يكون كوده كما يلي:
يُظهر السطر 12 عنوان URL [/do/delete?id=XX] للرابط [حذف]. يجب أن تقوم الطريقة [doDeletePerson]، التي تتعامل مع عنوان URL هذا، بحذف الشخص الذي يحمل المعرف id=XX ثم عرض القائمة المحدثة للأشخاص في المجموعة. وفيما يلي كودها:
- السطر 5: عنوان URL الذي تتم معالجته هو [/do/delete?id=XX]. نسترد القيمة [XX] من المعلمة [id].
- السطر 7: نطلب من طبقة [service] حذف الشخص الذي يحمل المعرف الذي تم الحصول عليه. لا نقوم بأي عملية تحقق من الصحة. إذا كان الشخص الذي نحاول حذفه غير موجود، فإن طبقة [dao] ترمي استثناءً ينتقل إلى طبقة [service]. ولا نتعامل معه هنا في وحدة التحكم أيضًا. وبالتالي سينتقل إلى خادم الويب، والذي، حسب التكوين، سيعرض صفحة [exception.jsp]، الموصوفة في القسم 14.8.2:

- السطر 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]:

والذي يكون كوده كما يلي:
في السطر 11، نرى عنوان URL [/do/edit?id=XX] لرابط [تحرير]، وفي السطر 17، عنوان URL [/do/edit?id=-1] لرابط [إضافة]. يجب أن تعرض طريقة [doEditPersonne] نموذج التحرير للشخص الذي يحمل المعرف id=XX، أو إذا كانت عملية إضافة، فيجب أن تعرض نموذجًا فارغًا.
![]() | ![]() |
فيما يلي كود طريقة [doEditPerson]:
- يستهدف طلب GET عنوان URL بالشكل [/do/edit?id=XX]. في السطر 5، نسترد قيمة [id]. ثم هناك حالتان:
- إذا لم تكن قيمة id تساوي -1، فهذا يعني أنه تحديث، وعلينا عرض نموذج مملوء مسبقًا بمعلومات الشخص المراد تعديله. في السطر 10، يتم طلب هذا الشخص من طبقة [service].
- إذا كانت 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]:

دعونا نستعرض عناصر الإدخال في نموذج HTML في العرض أعلاه:
يحتوي طلب POST على المعلمات [firstName, lastName, dateOfBirth, spouse, numberOfChildren, id, version] ويتم إرساله إلى عنوان URL [/do/validate] (السطر 1). تتم معالجته بواسطة الطريقة [doValidatePerson] التالية:
- الأسطر 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]:

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

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

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

يقوم المستخدم U1 بإجراء تغييرات وحفظها:
![]() |
يقوم المستخدم U2 بنفس الشيء:
![]() |
يعود المستخدم U2 إلى قائمة الأشخاص الذين يستخدمون رابط [إلغاء] في النموذج:

ويجد الشخص [Lemarchand] كما عدّله U1. والآن يقوم U2 بحذف [Lemarchand]:
![]() |
لا يزال لدى U1 قائمته الخاصة ويريد تعديل [Lemarchand] مرة أخرى:
![]() |
يستخدم U1 رابط [العودة إلى القائمة] لمعرفة ما يجري:

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

















