معالجة أخطاء التحقق من صحة البيانات
معالجة أخطاء التحقق من صحة البيانات
في الدرس السابق تعلّمت كيف تُعلن القيود على كائنات جسم الطلب وتُفعّلها بـ @Valid. لكن ماذا يحدث فعليًا عند انتهاك قيد ما؟ يرمي Spring استثناء MethodArgumentNotValidException، وما لم تتعامل معه ستحصل على استجابة 400 خام تحتوي على بيانات مكدس الاستدعاءات تكشف تفاصيل التنفيذ وهي عديمة الفائدة لمستهلكي الواجهة البرمجية. يعلّمك هذا الدرس كيف تقرأ هذا الاستثناء وتستخرج رسائل الأخطاء لكل حقل وتُعيد استجابة خطأ منظّمة وواضحة يمكن لعملائك التصرف بناءً عليها.
ما الذي يرميه Spring — ولماذا
عندما يربط Spring MVC كائن @RequestBody ويُشغّل فحص @Valid، تملأ Bean Validation كائن BindingResult داخليًا. إذا فشلت أي قيد، يرمي Spring فورًا استثناء MethodArgumentNotValidException — وهو فئة فرعية من BindException — قبل أن ينفّذ جسم طريقة المتحكم أصلًا. يحمل هذا الاستثناء كائن BindingResult الكامل وهو حاوية لكائنات FieldError وObjectError.
FieldError إلى خاصية واحدة (مثلًا حقل email فارغ). أما ObjectError (يُسمى أيضًا خطأ عامًا) فيشير إلى الكائن بأكمله — يُنتجه عادةً قيد على مستوى الفئة. كلاهما متاح عبر BindingResult.
الوصول إلى BindingResult مباشرةً
أبسط طريقة للتعامل مع الأخطاء دون معالج استثناء منفصل هي إضافة معامل BindingResult مباشرةً بعد معامل @Valid في توقيع طريقة المتحكم. يمتنع Spring عندئذٍ عن الرمي ويملأ النتيجة ويترك لطريقتك حرية القرار.
يعمل هذا الأسلوب لكنه يعاني من عيب جوهري: عليك تكرار منطق استخراج الأخطاء في كل طريقة متحكم تستقبل جسمًا خاضعًا للتحقق. في أي مشروع يتجاوز الحدّ البسيط، يكون مركزة هذا المنطق أفضل بكثير.
BindingResult فإن Spring يُخمد الاستثناء. ولن يُطلَق معالج @ExceptionHandler(MethodArgumentNotValidException.class) أبدًا لتلك النقطة النهائية. اختر استراتيجية واحدة لكل طريقة.
استخراج الأخطاء من MethodArgumentNotValidException
يستخدم الأسلوب المركزي — الذي تتناوله بعمق الدرسان 6 و7 — كائن @ExceptionHandler. في الوقت الحالي افهم الواجهة البرمجية التي ستستخدمها داخل هذا المعالج. يعرض MethodArgumentNotValidException كائن BindingResult ذاته:
تتعامل دالة الدمج في Collectors.toMap مع حالة انتهاك حقل واحد لأكثر من قيد في آنٍ واحد — مثلًا حقل كلمة مرور تكون قصيرة جدًا وتفتقر إلى حرف كبير في الوقت نفسه.
تصميم رسالة خطأ مفيدة
تُجيب استجابة الخطأ المصمّمة جيدًا عن ثلاثة أسئلة لمستهلك الواجهة البرمجية: ما الذي حدث؟ أي حقل تسبّب في ذلك؟ وماذا يجب أن أُرسل بدلًا من ذلك؟ تُمثّل الخريطة البسيطة Map<String, String> من الحقل إلى الرسالة الحد الأدنى الصالح للاستخدام. يُضيف سجلّ أغنى رمز حالة HTTP وطابعًا زمنيًا ورمز خطأ قابلًا للقراءة آليًا:
تبدو الاستجابة الناتجة عن هذا السجلّ كالتالي:
MessageSource. تجاوزها فقط حين تكون الرسالة الافتراضية مُربِكة فعلًا في سياق نطاقك — فالتخصيص الزائد يُضاعف عبء الصيانة.
توطين رسائل الأخطاء
تبحث Bean Validation عن رسائل القيود عبر MessageSource. يهيّئ Spring Boot تلقائيًا أحد يقرأ من src/main/resources/ValidationMessages.properties (ومتغيّرات المناطق اللغوية مثل ValidationMessages_ar.properties). تجاوز أي رسالة افتراضية بإضافة مفتاحها:
يمكنك أيضًا كتابة رسائل مُضمَّنة مباشرةً على التعليق التوضيحي وهي تأخذ الأولوية:
message في التعليقات التوضيحية، تُستبدَل {min} و{max} و{value} بقيم سمات القيد في وقت التشغيل. هذا يُجنّبك تكرار الأرقام السحرية في نصوص الأخطاء.
معالجة انتهاكات القيود على متغيرات المسار ومعاملات الاستعلام
عندما تُعلّق معاملات الطريقة مباشرةً — لا كائن جسم الطلب — ترمي Bean Validation استثناء ConstraintViolationException لا MethodArgumentNotValidException. يجب عليك معالجة الاثنين:
هذا أيضًا هو سبب ضرورة تعليق فئة المتحكم بـ @Validated (لا فقط @Valid على المعامل) عند التحقق من متغيرات المسار ومعاملات الطلب — فـ Spring يحتاج التعليق على مستوى الفئة لتطبيق وكيل AOP الذي يعترض استدعاءات الطريقة.
إعادة رمز حالة HTTP الصحيح
فشل التحقق من البيانات خطأ العميل دائمًا، لذا يكون رمز الحالة الصحيح دائمًا في نطاق 4xx:
- 400 Bad Request — جسم الطلب أو معاملاته تنتهك قيودًا. هذا هو الاختيار المعياري لأخطاء التحقق.
- 422 Unprocessable Entity — مُشكَّل بشكل صحيح من الناحية النحوية لكنه غير صالح منطقيًا (مثل تاريخ بدء بعد تاريخ الانتهاء). تُفضّل بعض الفرق 422 تحديدًا لانتهاكات قواعد الأعمال للتمييز بينها وبين أخطاء الصياغة.
- 404 Not Found — لا تُعيده لحقل مطلوب مفقود؛ احتفظ به لـ "المورد الذي طلبته غير موجود".
الخلاصة
عند فشل Bean Validation ترمي Spring إما MethodArgumentNotValidException (جسم الطلب) أو ConstraintViolationException (معاملات الطريقة). كلاهما يحمل قائمة أخطاء يمكنك التكرار عليها وتحويلها إلى خرائط حقل-إلى-رسالة. أعد استجابة JSON منظّمة برمز حالة 400 وأسماء الحقول ورسائل القيود — إما مُضمَّنةً في كل طريقة باستخدام BindingResult أو (والأفضل) بشكل مركزي في معالج استثناء. يُريك الدرس التالي كيف تكتب هذا المعالج باستخدام @ExceptionHandler.