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

تسمية الـ Beans والـ Qualifiers والـ Beans المتعددة

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

تسمية الـ Beans والـ Qualifiers والـ Beans المتعددة

يتيح لك Spring تسجيل عدد لا محدود من الـ beans من النوع ذاته — وهذه ميزة لا عيب. قد يكون لديك bean من نوع DataSource لقاعدة البيانات الرئيسية وآخر لنسخة القراءة، أو اثنان من RestTemplate مضبوطان بأوقات انتهاء مختلفة، أو عدة تنفيذات لواجهة PaymentGateway. يتحوّل التحدي من كيف أسجّل bean؟ إلى كيف يعرف Spring أيّ bean يحقن حين يوجد أكثر من مرشّح؟ هذا الدرس يجيب على هذا السؤال بشكل كامل.

كيف يسمّي Spring الـ Beans تلقائيًا

لكل bean اسم واحد على الأقل. حين تُعلن عن bean يشتق Spring اسمًا افتراضيًا من المصدر:

  • @Component (والصور المشتقة منها): اسم الفئة البسيط مع تصغير الحرف الأول. OrderService تصبح "orderService".
  • طريقة @Bean: اسم الطريقة. طريقة تُسمّى primaryDataSource() تُسجّل bean باسم "primaryDataSource".
  • XML <bean>: السمة id، مع الرجوع إلى اسم مُولَّد إن أُهمل.

يمكنك تجاوز الاسم الافتراضي في أي مكان يُشتق منه اسم:

// تجاوز الاسم على مستوى المكوّن @Component("orderSvc") public class OrderService { ... } // تجاوز الاسم على مستوى طريقة @Bean — القيمة الأولى هي الاسم الأساسي، // والقيم الباقية هي أسماء بديلة (aliases) @Bean({"primaryDs", "mainDatabase"}) public DataSource primaryDataSource() { ... }
الأسماء عامة داخل ApplicationContext. إن تشارك bean-ان نفس الاسم فإن التسجيل الثاني يُطغى على الأول بصمت (في إصدارات Spring القديمة) أو يرمي BeanDefinitionOverrideException (في Spring Boot 2.1+ بشكل افتراضي). اختر دائمًا أسماء فريدة ووصفية.

الحقن بالاسم باستخدام @Qualifier

حين يحلّ Spring تبعية بالنوع ويجد أكثر من bean مطابق، يرمي NoUniqueBeanDefinitionException. الحل المعياري هو @Qualifier الذي يُخبر Spring بالضبط باسم الـ bean المطلوب:

@Configuration public class DataSourceConfig { @Bean("primaryDs") public DataSource primaryDataSource() { // يُعيد تجمّع اتصال يشير إلى master الكتابة return buildPool("jdbc:postgresql://master:5432/shop"); } @Bean("replicaDs") public DataSource replicaDataSource() { // يُعيد تجمّع اتصال يشير إلى نسخة القراءة return buildPool("jdbc:postgresql://replica:5432/shop"); } private DataSource buildPool(String url) { /* إعداد HikariCP */ return null; } } @Service public class ReportService { private final DataSource dataSource; public ReportService(@Qualifier("replicaDs") DataSource dataSource) { this.dataSource = dataSource; } }

يعمل @Qualifier على معاملات المُنشئ ومعاملات الـ setter وحقول الـ field injection على حدٍّ سواء. يجب أن تطابق قيمة السلسلة تمامًا اسم bean مسجّلًا أو أحد أسمائه البديلة.

@Primary — اختيار الفائز الافتراضي

إن كان bean واحد ينبغي أن يكون الاختيار الافتراضي لغالبية نقاط الحقن، فضعه بـ @Primary. أي نقطة حقن لا تحمل @Qualifier ستستقبل الـ bean الأساسي؛ أما نقاط الحقن التي تحمل @Qualifier فتحصل على ما طلبته بالضبط.

@Configuration public class DataSourceConfig { @Bean @Primary public DataSource primaryDataSource() { ... } // يفوز بعمليات الحقن غير المؤهَّلة @Bean("replicaDs") public DataSource replicaDataSource() { ... } // يُحقن فقط حين يُطلب صراحةً بـ @Qualifier } @Service public class OrderService { // لا @Qualifier — يستقبل primaryDataSource تلقائيًا public OrderService(DataSource dataSource) { ... } }
استخدم @Primary للتعبير عن النية، لا لتجنّب تسمية الـ beans. إضافة @Primary دون إعطاء beans أسماء واضحة وصفة لفوضى حين يُضاف bean ثالث لاحقًا. سمّ الـ beans دائمًا بشكل صريح واستخدم @Primary حَكَمًا عند التعادل.

تعليقات توضيحية مخصصة للـ Qualifier

نثر سلاسل نصية مثل "replicaDs" عبر عشرات نقاط الحقن هشّ — خطأ إملائي يُترجَم بنجاح لكنه يفشل وقت التشغيل. النهج الاحترافي هو تعليق توضيحي مخصص للـ qualifier:

import org.springframework.beans.factory.annotation.Qualifier; import java.lang.annotation.*; @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Qualifier // يُعلَّق بـ @Qualifier كـ meta-annotation public @interface ReadReplica { } // علامتك المخصصة

طبّقه عند تعريف الـ bean وعند نقطة الحقن:

@Bean @ReadReplica public DataSource replicaDataSource() { ... } @Service public class ReportService { public ReportService(@ReadReplica DataSource dataSource) { ... } }

الآن إعادة الهيكلة عملية وقت الترجمة — أعد تسمية التعليق ويحدّث IDE كل الاستخدامات. الأخطاء الإملائية في السلاسل مستحيلة.

حقن جميع الـ Beans من نوع واحد

أحيانًا تريد كل تنفيذ مسجّل لا واحدًا فحسب. Spring يحقن بسعادة List<T> أو Map<String, T> تحتوي على جميع beans من النوع T:

public interface NotificationChannel { void send(String message); } @Component public class EmailChannel implements NotificationChannel { ... } @Component public class SmsChannel implements NotificationChannel { ... } @Component public class SlackChannel implements NotificationChannel { ... } @Service public class NotificationService { // يحقن Spring التنفيذات الثلاثة private final List<NotificationChannel> channels; public NotificationService(List<NotificationChannel> channels) { this.channels = channels; } public void broadcast(String message) { channels.forEach(ch -> ch.send(message)); } }

عند حقن Map<String, T> تكون المفاتيح أسماء الـ beans، ما يتيح لك البحث وقت التشغيل بالاسم دون ربط منطق الأعمال بـ Spring APIs:

@Service public class ChannelRouter { private final Map<String, NotificationChannel> channelMap; public ChannelRouter(Map<String, NotificationChannel> channelMap) { this.channelMap = channelMap; // channelMap = {"emailChannel": ..., "smsChannel": ..., "slackChannel": ...} } public void send(String channelName, String message) { NotificationChannel ch = channelMap.get(channelName); if (ch == null) throw new IllegalArgumentException("Unknown channel: " + channelName); ch.send(message); } }

التحكم في الترتيب داخل المجموعات — @Order و Ordered

حين يملأ Spring قائمة List<T>، لا يُضمن ترتيب العناصر ما لم تحدده. استخدم @Order على فئة الـ bean (أو نفّذ org.springframework.core.Ordered) لتعيين أولوية. القيم الأصغر تأتي أولًا:

@Component @Order(1) public class SmsChannel implements NotificationChannel { ... } @Component @Order(2) public class EmailChannel implements NotificationChannel { ... } @Component @Order(3) public class SlackChannel implements NotificationChannel { ... }
@Order لا تؤثر على أيّ bean يفوز بالحقن — فقط على ترتيب القائمة. لا تأثير لها على دقة @Qualifier أو انتخابات @Primary. إساءة استخدامها لمحاولة "تجاوز" المرشحين الأساسيين خطأ شائع يُفضي إلى أخطاء خفية.

حلّ الـ Beans برمجيًا

في حالات نادرة — الإرسال الديناميكي ومعماريات الإضافات — تحتاج للبحث عن bean بالاسم أو النوع وقت التشغيل. احقن ApplicationContext واستدع getBean():

@Service public class DynamicChannelService { private final ApplicationContext ctx; public DynamicChannelService(ApplicationContext ctx) { this.ctx = ctx; } public void send(String beanName, String message) { NotificationChannel ch = ctx.getBean(beanName, NotificationChannel.class); ch.send(message); } }

يُسمّى هذا النمط Service Locator. مقبول عند حواف المعمارية (مثلًا حين يأتي اسم الـ bean من صف قاعدة بيانات أو طلب مستخدم)، لكن استخدامه في وسط منطق الأعمال يُقيّد كودك بـ Spring ويُصعّب اختبار الوحدات. افضّل حقن المجموعة كلّما كانت جميع المتغيرات معروفة وقت بدء التشغيل.

دليل اتخاذ القرار

  • افتراضي واضح مع استثناءات نادرة: استخدم @Primary + @Qualifier في المواقع الاستثنائية القليلة.
  • اثنان أو أكثر من beans متكافئة الأهمية: أعطِ كلًّا منها اسمًا واضحًا أو تعليقًا توضيحيًا مخصصًا؛ بلا @Primary.
  • الإرسال لجميع التنفيذات: احقن List<T> أو Map<String, T>.
  • الإرسال الديناميكي وقت التشغيل: احقن Map<String, T> أو الجأ إلى ApplicationContext.getBean().
  • إعادة هيكلة آمنة وقت الترجمة على نطاق واسع: استبدل qualifiers النصية بتعليقات توضيحية مخصصة.

الخلاصة

يشتق Spring أسماء الـ beans تلقائيًا لكنه يتيح لك تجاوزها في كل مكان. حين يوجد عدة beans من النوع ذاته، يُثبّت @Qualifier الحقن على اسم محدد، ويُنتخب @Primary فائزًا افتراضيًا للمواقع غير المؤهَّلة. تُنقل التعليقات التوضيحية المخصصة للـ qualifier عملية التحديد من سلاسل هشة إلى نظام الأنواع. في سيناريوهات التوزيع الكامل، يُعدّ حقن List<T> أو Map<String, T> النهجَ الأنظف، مع @Order للتحكم في ترتيب القائمة. مجتمعةً تُتيح لك هذه الأدوات إدارة أي عدد من مرشحي الـ bean دون غموض ودون ربط منطق أعمالك بمكونات Spring الداخلية.