التحقّق ومعالجة الاستثناءات

التحقق من صحة البيانات (Jakarta Validation)

18 دقيقة الدرس 2 من 13

التحقق من صحة البيانات (Jakarta Validation)

قبل أن تصل أي طلب إلى منطق العمل الخاصة بك، تحتاج إلى التأكد من أن البيانات التي يحملها سليمة هيكليًا. Jakarta Bean Validation (كانت تُعرف سابقًا بـ javax.validation ثم أصبحت jakarta.validation) هي المواصفة القياسية في Java التي تتيح لك التعبير عن القيود مباشرةً على فئات النماذج باستخدام التعليقات التوضيحية (annotations). يدمج Spring Boot 3 هذه المواصفة بشكل كامل عبر Hibernate Validator، وهو التنفيذ المرجعي لها.

التبعية التي تحتاجها

أضف المُبدِّئ (starter) الخاص بـ Spring Boot — فهو يسحب Hibernate Validator وواجهة برمجة Jakarta Validation API تلقائيًا:

<!-- Maven (pom.xml) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>

بدون هذا المُبدِّئ تُترجَم التعليقات التوضيحية بشكل صحيح لكن القيود تُتجاهل بصمت في وقت التشغيل — وهو مصدر شائع للارتباك لدى المطورين القادمين من مشاريع Spring القديمة.

لا تعتمد على التضمين الضمني. لم يعد spring-boot-starter-web يشمل مُبدِّئ التحقق منذ Spring Boot 2.3. صرّح به صراحةً في كل وحدة تُجري فيها عمليات التحقق من المدخلات.

كيف يعمل Bean Validation

تُعرّف المواصفة كيانًا يُسمى Validator يفحص حقول الكائن وخصائصه ومعاملات المُنشئ مقابل مجموعة من القيود المُعلَن عنها. كل قيد هو تعليق توضيحي مدعوم بتنفيذ ConstraintValidator يحتوي على منطق الفحص الفعلي. عندما تُعالج Spring تعليق @Valid أو @Validated على معامل دالة، تستدعي المُحقِّق قبل تنفيذ جسم الدالة. إذا فشل أي قيد، يُرمى استثناء MethodArgumentNotValidException.

الفرق بين المواصفة والتنفيذ: تعيش واجهة البرمجة في jakarta.validation:jakarta.validation-api. أما Hibernate Validator فهو التنفيذ الذي يوفر مُحققي القيود الفعليين، مع إضافات كثيرة تتجاوز المواصفة. أنت تكتب الكود مقابل الواجهة؛ والتنفيذ قابل للاستبدال.

تعليقات القيود القياسية

تأتي مواصفة Jakarta Validation مع نحو 25 قيدًا مدمجًا. أكثرها استخدامًا في العمل اليومي هي:

  • @NotNull — يجب ألا يكون الحقل null. يقبل السلاسل الفارغة والسلاسل التي تحتوي على مسافات فقط.
  • @NotEmpty — يجب ألا يكون null ويجب أن يحتوي على حرف واحد على الأقل (أو عنصر واحد للمجموعات).
  • @NotBlank — يجب ألا يكون null ولا فارغًا ويجب أن يحتوي على حرف واحد على الأقل غير مسافة. هذا هو الخيار الصحيح لحقول النص التي يُدخلها المستخدمون.
  • @Size(min, max) — يقيّد الطول (String، المصفوفة، Collection، Map) أو العدد بين min و max. كلا الحدّين شاملان.
  • @Min(value) / @Max(value) — الحد الأدنى والأقصى للأرقام (يعمل مع int و long و BigDecimal وغيرها).
  • @Email — يجب أن تتطابق السلسلة مع تنسيق عنوان بريد إلكتروني صالح وفقًا لمعيار RFC.
  • @Pattern(regexp) — يجب أن تتطابق السلسلة مع التعبير النمطي المُحدد.
  • @Positive / @PositiveOrZero / @Negative — قيود الإشارة للأنواع الرقمية.
  • @Past / @Future — قيود زمنية لأنواع LocalDate و Instant وما يشابهها.
  • @Digits(integer, fraction) — يُحدد عدد الأرقام الصحيحة والعشرية المسموح بها.

إضافة التعليقات التوضيحية على كائن نقل البيانات (DTO)

النمط المعتاد في Spring Boot هو إضافة التعليقات على فئة Java بسيطة (تُسمى عادةً request DTO أو command object) تمثّل البيانات الواردة. هذا مثال واقعي لطلب تسجيل مستخدم:

