Skip to content

4. اقدامات: مدل

بیایید به معماری یک برنامه Spring MVC بازگردیم:

در فصل قبلی، ما فرآیند مسیریابی درخواست [1] به کنترل‌کننده و اکشن [2a] که آن را پردازش می‌کند را بررسی کردیم – مکانیزمی که به آن مسیریابی (routing) گفته می‌شود. ما همچنین پاسخ‌های مختلفی را که یک اکشن می‌تواند به مرورگر بازگرداند، تشریح کردیم. تا اینجا، ما به اکشن‌هایی نگاه کردیم که از درخواست ارائه‌شده به آنها استفاده نمی‌کردند. یک درخواست مانند [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 {

}
  • خط ۵: توجه داشته باشید که تفسیر [@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);
}
  • خط ۴: این اکشن دو پارامتر به نام‌های [nom] و [age] را می‌پذیرد. این پارامترها با پارامترهایی که در درخواست‌های HTTP و GET دارای همان نام‌ها هستند، مقداردهی اولیه می‌شوند؛

نتایج در کروم برای [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);
}
  • خط ۴: این اکشن دو پارامتر به نام‌های [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));
}
  • خط ۲: این اکشن یک پارامتر با نام [name[]] را می‌پذیرد. این پارامتر در اینجا با تمام پارامترهایی که این نام را دارند مقداردهی اولیه می‌شود، چه در یک GET و چه در یک POST، زیرا نوع درخواست در اینجا مشخص نشده است؛

نتایج به شرح زیر است:

  • از طریق POST و [1]، پارامترهای [2] ارسال می‌شوند؛
  • پارامترها همچنین در URL و [3] تنظیم شده‌اند؛
  • در [4]، چهار پارامتر با نام مشابه [nom]: [Query String parameters] پارامترهای URL هستند، در حالی که [Form Data] پارامترهای ارسال‌شده هستند؛
  • در [5]، می‌بینیم که اکشن [/m03] چهار پارامتر با نام [nom] را بازیابی کرده است؛

4.4. [/m04]: نگاشت پارامترهای اکشن به یک شیء جاوا

عمل جدید زیر را در نظر بگیرید، [/m04]:


    // ------ پارامترها را به یک شیء (Command Object) نگاشت کنید ---------------
    @RequestMapping(value = "/m04", method = RequestMethod.POST)
    public Personne m04(Personne personne) {
        return person;
}
  • خط ۳: این اقدام یک شخص از نوع زیر را به‌عنوان پارامتر می‌پذیرد:

public class Personne {

    // شناسگر
    private Integer id;
    // نام
    private String nom;
    // سن
    private int age;
....
    // گیرنده‌ها و تنظیم‌کننده‌ها
...
}
  • برای ایجاد پارامتر [Personne personneSpring MVC یک [new Personne()] ایجاد می‌کند؛
  • سپس، اگر پارامترهایی با نام‌های یکسان با فیلدهای [id, nom, age] شیء ایجاد شده وجود داشته باشند، شیء را با استفاده از آن فیلدها از طریق متدهای تنظیم‌کننده (setters) آن‌ها نمونه‌سازی می‌کند؛
  • خط ۴: متد یک نوع [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;
}
  • خط ۲: URL پردازش‌شده به شکل [/m05/{a}/x/{b}] است، که در آن {param} یک عنصر پارامتر از URL است؛
  • خط ۳: عناصر پارامتری URL با حاشیه‌نویسی [@PathVariable] بازیابی می‌شوند؛
  • خطوط ۴–۶: عناصر بازیابی‌شده [a] و [b] در یک فرهنگ لغت قرار داده می‌شوند؛
  • خط ۷: پاسخ رشته 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;
}
  • خط ۳: عناصر از هر دو URL و [Integer a, Double b]، و همچنین یک پارامتر (GET یا POST) [Double c]، بازیابی می‌شوند؛
  • خطوط ۴–۷: این عناصر در یک دیکشنری قرار می‌گیرند؛
  • خط ۸: که پاسخ کلاینت را تشکیل می‌دهد؛ بنابراین کلاینت رشته 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();
}
  • خط ۳: از Spring MVC می‌خواهیم که شیء [HttpServletRequest request] را تزریق کند، که تمام اطلاعاتی را که می‌توان از درخواست به دست آورد در خود جای داده است؛
  • خطوط ۵–۱۰: تمام سربرگ‌های درخواست (HTTP) بازیابی شده و در یک رشته قرار می‌گیرند، که سپس به کلاینت ارسال می‌شود (خط ۱۱);

نتایج به شرح زیر است:

  • در [1]، سربرگ‌های HTTP از درخواست؛
  • به [2]، پاسخ. تمام سربرگ‌های درخواست (HTTP) در اینجا واقعاً موجود هستند.

4.8. [/m08]: دسترسی به شیء [Writer]

بیایید اقدام زیر را در نظر بگیریم:


    // ----------------------- تزریق نویسنده ------------------------
    @RequestMapping(value = "/m08", method = RequestMethod.GET)
    public void m08(Writer writer) throws IOException {
        writer.write("Bonjour le monde !");
}
  • خط ۳: Spring MVC شیء [Writer writer] را تزریق می‌کند، که امکان نوشتن در جریان پاسخ به کلاینت را فراهم می‌آورد؛
  • خط ۳: این اقدام یک نوع [void] را بازمی‌گرداند، که نشان می‌دهد باید خود پاسخ را برای مشتری بسازد؛
  • خط ۴: متن به جریان پاسخ برای کلاینت اضافه می‌شود؛

نتایج به شرح زیر است:

  • در [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;
}
  • خط ۳: حاشیه‌نویسی [@RequestHeader("User-Agent")] اجازه می‌دهد تا سربرگ HTTP [User-Agent] بازیابی شود؛
  • خط ۴: متن این سربرگ بازگردانده می‌شود؛

نتایج به شرح زیر است:

  • در [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"));
}
  • خط ۳: ما شیء [HttpServletResponse response] را تزریق می‌کنیم تا کنترل کامل پاسخ را به دست آوریم؛
  • خط ۴: ما یک کوکی با کلید [cookie1] و مقدار [remember me] ایجاد می‌کنیم (توجه: کاراکترهای دارای علامت در مقدار کوکی باعث خطا می‌شوند);
  • خط ۳: این عمل هیچ چیزی را بازنمی‌گرداند. علاوه بر این، هیچ چیزی را در بدنه پاسخ نمی‌نویسد. بنابراین، کلاینت یک سند خالی دریافت خواهد کرد. این پاسخ صرفاً برای افزودن هدر کوکی HTTP استفاده می‌شود؛

بیایید نتایج را بررسی کنیم:

  • در [1]: درخواست؛
  • [2]: پاسخ خالی است؛
  • همان‌طور که در [3] مشاهده می‌شود: کوکی ایجادشده توسط این اقدام؛

اکنون بیایید یک اکشن برای بازیابی این کوکی ایجاد کنیم که مرورگر از این پس با هر درخواست آن را ارسال خواهد کرد:


    // ----------------------- تزریق کوکی ------------------------
    @RequestMapping(value = "/m11", method = RequestMethod.GET)
    public String m10(@CookieValue("cookie1") String cookie1) {
        return cookie1;
}
  • خط ۳: تفسیر [@CookieValue("cookie1")] کوکی را با کلید [cookie1] بازیابی می‌کند؛
  • خط ۴: این مقدار پاسخ ارسال‌شده به کلاینت خواهد بود؛

بیایید نتایج را بررسی کنیم:

  • در [2]، می‌توانیم ببینیم که مرورگر کوکی را بازمی‌گرداند؛
  • در [3]، عمل به طور موفقیت‌آمیز آن را بازیابی کرده است؛

4.11. [/m12]: دسترسی به بدنه یک POST

پارامترهای POST معمولاً با هدر HTTP [Content-Type: application/x-www-form-urlencoded] همراه هستند. کل رشته POST قابل دسترسی است. ما اقدام زیر را ایجاد می‌کنیم:


    // ----------- بازیابی بدنه یک POST از نوع String------------------------
    @RequestMapping(value = "/m12", method = RequestMethod.POST)
    public String m12(@RequestBody String requestBody) {
        return requestBody;
}
  • خط ۳: تگ [@RequestBody] به ما امکان می‌دهد تا به محتوای POST دسترسی پیدا کنیم. در اینجا، فرض می‌کنیم که این از نوع [String] است؛
  • خط ۴: این محتوا به کلاینت بازگردانده می‌شود؛

در اینجا یک مثال اولیه آورده شده است:

  • در [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] ایجاد می‌شود. به‌طور پیش‌فرض، به جای مورد تعریف‌شده در [6]، از [5] استفاده خواهد شد. ویژگی [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();
}
  • خط ۲: [consumes = "application/json"] مشخص می‌کند که این عمل انتظار یک بدنه را دارد jSON;
  • خط ۳: [@RequestBody] این بدنه را نشان می‌دهد. این حاشیه‌نویسی با یک شی از نوع [Personne] مرتبط شده است. بدنه jSON به طور خودکار به این شی سریالی‌سازی خواهد شد؛
  • خط ۴: متد [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();
}
  • خط ۲: ما مشخص کرده‌ایم که متد انتظار یک جریان از نوع [text/plain] را دارد. Spring MVC سپس بدنه درخواست را به‌عنوان نوع [String] (خط ۳) در نظر می‌گیرد؛
  • خط ۴: رشته jSON به یک شیء [Personne] تبدیل می‌شود (به بخش ۹.۷، صفحه ۵۴۲ مراجعه کنید)؛

