Skip to content

4. الإجراءات: النموذج

لنعد إلى بنية تطبيق Spring MVC:

في الفصل السابق، تناولنا العملية التي تنقل الطلب [1] إلى وحدة التحكم والإجراء [2a] اللذين سيقومان بمعالجته، وهي آلية تُعرف باسم التوجيه. كما عرضنا الاستجابات المختلفة التي يمكن أن يقدمها الإجراء إلى المتصفح. وقد عرضنا حتى الآن إجراءات لا تستفيد من الطلب المقدم إليها. يحمل الطلب [1] معه معلومات متنوعة يقدمها Spring MVC إلى الإجراء في شكل نموذج. ولا ينبغي الخلط بين هذا المصطلح ونموذج M الخاص بعرض V [2c] الذي تنتجه الإجراء:

  • يصل طلب العميل HTTP إلى [1]؛
  • في [2]، سيتم تحويل المعلومات الواردة في الطلب إلى نموذج الإجراء [3]، وهو فئة في الغالب — ولكن ليس بالضرورة — ستُستخدم كمدخل للإجراء [4]؛
  • في [4]، ستقوم الإجراء، انطلاقًا من هذا النموذج، بتوليد استجابة. وستتألف هذه الاستجابة من مكونين: عرض V [6] ونموذج M لهذا العرض [5]؛
  • ستستخدم طريقة العرض V [6] نموذجها M [5] لتوليد الاستجابة HTTP الموجهة إلى العميل.

في النموذج MVC، تُعد الإجراء [4] جزءًا من C (وحدة التحكم)، ونموذج العرض [5] هو M، والعرض [6] هو V.

يتناول هذا الفصل آليات الربط بين المعلومات التي تحملها الطلب، والتي هي بطبيعتها سلاسل أحرف، ونموذج الإجراء الذي قد يكون فئة ذات خصائص من أنواع مختلفة.

