إعداد Spring والملفّات الشخصية

التعمّق في فحص المكوّنات

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

التعمّق في فحص المكوّنات

في الدروس السابقة رأيت كيف تُعلن عن الـ beans بشكل صريح باستخدام توابع @Bean. فحص المكوّنات (Component Scanning) هو الآلية التكميلية: تجوب Spring مجموعة من الحزم، وتجد الفئات المُعلَّنة بـ @Component أو أي تعليق توضيحي ميتا (meta-annotation) مُعلَّن بدوره بـ @Component، ثم تسجّلها تلقائيًا كـ beans. إنّ فهم آلية عمل الفحص — وكيفية التحكم الدقيق في نطاقه — أمر جوهري حين ينمو تطبيقك إلى ما هو أكثر من حفنة من الفئات.

كيف يعمل فحص المكوّنات من الداخل

حين تبدأ سياق التطبيق، يتكرّر ClassPathScanningCandidateComponentProvider الخاص بـ Spring على كل ملف .class في مسار الفئات (classpath) ضمن الحزم الأساسية المُعدَّة. يقرأ البيانات الوصفية للبايت كود (دون تحميل الفئة بالكامل) ويتحقق من وجود تعليقات النمط النموذجي (stereotype annotations). أي فئة مطابقة يُنشئها ويسجّلها كـ bean.

التعليقات النموذجية التي تُشغّل الفحص بشكل افتراضي:

  • @Component — النمط العام؛ أصل جميع الأنماط الأخرى.
  • @Service — يُشير إلى bean في طبقة الخدمات (بدون سلوك إضافي من Spring، لكنه يوثّق القصد).
  • @Repository — يُشير إلى bean للوصول إلى البيانات؛ يُمكّن أيضًا ترجمة الاستثناءات الخاصة بـ Spring للاستثناءات المتعلقة بالمثابرة (persistence).
  • @Controller / @RestController — يُعلّم معالج Spring MVC.
جميع التعليقات النموذجية مُعلَّنة بالميتا مع @Component. لهذا السبب يلتقطها الفحص. يمكنك إنشاء نمطك الخاص بتعليق التعليق الخاص بك بـ @Component (أو أي نمط موجود)، وسيكتشفه Spring هو الآخر.

تحديد الحزم الأساسية باستخدام @ComponentScan

في تطبيق Spring 6 عادي تُضيف @ComponentScan إلى فئة @Configuration. في Spring Boot يتضمّن @SpringBootApplication هذا التعليق تلقائيًا، إذ يُعيَّن افتراضيًا على حزمة الفئة المُعلَّنة به. معرفة الشكل الصريح تتيح لك تجاوز هذا الافتراضي أو توسيعه.

import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan(basePackages = { "com.example.orders", "com.example.payments" }) public class AppConfig { }

بدلًا من ذلك، استخدم basePackageClasses لتوفير علامات آمنة نوعيًا بدلًا من سلاسل نصية. هذا الأسلوب مُوصى به لأن أدوات إعادة الهيكلة ستتتبع تغييرات أسماء الفئات تلقائيًا:

@ComponentScan(basePackageClasses = { OrdersMarker.class, // واجهة علامة (marker) في com.example.orders PaymentsMarker.class // واجهة علامة في com.example.payments })
فضّل basePackageClasses على أسماء الحزم النصية. السلسلة النصية تصمد أمام إعادة تسمية الحزمة بصمت؛ أما فئة العلامة فلن تُترجَم إذا نُقلت دون تحديث المرجع. واجهة Marker.java فارغة أو package-info.java لكل حزمة جذر هو كل ما تحتاجه.

مرشّحات التضمين: توسيع نطاق الفحص

يلتقط الفحص الافتراضي أي فئة مُعلَّنة بتعليق نمطي. يمكنك توسيع ذلك باستخدام includeFilters. كل مرشّح عبارة عن @ComponentScan.Filter يُحدد نوع المرشّح والهدف.

أنواع المرشّحات الشائعة (من FilterType):

  • ANNOTATION — تضمين جميع الفئات الحاملة لتعليق توضيحي معين.
  • ASSIGNABLE_TYPE — تضمين جميع الفئات القابلة للإسناد لنوع معين (فئة أو واجهة).
  • REGEX — تضمين الفئات التي يتطابق اسمها الكامل مع تعبير نظامي.
  • ASPECTJ — تضمين الفئات المطابقة لنمط نوع AspectJ.
  • CUSTOM — تضمين الفئات التي يقبلها تنفيذ مخصص لـ TypeFilter.

مثال: تسجيل كل فئة تُنفّذ EventHandler، حتى لو لم تحمل أي تعليق نمطي:

import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.Configuration; import com.example.events.EventHandler; @Configuration @ComponentScan( basePackages = "com.example", includeFilters = @ComponentScan.Filter( type = FilterType.ASSIGNABLE_TYPE, classes = EventHandler.class ) ) public class AppConfig { }
حين تُضيف includeFilters، غالبًا تحتاج إلى تعطيل مرشّح التعليقات الافتراضي. إذا كان useDefaultFilters لا يزال بقيمة true (الافتراضي)، يفحص Spring كلًّا من التعليقات النموذجية ومرشّحك المُضمَّن معًا. اضبط useDefaultFilters = false لمجموعة قواعد مخصّصة بالكامل — لكن تذكّر إضافة مرشّح النمط الذي ما زلت تريده.

مرشّحات الاستثناء: تضييق نطاق الفحص

مرشّحات الاستثناء أكثر شيوعًا في كود الإنتاج. أبرز حالات الاستخدام: عزل إعداد الاختبار، وتجنّب التسجيل المزدوج حين تتداخل إعلانات @ComponentScan متعددة، وتخطّي الفئات المولَّدة أو الخارجية التي تسكن تحت بادئة الحزمة الخاصة بك.

import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan( basePackages = "com.example", excludeFilters = { // استثناء جميع الـ beans المُعلَّنة بـ @Controller من هذا السياق غير الويب @ComponentScan.Filter( type = FilterType.ANNOTATION, classes = org.springframework.stereotype.Controller.class ), // استثناء أي فئة ينتهي اسمها بـ "Legacy" @ComponentScan.Filter( type = FilterType.REGEX, pattern = ".*Legacy" ) } ) public class ServiceLayerConfig { }

كتابة TypeFilter مخصّص

حين لا تكون أنواع المرشّحات المدمجة كافية التعبير، نفّذ الواجهة org.springframework.core.type.filter.TypeFilter. تستقبل بيانات وصفية عن كل فئة مرشّحة دون الحاجة إلى تحميلها بالكامل (تحميل الفئات عملية مكلفة).

import org.springframework.core.type.ClassMetadata; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.TypeFilter; import java.io.IOException; public class InternalClassFilter implements TypeFilter { @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { ClassMetadata meta = metadataReader.getClassMetadata(); // استثناء أي فئة يبدأ اسمها البسيط بـ "Internal" String className = meta.getClassName(); int dot = className.lastIndexOf('.'); return !className.substring(dot + 1).startsWith("Internal"); } }

ثم استخدمه مع FilterType.CUSTOM:

@ComponentScan( basePackages = "com.example", includeFilters = @ComponentScan.Filter( type = FilterType.CUSTOM, classes = InternalClassFilter.class ), useDefaultFilters = false )

التحميل الكسول و @Lazy

يُنشئ Spring جميع الـ beans الفردية (singleton) بشكل انتقائي عند بدء التشغيل. بالنسبة لعمليات الفحص الكبيرة قد يُبطّئ ذلك وقت الإقلاع بشكل ملحوظ. عليّن الفئة بـ @Lazy لتأجيل إنشائها حتى يُطلب منها أول مرة:

import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @Service @Lazy public class HeavyReportService { public HeavyReportService() { // تهيئة مكلفة } }

يمكنك أيضًا ضبط @ComponentScan(lazyInit = true) لجعل كل bean مفحوص كسولًا بشكل افتراضي — مفيد في وضع المطوّر لتسريع إعادة التشغيل المحلية مع إبقاء الإنتاج انتقائيًا (eager).

فحص المكوّنات في Spring Boot

يُعدّ @SpringBootApplication في Spring Boot تعليقًا مُركَّبًا يتضمّن @ComponentScan دون حزم صريحة، مما يعني أنه يفحص حزمة الفئة الرئيسية وجميع الحزم الفرعية. هذه الاصطلاحية "الفحص من الجذر" هي السبب في أنك يجب إبقاء فئتك الرئيسية في أعلى مستوى من الحزمة الأساسية:

// com/example/demo/DemoApplication.java <-- يفحص com.example.demo.* package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }

إذا احتجت فحص حزم إضافية (مثلًا مكتبة مشتركة تعيش تحت جذر مختلف)، أضف @ComponentScan إلى جانب @SpringBootApplication — أو بشكل أفضل، استخدم scanBasePackages مباشرةً على @SpringBootApplication:

@SpringBootApplication(scanBasePackages = { "com.example.demo", "com.shared.components" }) public class DemoApplication { ... }

مقايضات عملية وإرشادات

  • أبقِ الحزمة الأساسية محدودة النطاق. فحص كل فئة تحت com أو org سيكون كارثيًا لوقت الإقلاع. حدّد دائمًا أضيق الحزم التي تشمل كودك الخاص.
  • تجنّب تداخل عمليات الفحص. إذا فحصت فئتا @Configuration الحزمة ذاتها، يُسجَّل الـ beans مرتين (تُزيل Spring التكرار بحسب اسم الـ bean، لكن الفحص المزدوج يُضيع الوقت ويُسبّب أخطاء ترتيب دقيقة).
  • افصل السياقات الويب وغير الويب. تطبيقات Spring MVC الكلاسيكية لها سياقان — الجذر والـ servlet. استخدم excludeFilters لإبعاد الـ beans المُعلَّنة بـ @Controller عن سياق الجذر وإبعاد الـ beans من طبقة الخدمات/المستودعات عن سياق الـ servlet.
  • استخدم @Bean الصريح للفئات الخارجية. لا يمكنك إضافة @Component إلى فئة لا تملكها؛ عليك الإعلان عنها كـ @Bean في فئة @Configuration.

الخلاصة

يُؤتمت @ComponentScan اكتشاف الـ beans بقراءة بايت كود مسار الفئات. يُحدّد basePackages / basePackageClasses منطقة البحث. تتيح includeFilters وexcludeFilters — المدعومة بمنطق التعليقات التوضيحية والأنواع والتعابير النظامية أو TypeFilter مخصّص — وصف الفئات المؤهلة بدقة. إتقان هذه الضوابط يعني تصميم حدود سياق نظيفة وتسريع الإقلاع وتجنّب التسجيل العرضي للـ beans مع نمو قاعدة الكود.