نتایج به شرح زیر است:

  • به [3] – اطمینان حاصل کنید که [text/plain] است؛

4.13. [/m15]: بازیابی جلسه

بیایید معماری اجرای یک اقدام را دوباره بررسی کنیم:

کلاس کنترلر در ابتدای درخواست مشتری ایجاد و در پایان آن نابود می‌شود. بنابراین، نمی‌توان از آن برای ذخیره‌سازی داده‌ها بین درخواست‌ها استفاده کرد، حتی اگر بارها فراخوانی شود. دو نوع داده وجود دارد که ممکن است بخواهیم ذخیره کنیم:

  • داده‌های مشترک بین همه کاربران وب‌اپلیکیشن. این داده‌ها معمولاً فقط-خواندنی هستند؛
  • داده‌هایی که در طول درخواست‌های یک کلاینت مشترک هستند. این داده‌ها در ابجکتی به نام Session ذخیره می‌شوند. اصطلاح «جلسه کلاینت» برای اشاره به حافظه کلاینت به کار می‌رود. تمام درخواست‌های یک کلاینت به این جلسه دسترسی دارند. آنها می‌توانند اطلاعات را در آن ذخیره و از آن بخوانند.

در بالا، انواع حافظه‌ای را که یک اکشن به آن دسترسی دارد، نشان می‌دهیم:

  • حافظهٔ برنامه، که عمدتاً حاوی داده‌های فقط-خواندنی است و برای همهٔ کاربران قابل دسترسی است؛
  • حافظه یک کاربر خاص، یا جلسه، که حاوی داده‌های قابل خواندن/نوشتن است و برای درخواست‌های متوالی از همان کاربر قابل دسترسی است؛
  • در بالا نشان داده نشده است، یک حافظه درخواست، یا زمینه درخواست وجود دارد. درخواست یک کاربر ممکن است توسط چندین اقدام متوالی پردازش شود. زمینه درخواست به اقدام ۱ اجازه می‌دهد تا اطلاعات را به اقدام ۲ منتقل کند.

بیایید به یک مثال اولیه که این انواع مختلف حافظه را نشان می‌دهد، نگاهی بیندازیم:


    // ----------------------- بازیابی جلسه ------------------------
    @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);
}

اسپرینگ MVC جلسه (session) کاربر را در یک شیء از نوع [HttpSession] حفظ می‌کند.

  • خط ۳: از Spring خواسته می‌شود تا شیء [HttpSession] را به پارامترهای اکشن تزریق کند؛
  • خط ۵: یک ویژگی با نام [compteur] از پارامترهای اکشن بازیابی می‌شود؛ یک جلسه مانند یک فرهنگ لغت، مجموعه‌ای از جفت‌های کلید-مقدار [clé, valeur] رفتار می‌کند. اگر کلید [compteur] در جلسه وجود نداشته باشد، یک نشانگر null بازیابی می‌شود؛
  • خط ۷: مقداری که با کلید [compteur] مرتبط است از نوع [Integer] خواهد بود؛
  • خط ۸: شمارنده را افزایش دهید؛
  • خط ۱۰: شمارنده در جلسه به‌روزرسانی می‌شود؛
  • خط ۱۲: مقدار شمارنده به کلاینت ارسال می‌شود؛

زمانی که [/m15] برای: اجرا می‌شود

  • بار اول، در خط ۱۲، مقدار شمارنده برابر با ۱ خواهد بود؛
  • بار دوم، در خط ۵، این مقدار ۱ بازیابی شده و روی ۲ تنظیم می‌شود؛
  • ...