ملاحظة: المصطلح [Modèle d'action] ليس مصطلحًا معترفًا به.

نقوم بإنشاء وحدة تحكم جديدة لهذه الإجراءات الجديدة:

  

سيكون وحدة التحكم [ActionModelController] في الوقت الحالي كما يلي:


package istia.st.springmvc.controllers;

import org.springframework.web.bind.annotation.RestController;

@RestController
public class ActionModelController {

}
  • السطر 5: نذكر أن التعليق التوضيحي [@RestController] يجعل الرد المرسل إلى العميل عبارة عن تسلسل أحرف لنتيجة إجراءات وحدة التحكم؛

4.1. [/m01]: معلمات وحدة التحكم GET

نضيف الإجراء [/m01] التالي:



    // ----------------------- استرداد المعلمات باستخدام GET------------------------
    @RequestMapping(value = "/m01", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m01(String nom, String age) {
        return String.format("Hello [%s-%s]!, Greetings from Spring Boot!", nom, age);
}
  • السطر 4: تقبل الإجراء معلمتين باسم [nom] و [age]. سيتم تهيئتهما باستخدام المعلمات التي تحمل نفس الأسماء في الاستعلام HTTP GET؛

النتائج في متصفح Chrome هي كما يلي: [1-3]:

  • في [1]، الطلب GET مع المعلمات [nom] و [age]؛
  • في [3]، نرى أن الإجراء [/m01] قد استرد هذه المعلمات بالفعل؛

4.2. [/m02]: معلمات POST

نضيف الإجراء [/m02] التالي:



    // ----------------------- استرداد المعلمات باستخدام POST------------------------
    @RequestMapping(value = "/m02", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8")
    public String m02(String nom, String age) {
        return String.format("Hello [%s-%s]!, Greetings from Spring Boot!", nom, age);
}
  • السطر 4: تقبل الإجراء معلمتين باسم [nom] و [age]. سيتم تهيئتهما باستخدام المعلمات التي تحمل نفس الأسماء في الاستعلام HTTP POST؛

النتائج مع [Advanced rest Client] هي كما يلي:

  • في [1-3]، الاستعلام POST مع المعلمات [nom] و [age]؛
  • في [4-5]، يتم تعيين الرأس HTTP [Content-Type] لطلب POST. يجب أن يكون [Content-Type: application/x-www-form-urlencoded
  • في [6]، يقدم [Form Data] قائمة بمعلمات عملية POST. هنا نرى المعلمات [nom] و [age]؛
  • في [7]، رد الخادم الذي يوضح أن العملية [/m02] قد استردت بالفعل المعلمات [nom] و [age]؛؛

4.3. [/m03]: معلمات تحمل نفس الأسماء

لقد رأينا في الفقرة 2.5.2.8 أن قائمة الاختيار المتعدد يمكنها إرسال معلمات تحمل أسماء متطابقة إلى الخادم. لنرى كيف يمكن لعمل ما استردادها. نضيف العمل التالي [/m03]:


    // ----------------------- استرداد المعلمات التي تحمل نفس الأسماء-----------------
    @RequestMapping(value = "/m03", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8")
    public String m03(String nom[]) {
        return String.format("Hello [%s]!, Greetings from Spring Boot!", String.join("-", nom));
}
  • السطر 2: تقبل الإجراء معلمة باسم [name[]]. سيتم تهيئتها هنا بجميع المعلمات التي تحمل هذا الاسم سواء كانت في GET أو POST، حيث لم يتم تحديد نوع الطلب هنا؛

النتائج هي كما يلي:

  • عبر POST و[1]، يتم إرسال المعلمات [2]؛
  • كما يتم وضع معلمات في URL و [3]؛
  • في [4]، المعلمات الأربعة التي تحمل نفس الاسم [nom]: [Query String parameters] هي معلمات URL، و[Form Data] هي المعلمات التي تم إرسالها؛
  • في [5]، نرى أن الإجراء [/m03] قد استرد المعلمات الأربعة المسماة [nom]؛

4.4. [/m04]: تعيين معلمات الإجراء إلى كائن Java

لنفترض أن الإجراء الجديد [/m04] هو كما يلي:


    // ------ تعيين المعلمات في كائن (Command Object) ---------------
    @RequestMapping(value = "/m04", method = RequestMethod.POST)
    public Personne m04(Personne personne) {
        return person;
}
  • السطر 3: الإجراء له معلمة من النوع التالي:

public class Personne {

    // المعرف
    private Integer id;
    // الاسم
    private String nom;
    // العمر
    private int age;
....
    // وظائف الاسترجاع والتعيين
...
}
  • لإنشاء المعلمة [Personne personne]، يقوم Spring MVC بإنشاء [new Personne()]؛
  • ثم إذا كانت هناك معلمات تحمل أسماء الحقول [id, nom, age] للكائن الذي تم إنشاؤه، فإنه يقوم بإنشاء مثيل باستخدام الحقول عبر مُعيّنات القيم الخاصة بها؛
  • السطر 4: تُرجع العملية نوعًا [Personne] الذي سيتم تسلسله إلى سلسلة أحرف قبل إرساله إلى العميل. لقد رأينا أن التسلسل الذي يتم إجراؤه افتراضيًا هو تسلسل jSON. لذا، من المفترض أن يتلقى العميل السلسلة jSON الخاصة بشخص ما؛

فيما يلي مثال على ذلك:

  • إلى [1]، والمعلمات [id, nom, age] لإنشاء كائن [Personne
  • إلى [2]، السلسلة jSON الخاصة بهذا الشخص؛

ماذا يحدث إذا لم يتم إرسال جميع حقول شخص ما؟ لنجرب:

  • إلى [2]، لم يتم تهيئة سوى المعلمة [id]؛

4.5. [/m05]: استرداد عناصر من URL

أو الإجراء الجديد [/m05] التالي:


    // ----------------------- استرداد العناصر من URL ------------------------
    @RequestMapping(value = "/m05/{a}/x/{b}", method = RequestMethod.GET)
    public Map<String, String> m05(@PathVariable("a") String a, @PathVariable("b") String b) {
        Map<String, String> map = new HashMap<String, String>();
        map.put("a", a);
        map.put("b", b);
        return map;
}
  • السطر 2: URL التي تمت معالجتها هي من النوع [/m05/{a}/x/{b}] حيث {param} هو عنصر معلمة من URL؛
  • السطر 3: يتم استرداد عناصر المعلمات الخاصة بـ URL مع التعليق التوضيحي [@PathVariable
  • الأسطر 4-6: يتم وضع العناصر [a] و [b] التي تم استردادها في قاموس؛
  • السطر 7: ستكون الإجابة هي السلسلة jSON الموجودة في هذا القاموس؛

النتائج هي كما يلي:

 

4.6. [/m06]: استرداد عناصر من URL والمعلمات

إذن، الإجراء الجديد [/m06] هو كما يلي:


    // -------- استرداد عناصر من URL والمعلمات---------------
    @RequestMapping(value = "/m06/{a}/x/{b}", method = RequestMethod.GET)
    public Map<String, Object> m06(@PathVariable("a") Integer a, @PathVariable("b") Double b, Double c) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("a", a);
        map.put("b", b);
        map.put("c", c);
        return map;
}
  • السطر 3: يتم استرداد عناصر من كل من URL و[Integer a, Double b] بالإضافة إلى معلمة (GET أو POST) [Double c
  • الأسطر 4-7: يتم وضع هذه العناصر في قاموس؛
  • السطر 8: الذي يشكل استجابة العميل الذي سيتلقى بالتالي السلسلة jSON من هذا القاموس؛

فيما يلي النتائج:

 

لاحظ وجود الرمز / في نهاية المسار [http://localhost:8080/m06/100/x/200.43/]. وبدونه، نحصل على النتيجة الخاطئة التالية:

 

4.7. [/m07]: الوصول إلى الاستعلام بالكامل

لنفترض أن الإجراء الجديد [/m07] هو كما يلي:


    // ------ الوصول إلى الاستعلام HttpServletRequest ------------------------
    @RequestMapping(value = "/m07", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m07(HttpServletRequest request) {
        // رؤوس HTTP
        Enumeration<String> headerNames = request.getHeaderNames();
        StringBuffer buffer = new StringBuffer();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            buffer.append(String.format("%s : %s\n", name, request.getHeader(name)));
        }
        return buffer.toString();
}
  • السطر 3: نطلب من Spring MVC حقن الكائن [HttpServletRequest request] الذي يغلف جميع المعلومات التي يمكن الحصول عليها من الطلب؛
  • الأسطر 5-10: يتم استرداد جميع رؤوس الطلب HTTP لتجميعها في سلسلة أحرف يتم إرسالها إلى العميل (السطر 11)؛

النتائج هي كما يلي:

  • في [1]، رؤوس القاعدة HTTP الخاصة بالاستعلام؛
  • إلى [2]، وهي الاستجابة. نجد فيها بالفعل جميع رؤوس HTTP الخاصة بالطلب.

4.8. [/m08]: الوصول إلى الكائن [Writer]

لننظر إلى الإجراء التالي:


    // ----------------------- إدخال writer ------------------------
    @RequestMapping(value = "/m08", method = RequestMethod.GET)
    public void m08(Writer writer) throws IOException {
        writer.write("Bonjour le monde !");
}
  • السطر 3: يقوم Spring MVC بحقن الكائن [Writer writer] الذي يسمح بالكتابة في تدفق الرد الموجه إلى العميل؛
  • السطر 3: تُرجع الإجراء نوعًا [void]، مما يشير إلى أنه يجب عليه بناء الاستجابة للعميل بنفسه؛
  • السطر 4: إضافة نص إلى تدفق الاستجابة الموجهة إلى العميل؛

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

  • في [2]، نلاحظ أن الرأس HTTP [Content-Type] لم يتم إرساله؛
  • في [3]، الرد؛

4.9. [/m09]: الوصول إلى رأس HTTP

لننظر إلى الإجراء التالي:


    // ----------------------- حقن RequestHeader ------------------------
    @RequestMapping(value = "/m09", method = RequestMethod.GET)
    public String m09(@RequestHeader("User-Agent") String userAgent) {
        return userAgent;
}
  • السطر 3: يسمح التعليق التوضيحي [@RequestHeader("User-Agent")] باسترداد الرأس HTTP [User-Agent
  • السطر 4: يتم عرض نص هذا العنوان؛

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

  • في [2]، الرؤوس HTTP و [User-Agent
  • إلى [3]، وقد استردت العملية [/m08] هذه الرأس بشكل صحيح؛

4.10. [/m10, /m11]: الوصول إلى ملف تعريف ارتباط

ملف تعريف الارتباط هو عادةً رأس HTTP الذي:

  • إلى العميل للمرة الأولى؛
  • ثم يعيد العميل إرساله بشكل منهجي إلى الخادم؛

لنقم أولاً بإنشاء إجراء لإنشاء ملف تعريف الارتباط:


    // ----------------------- إنشاء ملف تعريف ارتباط ------------------------
    @RequestMapping(value = "/m10", method = RequestMethod.GET)
    public void m10(HttpServletResponse response) {
        response.addCookie(new Cookie("cookie1", "remember me"));
}
  • السطر 3: نقوم بإدخال الكائن [HttpServletResponse response] من أجل التحكم الكامل في الاستجابة؛
  • السطر 4: ننشئ ملف تعريف ارتباط بمفتاح [cookie1] وقيمة [remember me] (ملاحظة: الأحرف المُشَدَّدة في قيمة ملف تعريف الارتباط تتسبب في حدوث أخطاء)؛
  • السطر 3: لا تُرجع الإجراء أي نتيجة. علاوة على ذلك، لا تكتب أي شيء في نص الاستجابة. وبالتالي، سيتلقى العميل مستندًا فارغًا. تُستخدم الاستجابة فقط لإضافة رأس ملف تعريف الارتباط HTTP إليها؛

لنلقِ نظرة على النتائج:

  • في [1]: الطلب؛
  • في [2]: الرد فارغ؛
  • في [3]: ملف تعريف الارتباط الذي تم إنشاؤه بواسطة الإجراء؛

الآن لنقم بإنشاء إجراء لاسترداد ملف تعريف الارتباط هذا الذي سيقوم المتصفح بإرساله من الآن فصاعدًا مع كل طلب:


    // ----------------------- حقن ملف تعريف الارتباط ------------------------
    @RequestMapping(value = "/m11", method = RequestMethod.GET)
    public String m10(@CookieValue("cookie1") String cookie1) {
        return cookie1;
}
  • السطر 3: يسمح التعليق التوضيحي [@CookieValue("cookie1")] باسترداد ملف تعريف الارتباط ذي المفتاح [cookie1
  • السطر 4: ستكون هذه القيمة هي الرد المرسَل إلى العميل؛

لنلقِ نظرة على النتائج:

  • في [2]، نرى أن المتصفح يعيد ملف تعريف الارتباط؛
  • في [3]، نرى أن الإجراء قد استعاد ملف تعريف الارتباط بنجاح؛

4.11. [/m12]: الوصول إلى نص POST

عادةً ما تكون المعلمات المرسلة مصحوبة برأس HTTP [Content-Type: application/x-www-form-urlencoded]. يمكن الوصول إلى السلسلة المرسلة بالكامل. نقوم بإنشاء الإجراء التالي:


    // ----------- استرداد نص POST من النوع String------------------------
    @RequestMapping(value = "/m12", method = RequestMethod.POST)
    public String m12(@RequestBody String requestBody) {
        return requestBody;
}
  • السطر 3: يسمح التعليق التوضيحي [@RequestBody] باسترداد نص POST. هنا، نفترض أن هذا النص من النوع [String
  • السطر 4: يتم إرسال هذا النص إلى العميل؛

فيما يلي مثال أول:

  • في [2]، القيم المرسلة؛
  • في [3]، الرأس HTTP [Content-Type] للطلب؛
  • في [4]، استجابة الخادم؛

لا تأخذ المعلمات المرسلة دائمًا الشكل البسيط [p1=v1&p2=v2] الذي استخدمناه كثيرًا حتى الآن. لنأخذ مثالًا أكثر تعقيدًا:

  • في [2-3]: يتم إدخال القيم المرسلة بالشكل [clé:value
  • إلى [5]، وهي السلسلة التي تم إرسالها؛

مع النوع [Content-Type: application/x-www-form-urlencoded]، يجب أن تكون السلسلة المنشورة بالصيغة [p1=v1&p2=v2]. إذا أردنا نشر أي شيء، فسنستخدم النوع [Content-Type: text/plain]. إليك مثال:

  • في [2-3]، يتم إنشاء الرأس HTTP [Content-Type]. افتراضيًا، سيتم استخدام [5] بدلاً من الرمز المحدد في [6]. السمة [charset=utf-8] مهمة. فبدونها، نفقد الأحرف المُشَدَّدة في السلسلة المرسلة؛
  • في [4]، يتم استرداد السلسلة المرسلة بشكل صحيح في [7]؛

4.12. [/m13, /m14]: استرداد القيم المرسلة في jSON

يمكن إرسال المعلمات باستخدام الرأس HTTP [Content-Type: application/json]. نقوم بإنشاء الإجراء التالي:


    // ----------------------- استرداد نص ملف jSON من ملف POST
    @RequestMapping(value = "/m13", method = RequestMethod.POST, consumes = "application/json")
    public String m13(@RequestBody Personne personne) {
        return personne.toString();
}
  • السطر 2: [consumes = "application/json"] يوضح أن الفعل ينتظر مفعولًا به jSON؛
  • السطر 3: يمثل [@RequestBody] هذا الجسم. وقد تم ربط هذا التعليق التوضيحي بكائن من النوع [Personne]. وسيتم فك تسلسل الجسم jSON تلقائيًا في هذا الكائن؛
  • السطر 4: تُستخدم الطريقة [Personne].toString() لإرجاع قيمة غير السلسلة jSON المرسلة؛

فيما يلي مثال:

  • إلى [2]، وهي السلسلة jSON التي تم إرسالها؛
  • إلى [3]، وهي [Content-Type] الخاصة بالطلب؛
  • إلى [4]، استجابة الخادم؛

يمكن القيام بنفس الشيء بطريقة مختلفة:


    // ----------------------- استرداد نص رسالة jSON من رسالة POST 2 -------------------
    @RequestMapping(value = "/m14", method = RequestMethod.POST, consumes = "text/plain")
    public String m14(@RequestBody String requestBody) throws JsonParseException, JsonMappingException, IOException {
        Personne personne = new ObjectMapper().readValue(requestBody, Personne.class);
        return personne.toString();
}
  • السطر 2: تم تحديد أن الطريقة تتوقع تدفقًا من النوع [text/plain]. سيقوم Spring MVC عندئذٍ بمعالجة نص الطلب كنوع [String] (السطر 3)؛
  • السطر 4: يتم إزالة التسلسل عن السلسلة jSON لتحويلها إلى كائن من النوع [Personne] (انظر الفقرة 9.7، الصفحة 543

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

  • إلى [3]، يجب وضع [text/plain

4.13. [/m15]: استرداد الجلسة

لنعد إلى بنية تنفيذ الإجراء:

يتم إنشاء مثيل لفئة وحدة التحكم في بداية طلب العميل ويتم إتلافه في نهايته. ولذلك لا يمكن استخدامها لتخزين البيانات بين طلبين حتى لو تم استدعاؤها بشكل متكرر. قد نرغب في تخزين نوعين من البيانات:

  • البيانات المشتركة بين جميع مستخدمي تطبيق الويب. وعادةً ما تكون هذه البيانات للقراءة فقط؛
  • البيانات المشتركة بين طلبات العميل نفسه. يتم تخزين هذه البيانات في كائن يُسمى «الجلسة» (Session). ونستخدم مصطلح «جلسة العميل» للإشارة إلى ذاكرة العميل. ويمكن لجميع طلبات العميل الوصول إلى هذه الجلسة، حيث يمكنها تخزين المعلومات وقراءتها منها.

فيما يلي، نعرض أنواع الذاكرة التي يمكن لأي إجراء الوصول إليها:

  • ذاكرة التطبيق التي تحتوي في الغالب على بيانات للقراءة فقط ويمكن لجميع المستخدمين الوصول إليها؛
  • ذاكرة مستخدم معين، أو الجلسة، التي تحتوي على بيانات للقراءة/الكتابة ويمكن الوصول إليها من خلال الطلبات المتتالية لنفس المستخدم؛
  • غير موضحة أعلاه، توجد ذاكرة الطلب، أو سياق الطلب. يمكن معالجة طلب المستخدم من خلال عدة إجراءات متتالية. يسمح سياق الطلب للإجراء 1 بنقل المعلومات إلى الإجراء 2.

لنلقِ نظرة على مثال أول يسلط الضوء على هذه الذاكرات المختلفة:


    // ----------------------- استرداد الجلسة ------------------------
    @RequestMapping(value = "/m15", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m15(HttpSession session) {
        // يتم استرداد كائن المفتاح [compteur] من الجلسة
        Object objCompteur = session.getAttribute("compteur");
        // يتم تحويله إلى عدد صحيح لزيادة قيمته
        int iCompteur = objCompteur == null ? 0 : (Integer) objCompteur;
        iCompteur++;
        // إعادة إدراجه في الجلسة
        session.setAttribute("compteur", iCompteur);
        // يتم إرجاعه كنتيجة للإجراء
        return String.valueOf(iCompteur);
}

يحافظ Spring MVC على جلسة عمل المستخدم في كائن من النوع [HttpSession].

  • السطر 3: يُطلب من Spring MVC حقن الكائن [HttpSession] في معلمات الإجراء؛
  • السطر 5: يتم استرداد سمة تسمى [compteur] من هذه المعلمات. تتصرف الجلسة كقاموس، أي كمجموعة من الأزواج [clé, valeur]. إذا لم يكن المفتاح [compteur] موجودًا في الجلسة، يتم استرداد مؤشر null؛
  • السطر 7: ستكون القيمة المرتبطة بالمفتاح [compteur] من النوع [Integer
  • السطر 8: زيادة العداد؛
  • السطر 10: تحديث العداد في الجلسة؛
  • السطر 12: يتم إرسال قيمة العداد إلى العميل؛

عند تنفيذ [/m15] للمرة:

  • للمرة الأولى، في السطر 12، سيكون العداد بقيمة 1؛
  • في المرة الثانية، في السطر 5، سيتم استرداد هذه القيمة 1 لتغييرها إلى 2؛
  • ...

فيما يلي مثال على التنفيذ:

  • في [1]، نحصل بالفعل على القيمة الأولى للعداد؛
  • في [2]، أرسل الخادم ملف تعريف ارتباط للجلسة. ويحتوي على المفتاح [JSESSIONID] وقيمة عبارة عن سلسلة أحرف فريدة لكل مستخدم. نتذكر أن المتصفح يعيد إرسال ملفات تعريف الارتباط التي يتلقاها بشكل منهجي. وبالتالي، عندما نطلب الإجراء [/m15] للمرة الثانية، سيقوم العميل بإعادة إرسال ملف تعريف الارتباط هذا، مما سيسمح للخادم بالتعرف عليه وربطه بجلسة العمل الخاصة به. وبهذه الطريقة يتم الحفاظ على ذاكرة المستخدم؛

لنلقِ نظرة على الطلب الثاني:

  • في [3]، نلاحظ أن العميل يعيد إرسال ملف تعريف الارتباط الخاص بجلسة العمل. ويمكن ملاحظة أنه في استجابة الخادم، لم يعد ملف تعريف الارتباط هذا موجودًا. أصبح العميل هو الذي يرسله الآن ليتم التعرف عليه؛
  • في [4]، القيمة الثانية للعداد. لقد تمت زيادتها بالفعل؛

4.14. [/m16]: استرداد كائن من نطاق [session]

قد نرغب في وضع جميع بيانات جلسة عمل المستخدم في كائن واحد ووضع هذا الكائن وحده في الجلسة. سنتبع هذا النهج. نضع العداد في الكائن التالي [SessionModel]:

  

package istia.st.sprinmvc.models;

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionModel {

    private int compteur;

    public int getCompteur() {
        return compteur;
    }

    public void setCompteur(int compteur) {
        this.compteur = compteur;
    }

}
  • السطر 7: التعليق التوضيحي [@Component] هو تعليق توضيحي لـ Spring (السطر 5) يجعل من الفئة [SessionModel] مكونًا تدير Spring دورة حياته؛
  • السطر 8: التعليق التوضيحي [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] هو أيضًا تعليق توضيحي لـ Spring (السطران 3-4). عندما يصادفه Spring MVC، يتم إنشاء الفئة المقابلة ووضعها في جلسة عمل المستخدم. السمة [proxyMode = ScopedProxyMode.TARGET_CLASS] مهمة. فبفضلها يقوم Spring MVC بإنشاء مثيل لكل مستخدم وليس مثيلًا واحدًا لجميع المستخدمين (singleton
  • السطر 11: العداد؛

لكي يتم التعرف على مكون Spring الجديد هذا، يجب التحقق من تكوين التطبيق في الفئة [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: يتم البحث عن مكونات Spring في الحزمة [istia.st.springmvc.controllers]. لم يعد هذا كافيًا. نقوم بتعديل هذا السطر على النحو التالي:

@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })

لقد أضفنا الحزمة التي توجد فيها الفئة [SessionModel].

الآن، نضيف الإجراء التالي:


    @Autowired
    private SessionModel session;
    
    // ------ إدارة كائن نطاق (scope) الجلسة [Autowired] -----------
    @RequestMapping(value = "/m16", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m16() {
        session.setCompteur(session.getCompteur() + 1);
        return String.valueOf(session.getCompteur());
}
  • السطران 1-2: يتم حقن مكون Spring [SessionModel] في وحدة التحكم. تجدر الإشارة هنا إلى أن وحدة التحكم في Spring هي عنصر فريد (singleton). لذا، فإن حقن مكون ذي نطاق أضيق فيها، وهو في هذه الحالة [Session]، يمثل تناقضًا. وهنا يأتي دور التعليق التوضيحي [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] للمكون [SessionModel]. في كل مرة يصل فيها كود وحدة التحكم إلى الحقل [session] في السطر 2، يتم تنفيذ طريقة بروكسي لجعل جلسة الطلب قيد المعالجة حاليًا من قبل وحدة التحكم؛
  • السطر 6: لم يعد هناك حاجة إلى الكائن [HttpSession] في معلمات الإجراء؛
  • السطر 7: يتم استرداد/زيادة العداد؛
  • السطر 8: يتم إرجاع قيمته؛

فيما يلي مثال على التنفيذ:

المرة الأولى

المرة الثانية

الآن، لنأخذ متصفحًا آخر يمثل مستخدمًا ثانيًا. سنستخدم هنا متصفح Opera:

في ما سبق في [1]، يحصل هذا المستخدم الثاني على قيمة عداد تساوي 1. وهذا يدل على أن جلسته تختلف عن جلسة المستخدم الأول. إذا نظرنا إلى التبادلات بين العميل والخادم (Ctrl-Shift-I لمتصفح Opera أيضًا)، نرى في [2] أن هذا المستخدم الثاني لديه ملف تعريف ارتباط جلسة عمل مختلف عن ذلك الخاص بالمستخدم الأول. وهذا ما يضمن استقلالية الجلسات.

4.15. [/m17]: استرداد كائن من نطاق [application]

لنعد إلى بنية تنفيذ الإجراء:

نحن نعرف كيفية إنشاء جلسة عمل المستخدم. سنقوم الآن بإنشاء كائن نطاق [application] يكون محتواه للقراءة فقط ومتاحًا لجميع المستخدمين. نقدم الفئة [ApplicationModel] التي ستكون كائن النطاق [application]:

 

package istia.st.springmvc.models;

import java.util.concurrent.atomic.AtomicLong;

import org.springframework.stereotype.Component;

@Component
public class ApplicationModel {

    // عداد
    private AtomicLong compteur = new AtomicLong(0);

    // وظائف القراءة والكتابة
    public AtomicLong getCompteur() {
        return compteur;
    }

    public void setCompteur(AtomicLong compteur) {
        this.compteur = compteur;
    }

}
  • السطر 5: التعليق التوضيحي [@Component] يجعل الفئة [ApplicationModel] مكونًا يديره Spring. الطبيعة الافتراضية لمكونات Spring هي النوع [singleton]: يتم إنشاء المكون في نسخة فريدة عند إنشاء مثيل حاوية Spring، أي عمومًا عند بدء تشغيل التطبيق. يمكننا استخدام دورة الحياة هذه لتخزين معلومات التكوين في المكون الفريد (singleton) بحيث تكون متاحة لجميع المستخدمين؛
  • السطر 11: عداد من النوع [AtomicLong]. يحتوي هذا النوع على طريقة تُسمى «[incrementAndGet]» تُعرف بأنها «ذرية». وهذا يعني أن الخيط الذي ينفذ هذه الطريقة يضمن ألا يقوم خيط آخر بقراءة قيمة العداد (Get) بين قراءتها (Get) وزيادتها (increment) بواسطة الخيط الأول، مما قد يتسبب في حدوث أخطاء لأن خيطين سيقرآن نفس قيمة العداد، وبالتالي سيتم زيادة العداد بمقدار واحد بدلاً من اثنين؛

نقوم بإنشاء الإجراء الجديد [/m17] التالي:


@Autowired
    private ApplicationModel application;

    // ----- إدارة كائن نطاق التطبيق [Autowired] ------------------------
    @RequestMapping(value = "/m17", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m17() {
        return String.valueOf(application.getCompteur().incrementAndGet());
    }
  • السطران 1-2: نقوم بإدخال المكون [ApplicationModel] في وحدة التحكم. وهو مكون فريد (singleton). وبالتالي سيكون لكل مستخدم مرجع إلى نفس الكائن؛
  • السطر 7: نُرجع عداد النطاق [application] بعد زيادته؛

فيما يلي مثالان، أحدهما باستخدام متصفح Chrome، والآخر باستخدام متصفح Opera:

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

4.16. [/m18]: استرداد كائن من النطاق [session] باستخدام [@SessionAttributes]

هناك طريقة أخرى لاسترداد المعلومات من النطاق [session]. سنقوم بتسجيل الكائن التالي في الجلسة:


package istia.st.springmvc.models;

public class Container {
    // العداد
    public int compteur=10;

    // دالات القراءة والكتابة
    public int getCompteur() {
        return compteur;
    }

    public void setCompteur(int compteur) {
        this.compteur = compteur;
    }
}

سنستخدم هذا الكائن مع الإجراءين التاليين:


    // استخدام [@SessionAttribute] ----------------------
    @RequestMapping(value = "/m18", method = RequestMethod.GET)
    public void m18(HttpSession session) {
        // هنا نضع المفتاح [container] في الجلسة
        session.setAttribute("container", new Container());
    }

    // استخدام [@ModelAttribute] ----------------------
    // سيتم هنا إدخال المفتاح [container] الخاص بالجلسة
    @RequestMapping(value = "/m19", method = RequestMethod.GET)
    public String m19(@ModelAttribute("container") Container container) {
        container.setCompteur(1 + container.getCompteur());
        return String.valueOf(container.getCompteur());
    }
  • الأسطر 3-6: لا تُرجع الإجراء [/m18] أي نتيجة. فهي تُستخدم فقط لإنشاء كائن في الجلسة باستخدام المفتاح [container
  • السطر 11: في الإجراء [/m19]، يتم استخدام التعليق التوضيحي [@ModelAttribute]. سلوك هذا التعليق التوضيحي معقد إلى حد ما. يمكن أن يشير المعلمة [container] في هذا التعليق التوضيحي إلى عدة أشياء، ولا سيما كائن في الجلسة. ولذلك، يجب أن يكون هذا الكائن قد تم إعلانه باستخدام التعليق التوضيحي [@SessionAttributes] على الفئة نفسها:

@RestController
@SessionAttributes({"container"})
public class ActionModelController {
  • السطر 2 أعلاه يشير إلى المفتاح [container] باعتباره جزءًا من سمات الجلسة؛

لنلخص:

  • في [/m18]، يتم إدراج المفتاح [container] في الجلسة؛
  • التعليق التوضيحي [@SessionAttributes({"container"})] يجعل من الممكن إدراج هذا المفتاح في معلمة مرفقة بالتعليق التوضيحي [@ModelAttribute("container")]؛
  • لا يظهر ذلك في مثال التنفيذ التالي، لكن المعلومات المُعلَّمة بـ [@ModelAttribute] تصبح تلقائيًّا جزءًا من النموذج M الذي يتم إرساله إلى العرض V؛

فيما يلي مثال على التنفيذ. أولاً، نضع المفتاح [container] في الجلسة باستخدام الإجراء [/m18] [1]. بعد ذلك، يتم استدعاء الإجراء [/m19] مرتين لمشاهدة زيادة العداد.

4.17. [/m20-/m23]: إدخال المعلومات باستخدام [@ModelAttribute]

لننظر إلى الإجراء الجديد التالي:


    // سيكون السمة p جزءًا من جميع قوالب العرض [Model] ----------------
    @ModelAttribute("p")
    public Personne getPersonne() {
        return new Personne(7,"abcd", 14);
    }

    // ---------------إنشاء مثيل لـ @ModelAttribute --------------------------
    // سيتم إدراجه إذا كان موجودًا في الجلسة
    // سيتم إدراجه إذا كان المتحكم قد عرّف طريقة لهذا السمة
    // قد يأتي من حقول URL إذا كان هناك محول من String إلى نوع السمة
    // وإلا يتم إنشاؤه باستخدام المنشئ الافتراضي
    // ثم يتم تهيئة سمات النموذج باستخدام معلمات GET أو POST
    // وستكون النتيجة النهائية جزءًا من النموذج الذي تنتجه الإجراء
    
    // يتم إدراج السمة p في الوسيطات------------------------
    @RequestMapping(value = "/m20", method = RequestMethod.GET)
    public Personne m20(@ModelAttribute("p") Personne personne) {
        return personne;
}
  • السطر 2-5: يحددان سمة نموذج باسم [p]. وهو النموذج M لعرض V، وهو نموذج يمثله النوع [Model] في Spring MVC. يتصرف النموذج كقاموس من الأزواج [clé, valeur]. هنا، ترتبط المفتاح [p] بالكائن [Personne] الذي تم إنشاؤه بواسطة الطريقة [getPersonne]. يمكن أن يكون اسم الطريقة أي اسم؛
  • السطر 17: يتم إدراج سمة نموذج المفتاح [p] في معلمات الإجراء. ويتم هذا الإدراج وفقًا للقواعد الواردة في الأسطر 8-12. وهنا، سنكون في الحالة المحددة في السطر 9. لذا، في السطر 17، سيكون المعلمة [Personne personne] هو الكائن [Personne(7,'abcd',14)]؛
  • السطر 18: يتم إرجاع الكائن [personne] للتحقق. وسيتم تسلسله إلى jSON قبل إرساله إلى العميل.

فيما يلي مثال:

 

الآن، دعونا ندرس الإجراء التالي:


    // --------- تصبح السمة p تلقائيًا جزءًا من النموذج M للطريقة العرض V
    @RequestMapping(value = "/m21", method = RequestMethod.GET)
    public String m21(Model model) {
        return model.toString();
}

يجب على الإجراء الذي يهدف إلى عرض طريقة عرض V أن يقوم ببناء النموذج M الخاص بها. يدير Spring MVC هذا النموذج باستخدام نوع [Model] الذي يمكن حقنه في معلمات الإجراء. في البداية، يكون هذا النموذج فارغًا أو يحتوي على المعلومات المُعلَّمة بعلامة [@ModelAttribute]. تقوم الإجراء بتعزيز هذا النموذج أو عدم تعزيزه قبل إرساله إلى العرض.

  • السطر 3: إدراج النموذج M؛
  • السطر 4: نريد أن نرى ما بداخله. نقوم بتحويله إلى سلسلة أحرف لإرساله إلى العميل. هنا، سيتم استخدام الطريقة [Personne.toString]. لذا يجب أن تكون موجودة؛

فيما يلي مثال على التنفيذ:

 

فيما سبق، نرى أن التعليمات التالية:


    @ModelAttribute("p")
    public Personne getPersonne() {
        return new Personne(7,"abcd", 14);
}

قد أنشأت إدخالًا باسم [p, Personne(7,'abcd',14)] في النموذج. وهذا هو الحال دائمًا.

لننظر الآن إلى الحالة التالية:


    // وإلا يتم إنشاؤه باستخدام المنشئ الافتراضي
    // ثم يتم تهيئة سمات النموذج باستخدام معلمات GET أو POST

مع الإجراء التالي:


    // --------- السمة النموذجية [param1] هي جزء من النموذج ولكنها غير مُهيأة
    @RequestMapping(value = "/m22", method = RequestMethod.GET)
    public String m22(@ModelAttribute("param1") String p1, Model model) {
        return model.toString();
}
  • السطر 3: سمة نموذج المفتاح [param1] غير موجودة. في هذه الحالة، يجب أن يكون للنوع المرتبط منشئ افتراضي. وهذا هو الحال هنا بالنسبة للنوع [String]، لكن لا يمكن كتابة [@ModelAttribute("param1") Integer p1] لأن الفئة [Integer] لا تحتوي على منشئ افتراضي؛
  • السطر 4: يتم إرجاع النموذج لمعرفة ما إذا كان سمة نموذج المفتاح [param1] جزءًا منه؛

فيما يلي مثال على التنفيذ:

 

السمة النموذجية [param1] موجودة بالفعل في النموذج، لكن الطريقة [toString] للقيمة المرتبطة بها لا تقدم أي إشارة إلى هذه القيمة.

لننظر الآن إلى الإجراء التالي، حيث نضع معلومة بشكل صريح في النموذج:


    // --------- يتم إدراج سمة النموذج [param2] صراحةً في النموذج
    @RequestMapping(value = "/m23", method = RequestMethod.GET)
    public String m23(String p2, Model model) {
        model.addAttribute("param2",p2);
        return model.toString();
}
  • السطر 4: يتم إدراج القيمة [p2] التي تم استردادها في السطر 3 في النموذج المرتبط بالمفتاح [param2]:

فيما يلي مثال على التنفيذ:

 

تتغير القواعد إذا كان معلمة الإجراء كائنًا. فيما يلي مثال أول:


    // ------ تم إدراج سمة النموذج [unePersonne] تلقائيًا في النموذج
    @RequestMapping(value = "/m23b", method = RequestMethod.GET)
    public String m23b(@ModelAttribute("unePersonne") Personne p1, Model model) {
        return model.toString();
}

لا تقوم الإجراء بتعديل النموذج الذي تم تزويدها به. والنتيجة هي كما يلي:

نلاحظ أن التعليق التوضيحي [@ModelAttribute("unePersonne") Personne p1] قد أدرج الشخص [p1] في النموذج، مرتبطًا بالمفتاح [unePersonne].

لننظر الآن إلى الإجراء التالي:


    // --------- يتم إدراج الشخص p1 تلقائيًا في النموذج
    // -------- باستخدام اسم فئته كمفتاح، مع كتابة الحرف الأول بحرف صغير
    @RequestMapping(value = "/m23c", method = RequestMethod.GET)
    public String m23c(Personne p1, Model model) {
        return model.toString();
}
  • السطر 4: لم يتم إدراج التعليق التوضيحي [@ModelAttribute

والنتيجة هي كما يلي:

نلاحظ أن وجود المعلمة [Personne p1] أدى إلى إدراج الشخص [p1] في النموذج، مرتبطًا بالمفتاح [personne] الذي يمثل اسم الفئة [Personne] مع الحرف الأول صغيرًا.

4.18. [/m24]: التحقق من صحة نموذج الإجراء

لنأخذ نموذج الإجراء [ActionModel01] التالي:

 

package istia.st.springmvc.models;

import javax.validation.constraints.NotNull;

public class ActionModel01 {

    // البيانات
    @NotNull
    private Integer a;
    @NotNull
    private Double b;

    // وظائف الحصول والتعيين
...
    }
  • السطران 8 و9: التعليق التوضيحي [@NotNull] هو قيد للتحقق يشير إلى أن البيانات المُعلَّمة لا يمكن أن تأخذ القيمة null؛

لننظر الآن إلى الإجراء التالي:


    // ----------------------- التحقق من صحة النموذج ------------------------
    @RequestMapping(value = "/m24", method = RequestMethod.GET)
    public Map<String, Object> m24(@Valid ActionModel01 data, BindingResult result) {
        Map<String, Object> map = new HashMap<String, Object>();
        // هل توجد أخطاء؟
        if (result.hasErrors()) {
            StringBuffer buffer = new StringBuffer();
            // تصفح قائمة الأخطاء
            for (FieldError error : result.getFieldErrors()) {
                buffer.append(String.format("[%s:%s:%s:%s:%s]", error.getField(), error.getRejectedValue(),
                        String.join(" - ", error.getCodes()), error.getCode(),error.getDefaultMessage()));
            }
            map.put("errors", buffer.toString());
        } else {
            // لا توجد أخطاء
            Map<String, Object> mapData = new HashMap<String, Object>();
            mapData.put("a", data.getA());
            mapData.put("b", data.getB());
            map.put("data", mapData);
        }
        return map;
}
  • السطر 3: سيتم إنشاء مثيل لكائن [ActionModel01] وتهيئة حقوله [a, b] باستخدام معلمات تحمل نفس الأسماء. يشير التعليق التوضيحي [@Valid] إلى أنه يجب التحقق من قيود الصحة. سيتم وضع نتائج هذا التحقق في المعلمة من النوع [BindingResult] (المعلمة الثانية). وستجرى عمليات التحقق التالية:
    • بسبب التعليقات التوضيحية [@NotNull]، يجب أن تكون المعلمات [a] و [b] موجودتين؛
    • بسبب النوع [Integer a]، يجب أن يكون المعلمة [a] — التي هي بطبيعتها من النوع [String] — قابلة للتحويل إلى النوع [Integer
    • بسبب النوع [Double b]، يجب أن يكون المعلمة [b]، التي هي بطبيعتها من النوع [String]، قابلة للتحويل إلى النوع [Double

مع التعليق التوضيحي [@Valid]، سيتم ترحيل أخطاء التحقق إلى المعلمة [BindingResult result]. بدون التعليق التوضيحي [@Valid]، تتسبب أخطاء التحقق في تعطل العملية، ويقوم الخادم بإرسال استجابة HTTP إلى العميل مع الحالة 500 (خطأ داخلي في الخادم).

  • السطر 3: نتيجة الإجراء من النوع [Map]. وسيتم إرسال السلسلة jSON من هذه النتيجة إلى العميل. يتم إنشاء نوعين من القواميس:
    • في حالة الفشل، قاموس يحتوي على مدخل ['errors', value] حيث [value] هي سلسلة أحرف تصف جميع الأخطاء (السطر 13)؛
    • في حالة النجاح، يتم إنشاء قاموس يحتوي على مدخل واحد هو ['data',value]، حيث [value] هو في حد ذاته قاموس يحتوي على مدخلين: ['a', value]، ['b', value] (السطر 19)؛
  • الأسطر 9-12: لكل خطأ [error] يتم اكتشافه، يتم إنشاء السلسلة [error.getField(), error.getRejectedValue(), error.Codes, error.getDefaultMessage()]:
    • العنصر الأول هو الحقل الذي يحتوي على الخطأ، [a] أو [b]،
    • العنصر الثاني هو القيمة المرفوضة، [x] على سبيل المثال،
    • العنصر الثالث هو قائمة برموز الأخطاء. سنستعرض أدوارها لاحقًا؛
    • العنصر الرابع هو رمز الخطأ. وهو جزء من القائمة السابقة؛
    • العنصر الأخير هو رسالة الخطأ الافتراضية. يمكن بالفعل أن يكون هناك عدة رسائل خطأ؛

فيما يلي بعض أمثلة التنفيذ:

في المثال أعلاه، نلاحظ ما يلي:

  • فشل تعيين القيمة «x» للحقل [ActionModel01.a]، وتوضح رسالة الخطأ سبب ذلك؛
  • فشل تعيين القيمة "y" للحقل [ActionModel01.b]، وتوضح رسالة الخطأ سبب ذلك؛

يُلاحظ وجود رموز الخطأ في الحقل [a]: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. سنعود إلى رموز الخطأ هذه عندما يتعين تخصيص رسالة الخطأ. يُلاحظ أن رمز الخطأ هو [typeMismatch].

مثال آخر:

هنا، لم يتم تمرير المعلمات [a] و [b]. عندئذٍ قامت أدوات التحقق [@NotNull] الخاصة بنموذج الإجراء [ActionModel01] بدورها؛

وأخيرًا، القيم الصحيحة:

4.19. [m/24]: تخصيص رسائل الخطأ

لنعد إلى لقطة شاشة من المثال السابق:

نرى أعلاه رسائل الخطأ الافتراضية. من الواضح أنه لا يمكننا الاحتفاظ بها في تطبيق حقيقي. من الممكن تعريف رسائل الخطأ هذه. وللقيام بذلك، سنستعين برموز الخطأ. في الأعلى، نرى أن الخطأ الخاص بالحقل [a] له الرموز التالية: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. تتراوح رموز الأخطاء هذه من الأكثر دقة إلى الأقل دقة:

  • [typeMismatch.actionModel01.a]: خطأ في نوع الحقل [a] الذي ينتمي إلى النوع [ActionModel01
  • [typeMismatch.a]: خطأ في نوع الحقل المسمى [a]؛
  • [typeMismatch.java.lang.Integer]: خطأ في النوع في نوع Integer؛
  • [typeMismatch]: خطأ في النوع؛

ونلاحظ أيضًا أن رمز الخطأ في الحقل [a] الذي تم الحصول عليه من [error.getCode()] هو [typeMismatch] (انظر لقطة الشاشة أعلاه).

سنقوم بوضع رسائل الخطأ في ملف خصائص:

  

سيكون ملف [messages.properties] المذكور أعلاه كما يلي:


NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.model01.a=Le paramètre [a] doit être entier

يكون شكل كل سطر كما يلي:

    clé=message

هنا، سيكون المفتاح عبارة عن رمز خطأ، والرسالة هي رسالة الخطأ المرتبطة بهذا الرمز.

دعونا نستعرض رموز الأخطاء الخاصة بالحقلين:

  • [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]، عندما يكون المعلمة [a] غير صالحة؛
  • [typeMismatch.actionModel01.b - typeMismatch.b - typeMismatch.java.lang.Double - typeMismatch:typeMismatch ] عندما يكون المعلمة [b] غير صالحة؛
  • [NotNull.actionModel01.a - NotNull.a - NotNull.java.lang.Integer - NotNull] عندما يكون المعلمة [a] غير موجودة؛
  • [NotNull.actionModel01.b - NotNull.b - NotNull.java.lang.Double - NotNull] عندما يكون المعلمة [b] غير موجودة؛

يجب أن يتضمن الملف [messages.properties] رسالة خطأ لكل حالات الخطأ المحتملة. بالنسبة للحالة التالية:

  • في حالة عدم وجود المعلمتين [a] و [b]، سيتم استخدام الرمز [NotNull
  • في حالة وجود خطأ في المعلمة [a]، فقد أضفنا رسائل لرمزين هما [typeMismatch.actionModel01.a, typeMismatch]. وسنرى أيهما سيتم استخدامه؛
  • في حالة وجود خطأ في المعلمة [b]، سيتم استخدام الرمز [typeMismatch

لكي يتم استخدام الملف [messages.properties]، يجب تكوين Spring:

  

نقوم بإزالة تعليقات التكوين من الفئة [Application]:


package istia.st.springmvc.main;

import org.springframework.boot.SpringApplication;

public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Config.class, args);
    }
}
  • السطر 8: يتم تشغيل تطبيق Spring Boot. المعلمة الأولى للطريقة الثابتة [SpringApplication.run] هي الفئة التي تقوم الآن بتكوين التطبيق؛

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


package istia.st.springmvc.main;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }
}
  • الأسطر 11-13: نجد تعليقات التكوين التي كانت موجودة سابقًا في الفئة [Application
  • السطر 14: لتكوين تطبيق Spring MVC، يجب توسيع الفئة [WebMvcConfigurerAdapter
  • السطر 15: يُدخل التعليق التوضيحي [@Bean] مكونًا من Spring، وهو عنصر فريد (singleton
  • السطر 16: يتم تعريف مكون (bean) باسم [messageSource] (اسم الأسلوب). يستخدم هذا المكون لتعريف ملفات الرسائل الخاصة بالتطبيق ويجب أن يحمل هذا الاسم بالضرورة؛
  • الأسطر 17-19: تُشير إلى Spring بأن ملف الرسائل:
    • موجود في المجلد [i18n] ضمن مسار الفئات (Classpath) للمشروع (السطر 18)،
    • ويسمى [messages.properties] (السطر 18). في الواقع، المصطلح [messages] هو جذر أسماء ملفات الرسائل وليس الاسم نفسه. سنرى أنه في سياق التدويل، يمكن أن نجد عدة ملفات رسائل، واحد لكل ثقافة مدعومة. وبالتالي، قد يكون لدينا [messages_fr.properties] للغة الفرنسية و[messages_en.properties] للغة الإنجليزية. اللواحق المضافة إلى الجذر [messages] موحدة. لا يمكن إضافة أي شيء عشوائي؛

في المشروع STS، يجب وضع المجلد [i18n] في مجلد الموارد لأن هذا المجلد يتم وضعه في مسار الفئات (Classpath) للمشروع:

  

لاستخدام هذا الملف، نقوم بإنشاء الإجراء الجديد التالي:


// التحقق من صحة النموذج، إدارة رسائل الخطأ ------------------------
    @RequestMapping(value = "/m25", method = RequestMethod.GET)
    public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
            throws Exception {
        // قاموس النتائج
        Map<String, Object> map = new HashMap<String, Object>();
        // سياق تطبيق Spring
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // الإعدادات المحلية
        Locale locale = RequestContextUtils.getLocale(request);
        // هل توجد أخطاء؟
        if (result.hasErrors()) {
            StringBuffer buffer = new StringBuffer();
            for (FieldError error : result.getFieldErrors()) {
                // البحث عن رسالة الخطأ بناءً على رموز الأخطاء
                // يتم البحث عن الرسالة في ملفات الرسائل
                // رموز الأخطاء في شكل جدول
                String[] codes = error.getCodes();
                // في شكل سلسلة
                String listCodes = String.join(" - ", codes);
                // البحث
                String msg = null;
                int i = 0;
                while (msg == null && i < codes.length) {
                    try {
                        msg = ctx.getMessage(codes[i], null, locale);
                    } catch (Exception e) {

                    }
                    i++;
                }
                // هل تم العثور عليها؟
                if (msg == null) {
                    throw new Exception(String.format("Indiquez un message pour l'un des codes [%s]", listCodes));
                }
                // تم العثور عليه - يتم إضافة رسالة الخطأ إلى سلسلة رسائل الخطأ
                buffer.append(String.format("[%s:%s:%s:%s]", locale.toString(), error.getField(), error.getRejectedValue(),
                        String.join(" - ", msg)));
            }
            map.put("errors", buffer.toString());
        } else {
            // حسناً
            Map<String, Object> mapData = new HashMap<String, Object>();
            mapData.put("a", data.getA());
            mapData.put("b", data.getB());
            map.put("data", mapData);
        }
        return map;
    }

هذا الكود مشابه لكود الإجراء [/m24]. نوضح الاختلافات:

  • السطر 3: نقوم بإدراج الاستعلام [HttpServletRequest request] في معلمات الإجراء. سنحتاج إليه لاحقًا؛
  • السطران 7-8: نسترد سياق Spring. يحتوي هذا السياق على جميع مكونات Spring الخاصة بالتطبيق. كما يتيح الوصول إلى ملفات الرسائل؛
  • السطر 10: نسترد الإعدادات المحلية للتطبيق. سيتم توضيح هذا المصطلح لاحقًا؛
  • الأسطر 15-31: لكل خطأ، نبحث عن رسالة تتوافق مع أحد رموز الخطأ هذه. يتم البحث عنها حسب ترتيب الرموز الموجودة في [error.getCodes()]. بمجرد العثور على رسالة، نتوقف؛
  • السطر 26: طريقة استرداد رسالة من ملف [messages.properties]:
    • المعلمة الأولى هي الرمز المطلوب في [messages.properties
    • والمعلمة الثانية عبارة عن مصفوفة من المعلمات لأن الرسائل تكون أحيانًا معلمة. وهذا ليس هو الحال هنا،
    • والمعلمة الثالثة هي الإعدادات المحلية المستخدمة (المستمدة من السطر 10). تشير الإعدادات المحلية إلى اللغة المستخدمة، [fr_FR] للغة الفرنسية الفرنسية، و[en_US] للغة الإنجليزية في USA. يتم البحث عن الرسالة في ملف messages_[locale].properties، أي على سبيل المثال في [messages_fr_FR.properties]. إذا لم يكن هذا الملف موجودًا، يتم البحث عن الرسالة في ملف [messages_fr.properties]. وإذا لم يكن هذا الملف موجودًا، يتم البحث عن الرسالة في [messages.properties]. وهذه الحالة الأخيرة هي التي ستعمل بالنسبة لنا؛
  • الأسطر 25-29: بشكل غير متوقع بعض الشيء، عند البحث عن رمز غير موجود في ملف الرسائل، نحصل على استثناء بدلاً من مؤشر null؛
  • الأسطر 33-35: يتم التعامل مع حالة عدم وجود رسالة خطأ؛
  • السطور 37-38: يتم إنشاء سلسلة الخطأ. يتم تضمين الإعدادات المحلية ورسالة الخطأ التي تم العثور عليها في هذه السلسلة؛

فيما يلي أمثلة على التنفيذ:

 

نلاحظ أن:

  • الإعدادات اللغوية للتطبيق هي [fr_FR]. وهي قيمة افتراضية لأننا لم نقم بأي شيء لتهيئتها؛
  • أن الرسالة المستخدمة للحقلين هي التالية:

NotNull=Le champ ne peut être vide

مثال آخر:

 

ونلاحظ أن:

  • رسالة الخطأ المستخدمة للمعلمة [a] هي التالية:

typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
  • رسالة الخطأ المستخدمة للمعلمة [b] هي التالية:

typeMismatch=Format invalide

لماذا توجد رسالتان مختلفتان؟ بالنسبة للمعلمة [a]، كانت هناك رسالتان محتملتان:


typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier

تم استكشاف رموز الأخطاء وفقًا لترتيب الجدول [error.getCodes()]. وتبين أن هذا الترتيب يبدأ من الرمز الأكثر تحديدًا وصولًا إلى الرمز الأكثر عمومية. ولهذا السبب تم العثور على الرمز [typeMismatch.model01.a] أولاً.

4.20. [/m25]: تدويل تطبيق Spring MVC

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


package istia.st.springmvc.main;

import java.util.Locale;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        return localeChangeInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    @Bean
    public CookieLocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setCookieName("lang");
        localeResolver.setDefaultLocale(new Locale("fr"));
        return localeResolver;
    }
}
  • الأسطر 28-32: نقوم بإنشاء مُعترض طلبات. يُوسّع مُعترض الطلبات واجهة [HandlerInterceptor]. تقوم هذه الفئة بفحص الطلب الوارد قبل أن تتم معالجته بواسطة إجراء ما. هنا، سيبحث المعترض [localeChangeInterceptor] عن معلمة باسم [lang] في الطلب الوارد، أو GET أو POST، وسيقوم بتغيير الإعدادات اللغوية للتطبيق وفقًا لهذا المعامل. وبالتالي، إذا كان المعلمة هي [lang=en_US]، فستصبح لغة التطبيق الإنجليزية وفقًا لـ USA؛
  • الأسطر 34-37: يتم إعادة تعريف الطريقة [WebMvcConfigurerAdapter.addInterceptors] لإضافة المعترض السابق؛
  • الأسطر 39-45: تُستخدم لضبط الطريقة التي سيتم بها تغليف الإعدادات اللغوية في ملف تعريف ارتباط. من المعروف أن ملف تعريف الارتباط يمكن أن يعمل كذاكرة للمستخدم، حيث يقوم متصفح العميل بإرساله بشكل منهجي إلى الخادم. يقوم المعترض السابق [localeChangeInterceptor] بإنشاء ملف تعريف ارتباط يغلف الإعدادات اللغوية. السطر 42 يطلق اسم [lang] على ملف تعريف الارتباط هذا. ويُستخدم ملف تعريف الارتباط أيضًا لتغيير الإعدادات اللغوية؛
  • السطر 43: يشير إلى أنه في حالة عدم وجود ملف تعريف الارتباط [lang]، ستكون الإعدادات المحلية هي [fr]؛

باختصار، يمكن تحديد الإعدادات المحلية لطلب ما بطريقتين:

  • عن طريق تمرير معلمة باسم [lang
  • عن طريق إرسال ملف تعريف ارتباط باسم [lang]. يتم إنشاء ملف تعريف الارتباط هذا تلقائيًا عند تنفيذ الطريقة السابقة؛

لاستخدام هذه الإعدادات المحلية، سنقوم بإنشاء ملفات رسائل للإعدادات المحلية [fr] و [en]:

 

ملف [messages_fr.properties] هو كما يلي:


NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier

الملف [messages_en.properties] هو التالي:


NotNull=The field can't be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer

الملف [messages.properties] هو نسخة مطابقة للملف [messages_en.properties]. تجدر الإشارة إلى أن الملف [messages.properties] يُستخدم عندما لا يتم العثور على أي ملف مطابق للإعدادات المحلية للطلب. في حالتنا هذه، إذا أرسل المستخدم المعلمة [lang=en]، وبما أن الملف [messages_en.properties] غير موجود، فسيتم استخدام الملف [messages.properties]. وبالتالي، ستظهر الرسائل للمستخدم باللغة الإنجليزية.

لنجرب ذلك. أولاً، في بيئة تطوير Chrome (Ctrl-Shift-I)، تحقق من ملفات تعريف الارتباط الخاصة بك:

 

إذا كان لديك ملف تعريف ارتباط باسم [lang]، فاحذفه. ثم باستخدام Chrome، اطلب ملفات URL و [http://localhost:8080/m25]:

 

أرسل المتصفح الرؤوس التالية: HTTP:

GET /m25 HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36
Referer: http://localhost:8080/m25
Accept-Encoding: gzip, deflate, sdch
Accept-Language: fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4

نلاحظ أنه لا توجد ملفات تعريف الارتباط [lang] في هذه الرؤوس. في هذه الحالة، يستخدم الكود الخاص بنا الإعدادات المحلية [fr]. وهذا ما تظهره لقطة الشاشة. لنجرب حالة أخرى:

  • في [1]، قمنا بتمرير المعلمة [lang=en] لتغيير الإعدادات المحلية إلى [en]؛
  • في [2]، نرى الإعدادات المحلية الجديدة؛
  • في [3]، أصبحت الرسالة باللغة الإنجليزية؛

لنلقِ نظرة الآن على التبادلات في HTTP:

 

نرى أعلاه أن الخادم أعاد إرسال ملف تعريف ارتباط [lang]. ولهذا الأمر نتيجة مهمة: ستكون الإعدادات المحلية للطلب التالي هي [en] مرة أخرى بسبب ملف تعريف الارتباط [lang] الذي سيقوم المتصفح بإرساله. لذلك، ينبغي أن نحتفظ بالرسائل باللغة الإنجليزية. دعونا نتحقق من ذلك:

 

في المثال أعلاه، نرى أن الإعدادات اللغوية بقيت على [en]. وبسبب ملف تعريف الارتباط الذي يرسله المتصفح تلقائيًا، ستبقى على هذا النحو ما لم يقم المستخدم بتغييرها عن طريق إرسال المعلمة [lang] على النحو التالي:

 

4.21. [/m26]: إدراج الإعدادات المحلية في قالب الإجراء

في المثال السابق، رأينا طريقة لاسترداد الإعدادات المحلية من الطلب:


    @RequestMapping(value = "/m25", method = RequestMethod.GET)
    public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
            throws Exception {
...
        // محلي
        Locale locale = RequestContextUtils.getLocale(request);
// أخطاء؟

يمكن إدراج الإعدادات المحلية مباشرةً في معلمات الإجراء. وفيما يلي مثال على ذلك:


    @RequestMapping(value = "/m26", method = RequestMethod.GET)
    public String m26(Locale locale) {
        return String.format("locale=%s", locale.toString());
}
 

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

4.22. [/m27]: التحقق من صحة نموذج باستخدام Hibernate Validator

لننظر إلى الإجراء الجديد التالي:


    //التحقق من صحة نموذج باستخدام Hibernate Validator ------------------------
    @RequestMapping(value = "/m27", method = RequestMethod.POST)
    public Map<String, Object> m27(@Valid ActionModel02 data, BindingResult result) {
        Map<String, Object> map = new HashMap<String, Object>();
        // هل توجد أخطاء؟
        if (result.hasErrors()) {
            // تصفح قائمة الأخطاء
            for (FieldError error : result.getFieldErrors()) {
                map.put(error.getField(),
                        String.format("[message=%s, codes=%s]", error.getDefaultMessage(), String.join("|", error.getCodes())));
            }
        } else {
            // لا توجد أخطاء
            map.put("data", data);
        }
        return map;
}

هذا كود رأيناه عدة مرات حتى الآن:

  • السطر 3: يتم طلب الإجراء [/m27] عبر POST؛
  • السطور 8-11، سيتم تحديد كل خطأ بواسطة [champ, message] مع:
    • الحقل: الحقل الذي يحتوي على الخطأ،
    • الرسالة: رسالة الخطأ المرتبطة به بالإضافة إلى قائمة رموز الأخطاء؛
  • السطر 14: في حالة عدم وجود أخطاء، يتم إرجاع السلسلة jSON للقيم المرسلة؛

في السطر 3، يتم استخدام نموذج الإجراء [ActionModel02] التالي:

  

package istia.st.springmvc.models;

import java.util.Date;

import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;

public class ActionModel02 {

    @NotNull(message = "La donnée est obligatoire")
    @AssertFalse(message = "Seule la valeur [false] est acceptée")
    private Boolean assertFalse;
    
    @NotNull(message = "La donnée est obligatoire")
    @AssertTrue(message = "Seule la valeur [true] est acceptée")
    private Boolean assertTrue;
    
    @NotNull(message = "La donnée est obligatoire")
    @Future(message = "Il faut une date postérieure à aujourd'hui")
    private Date dateInFuture;
    
    @NotNull(message = "La donnée est obligatoire")
    @Past(message = "Il faut une date antérieure à aujourd'hui")
    private Date dateInPast;
    
    @NotNull(message = "La donnée est obligatoire")
    @Max(value = 100, message = "Maximum 100")
    private Integer intMax100;
    
    @NotNull(message = "La donnée est obligatoire")
    @Min(value = 10, message = "Minimum 10")
    private Integer intMin10;
    
    @NotNull(message = "La donnée est obligatoire")
    @NotBlank(message = "La chaîne doit être non blanche")
    private String strNotBlank;
    
    @NotNull(message = "La donnée est obligatoire")
    @Size(min = 4, max = 6, message = "La chaîne doit avoir entre 4 et 6 caractères")
    private String strBetween4and6;
    
    @NotNull(message = "La donnée est obligatoire")
    @Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$", message = "Le format doit être hh:mm:ss")
    private String hhmmss;
    
    @NotNull(message = "La donnée est obligatoire")
    @Email(message = "Adresse invalide")
    private String email;
    
    @NotNull(message = "La donnée est obligatoire")
    @Length(max = 4, min = 4, message = "La chaîne doit avoir 4 caractères exactement")
    private String str4;
    
    @Range(min = 10, max = 14, message = "La valeur doit être dans l'intervalle [10,14]")
    @NotNull(message = "La donnée est obligatoire")
    private Integer int1014;
    
    @URL(message = "URL invalide")
    private String url;

    // دالات الحصول والتعيين

...
}

تستخدم الفئة قيود التحقق من الصحة المستمدة من حزمتين:

  • [javax.validation.constraints] في الأسطر 5-13؛
  • [org.hibernate.validator.constraints] في الأسطر 15-19؛

توجد تبعيات Maven لهاتين الحزمتين في المشروع:

  

هنا، لن نستخدم رسائل مُترجمة بل رسائل مُعرَّفة داخل القيد باستخدام السمة [message]. لاختبار هذا الإجراء، سنستخدم [Advanced Rest Client]:

  • في [1-2]، الاستعلام POST؛
  • إلى [3]، الرأس HTTP [Content-Type] المطلوب استخدامه؛
  • في [4]، يتيح الرابط [Add new value] إضافة زوج [paramètre, value
  • في [5]، أدخل حقلًا من [ActionModel02]، وهنا الحقل [assertFalse]:

    @NotNull(message = "La donnée est obligatoire")
    @AssertFalse(message = "Seule la valeur [false] est acceptée")
private Boolean assertFalse;
  • في [6]، أدخل قيمة خاطئة لترى رسالة خطأ. أعلاه، يشترط القيد [@AssertFalse] أن يكون للحقل [assertFalse] القيمة [false
  • في [7]، رد الخادم: تم تفعيل القيد [@NotNull] الخاص بالحقول الفارغة وعرض رسالة الخطأ المرتبطة به؛
  • في [8]، رسالة الحقل [assertFalse] الذي لم يتم التحقق من تقييد [@AssertFalse] الخاص به، بالإضافة إلى رموز هذا الخطأ. تجدر الإشارة إلى أن هذه الرموز قد تكون مرتبطة برسائل مدوللة؛

فيما يلي مثال آخر:

 

Image

ندعو القارئ إلى اختبار حالات الخطأ المختلفة حتى الوصول إلى POST الذي يحتوي على بيانات صالحة بالكامل:

ملاحظة: تنسيق التواريخ هو التنسيق الأنجلوساكسوني: شهر/يوم/سنة.

4.23. [/m28]: فصل رسائل الخطأ

في الفئة [ActionModel02]، قمنا بتضمين الرسائل بشكل ثابت. من الأفضل نقلها إلى ملفات رسائل منفصلة. نتبع نموذج الإجراء [/m25]. نقوم بإنشاء نموذج الإجراء الجديد [ActionModel03] التالي:

  

package istia.st.springmvc.models;

import java.util.Date;

import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;

public class ActionModel03 {

    @NotNull
    @AssertFalse
    private Boolean assertFalse;
    
    @NotNull
    @AssertTrue
    private Boolean assertTrue;
    
    @NotNull
    @Future
    private Date dateInFuture;
    
    @NotNull
    @Past
    private Date dateInPast;
    
    @NotNull
    @Max(value = 100)
    private Integer intMax100;
    
    @NotNull
    @Min(value = 10)
    private Integer intMin10;
    
    @NotNull
    @NotBlank
    private String strNotBlank;
    
    @NotNull
    @Size(min = 4, max = 6)
    private String strBetween4and6;
    
    @NotNull
    @Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$")
    private String hhmmss;
    
    @NotNull
    @Email
    private String email;
    
    @NotNull
    @Length(max = 4, min = 4)
    private String str4;
    
    @Range(min = 10, max = 14)
    @NotNull
    private Integer int1014;
    
    @URL
    private String url;

    // دالات الاسترجاع والتعيين
        ...
}

يتم نقل رسائل الخطأ إلى الملفات [messages.properties]:

  

الملف [messages_fr.properties] هو التالي:


NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
Range.actionModel03.int1014=La valeur doit être dans l'intervalle [10,14]
NotBlank.actionModel03.strNotBlank=La chaîne doit être non blanche
AssertFalse.actionModel03.assertFalse=Seule la valeur [false] est acceptée
Pattern.actionModel03.hhmmss=Le format doit être hh:mm:ss
Past.actionModel03.dateInPast=Il faut une date antérieure ou égale à celle d'aujourd'hui
Future.actionModel03.dateInFuture=Il faut une date postérieure à celle d'aujourd'hui
Length.actionModel03.str4=La chaîne doit avoir 4 caractères exactement
Min.actionModel03.intMin10=Minimum 10
Max.actionModel03.intMax100=Maximum 100
AssertTrue.actionModel03.assertTrue=Seule la valeur [true] est acceptée
Email.actionModel03.email=Adresse invalide
Size.actionModel03.strBetween4and6=La chaîne doit avoir entre 4 et 6 caractères
URL.actionModel03.url=URL invalide

تمت إضافة رسائل الخطأ إلى الأسطر من 4 إلى 16. وهي بالصيغة التالية:

code=message

لا يمكن أن تكون الرموز عشوائية. فهي تلك التي ظهرت في الإجراء [/m27] السابق. على سبيل المثال:

Image

في ملفات الرسائل، يجب استخدام أحد الرموز الأربعة المذكورة أعلاه للحقل [int1014].

ملف [messages_en.properties] هو كما يلي:


NotNull=The field can't be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer
Range.actionModel03.int1014=Value must be in [10,14] interval
NotBlank.actionModel03.strNotBlank=String can't be empty
AssertFalse.actionModel03.assertFalse=Only boolean [false] is allowed
Pattern.actionModel03.hhmmss=String format is hh:mm:ss
Past.actionModel03.dateInPast=Date must be before or equal to today's date
Future.actionModel03.dateInFuture=Date must be after today's date
Length.actionModel03.str4=String must be four characters long
Min.actionModel03.intMin10=Minimum 10
Max.actionModel03.intMax100=Maximum 100
AssertTrue.actionModel03.assertTrue=Only boolean [true] is allowed
Email.actionModel03.email=Invalid email
Size.actionModel03.strBetween4and6=String must be between four and six characters long
URL.actionModel03.url=Invalid URL

يتم استخدام نموذج الإجراء [ActionModel03] في الإجراء التالي:


// ----------------------- فصل رسائل الأخطاء ------------------------
    @RequestMapping(value = "/m28", method = RequestMethod.POST)
    public Map<String, Object> m28(@Valid ActionModel03 data, BindingResult result, HttpServletRequest request) {
        Map<String, Object> map = new HashMap<String, Object>();
        // سياق تطبيق Spring
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // الإعدادات المحلية
        Locale locale = RequestContextUtils.getLocale(request);
        // الأخطاء؟
        if (result.hasErrors()) {
            for (FieldError error : result.getFieldErrors()) {
                // البحث عن رسالة الخطأ بناءً على رموز الأخطاء
                // يتم البحث عن الرسالة في ملفات الرسائل
                // رموز الأخطاء في شكل جدول
                String[] codes = error.getCodes();
                // في شكل سلسلة
                String listCodes = String.join(" - ", codes);
                // البحث
                String msg = null;
                int i = 0;
                while (msg == null && i < codes.length) {
                    try {
                        msg = ctx.getMessage(codes[i], null, locale);
                    } catch (Exception e) {

                    }
                    i++;
                }
                // هل تم العثور عليها؟
                if (msg == null) {
                    msg = String.format("Indiquez un message pour l'un des codes [%s]", listCodes);
                }
                // تم العثور عليه - يتم إضافة الخطأ إلى القاموس
                map.put(error.getField(), msg);
            }
        } else {
            // لا توجد أخطاء
            map.put("data", data);
        }
        return map;
    }

لقد سبق أن علقنا على هذا النوع من الرموز. الشيء الوحيد المهم حقًا هو السطر 23: تعتمد رسالة الخطأ التي يتم استردادها على الإعدادات المحلية للطلب.

فيما يلي مثال باللغة الفرنسية:

والآن باللغة الإنجليزية: