نطاقات الـ Bean
نطاقات الـ Bean
لكل bean في Spring نطاق (scope) — قاعدة تحكم عدد النسخ التي ينشئها Spring ومدة حياة كل نسخة. اختيار النطاق الخاطئ هو أحد أكثر مصادر الأخطاء الخفية شيوعًا في تطبيقات Spring، لذا فإنّ الفهم العميق لكل نطاق أمر لا غنى عنه لأي مطوّر محترف.
النطاقات الستة المدمجة
يأتي Spring مع ستة نطاقات مدمجة. اثنان منها متاحان دائمًا (singleton وprototype)؛ وأربعة تتطلب ApplicationContext مدركًا للويب (request وsession وapplication وwebsocket). يغطي هذا الدرس الأربعة التي ستصادفها يوميًا.
Singleton — النطاق الافتراضي
عندما لا يُعلَن عن نطاق، يستخدم Spring النطاق singleton: ينشئ الحاوي نسخةً واحدةً بالضبط من الـ bean لكل ApplicationContext، ويخزّنها في ذاكرة التخزين المؤقت، ويحقن تلك المرجعية ذاتها في كل فئة تعتمد عليها. تُنشأ النسخة عند بدء تشغيل السياق (التهيئة الفورية) وتُدمَّر عند إغلاقه.
eurToUsd أعلاه يمكن قراءتها وكتابتها في آنٍ واحد. صمّم الـ singleton ليكون عديم الحالة أو غير قابل للتعديل فعليًا — أو تزامن صراحةً. معظم الخدمات (المستودعات، عملاء HTTP، الحاسبات) عديمة الحالة بطبيعتها وتُعدّ singleton ممتازة.
يمكنك أيضًا تعريف النطاق صراحةً للتوضيح:
Prototype — نسخة جديدة في كل مرة
مع نطاق prototype ينشئ Spring نسخةً جديدةً تمامًا في كل مرة يُطلَب فيها الـ bean — سواء عبر applicationContext.getBean() أو عبر الحقن. لا يخزّن Spring نسخ prototype ولا يستدعي تابع @PreDestroy الخاص بها؛ وتنظيف دورة الحياة يقع على عاتق المستدعي.
كل استدعاء لـ getBean(CsvReportBuilder.class) (أو كل نقطة حقن تطلب واحدةً) تحصل على buffer نظيف خاص بها. استخدم prototype للـ bean التي تحمل حالةً قابلةً للتعديل خاصة بعملية معينة — منشئو التقارير، كائنات الأوامر، المحللون — حيث إن مشاركة نسخة واحدة ستُسبّب تلف البيانات.
ApplicationContext أو استخدام lookup method عبر @Lookup حتى يجلب الـ singleton نسخة prototype جديدة عند كل استخدام. ستشاهد هذا النمط في الدرس التالي.
نطاق Request — نسخة واحدة لكل طلب HTTP
في تطبيق الويب ينشئ نطاق request نسخةً واحدةً من الـ bean طوال عمر طلب HTTP واحد. تُنشأ النسخة عند وصول الطلب وتُحذف عند إرسال الاستجابة. يمكن حقنها في الـ singleton بأمان عبر وكيل نطاقي (scoped proxy) — يضع Spring وكيلًا في وقت الترجمة؛ وفي وقت التشغيل يُفوّض الوكيل إلى النسخة الخاصة بالطلب الحالي.
يُعبئ فلتر أو معترض singleton كائن RequestContext عند بداية كل طلب، ويمكن لأي خدمة في اتجاه المصب حقنه لقراءة المستخدم الحالي أو معرّف الارتباط — دون تمرير معاملات عبر كل استدعاء تابع. هذا بديل نظيف لـ ThreadLocal.
proxyMode = ScopedProxyMode.TARGET_CLASS عند حقن bean بنطاق request أو session داخل singleton. بدون وكيل لا يستطيع Spring إنشاء الـ singleton لأن الـ bean بنطاق request غير موجودة بعد عند بدء تشغيل السياق — وستحصل على BeanCreationException.
نطاق Session — نسخة واحدة لكل جلسة HTTP
يربط نطاق session نسخةً واحدةً من الـ bean بجلسة HTTP (الجلسة التي ينشئها حاوي servlet، وتُعرَّف بملف تعريف ارتباط أو إعادة كتابة URL). تظل النسخة قائمةً عبر طلبات متعددة من المستخدم ذاته حتى تنتهي صلاحية الجلسة أو تُبطَل.
يحقن المتحكم الذي يعالج POST /cart/add كائن ShoppingCart كتبعية عادية. يُوجّه الوكيل الخاص بـ Spring كل استدعاء بشفافية إلى سلة التسوق التابعة لجلسة المستخدم الحالي.
java.io.Serializable وأضف serialVersionUID إن كان تطبيقك سيعمل خلف موازن تحميل.
اختيار النطاق الصحيح — دليل القرار
- الخدمات عديمة الحالة، المستودعات، الإعدادات، عملاء HTTP →
singleton. آمن للخيوط بحكم تصميمه، والخيار الأقل تكلفةً. - الكائنات ذات الحالة الخاصة بعملية معينة (منشئو التقارير، المحللون، كائنات الأوامر) →
prototype. كل مستدعٍ يحصل على لوحة بيضاء نظيفة. - البيانات المشتركة عبر الطلب (معرّف الارتباط، سياق التدقيق، المستأجر الحالي) →
requestمع وكيل نطاقي. - حالة المستخدم عبر طلبات متعددة (سلة التسوق، خطوة المعالج، تفضيلات المستخدم) →
sessionمع وكيل نطاقي.
الوكيل النطاقي من الداخل
عندما تحدد proxyMode = ScopedProxyMode.TARGET_CLASS يستخدم Spring مكتبة CGLIB لتوليد فئة فرعية من الـ bean الخاصة بك عند بدء التشغيل. تلك الفئة الفرعية هي الـ singleton الفعلي المخزَّن في السياق. كل استدعاء تابع عليها يستشير متغير ThreadLocal لتحديد النسخة الحقيقية المرتبطة بالطلب أو الجلسة الحالية، ثم يُحيل الاستدعاء إليها. النتيجة: متحكمك singleton لا يحمل أبدًا مرجعًا قديمًا، وتكتب كود الحقن العادي دون أي شيفرة خيوط معقدة.
الخلاصة
يحدد نطاق الـ bean عمر النسخة. singleton هو الافتراضي الصحيح للمتعاونين عديمي الحالة؛ أبعد الحالة المشتركة عن الـ singleton أو تزامن عليها بعناية. prototype يوفر نسخةً جديدةً عند كل طلب ويناسب الكائنات ذات الحالة الجوهرية قصيرة العمر. نطاقا request وsession يربطان الـ bean مباشرةً بدورة حياة طلب أو جلسة HTTP ويلغيان الحاجة إلى ThreadLocal أو إدارة سمات الجلسة يدويًا — لكن اقرن دائمًا هذين النطاقين بـ ScopedProxyMode.TARGET_CLASS عند الحقن في الـ singleton.