حقن التبعيات ودورة حياة الـ Bean

@Autowired و @Qualifier و @Primary

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

@Autowired و @Qualifier و @Primary

تبدأ قوة حقن التبعيات في Spring حين تتوقف عن توصيل الـ beans يدويًا وتترك للإطار مهمة حلّها تلقائيًا. التعليقات التوضيحية الثلاثة التي يتناولها هذا الدرس — @Autowired و@Qualifier و@Primary — هي الأدوات الأساسية التي يوفّرها Spring لهذا الغرض. فهمُ كيفية استخدامها ومتى ولماذا سيوفّر عليك أكثر الأخطاء شيوعًا في حقن التبعيات ويُبقي إعداداتك نظيفة على نطاق واسع.

كيف يحلّ Spring التبعية

حين يصادف Spring نقطة حقن تبعية — معامل منشئ، أو ضبّاط (setter)، أو حقل — يتّبع بحثًا من خطوتين:

  1. المطابقة بالنوع: إيجاد جميع الـ beans في السياق التطبيقي التي يكون نوعها متوافقًا مع النوع المطلوب.
  2. المطابقة بالاسم (فاصل التعادل): إذا بقي مرشّح واحد بالضبط، يُستخدم. أما إذا بقي عدة مرشّحين، تُحاول المطابقة باسم متغيّر نقطة الحقن مع اسم bean. وإذا ظلّ الغموض قائمًا، يُطلق الاستثناء NoUniqueBeanDefinitionException.

تمنحك التعليقات التوضيحية في هذا الدرس تحكّمًا صريحًا في كل خطوة من خطوات تلك العملية.

@Autowired — تحديد نقطة الحقن

يُخبر @Autowired (من حزمة org.springframework.beans.factory.annotation) Spring بأن: احشُ هذه التبعية من السياق. يمكن وضعه على منشئ، أو دالة ضبط، أو مباشرةً على حقل.

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class OrderService { private final PaymentGateway paymentGateway; private NotificationSender notificationSender; // حقن المنشئ (مُفضَّل — الحقل نهائي وسهل الاختبار) @Autowired public OrderService(PaymentGateway paymentGateway) { this.paymentGateway = paymentGateway; } // حقن الضبّاط (تبعية اختيارية — Spring تستدعيه فقط إذا كان bean موجودًا) @Autowired(required = false) public void setNotificationSender(NotificationSender notificationSender) { this.notificationSender = notificationSender; } }
حقن المنشئ يُغني عن @Autowired في Spring الحديث. منذ Spring 4.3، إذا كان للفئة منشئ واحد بالضبط، يقوم Spring بحقنه تلقائيًا دون الحاجة إلى أي تعليق توضيحي. ستصادف هذا النمط في كل مكان في قواعد كود Spring Boot: فئة @Service بمنشئ واحد بلا @Autowired على الإطلاق.

السمة required = false مهمة: فهي تُخبر Spring بعدم إيقاف التطبيق عند بدء التشغيل إذا لم يُعثر على bean مطابق. استخدمها للتكاملات الاختيارية حقًا (التحليلات، مزوّدو أعلام الميزات) — لا لإخفاء تبعية إلزامية مفقودة.

مشكلة الغموض

فور وجود أكثر من bean من النوع ذاته في السياق، لا يستطيع Spring تحديد أيّهما يحقن ويُطلق الاستثناء NoUniqueBeanDefinitionException. وهذا شائع جدًا مع الواجهات:

public interface PaymentGateway { void charge(long amountCents, String currency); } @Component public class StripeGateway implements PaymentGateway { ... } @Component public class PayPalGateway implements PaymentGateway { ... }

الآن أصبح حقن PaymentGateway في أي مكان غامضًا. يجد Spring مرشّحَين ولا قاعدة للاختيار بينهما. لديك أداتان لحل هذا: @Primary و@Qualifier.

@Primary — تحديد القيمة الافتراضية

تُعلّم @Primary bean معيّنًا بوصفه المرشّح المُفضَّل عند الطلب بالنوع دون تحديد qualifier. فكّر فيه كتعيين الافتراضي على مستوى النظام.

import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; @Component @Primary public class StripeGateway implements PaymentGateway { @Override public void charge(long amountCents, String currency) { // استدعاء Stripe SDK } } @Component public class PayPalGateway implements PaymentGateway { @Override public void charge(long amountCents, String currency) { // استدعاء PayPal SDK } }

الآن أي نقطة حقن تطلب PaymentGateway — دون تأهيل إضافي — تستقبل StripeGateway:

@Service public class OrderService { private final PaymentGateway paymentGateway; // يستلم StripeGateway public OrderService(PaymentGateway paymentGateway) { this.paymentGateway = paymentGateway; } }
@Primary للحالة الشائعة لا الوحيدة. يعمل بشكل أفضل حين يغطي تنفيذ واحد 80-90% من نقاط الحقن ولا يحتاج إلا عدد محدود منها إلى بديل محدد. إذا كان كل مستهلك يحتاج إلى تنفيذ مختلف، فإن @Primary يُعطي وضوحًا زائفًا — بدلًا من ذلك استخدم @Qualifier في كل مكان.

@Qualifier — الاختيار الدقيق لكل نقطة

تُرفق @Qualifier تسمية نصية بـ bean وتشترط تلك التسمية بالضبط عند نقطة الحقن، متجاوزةً الغموض الناتج عن تطابق النوع كليًا. التسمية تأخذ افتراضيًا اسم الفئة بحرف أول صغير (الاسم الافتراضي للـ bean)، أو يمكنك تعيينها صراحةً.

import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @Component @Qualifier("stripe") public class StripeGateway implements PaymentGateway { ... } @Component @Qualifier("paypal") public class PayPalGateway implements PaymentGateway { ... }

عند نقطة الحقن تكرّر الـ qualifier:

@Service public class RefundService { private final PaymentGateway gateway; // يجب أن تتم عمليات الاسترداد عبر PayPal وفق عقد العمل public RefundService(@Qualifier("paypal") PaymentGateway gateway) { this.gateway = gateway; } }
سلاسل Qualifier هي قيم نصية سحرية عرضة للأخطاء. خطأ مطبعي يتحوّل صامتًا إلى خطأ "لم يُعثر على bean" عند بدء التشغيل لا في وقت الترجمة. تعتمد الفِرق الكبيرة في الغالب على ثوابت qualifier أو — والأفضل — تنشئ تعليقًا توضيحيًا qualifier مخصصًا للحصول على أمان وقت الترجمة وتنقّل أفضل في بيئة التطوير.

تعليقات Qualifier المخصصة — النمط الاحترافي

تعليق qualifier مخصّص يُلغي مشكلة القيم النصية المكتوبة صراحةً كليًا. تعرّف تعليقًا توضيحيًا مزوّدًا بـ meta-annotation مرةً واحدة وتستخدمه كـ qualifier آمن من حيث النوع في كل مكان:

import org.springframework.beans.factory.annotation.Qualifier; import java.lang.annotation.*; @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface PayPalPayment {}
@Component @PayPalPayment public class PayPalGateway implements PaymentGateway { ... } // الاستخدام عند نقطة الحقن public RefundService(@PayPalPayment PaymentGateway gateway) { this.gateway = gateway; }

الآن يُطبّق المُترجم العقدَ. إذا أُعيدت تسمية @PayPalPayment أو حُذفت، يفشل كل موقع استخدام في الترجمة فورًا.

دمج @Primary و@Qualifier معًا

يعمل هذان التعليقان التوضيحيان معًا بصورة متوقّعة: @Qualifier يتغلّب دائمًا على @Primary. إذا كان bean مُعلَّمًا بـ @Primary لكن نقطة الحقن تحمل @Qualifier، يتجاهل Spring تعيين primary ويستخدم الـ qualifier لاختيار الـ bean.

  • لا qualifier عند نقطة الحقن ← استخدم الـ bean ذا @Primary (إن وُجد)، وإلا أخفق بغموض.
  • Qualifier موجود ← طابق بالـ qualifier وتجاهل @Primary كليًا.
  • لا هذا ولا ذاك ← يلجأ Spring إلى مطابقة اسم المتغيّر باسم الـ bean (هشّ؛ تجنّب الاعتماد عليه).

حقن كل المرشّحين — المجموعات والـ Optional

في بعض الأحيان تريد كل التنفيذات — مثلًا، لإرسال إشعار لجميع القنوات المسجّلة. يتعامل Spring مع هذا تلقائيًا حين تحقن List أو Map:

import java.util.List; @Service public class NotificationDispatcher { private final List<NotificationSender> senders; // يحقن Spring جميع الـ beans التي تنفّذ NotificationSender public NotificationDispatcher(List<NotificationSender> senders) { this.senders = senders; } public void broadcast(String message) { senders.forEach(s -> s.send(message)); } }

حقن Map<String, NotificationSender> يمنحك اسم الـ bean كمفتاح — مفيد حين تحتاج إلى اختيار تنفيذ في وقت التشغيل بالاسم.

الخلاصة

استخدم @Autowired (أو ببساطة فئة بمنشئ واحد) للإعلان عن نقاط الحقن. حين يوجد مرشّحون متعددون من النوع ذاته، حلّ الغموض بـ @Primary لـ bean افتراضي واحد، أو بـ @Qualifier للدقة في كل موقع. فضّل تعليقات qualifier المخصصة على القيم النصية الحرفية في أي قاعدة كود تتوقع صيانتها. وتذكّر: @Qualifier يتغلّب دائمًا على @Primary.