إطار Spring وحاوية IoC

فحص المكونات والصور النمطية

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

فحص المكونات والصور النمطية

في الدروس السابقة سجّلت الـ beans بشكل صريح — إما في فئة @Configuration عبر توابع @Bean أو في ملف XML. يُناسب هذا الأسلوب جيدًا حين تملك كل تعريفات الـ bean وتريد رؤية كاملة لها. غير أن التطبيقات الحقيقية قد تضم عشرات أو مئات الفئات، وإعلان كل واحدة يدويًا يصبح مرهقًا وعُرضةً للأخطاء. فحص المكونات (Component Scanning) هو إجابة Spring: تُضيف التعليقات التوضيحية (annotations) على فئاتك وتدع الحاوية تكتشفها تلقائيًا.

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

عند بدء تشغيل حاوية Spring وهي مُهيَّأة لفحص المكونات، تجتاز مسار الفئات (classpath) — بدءًا من حزمة أساسية واحدة أو أكثر — بحثًا عن الفئات التي تحمل تعليقات توضيحية محددة. أي فئة تجدها تُسجَّل تلقائيًا كتعريف bean، كأنك كتبت تابع @Bean بنفسك.

تُفعّل الفحص على فئة التهيئة باستخدام @ComponentScan:

import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @Configuration @ComponentScan(basePackages = "com.example.shop") public class AppConfig { // لا حاجة لتوابع @Bean للفئات المُكتشَفة تلقائيًا } // نقطة الإقلاع public class Main { public static void main(String[] args) { var ctx = new AnnotationConfigApplicationContext(AppConfig.class); // كل الفئات المُعلَّمة بـ @Component في com.example.shop هي الآن beans ctx.close(); } }
اختصار Spring Boot: يتضمن @SpringBootApplication بالفعل @ComponentScan مجذَّرًا في حزمة الفئة المُعلَّمة. في مشاريع Boot نادرًا ما تكتب @ComponentScan صراحةً — لكن فهم ما تفعله ضروري لتشخيص إخفاقات الفحص.

@Component — الصورة النمطية الأساسية

@Component هو التعليق التوضيحي الجذر. أي فئة مُعلَّمة به تُصبح مرشحة للاكتشاف التلقائي. الاسم الافتراضي للـ bean هو اسم الفئة البسيط بأول حرف صغير:

import org.springframework.stereotype.Component; @Component public class EmailValidator { public boolean isValid(String address) { return address != null && address.contains("@"); } }

هذه الفئة الآن bean وحيد (singleton) باسم emailValidator. أي bean آخر يمكنه الإعلان عن حاجته إليه وسيحقن Spring نفس النسخة في كل مكان.

تعليقات الصور النمطية — طبقات دلالية

تشحن Spring بثلاثة تعليقات متخصصة تُوسّع جميعها @Component:

  • @Service — يُعلّم فئة على أنها تنتمي إلى طبقة الأعمال / الخدمات. تحمل منطق الأعمال، وتُنظّم استدعاءات المستودعات، وقد تُطبّق حدود المعاملات (transactions).
  • @Repository — يُعلّم فئة على أنها كائن وصول للبيانات (DAO). تتحدث إلى قاعدة بيانات أو مخزن خارجي. تُضيف Spring عليها أيضًا موجّه AOP يُترجم استثناءات الوصول للبيانات إلى التسلسل الهرمي الموحّد DataAccessException.
  • @Controller — يُعلّم فئة على أنها متحكم ويب MVC. تفحص دودة إرسال Spring MVC هذه التعليقات لربط طلبات HTTP بتوابع المعالج. (لواجهات REST تستخدم @RestController التي تُضيف @ResponseBody.)
استخدم الصورة النمطية الصحيحة. التعليقات الأربعة تتصرف بشكل متطابق من حيث تسجيل الـ bean، لكن التسمية الدلالية مهمة: تنقل النية، وتُفعّل AOP الخاصة بالطبقة (ترجمة الاستثناءات لـ @Repository، وربط الطلبات لـ @Controller)، وتجعل تنظيم الحزم مقروءًا فورًا.

مثال واقعي ثلاثي الطبقات

إليك شريحة بسيطة لمعالجة الطلبات تُظهر الصور النمطية الثلاثة تعمل معًا. Spring تُحكم الحقن تلقائيًا في سلسلة التبعيات.

// --- طبقة المستودع --- import org.springframework.stereotype.Repository; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Repository public class OrderRepository { private final Map<Long, String> store = new ConcurrentHashMap<>(); public void save(Long id, String order) { store.put(id, order); } public String findById(Long id) { return store.get(id); } }
// --- طبقة الخدمات --- import org.springframework.stereotype.Service; @Service public class OrderService { private final OrderRepository orderRepository; // حقن المُنشئ — تكتشف Spring المُنشئ الواحد تلقائيًا public OrderService(OrderRepository orderRepository) { this.orderRepository = orderRepository; } public void placeOrder(Long id, String description) { if (description == null || description.isBlank()) { throw new IllegalArgumentException("Order description cannot be blank"); } orderRepository.save(id, description); } public String getOrder(Long id) { return orderRepository.findById(id); } }
// --- طبقة المتحكم (Spring MVC) --- import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService = orderService; } @GetMapping("/orders/{id}") @ResponseBody public String getOrder(@PathVariable Long id) { return orderService.getOrder(id); } }

لاحظ أنه لا يوجد تابع @Bean لأي من هذه الفئات الثلاث. تكتشفها Spring، وتحلّ سلسلة التبعيات (OrderControllerOrderServiceOrderRepository)، وتُحكم التوصيلات تلقائيًا.

التحكم في اسم الـ Bean

الاسم الافتراضي للـ bean هو اسم الفئة البسيط بحرف أول صغير (orderService، orderRepository). يمكنك تجاوزه:

@Service("legacyOrderService") public class LegacyOrderService implements OrderService { ... }

الأسماء المخصصة مفيدة حين يكون لديك تنفيذات متعددة للواجهة ذاتها وتحتاج لتأهيل نقاط الحقن — وهو ما ستتناوله بشكل رسمي في الدرس الثامن حول المُؤهِّلات (qualifiers).

تصفية ما يُفحص

يقبل @ComponentScan الخاصيتين includeFilters وexcludeFilters حتى تضبط بدقة ما يلتقطه الماسح:

import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan( basePackages = "com.example.shop", excludeFilters = @Filter( type = FilterType.ANNOTATION, classes = org.springframework.stereotype.Controller.class ) ) public class ServiceConfig { // المتحكمات مستثناة — مفيد لسياقات التطبيق غير الويب }

هذا النمط شائع في التطبيقات متعددة السياقات حيث يحمل السياق الجذري الخدمات والمستودعات بينما يحمل سياق الويب المتحكمات.

@Repository وترجمة الاستثناءات

أحد الفوارق الوظيفية الحقيقية بين @Repository والـ @Component العادي هو ترجمة استثناءات الاستمرارية. تُطبّق Spring معالجًا لاحقًا (PersistenceExceptionTranslationPostProcessor) على كل bean مُعلَّم بـ @Repository. إذا رمى تابع ما استثناءً منخفض المستوى خاصًا بمزوّد معين (مثل ConstraintViolationException لـ Hibernate أو SQLException لـ JDBC)، يلفّه الوكيل في الفئة الفرعية المناسبة من DataAccessException. تبقى طبقة الخدمات مُنفصلة عن تقنية الاستمرارية.

لا تضع منطق الأعمال في فئات @Repository. ترجمة الاستثناءات ومبدأ المسؤولية الواحدة كلاهما يقتضيان أن تتعامل فئات DAO فقط مع الوصول للبيانات. قواعد الأعمال تنتمي إلى beans الـ @Service.

الخلاصة

يُزيل فحص المكونات العبء الرتيب لتسجيل الـ beans صراحةً. علّم فئاتك بالصورة النمطية الصحيحة — @Component للمساعدين العامين، و@Service لمنطق الأعمال، و@Repository للوصول للبيانات، و@Controller أو @RestController لنقاط نهاية الويب — وسيتولى @ComponentScan الاكتشاف. اختيار الصورة النمطية ليس تجميليًا: إنه يُشغّل AOP وترجمة الاستثناءات ويعكس قراءة بنيتك. في الدرس القادم ستُشغّل ApplicationContext كاملًا وتتفاعل مع الـ beans التي تعلّمت الآن كيف تُعلن عنها.