در اینجا مثالی از اجرا آورده شده است:

  • در [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;
    }

}
  • خط ۷: آناوتیشن [@Component] یک آناوتیشن اسپرینگ (خط ۵) است که کلاس [SessionModel] را به یک کامپوننت تبدیل می‌کند که چرخه عمر آن توسط اسپرینگ مدیریت می‌شود؛
  • خط ۸: anotation [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] نیز یک anotation از Spring (خطوط ۳–۴) است. وقتی Spring MVC با آن مواجه می‌شود، کلاس مربوطه ایجاد شده و در جلسه کاربر قرار می‌گیرد. ویژگی [proxyMode = ScopedProxyMode.TARGET_CLASS] مهم است. به لطف همین ویژگی است که Spring MVC به ازای هر کاربر یک نمونه ایجاد می‌کند، نه یک نمونه واحد برای همه کاربران (singleton
  • خط ۱۱: شمارنده؛

برای شناسایی این کامپوننت جدید 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);
    }
}
  • خط ۹: کامپوننت‌های Spring در پکیج [istia.st.springmvc.controllers] جستجو می‌شوند. این دیگر کافی نیست. ما این خط را به شرح زیر به‌روزرسانی می‌کنیم:

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

ما بسته‌ای را که حاوی کلاس [SessionModel] است اضافه کرده‌ایم.

اکنون، ما اقدام زیر را اضافه می‌کنیم:


    @Autowired
    private SessionModel session;
    
    //------- مدیریت یک شیء در محدوده جلسه [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());
}
  • خطوط ۱–۲: کامپوننت Spring با شناسه [SessionModel] به کنترلر به عنوان [@Autowired] تزریق می‌شود. شایان ذکر است که یک کنترلر Spring یک نمونهٔ واحد (singleton) است. بنابراین تزریق یک کامپوننت با دامنهٔ محدودتر – در این مورد، دامنهٔ [Session] – به آن متناقض است. در اینجا anotation [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] روی کامپوننت [SessionModel] وارد عمل می‌شود. هرگاه کد کنترل‌کننده در خط ۲ به فیلد [session] دسترسی پیدا می‌کند، یک متد پروکسی اجرا می‌شود تا جلسه را برای درخواستی که در حال حاضر توسط کنترل‌کننده پردازش می‌شود، بازگرداند؛
  • خط ۶: شیء [HttpSession] دیگر در پارامترهای اکشن مورد نیاز نیست؛
  • خط ۷: شمارنده بازیابی شده و مقدار آن افزایش می‌یابد؛
  • خط ۸: مقدار آن بازگردانده می‌شود؛

در اینجا مثالی از اجرا آورده شده است:

اولین بار

بار دوم

حالا، بیایید یک مرورگر دیگر را برای نمایش کاربر دوم در نظر بگیریم. در اینجا از مرورگر اپرا استفاده خواهیم کرد:

همان‌طور که در [1] در بالا نشان داده شده است، این کاربر دوم مقدار شمارنده ۱ را دریافت می‌کند. این نشان می‌دهد که جلسه او با جلسه کاربر اول متفاوت است. اگر به مبادلات کلاینت/سرور (برای اپرا نیز Ctrl+Shift+I) نگاه کنیم، می‌توانیم در [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;
    }

}
  • خط ۵: تذکر [@Component] تضمین می‌کند که کلاس [ApplicationModel] یک کامپوننت مدیریت‌شده توسط Spring خواهد بود. ماهیت پیش‌فرض کامپوننت‌های Spring از نوع [singleton] است: یک نمونه واحد از کامپوننت هنگام نمونه‌سازی کانتینر Spring ایجاد می‌شود، یعنی عموماً هنگام راه‌اندازی برنامه. ما می‌توانیم از این چرخه عمر برای ذخیره اطلاعات پیکربندی در singleton استفاده کنیم که برای همه کاربران قابل دسترسی خواهد بود؛
  • خط ۱۱: یک شمارنده از نوع [AtomicLong]. این نوع دارای متدی به نام [incrementAndGet] است که به عنوان یک متد اتمی شناخته می‌شود. این بدان معناست که یک تار (thread) که این متد را اجرا می‌کند، می‌تواند مطمئن باشد که هیچ تار دیگری بین خواندن (Get) تار اول و افزایش (increment) آن، مقدار شمارنده را نخواهد خواند (Get)، که این امر باعث خطا می‌شود زیرا دو تار یکسان مقدار شمارنده را می‌خوانند و شمارنده به جای دو واحد، تنها یک واحد افزایش می‌یابد؛

ما اقدام جدید زیر را ایجاد می‌کنیم، [/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());
    }
  • خطوط ۱–۲: ما کامپوننت [ApplicationModel] را به کنترلر تزریق می‌کنیم. این یک اشیاء واحد (singleton) است. بنابراین، هر کاربر به یک نمونه از همان شیء ارجاع خواهد داشت؛
  • خط ۷: ما شمارنده دامنه [application] را پس از افزایش آن برمی‌گردانیم؛

در اینجا دو مثال آورده شده است، یکی با استفاده از کروم و دیگری با استفاده از اپرا:

در بالا، می‌بینیم که هر دو مرورگر با یک شمارنده یکسان کار کرده‌اند، که این موضوع در مورد جلسه (session) صدق نمی‌کرد. این دو مرورگر نماینده دو کاربر مختلف هستند که هر دو به داده‌های دامنه [application] دسترسی دارند. به‌طور کلی، باید از قرار دادن اطلاعات خواندن/نوشتن در اشیاء دامنه [application] خودداری کنید، همان‌طور که در بالا با شمارنده انجام شد. این به آن دلیل است که نخ‌های اجرایی (execution threads) کاربران مختلف همزمان به داده‌های موجود در دامنه [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());
    }
  • خطوط ۳–۶: اکشن [/m18] هیچ نتیجه‌ای بر نمی‌گرداند. این اکشن فقط برای ایجاد یک شیء در جلسه با کلید [container] استفاده می‌شود؛
  • خط ۱۱: در عمل [/m19]، حاشیه‌نویسی [@ModelAttribute] استفاده می‌شود. رفتار این حاشیه‌نویسی نسبتاً پیچیده است. پارامتر [container] این حاشیه‌نویسی می‌تواند به موارد مختلفی اشاره کند، و به‌ویژه به یک شیء در جلسه. برای اینکه این کار کند، شیء باید با یک حاشیه‌نویسی [@SessionAttributes] روی خود کلاس اعلام شده باشد:

@RestController
@SessionAttributes({"container"})
public class ActionModelController {
  • خط ۲ بالا، کلید [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 مشتق شود اگر یک مبدل رشته به نوع ویژگی وجود داشته باشد
    //در غیر این صورت، با استفاده از سازنده پیش‌فرض ساخته می‌شود
    // سپس ویژگی‌های مدل با پارامترهای موجود در GET یا POST مقداردهی اولیه می‌شوند
    // نتیجهٔ نهایی بخشی از مدل تولیدشده توسط اکشن را تشکیل خواهد داد
    
    // ویژگی p به آرگومان‌ها تزریق می‌شود------------------------
    @RequestMapping(value = "/m20", method = RequestMethod.GET)
    public Personne m20(@ModelAttribute("p") Personne personne) {
        return personne;
}
  • خطوط ۲–۵: یک ویژگی مدل به نام [p] را تعریف کنید. این مدل M یک نما V است که توسط نوع [Model] در Spring MVC نمایش داده می‌شود. یک مدل مانند یک فرهنگ لغت از جفت‌های [clé, valeur] رفتار می‌کند. در اینجا، کلید [p] با شی [Personne] که توسط متد [getPersonne] ساخته شده است، مرتبط است. نام متد می‌تواند هر چیزی باشد؛
  • خط 17: ویژگی قالب کلید [p] به پارامترهای اکشن تزریق می‌شود. این تزریق مطابق قواعدی است که در خطوط 8–12 تعریف شده‌اند. در اینجا، ما با مورد تعریف‌شده در خط ۹ سروکار داریم. بنابراین، در خط ۱۷، پارامتر [Personne personne] شیء [Personne(7,'abcd',14)] خواهد بود؛
  • خط ۱۸: شیء [personne] برای تأیید بازگردانده می‌شود. این شیء قبل از ارسال به کلاینت به صورت jSON سریالیزه خواهد شد.

در اینجا یک مثال آورده شده است:

 

اکنون، بیایید اقدام زیر را بررسی کنیم:


    // --------- ویژگی p به‌طور خودکار بخشی از قالب M برای نما V را تشکیل می‌دهد
    @RequestMapping(value = "/m21", method = RequestMethod.GET)
    public String m21(Model model) {
        return model.toString();
}

یک اکشن که هدف آن نمایش یک ویو V است، باید مدل M خود را بسازد. اسپرینگ MVC این کار را با استفاده از یک نوع [Model] مدیریت می‌کند که می‌تواند به پارامترهای اکشن تزریق شود. در ابتدا، این مدل خالی است یا شامل اطلاعاتی است که با حاشیه‌نویسی [@ModelAttribute] برچسب‌گذاری شده‌اند. اکشن ممکن است این مدل را قبل از ارسال به ویو غنی‌سازی کند یا نکند.

  • خط ۳: تزریق مدل M؛
  • خط ۴: می‌خواهیم ببینیم داخل آن چیست. آن را به صورت یک رشته سریالیزه می‌کنیم تا به کلاینت ارسال شود. در اینجا از متد [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();
}
  • خط ۳: ویژگی قالب کلیدی [param1] وجود ندارد. در این مورد، نوع مربوطه باید یک سازنده پیش‌فرض داشته باشد. این امر برای نوع [String] صدق می‌کند، اما نمی‌توانیم [@ModelAttribute("param1") Integer p1] را بنویسیم زیرا کلاس [Integer] سازنده پیش‌فرض ندارد؛
  • خط ۴: مدل بازگردانده می‌شود تا بررسی شود که آیا ویژگی کلیدی مدل [param1] بخشی از آن است؛

در اینجا مثالی از اجرای آن آمده است:

 

ویژگی قالب [param1] در واقع در قالب وجود دارد، اما متد [toString] برای مقدار مربوطه هیچ اطلاعاتی در مورد این مقدار ارائه نمی‌دهد.

اکنون بیایید اقدام زیر را در نظر بگیریم که در آن به‌طور صریح اطلاعاتی را در مدل وارد می‌کنیم:


    // --------- ویژگی قالب [param2] به‌طور صریح در قالب گنجانده شده است
    @RequestMapping(value = "/m23", method = RequestMethod.GET)
    public String m23(String p2, Model model) {
        model.addAttribute("param2",p2);
        return model.toString();
}
  • خط ۴: مقدار [p2] که در خط ۳ بازیابی شده است، در مدلی که با کلید [param2] مرتبط است، قرار می‌گیرد:

در اینجا مثالی از اجرا آورده شده است:

 

قوانین در صورتی که پارامتر action یک شیء باشد تغییر می‌کنند. در اینجا یک مثال اول آورده شده است:


    // ------ ویژگی قالب [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();
}
  • خط ۴: حاشیه‌نویسی [@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;

    // گیرنده‌ها و تنظیم‌کننده‌ها
...
    }
  • خطوط ۸ و ۹: انوتیشن [@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;
}
  • خط ۳: یک شیء [ActionModel01] نمونه برداری شده و فیلدهای [a, b] آن با پارامترهایی با همان نام‌ها مقداردهی اولیه می‌شود. حاشیه‌نویسی [@Valid] نشان می‌دهد که محدودیت‌های اعتبار باید بررسی شوند. نتایج این بررسی در پارامتر از نوع [BindingResult] (پارامتر دوم) ذخیره خواهد شد. بررسی‌های زیر انجام خواهند شد:
    • به دلیل حاشیه‌نویسی‌های [@NotNull]، پارامترهای [a] و [b] باید موجود باشند؛
    • به دلیل نوع [Integer a]، پارامتر [a] – که ذاتاً از نوع [String] است – باید قابل تبدیل به نوع [Integer] باشد؛
    • به دلیل نوع [Double b]، پارامتر [b]، که ذاتاً از نوع [String] است، باید قابل تبدیل به نوع [Double] باشد؛

با انوتیشن [@Valid]، خطاهای اعتبارسنجی در پارامتر [BindingResult result] گزارش خواهند شد. بدون آنوتیشن [@Valid]، خطاهای اعتبارسنجی باعث کرش شدن اکشن می‌شوند و سرور یک پاسخ HTTP با کد وضعیت 500 (خطای داخلی سرور) به کلاینت ارسال می‌کند.