package com.example.api.dto; import jakarta.validation.constraints.*; public class RegisterRequest { @NotBlank(message = "Username is required") @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters") private String username; @NotBlank(message = "Email is required") @Email(message = "Must be a valid email address") @Size(max = 255) private String email; @NotBlank(message = "Password is required") @Size(min = 8, message = "Password must be at least 8 characters") private String password; @NotNull(message = "Age is required") @Min(value = 18, message = "Must be at least 18 years old") @Max(value = 120, message = "Age does not look realistic") private Integer age; // getters and setters omitted for brevity }

لاحظ عدة أنماط تستحق الترسيخ:

  • استخدم @NotBlank بدلًا من @NotNull لحقول String حيث يجب أيضًا رفض السلاسل الفارغة أو التي تحتوي على مسافات فقط.
  • أضف دائمًا سمة message تخاطب المستخدم النهائي أو مستهلك الـ API، لا المطوّر.
  • ضع تعليقات متعددة على الحقل نفسه — تعمل جميعها، وتُبلَّغ جميع الإخفاقات في تمريرة واحدة.
  • لأنواع المجمّعات البدائية (Integer و Long) استخدم @NotNull بشكل منفصل عن @Min/@Max لأن قيود الأرقام هذه لا تشترط عدم كون القيمة null.

@NotNull مقابل @NotEmpty مقابل @NotBlank

هذه الثلاثة هي مصدر معظم أخطاء المبتدئين. مقارنة جانبية:

  • @NotNull: يقبل ""، يقبل " "، يرفض فقط null.
  • @NotEmpty: يرفض null و ""، يقبل " ".
  • @NotBlank: يرفض null و "" و " " — الأكثر صرامةً بين الثلاثة للسلاسل النصية.
قاعدة عامة للحقول النصية: استخدم @NotBlank على السلاسل التي يُدخلها المستخدم (الاسم، اسم المستخدم، العنوان). احتفظ بـ @NotNull للحقول غير النصية (الأرقام، القيم المنطقية، الكائنات المتداخلة) حيث تحتاج ببساطة إلى التأكد من الوجود. أما @NotEmpty فهو مفيد للمجموعات والمصفوفات.

قيد @Email في التطبيق العملي

تعليق @Email في المواصفة يتحقق من التنسيق الهيكلي لعنوان البريد الإلكتروني. افتراضيًا، فحص Hibernate Validator متساهل نسبيًا — يقبل بعض التنسيقات الصالحة تقنيًا لكن غير المعتادة. للتحقق الأكثر صرامةً المطابق للمعيار RFC 5322، مرّر سمة regexp:

// Jakarta القياسي — متساهل لكن متوافق مع المواصفة @Email private String email; // أكثر صرامة: يجب أن يحتوي الدومين على نقطة @Email(regexp = "^[\\w._%+\\-]+@[\\w.\\-]+\\.[a-zA-Z]{2,}$", message = "Email address is not valid") private String email;

في معظم الـ APIs يكفي @Email القياسي. احتفظ بالتعديل بتعبير نمطي للحالات التي تتحكم فيها بشكل صارم في تنسيق العنوان (مثل أدوات المؤسسات الداخلية).

@Size على المجموعات والمصفوفات

@Size لا يقتصر على السلاسل النصية. ينطبق على أي نوع يمتلك حجمًا قابلًا للقياس:

import jakarta.validation.constraints.Size; import java.util.List; public class CreateTagsRequest { @Size(min = 1, max = 10, message = "Provide between 1 and 10 tags") private List<String> tags; }

عندما تكون القائمة null، يجتاز @Size الفحص (تعتبر معظم القيود أن null صالحة ما لم تُدمج مع @NotNull). ادمجهما عندما تهمّك كلٌّ من الحضور والحجم.

تخصيص الرسائل الافتراضية

لكل قيد رسالة افتراضية مخزّنة في ValidationMessages.properties على مسار الفئات. يمكنك تجاوزها عالميًا بوضع ملفك الخاص في src/main/resources/ValidationMessages.properties:

# src/main/resources/ValidationMessages.properties jakarta.validation.constraints.NotBlank.message=This field is required and cannot be blank. jakarta.validation.constraints.Email.message=Please provide a valid email address. jakarta.validation.constraints.Size.message=Length must be between {min} and {max}.

تُحلّ العناصر النائبة {min} و {max} تلقائيًا من سمات التعليق التوضيحي.

الخلاصة

يمنحك Jakarta Bean Validation طريقة تصريحية تعتمد على التعليقات التوضيحية للتعبير عن قيود البيانات مباشرةً على فئات النماذج. تُغطي القيود الأساسية — @NotNull و @NotBlank و @NotEmpty و @Size و @Email و @Min و @Max وأخواتها — الغالبية العظمى من احتياجات التحقق في العالم الحقيقي. ادمجها بحرية، وأضف قيم message ذات معنى، وضعها على كائنات نقل البيانات الخاصة بالطلبات بدلًا من كيانات النطاق للإبقاء على طبقة التحقق منفصلة عن طبقة الاستمرارية. يوضح الدرس التالي كيف تُفعِّل Spring Boot كل هذا تلقائيًا عند إضافة @Valid إلى معامل دالة المتحكم.