معالجة الاستثناءات باستخدام @ExceptionHandler
معالجة الاستثناءات باستخدام @ExceptionHandler
كل واجهة برمجية حقيقية تُطلق استثناءات. المورد المطلوب غير موجود، أو المُستدعي يُمرّر معرّفًا خاطئًا، أو خدمة خارجية غير متاحة. السؤال ليس هل ستقع الاستثناءات بل أين تريد التعامل معها، وماذا يتلقّى المُستدعي حين تقع. تمنحك آلية @ExceptionHandler في Spring MVC إجابةً نظيفة وتصريحية: أضف التوصيف على دالة مع نوع الاستثناء الذي تريد اعتراضه، وسيوجّه Spring الاستدعاء إلى تلك الدالة في أي وقت يُفلت فيه أحد هذه الاستثناءات من أي إجراء في وحدة التحكم.
مشكلة try-catch داخل دوال وحدة التحكم
قبل وجود @ExceptionHandler، كان المطوّرون يلفّون كل جسم إجراء بـ try-catch، ويُعيّنون الاستثناء إلى رمز حالة HTTP يدويًا، ثم يُعيدون استجابة الخطأ. هذا الأسلوب ينتج كودًا متكرّرًا وصعب الصيانة، ويشتّت منطق معالجة الأخطاء في كل مكان بدلًا من مركزته.
مع @ExceptionHandler تُعرّف هذا التعيين مرة واحدة، وتستفيد منه كل إجراءات وحدة التحكم تلقائيًا.
الإعلان عن دالة @ExceptionHandler
ضع دالة مُوصَّفة بـ @ExceptionHandler داخل @RestController (أو @Controller). يعترض Spring أي استثناء من النوع المُعلَن يتصاعد من أي دالة @RequestMapping في نفس الفئة ويوجّهه إلى معالجك بدلًا من السماح له بالانتشار.
لاحظ أن getOrder لا تحتوي على أي try-catch بعد الآن. إنها ببساطة تُفوّض عمل المسار السعيد إلى الخدمة وتترك الاستثناء يتصاعد. يعترضه Spring قبل أن يصل إلى حاوية السيرفلت ويستدعي handleNotFound عوضًا عنه.
توقيع دالة المعالج
دوال المعالج مرنة. يُحلّل Spring عدة معاملات مفيدة تلقائيًا:
- الاستثناء نفسه — مُكتَّب بنوع الاستثناء (أو نوع أب له).
HttpServletRequest/HttpServletResponse— كائنات السيرفلت الخام عند الحاجة إليها.WebRequest— تجريد محمول فوق الطلب.Locale،TimeZone— لرسائل الخطأ المُوطَّنة.Model— لوحدات تحكم MVC (غير REST) التي تُعيد اسم عرض.
نوع الإعادة مرن بالقدر ذاته: ResponseEntity<T> (مُوصى به لـ REST)، أو كائن عادي (يستخدم Spring محوّلات الرسائل المعتادة)، أو ModelAndView (لـ MVC)، أو void إذا كتبت مباشرةً في الاستجابة.
معالجة أنواع استثناءات متعددة في دالة واحدة
يمكنك سرد عدة فئات استثناءات في قيمة التوصيف واستقبال النوع الأساسي في المعامل:
RuntimeException.class، فسيعترض كل استثناء غير محقَّق لم يُدَّعَ بالفعل من قِبَل معالج أكثر تحديدًا في نفس الفئة. يوجّه Spring دائمًا إلى أكثر معالج محدَّدًا أولًا.
الوصول إلى معلومات الطلب داخل المعالج
أحيانًا تحتاج إلى تسجيل مسار الطلب إلى جانب الخطأ، أو تضمينه في جسم الخطأ حتى يتمكّن العملاء من ربط الاستجابات بطلباتهم. أضف HttpServletRequest كمعامل:
سجل ErrorResponse واقعي
احرص على أن يكون كائن نقل بيانات الخطأ متسقًا وسهل التسلسل. يعمل سجل Java بشكل مثالي — فهو غير قابل للتغيير وموجز، وJackson يُسلسله مباشرةً:
مع هذا السجل يبدو جسم استجابة الخطأ هكذا:
قيد النطاق — ومتى تنتقل إلى ما هو أبعد
القيد الرئيسي لـ @ExceptionHandler المحلي لوحدة التحكم هو نطاقه: إنه يعترض فقط الاستثناءات المُطلَقة من دوال في تلك الفئة ذاتها. لو كان لديك عشرون وحدة تحكم وتُطلق كل منها OrderNotFoundException، ستضطر إلى نسخ المعالج في كل واحدة منها.
@ExceptionHandler المحلي لوحدة التحكم حين يكون منطق المعالجة حقًا خاصًا بوحدة تحكم واحدة — على سبيل المثال حين تُشير رسائل الخطأ إلى سياق وحدة التحكم، أو حين تريد عمدًا رموز حالة مختلفة للاستثناء ذاته في أجزاء مختلفة من الـ API. للمعالجة التقاطعية على مستوى التطبيق بأكمله، انتقل إلى @ControllerAdvice الذي سيكون موضوع الدرس التالي.
تجميع كل شيء معًا — مثال كامل
تتشارك كلتا دالتَي المعالجة نفس شكل ErrorResponse، وتُعيّنان إلى رموز حالة HTTP مناسبة، وتتضمّنان مسار الطلب — مما يمنح مستهلكي الـ API كل ما يحتاجونه لفهم الخطأ والتصرف بناءً عليه دون الكشف عن تتبّعات المكدس الداخلية.
الخلاصة
يُزيل @ExceptionHandler النمطي المتكرّر من try-catch في دوال إجراءات وحدة التحكم بمركزة تعيين الأخطاء في دوال معالجة مخصّصة داخل نفس وحدة التحكم. تُعلن عن نوع الاستثناء الذي تريد اعتراضه، وتختار رمز حالة HTTP الصحيح، وتبني جسم خطأ منظّمًا، ويتولّى Spring التوجيه. أما المقايضة فهي النطاق: المعالجات تعيش في فئة واحدة. في الدرس التالي ستتعرف على كيفية رفع @ControllerAdvice لهذا القيد والسماح لفئة واحدة بمعالجة الاستثناءات عبر التطبيق بأكمله.