  • خط ۳: نتیجهٔ اقدام از نوع [Map] است. رشته‌ای از این نتیجه با مقدار jSON به کلاینت ارسال می‌شود. دو نوع دیکشنری ساخته می‌شوند:
    • در صورت بروز خطا، یک دیکشنری با یک ورودی ['errors', value]، که در آن [value] رشته‌ای است که تمام خطاها را توصیف می‌کند (خط ۱۳) ایجاد می‌شود؛
    • در صورت موفقیت، یک دیکشنری با یک ورودی واحد ['data',value]، که در آن [value] خود یک دیکشنری با دو ورودی است: ['a', value]، ['b', value] (خط ۱۹);
  • خطوط ۹–۱۲: برای هر خطای شناسایی‌شده [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]: خطای نوع روی نوع عدد صحیح؛
  • [typeMismatch]: خطای نوع؛

همچنین توجه داریم که کد خطا برای فیلد [a] که از [error.getCode()] مشتق شده است، [typeMismatch] است (به اسکرین‌شات بالا مراجعه کنید).

ما پیام‌های خطا را در یک فایل properties قرار خواهیم داد:

  

فایل [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 به شرح زیر پیکربندی شود:

  

ما anotationهای پیکربندی را از کلاس [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);
    }
}
  • خط ۸: برنامه 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;
    }
}
  • خطوط ۱۱–۱۳: این خطوط حاوی anotationهای پیکربندی هستند که قبلاً در کلاس [Application] قرار داشتند؛
  • خط ۱۴: برای پیکربندی یک برنامه Spring MVC، باید کلاس [WebMvcConfigurerAdapter] را گسترش دهید؛
  • خط ۱۵: آناوتیشن [@Bean] یک کامپوننت اسپرینگ، یک سنگل‌تون، را معرفی می‌کند؛
  • خط ۱۶: یک بیون به نام [messageSource] (نام متد) تعریف شده است. این بیون برای تعریف فایل‌های پیام برنامه استفاده می‌شود و باید این نام را داشته باشد؛
  • خطوط 17–19: به اسپرینگ اطلاع می‌دهند که فایل پیام:
    • در پوشه [i18n] در داخل کلاس‌پات پروژه قرار دارد (خط ۱۸)،
    • نام آن [messages.properties] است (خط ۱۸). در واقع، عبارت [messages] ریشه نام فایل پیام است، نه خود نام. خواهیم دید که در زمینه بین‌المللی‌سازی ممکن است چندین فایل پیام وجود داشته باشد، یکی برای هر زبان پشتیبانی‌شده. بنابراین، ممکن است ما [messages_fr.properties] را برای زبان فرانسوی و [messages_en.properties] را برای زبان انگلیسی داشته باشیم. پسوندهای افزوده شده به ریشه [messages] استاندارد شده‌اند. شما نمی‌توانید هر چیزی را استفاده کنید؛

