3. الإجراءات: الرد
لننظر إلى بنية تطبيق Spring MVC:
![]() |
في هذا الفصل، نلقي نظرة على العملية التي تنقل الطلب [1] إلى وحدة التحكم والإجراء [2a] اللذين سيقومان بمعالجته، وهي آلية تُسمى التوجيه. كما نقدم الاستجابات المختلفة [3] التي يمكن أن ترسلها الإجراء إلى المتصفح. وقد تكون هذه الاستجابات غير عرض V [4b].
3.1. المشروع الجديد
نقوم بإنشاء مشروع Spring جديد MVC:
![]() |
- في [1-2]، نقوم بإنشاء مشروع جديد قائم على Spring Boot؛
![]() |
- في [3]، اسم مشروع Maven؛
- في [4]، مجموعة Maven التي سيتم وضع نتيجة تجميع المشروع فيها؛
- في [5]، الاسم الممنوح لمنتج التجميع؛
- في [6]، وصف للمشروع؛
- في [7]، الحزمة التي سيتم وضع الفئة القابلة للتنفيذ للمشروع فيها؛
- في [8]، طبيعة المشروع. إنه مشروع ويب مع طرق عرض Thymeleaf. نرى هنا جميع تبعيات Maven الجاهزة للاستخدام التي يوفرها مشروع Spring Boot؛
- في [9]، نحدد أن المنتج الناتج عن عملية البناء في Maven سيتم تعليبه في أرشيف jar وليس war. سيستخدم المشروع عندئذٍ خادم Tomcat مدمجًا سيكون موجودًا ضمن تبعياته؛
- في [10]، ننتقل إلى الخطوة التالية من المساعد؛
- في [11]، يتم تحديد مجلد المشروع؛
![]() |
- في [12]، المشروع الذي تم إنشاؤه؛
- في [14-15]، يتم إعادة تسمية الحزمة [istia.st.springmvc]؛
![]() |
- إلى [16]، وهو الاسم الجديد للحزمة؛
- في [17]، المشروع الجديد؛
نقوم الآن بإنشاء فئة جديدة؛
![]() |
- في [1-3]، نقوم بإنشاء فئة جديدة؛
![]() |
- في [5] نحدد اسمها، وفي [4] نحدد الحزمة التي تنتمي إليها؛
- في [6] المشروع الجديد؛
الفئة في الوقت الحالي هي كما يلي:
package istia.st.springmvc;
public class ActionsController {
}
نقوم بتطوير هذا الكود بالطريقة التالية:
package istia.st.springmvc;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ActionsController {
}
- السطر 6: تشير التعليقات التوضيحية [@RestController] إلى أمرين:
- أن الفئة [ActionsController] المُعلَّمة بهذه الطريقة هي وحدة تحكم Spring MVC، وبالتالي فهي تحتوي على إجراءات تعالج طلبات العملاء URL؛
- أن نتيجة هذه الإجراءات تُرسل إلى العميل؛
أما التعليق التوضيحي الآخر [@Controller] الذي صادفناه فهو مختلف: حيث تُرجع الإجراءات الخاصة بوحدة التحكم المُعلَّمة بهذا التعليق اسم العرض الذي يجب عرضه. ومن ثم، فإن الجمع بين هذا العرض والنموذج الذي أنشأته الإجراء لهذا العرض هو ما يوفر الاستجابة المرسلة إلى العميل.
يؤدي تغيير هيكل مشروعنا إلى تغيير في تكوينه:
![]() |
تتطور الفئة [Application] على النحو التالي:
package istia.st.springmvc.main;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan({"istia.st.springmvc.controllers"})
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- السطر 9: تقبل العلامة [ComponentScan] كمعلمة مصفوفة من أسماء الحزم التي يجب أن يبحث فيها Spring Boot عن مكونات Spring. هنا نضع في هذه المصفوفة الحزمة [istia.st.springmvc.controllers] حتى يتم العثور على وحدة التحكم المُعلَّمة بـ [@RestController]؛
سنقوم بإنشاء إجراءات متنوعة في وحدة التحكم لتوضيح خصائصها الرئيسية. سنركز أولاً على الأنواع المختلفة من الاستجابات الممكنة لإجراء ما في تطبيق بدون طرق عرض.
3.2. [/a01, /a02] - Hello world
سيكون الإجراء الأول لدينا كما يلي:
@RestController
public class ActionsController {
// ----------------------- مرحبًا بالعالم ------------------------
@RequestMapping(value = "/a01", method = RequestMethod.GET)
public String a01() {
return "Greetings from Spring Boot!";
}
}
- السطر 4: التعليق التوضيحي [RequestMapping] يحدد الطلب الذي تعالجه الإجراء المُعلَّم:
- السمة [value] هي URL المعالجة،
- السمة [method] تحدد الطريقة المقبولة؛
وبالتالي، فإن الطريقة [a01] تعالج الطلب HTTP [GET /a01].
- السطر 5: تُرجع الطريقة [a01] نوعًا [String] الذي سيتم إرساله كما هو إلى العميل؛
- السطر 6: السلسلة التي تم إرجاعها؛
دعونا نُشغّل التطبيق كما فعلنا عدة مرات من قبل، ثم باستخدام العميل [Advanced Rest Client]، نطلب URL و [/a01] باستخدام GET و [1-2]:
![]() |
- في [3]، استجابة الخادم؛
- إلى [4]، وهي رؤوس الاستجابة HTTP. نلاحظ أن الترميز المستخدم هو [ISO-8859-1]. يمكننا تفضيل الترميز UTF-8. يمكن تكوين ذلك؛
- في [5]، نطلب نفس URL باستخدام متصفح Chrome؛
نضيف الإجراء [/a02] التالي في وحدة التحكم [ActionsController] (وبالتالي قد يتم الخلط أحيانًا بين URL والطريقة التي تعالجها تحت اسم الإجراء):
// ----------------------- أحرف ذات علامات تشكيل - UTF8 ------------------------
@RequestMapping(value = "/a02", method = RequestMethod.GET, produces="text/plain;charset=UTF-8")
public String a02() {
return "caractères accentués : éèàôûî";
}
- السطر 2: يشير السمة [produces="text/plain;charset=UTF-8"] إلى أن الإجراء يرسل تدفقًا نصيًّا يحتوي على أحرف مُشفَّرة بتنسيق [UTF-8]. ويسمح هذا التنسيق بشكل خاص باستخدام الأحرف المُشَدَّدة؛
لأخذ هذا الإجراء الجديد في الاعتبار، يتعين علينا إعادة تشغيل التطبيق:
![]() |
والنتيجة هي كما يلي:
![]() |
- في [1]، نرى طبيعة المستند المرسل من الخادم؛
- في [2-3]، تظهر الأحرف المُشَدَّدة بشكل صحيح؛
3.3. [/a03]: تحويل تدفق XML
نضيف الإجراء [/a03] التالي:
// ----------------------- text/xml ------------------------
@RequestMapping(value = "/a03", method = RequestMethod.GET, produces = "text/xml;charset=UTF-8")
public String a03() {
String greeting = "<greetings><greeting>Greetings from Spring Boot!</greeting></greetings>";
return greeting;
}
- السطر 2: يشير السمة [produces="text/xml;charset=UTF-8"] إلى أن الإجراء يرسل تدفقًا XML بأحرف مشفرة بتنسيق [UTF-8]؛
وينتج عن تنفيذه ما يلي:
![]() |
- في [1]، يحدد الرأس HTTP أن المستند المرسل هو HTML؛
- في [2]، يستخدم متصفح Chrome هذه المعلومات لتنسيق النص XML المستلم؛
تجدر الإشارة إلى أنه في متصفح Chrome، يمكن الوصول إلى التبادلات بين العميل والخادم في نافذة المطور (Ctrl-Shift-I):