در پروژه STS، پوشه [i18n] باید در پوشه منابع قرار داده شود، زیرا این پوشه در مسیر کلاس پروژه گنجانده شده است:

  

برای استفاده از این فایل، ما اقدام جدید زیر را ایجاد می‌کنیم:


// اعتبارسنجی یک مدل، رسیدگی به پیام‌های خطا ------------------------
    @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] است. در اینجا توضیحی از تفاوت‌ها آورده شده است:

  • خط ۳: ما درخواست [HttpServletRequest request] را به پارامترهای اکشن تزریق می‌کنیم. به این درخواست نیاز خواهیم داشت؛
  • خطوط ۷–۸: ما کانکست Spring را بازیابی می‌کنیم. این کانکست شامل تمام Spring beansهای برنامه است. همچنین دسترسی به فایل‌های پیام را فراهم می‌کند؛
  • خط ۱۰: ما لوکال (محلی‌سازی) برنامه را بازیابی می‌کنیم. این اصطلاح در ادامه با جزئیات بیشتری توضیح داده شده است؛
  • خطوط ۱۵–۳۱: برای هر خطا، به دنبال پیامی متناسب با یکی از این کدهای خطا می‌گردیم. این پیام‌ها به ترتیبی که کدها در [error.getCodes()] یافت شده‌اند، جستجو می‌شوند. به محض یافتن یک پیام، جستجو متوقف می‌شود؛
  • خط ۲۶: چگونه یک پیام را در [messages.properties] بازیابی کنیم:
    • پارامتر اول کدی است که در [messages.properties] جستجو می‌شود،
    • دومین یک آرایه از پارامترها است، زیرا پیام‌ها گاهی اوقات پارامتریک هستند. در اینجا اینطور نیست،
    • سومین مورد، لوکال مورد استفاده است (که از خط ۱۰ به دست آمده است). لوکال، زبان مورد استفاده را مشخص می‌کند: [fr_FR] برای فرانسوی (فرانسه)، [en_US] برای انگلیسی (USA). پیام در فایل messages_[locale].properties جستجو می‌شود، بنابراین برای مثال [messages_fr_FR.properties]. اگر این فایل وجود نداشته باشد، پیام در [messages_fr.properties] جستجو می‌شود. اگر آن فایل وجود نداشته باشد، پیام در [messages.properties] جستجو می‌شود. این مورد آخر است که برای ما کار می‌کند؛
  • خطوط ۲۵–۲۹: به طور نسبتاً غیرمنتظره، هنگام جستجوی یک کد وجود نداشته در یک فایل پیام، به جای یک اشاره‌گر null، یک استثنا پرتاب می‌شود؛
  • خطوط ۳۳–۳۵: ما موردی را که در آن پیام خطا وجود ندارد، مدیریت می‌کنیم؛
  • خطوط ۳۷–۳۸: ما رشته خطا را می‌سازیم. این شامل لوکال و پیام خطای یافت‌شده است؛

در اینجا چند مثال از خروجی آورده شده است:

 