من الآن فصاعدًا، لن يتم التقاط لقطات شاشة بشكل منهجي للمراسلات HTTP بين العميل والخادم. في بعض الأحيان، سنكتفي بذكر نص هذه المراسلات.
3.4. [/a04, /a05]: إرجاع تدفق jSON
نضيف الإجراء [/a04] التالي:
// ----------------------- إنتاج jSON ------------------------
@RequestMapping(value = "/a04", method = RequestMethod.GET)
public Map<String, Object> a04() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("1", "un");
map.put("2", new int[] { 4, 5 });
return map;
}
- السطر 3: تُرجع الإجراء نوعًا [Map]، وهو قاموس. نتذكر أنه مع وحدة تحكم من النوع [@RestController]، تكون نتيجة الإجراء هي الاستجابة المرسلة إلى العميل. وبما أن بروتوكول HTTP هو بروتوكول لتبادل أسطر النص، يجب تسلسل استجابة العميل إلى سلسلة أحرف. ولتحقيق ذلك، يستخدم Spring MVC محولات متنوعة من نوع [Objet <---> chaîne de caractères]. ويتم ربط كائن معين بمحول ما عن طريق التهيئة. وهنا، ستقوم ميزة التهيئة التلقائية في Spring Boot بفحص تبعيات المشروع:
![]() |
التبعيات الخاصة بـ Jackson المذكورة أعلاه هي مكتبات لتسلسل/إلغاء تسلسل الكائنات إلى سلاسل jSON. سيقوم Spring Boot بعد ذلك باستخدام هذه المكتبات لتسلسل/إلغاء تسلسل الكائنات التي تُرجعها الإجراءات. يمكن العثور على مثال لرمز Java لتسلسل/إلغاء تسلسل كائنات Java في jSON في الفقرة 9.7.
يُلاحظ في السطر 2 أننا لم نحدد نوع الاستجابة المرسلة. سنرى الآن النوع الافتراضي الذي سيتم إرساله.
في متصفح Chrome، تظهر النتائج على النحو التالي: [1-3]:
![]() |
لنضف الآن الإجراء التالي [/a05]:
// ----------------------- إنتاج jSON - 2 ------------------------
@RequestMapping(value = "/a05", method = RequestMethod.GET)
public Personne a05() {
return new Personne(1,"carole",45);
}
الفئة [Personne] هي كما يلي:
![]() |
package istia.st.sprinmvc.models;
public class Personne {
// المعرف
private Integer id;
// الاسم
private String nom;
// العمر
private int age;
// المُنشئون
public Personne() {
}
public Personne(String nom, int age) {
this.nom = nom;
this.age = age;
}
public Personne(Integer id, String nom, int age) {
this(nom, age);
this.id = id;
}
@Override
public String toString() {
return String.format("[id=%s, nom=%s, age=%d]", id, nom, age);
}
// المنشورات والمُعيّنات
...
}
يُنتج التنفيذ النتائج التالية:
![]() |
- في [1]، يشير الخادم إلى أن المستند الذي يرسله هو jSON؛
- في [2]، المستند jSON المستلم؛
3.5. [/a06]: إرجاع تدفق فارغ
نضيف الإجراء [/a06] التالي:
// ----------------------- إرجاع تدفق فارغ ------------------------
@RequestMapping(value = "/a06")
public void a06() {
}
- السطر 3، الإجراء [/a06] لا يُرجع أي شيء. عندئذٍ سيقوم Spring MVC بتوليد استجابة فارغة للعميل؛
يُنتج التنفيذ النتائج التالية:
![]() |
فيما سبق، يشير السمة HTTP [Content-Length] في الاستجابة إلى أن الخادم يرسل مستندًا فارغًا.
3.6. [/a07, /a08, /a09]: طبيعة التدفق مع [Content-Type]
نضيف الإجراء [/a07] التالي:
// ----------------------- text/html ------------------------
@RequestMapping(value = "/a07", method = RequestMethod.GET, produces = "text/html;charset=UTF-8")
public String a07() {
String greeting = "<h1>Greetings from Spring Boot!</h1>";
return greeting;
}
- السطر 2، تُنتج الإجراء [/a07] تدفقًا HTML [text/html]؛
- السطر 4: سلسلة HTML؛
يؤدي التنفيذ إلى النتائج التالية:
![]() |
- في [1]، نرى أن Chrome قد فسّر العلامة HTML <h1> التي تعرض محتواها بأحرف كبيرة؛
الآن لنفعل الشيء نفسه مع الإجراء التالي [/a08]:
// ----------------------- نتيجة HTML بتنسيق text/plain ------------------------
@RequestMapping(value = "/a08", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String a08() {
String greeting = "<h1>Greetings from Spring Boot!</h1>";
return greeting;
}
- السطر 2: استجابة الإجراء هي من النوع [text/plain]؛
والنتائج هي كما يلي:
![]() |
- في [1]، لم يفسر Chrome العلامة <h1> الخاصة بـ HTML لأن الخادم أبلغه بأنه يرسل له تدفقًا من نوع [text/plain] [2]؛
لنكرر الأمر بشكل مشابه مع الإجراء التالي [/a09]:
// ----------------------- نتيجة HTML بتنسيق text/xml ------------------------
@RequestMapping(value = "/a09", method = RequestMethod.GET, produces = "text/xml;charset=UTF-8")
public String a09() {
String greeting = "<h1>Greetings from Spring Boot!</h1>";
return greeting;
}
- السطر 2: نرسل تدفقًا من النوع [text/xml]؛
والنتائج هي كما يلي:
![]() |
- في [1]، لم يفسر Chrome العلامة HTML <h1> لأن الخادم أبلغه بأنه يرسل له تدفقًا من نوع [text/xml] [2]. لذلك تعامل مع العلامة <h1> على أنها علامة XML؛
ونستخلص من هذه الأمثلة أهمية رأس HTTP [Content-Type] في استجابة الخادم. يستخدم المتصفح هذا الرأس لمعرفة كيفية تفسير المستند الذي يتلقاه؛
3.7. [/a10, /a11, /a12]: إعادة توجيه العميل
نقوم بإنشاء وحدة تحكم جديدة [RedirectController]:
![]() |
سيكون كود [RedirectCntroller] في الوقت الحالي كما يلي:
package istia.st.springmvc.controllers;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class RedirectController {
}
- السطر 7: نستخدم التعليق التوضيحي [@Controller]، مما يعني أنه من الآن فصاعدًا، سيشير النوع [String] لنتائج الإجراءات بشكل افتراضي إلى اسم إجراء أو عرض؛
نقوم بإنشاء الإجراء [/a10] التالي:
// ------------ إعادة توجيه إلى إجراء تابع لجهة خارجية -----------------------
@RequestMapping(value = "/a10", method = RequestMethod.GET)
public String a10() {
return "a01";
}
- السطر 4: نُرجع «a01» كنتيجة، وهو اسم إجراء. وسيكون هذا الإجراء هو الذي سيرسل الرد إلى العميل؛
فيما يلي مثال:
![]() |
- في [2]، تلقينا تدفق الإجراء [/a01]؛
- في [3]، يعرض المتصفح URL الخاص بالإجراء [/a10]؛
نقوم الآن بإنشاء الإجراء التالي [/a11]:
// ------------ إعادة توجيه مؤقتة 302 إلى إجراء تابع لجهة خارجية -----------------------
@RequestMapping(value = "/a11", method = RequestMethod.GET)
public String a11() {
return "redirect:/a01";
}
ونحصل على النتائج التالية:
![]() |
- في سجلات Chrome الخاصة بـ [1-2]، نرى طلبين، أحدهما موجه إلى [/a11]، والآخر إلى [/a01]؛
- في [3]، يرد الخادم برمز [302] الذي يطلب من متصفح العميل إعادة التوجيه إلىURL المشار إليه في الرأس HTTP [Location:] [4]. الرمز [302] هو رمز إعادة توجيه مؤقت؛
ثم يقوم المتصفح بإرسال الطلب الثاني إلى عنوان إعادة التوجيه URL:
![]() |
- في [5]، الطلب الثاني للعميل؛
- في [6]، يعرض متصفح العميل URL الخاص بطلب التوجيه؛
قد نرغب في الإشارة إلى إعادة توجيه دائمة، وفي هذه الحالة، يجب إرسال الرأس HTTP التالي إلى العميل:
وهو ما يعني أن إعادة التوجيه دائمة. ويأخذ بعض محركات البحث هذا الفرق بين إعادة التوجيه المؤقتة (302) والدائمة (301) في الاعتبار.
نكتب الإجراء [/a12] الذي سيقوم بتنفيذ عملية إعادة التوجيه الدائمة هذه:
// ------------ إعادة توجيه دائمة 301 إلى إجراء تابع لجهة خارجية----------------
@RequestMapping(value = "/a12", method = RequestMethod.GET)
public void a12(HttpServletResponse response) {
response.setStatus(301);
response.addHeader("Location", "/a01");
}
- السطر 3: نطلب من Spring MVC إدراج الكائن [HttpServletResponse] الذي يغلف الاستجابة المرسلة إلى العميل؛
- السطر 4: يتم تعيين [status] للاستجابة، و[301] لرأس HTTP:
- السطر 5: يتم إنشاء الرأس HTTP التالي يدويًّا:
وهو عنوان إعادة التوجيه URL.
يؤدي التنفيذ إلى النتائج التالية:
![]() | ![]() |
ونستخلص من هذا المثال كيفية:
- إنشاء حالة الاستجابة HTTP؛
- تضمين رأس HTTP في الاستجابة؛
3.8. [/a13]: إنشاء الرد الكامل
من الممكن التحكم بشكل كامل في الاستجابة كما يوضح الإجراء التالي للفئة [ResponsesController]:
![]() |
// ----------------------- إنشاء الاستجابة بالكامل ------------------------
@RequestMapping(value = "/a13")
public void a13(HttpServletResponse response) throws IOException {
response.setStatus(666);
response.addHeader("header1", "qq chose");
response.addHeader("Content-Type", "text/html;charset=UTF-8");
String greeting = "<h1>Greetings from Spring Boot!</h1>";
response.getWriter().write(greeting);
}
- السطر 3: نتيجة الإجراء هي [void]. في هذه الحالة، لإرسال استجابة غير فارغة إلى العميل، يجب استخدام الكائن [HttpServletResponse response] المقدم من Spring MVC؛
- السطر 4: نمنح الرد حالة لن يتعرف عليها العميل؛
- السطر 5: نضيف رأسًا HTTP لن يتعرف عليه العميل؛
- السطر 6: نضيف رأسًا HTTP [Content-Type] لتحديد نوع التدفق الذي سنرسله، وهو هنا HTML؛
- السطران 7-8: المستند الذي سيتبع الرؤوس HTTP في الرد؛
النتائج هي كما يلي:
![]() |
- في [1]، يمكننا التعرف على عناصر ردنا؛
- في [2-3]، نلاحظ أن Chrome تجاهل حقيقة أن:
- أن حالة HTTP للاستجابة لم تكن حالة HTTP معترف بها،
- أن الرأس [header1] لم يكن رأسًا HTTP معترفًا به؛
إذا لم يكن العميل متصفحًا بل عميلاً مبرمجًا، فيمكن استخدام أي حالات ورؤوس نريدها.



