می‌توانیم ببینیم که:

  • محلی‌سازی برنامه [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;
    }
}
  • خطوط ۲۸–۳۲: ما یک interceptor درخواست ایجاد می‌کنیم. یک interceptor درخواست، رابط [HandlerInterceptor] را گسترش می‌دهد. چنین کلاسی، درخواست ورودی را قبل از اینکه توسط یک action پردازش شود، بررسی می‌کند. در اینجا، اینترسپتور [localeChangeInterceptor] در درخواست ورودی به دنبال پارامتر با نام [lang] خواهد گشت، GET یا POST، و لوکال برنامه را بر اساس این پارامتر تغییر خواهد داد. بنابراین، اگر پارامتر [lang=en_US] باشد، زبان برنامه به انگلیسی (USA) تغییر خواهد کرد؛
  • خطوط ۳۴–۳۷: متد [WebMvcConfigurerAdapter.addInterceptors] برای افزودن اینترسپتور قبلی بازتعریف شده است؛
  • خطوط ۳۹–۴۵: این‌ها برای پیکربندی نحوهٔ جاسازی لوکال در یک کوکی استفاده می‌شوند. می‌دانیم که یک کوکی می‌تواند به‌عنوان حافظهٔ کاربر عمل کند، زیرا مرورگر کلاینت به‌طور سیستماتیک آن را به سرور بازمی‌فرستد. اینترسپتور قبلی [localeChangeInterceptor] یک کوکی ایجاد می‌کند که لوکال را در خود جاسازی می‌کند. خط ۴۲ نام [lang] را به این کوکی اختصاص می‌دهد. این کوکی همچنین برای تغییر لوکال استفاده می‌شود؛
  • خط ۴۳: مشخص می‌کند که در صورت عدم وجود کوکی [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] استفاده خواهد شد. بنابراین کاربر پیام‌ها را به زبان انگلیسی خواهد دید.

بیایید آن را امتحان کنیم. ابتدا، در ابزار توسعه‌دهنده کروم (Ctrl+Shift+I)، کوکی‌های خود را بررسی کنید:

 

اگر کوکی‌ای با نام [lang] دارید، آن را حذف کنید. سپس، با استفاده از کروم، درخواست URL و [http://localhost:8080/m25] را بدهید:

 

مرورگر سربرگ‌های زیر را ارسال کرد:

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] وجود ندارد. در این مورد، کد ما از locale [fr] استفاده می‌کند. این در اسکرین‌شات نشان داده شده است. بیایید سناریوی دیگری را امتحان کنیم:

  • در [1]، ما پارامتر [lang=en] را برای تغییر لوکال به [en] ارسال کرده‌ایم؛
  • در [2]، می‌توانیم محل جدید را ببینیم؛
  • در [3]، پیام به انگلیسی تغییر کرده است؛

حال بیایید نگاهی به مبادلات در HTTP بیندازیم:

 

همان‌طور که در بالا مشاهده می‌کنیم، سرور یک کوکی بازگردانده است: [lang]. این یک پیامد مهم دارد: به دلیل کوکی [lang] که توسط مرورگر بازگردانده می‌شود، locale برای درخواست بعدی دوباره [en] خواهد بود. بنابراین باید پیام‌ها را به زبان انگلیسی نگه داریم. بیایید این را بررسی کنیم:

 

در بالا، می‌بینیم که لوکال به [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());
}
 

همان‌طور که در بالا مشاهده می‌شود، اعتبار locale درخواست‌شده بررسی نمی‌شود. با این حال، درخواست بعدی مرورگر یک استثنای سمت سرور را فعال می‌کند زیرا کوکی locale که دریافت می‌کند نادرست است.

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;
}

این کدی است که ما چندین بار آن را دیده‌ایم:

  • خط ۳: اقدام [/m27] از طریق POST درخواست شده است؛
  • خطوط ۸–۱۱: هر خطا توسط [champ, message] با: شناسایی خواهد شد
    • field: فیلدی که خطا در آن وجود دارد،
    • پیغام: پیغام خطای مرتبط و فهرست کدهای خطا؛
  • خط 14: اگر هیچ خطایی وجود نداشته باشد، رشته jSON حاوی مقادیر ارسال‌شده بازگردانده می‌شود؛

در خط ۳، از قالب اقدام زیر [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] در خطوط ۱۵–۱۹؛

وابستگی‌های 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، که در آن تمام داده‌ها معتبر هستند، آزمایش کنند:

توجه: فرمت تاریخ، فرمت آنگلوساکسون است: mm/dd/yyyy.

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

پیام‌های خطا به خطوط ۴–۱۶ اضافه شده‌اند. آن‌ها در قالب زیر هستند:

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;
    }

ما قبلاً در مورد این نوع کد بحث کرده‌ایم. تنها چیزی که واقعاً اهمیت دارد خط ۲۳ است: پیام خطای بازگردانده‌شده به locale درخواست بستگی دارد.

در اینجا یک مثال به زبان فرانسوی آورده شده است:

و اکنون به زبان انگلیسی: