انتقل إلى المحتوى الرئيسي

TP5.1 - حقن التبعيات المتقدم

الأهداف​

في نهاية هذا التمرين، ستكون قادرًا على:

  1. ✅ استخدام الأنواع الثلاثة للحقن (المُنشئ، المُعَيّن، الحقل)
  2. ✅ الاختيار بين عدة تنفيذات باستخدام @Qualifier
  3. ✅ إدارة التبعيات الاختيارية
  4. ✅ فهم وحل التبعيات الدائرية

المدة المقدرة: 1 ساعة و30 دقيقة


المتطلبات الأساسية​

  • ✅ إكمال الفصول 1 و2 من الوحدة 5
  • ✅ تثبيت Maven 3.9+
  • ✅ تثبيت Java 17+
  • ✅ VS Code مع إضافات Java

التحضير: إنشاء المشروع​

الخطوة 1: إنشاء المشروع باستخدام Spring Initializr​

عبر الموقع الإلكتروني:

  1. اذهب إلى https://start.spring.io/

  2. قم بتكوين:

    • Project: Maven
    • Language: Java
    • Spring Boot: 3.2.0 (أو أحدث إصدار)
    • Group: com.example
    • Artifact: injection-demo
    • Name: injection-demo
    • Package name: com.example.injection
    • Packaging: Jar
    • Java: 17
  3. التبعيات التي يجب إضافتها:

    • Spring Web
  4. انقر على GENERATE

  5. فك الضغط وافتح في VS Code


الخطوة 2: هيكل المشروع​

قم بإنشاء الهيكل التالي:

src/main/java/com/example/injection/
├── InjectionDemoApplication.java
├── service/
│ ├── EmailService.java
│ ├── GmailService.java
│ ├── SendGridService.java
│ └── NotificationService.java
├── controller/
│ └── TestController.java
└── config/
└── AppConfig.java

التمرين 1: الحقن عبر المُنشئ ✅​

الهدف​

إنشاء خدمة مع الحقن عبر المُنشئ (أفضل ممارسة).

الخطوة 1.1: إنشاء واجهة EmailService​

الملف: service/EmailService.java

package com.example.injection.service;

public interface EmailService {
void send(String to, String message);
String getProviderName();
}

ما نقوم به: إنشاء واجهة لوجود عدة تنفيذات ممكنة.


الخطوة 1.2: إنشاء تنفيذ GmailService​

الملف: service/GmailService.java

package com.example.injection.service;

import org.springframework.stereotype.Service;

@Service
public class GmailService implements EmailService {

@Override
public void send(String to, String message) {
System.out.println("[Gmail] إرسال بريد إلكتروني إلى " + to + ": " + message);
}

@Override
public String getProviderName() {
return "Gmail";
}
}

النقاط الرئيسية:

  • @Service → Spring يدير هذا الـ Bean
  • تنفيذ EmailService

الخطوة 1.3: إنشاء NotificationService مع الحقن عبر المُنشئ​

الملف: service/NotificationService.java

package com.example.injection.service;

import org.springframework.stereotype.Service;

@Service
public class NotificationService {

// التبعية معلنة كـ 'final' (غير قابلة للتغيير)
private final EmailService emailService;

// الحقن عبر المُنشئ (أفضل ممارسة ✅)
public NotificationService(EmailService emailService) {
this.emailService = emailService;
System.out.println("NotificationService تم إنشاؤه مع: " +
emailService.getProviderName());
}

public void notifyUser(String email, String message) {
System.out.println("=== NotificationService ===");
emailService.send(email, message);
}
}

النقاط الرئيسية:

  • private final → غير قابل للتغيير
  • الحقن عبر المُنشئ
  • لا حاجة لـ @Autowired (اختياري على مُنشئ وحيد)

الخطوة 1.4: إنشاء وحدة التحكم للاختبار​

الملف: controller/TestController.java

package com.example.injection.controller;

import com.example.injection.service.NotificationService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/test")
public class TestController {

private final NotificationService notificationService;

// الحقن عبر المُنشئ هنا أيضًا
public TestController(NotificationService notificationService) {
this.notificationService = notificationService;
}

@GetMapping("/constructor")
public String testConstructorInjection() {
notificationService.notifyUser("user@example.com",
"اختبار الحقن عبر المُنشئ");
return "✅ تم اختبار الحقن عبر المُنشئ - انظر إلى وحدة التحكم";
}
}

الخطوة 1.5: الاختبار​

تشغيل التطبيق:

mvn clean install -DskipTests spring-boot:run

الاختبار في المتصفح:

http://localhost:8080/api/test/constructor

يجب أن تعرض وحدة التحكم:

NotificationService تم إنشاؤه مع: Gmail
=== NotificationService ===
[Gmail] إرسال بريد إلكتروني إلى user@example.com: اختبار الحقن عبر المُنشئ

✅ النتيجة المتوقعة: "✅ تم اختبار الحقن عبر المُنشئ - انظر إلى وحدة التحكم"


التمرين 2: إدارة عدة تنفيذات باستخدام @Qualifier​

الهدف​

وجود تنفيذين لـ EmailService واختيار أيهما يتم استخدامه.

الخطوة 2.1: إنشاء SendGridService​

الملف: service/SendGridService.java

package com.example.injection.service;

import org.springframework.stereotype.Service;

@Service("sendGridService") // ← اسم واضح
public class SendGridService implements EmailService {

@Override
public void send(String to, String message) {
System.out.println("[SendGrid] إرسال بريد إلكتروني إلى " + to + ": " + message);
}

@Override
public String getProviderName() {
return "SendGrid";
}
}

الخطوة 2.2: المشكلة - تنفيذان!​

أعد تشغيل التطبيق:

mvn clean install -DskipTests spring-boot:run

يجب أن ترى خطأ:

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.example.injection.service.NotificationService
required a single bean, but 2 were found:
- gmailService: defined in file [...]
- sendGridService: defined in file [...]

Action:

Consider marking one of the beans as @Primary, or use @Qualifier

هذا طبيعي! Spring لا يعرف أيهما يختار.


الخطوة 2.3: الحل 1 - @Primary​

عدل GmailService.java:

package com.example.injection.service;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

@Service
@Primary // ← التنفيذ الافتراضي
public class GmailService implements EmailService {

@Override
public void send(String to, String message) {
System.out.println("[Gmail] إرسال بريد إلكتروني إلى " + to + ": " + message);
}

@Override
public String getProviderName() {
return "Gmail";
}
}

أعد التشغيل: التطبيق يعمل الآن! يتم استخدام Gmail افتراضيًا.

اختبر:

http://localhost:8080/api/test/constructor

وحدة التحكم:

[Gmail] إرسال بريد إلكتروني إلى user@example.com: ...

الخطوة 2.4: الحل 2 - @Qualifier للاختيار​

أنشئ خدمة جديدة تستخدم SendGrid:

الملف: service/SendGridNotificationService.java

package com.example.injection.service;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service
public class SendGridNotificationService {

private final EmailService emailService;

// استخدام @Qualifier لاختيار SendGrid
public SendGridNotificationService(
@Qualifier("sendGridService") EmailService emailService
) {
this.emailService = emailService;
System.out.println("SendGridNotificationService تم إنشاؤه مع: " +
emailService.getProviderName());
}

public void notifyUser(String email, String message) {
System.out.println("=== SendGridNotificationService ===");
emailService.send(email, message);
}
}

الخطوة 2.5: اختبار @Qualifier​

أضف طريقة في TestController.java:

@RestController
@RequestMapping("/api/test")
public class TestController {

private final NotificationService notificationService;
private final SendGridNotificationService sendGridNotificationService;

public TestController(
NotificationService notificationService,
SendGridNotificationService sendGridNotificationService
) {
this.notificationService = notificationService;
this.sendGridNotificationService = sendGridNotificationService;
}

@GetMapping("/constructor")
public String testConstructorInjection() {
notificationService.notifyUser("user@example.com",
"اختبار مع Gmail");
return "✅ تم استخدام Gmail - انظر إلى وحدة التحكم";
}

@GetMapping("/qualifier")
public String testQualifier() {
sendGridNotificationService.notifyUser("user@example.com",
"اختبار مع SendGrid");
return "✅ تم استخدام SendGrid عبر @Qualifier - انظر إلى وحدة التحكم";
}
}

اختبر:

http://localhost:8080/api/test/qualifier

وحدة التحكم:

=== SendGridNotificationService ===
[SendGrid] إرسال بريد إلكتروني إلى user@example.com: اختبار مع SendGrid

✅ لديك خدمتان تستخدمان تنفيذات مختلفة!


التمرين 3: التبعيات الاختيارية​

الهدف​

إنشاء خدمة يمكنها العمل مع أو بدون تبعية.

الخطوة 3.1: إنشاء SmsService (اختياري)​

الملف: service/SmsService.java

package com.example.injection.service;

public interface SmsService {
void send(String phone, String message);
}

لا تنشئ تنفيذًا في الوقت الحالي!


الخطوة 3.2: إنشاء CompleteNotificationService مع تبعية اختيارية​

الملف: service/CompleteNotificationService.java

package com.example.injection.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
public class CompleteNotificationService {

private final EmailService emailService;
private final Optional<SmsService> smsService; // ← اختياري!

public CompleteNotificationService(
EmailService emailService,
Optional<SmsService> smsService // ← مغلف Optional
) {
this.emailService = emailService;
this.smsService = smsService;

System.out.println("CompleteNotificationService تم إنشاؤه");
System.out.println("- البريد الإلكتروني: " + emailService.getProviderName());
System.out.println("- الرسائل القصيرة: " + (smsService.isPresent() ? "متاح" : "غير متاح"));
}

public void notify(String email, String phone, String message) {
// البريد الإلكتروني دائمًا يُرسل
emailService.send(email, message);

// الرسائل القصيرة فقط إذا كانت متاحة
smsService.ifPresent(sms -> sms.send(phone, message));
}
}

النقاط الرئيسية:

  • Optional<SmsService> → قد يكون غائبًا
  • ifPresent() → يُنفذ فقط إذا كان موجودًا

الخطوة 3.3: اختبار بدون SmsService​

أضف في TestController.java:

private final CompleteNotificationService completeService;

public TestController(
NotificationService notificationService,
SendGridNotificationService sendGridNotificationService,
CompleteNotificationService completeService
) {
this.notificationService = notificationService;
this.sendGridNotificationService = sendGridNotificationService;
this.completeService = completeService;
}

@GetMapping("/optional")
public String testOptional() {
completeService.notify("user@example.com", "123-456-7890",
"اختبار تبعية اختيارية");
return "✅ تم إرسال البريد الإلكتروني، الرسائل القصيرة غير متاحة - انظر إلى وحدة التحكم";
}

اختبر:

http://localhost:8080/api/test/optional

وحدة التحكم:

CompleteNotificationService تم إنشاؤه
- البريد الإلكتروني: Gmail
- الرسائل القصيرة: غير متاح
[Gmail] إرسال بريد إلكتروني إلى user@example.com: اختبار تبعية اختيارية

✅ التطبيق يعمل حتى بدون SmsService!


الخطوة 3.4: إضافة SmsService وإعادة الاختبار​

الملف: service/SimpleSmsService.java

package com.example.injection.service;

import org.springframework.stereotype.Service;

@Service
public class SimpleSmsService implements SmsService {

@Override
public void send(String phone, String message) {
System.out.println("[SMS] إرسال إلى " + phone + ": " + message);
}
}

أعد التشغيل واختبر:

http://localhost:8080/api/test/optional

وحدة التحكم:

CompleteNotificationService تم إنشاؤه
- البريد الإلكتروني: Gmail
- الرسائل القصيرة: متاح
[Gmail] إرسال بريد إلكتروني إلى user@example.com: اختبار تبعية اختيارية
[SMS] إرسال إلى 123-456-7890: اختبار تبعية اختيارية

✅ الآن يتم استخدام كلاهما!


التمرين 4: حقن جميع التنفيذات​

الهدف​

استرجاع جميع التنفيذات لواجهة معينة.

الخطوة 4.1: إنشاء MultiChannelNotificationService​

الملف: service/MultiChannelNotificationService.java

package com.example.injection.service;

import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class MultiChannelNotificationService {

private final List<EmailService> allEmailServices;

// Spring يحقن قائمة بجميع التنفيذات
public MultiChannelNotificationService(List<EmailService> allEmailServices) {
this.allEmailServices = allEmailServices;
System.out.println("MultiChannelNotificationService تم إنشاؤه مع " +
allEmailServices.size() + " خدمات البريد الإلكتروني:");
allEmailServices.forEach(service ->
System.out.println(" - " + service.getProviderName())
);
}

public void broadcastEmail(String to, String message) {
System.out.println("=== إرسال عبر جميع المزودين ===");
allEmailServices.forEach(service -> service.send(to, message));
}
}

الخطوة 4.2: الاختبار​

أضف في TestController.java:

private final MultiChannelNotificationService multiChannelService;

public TestController(
NotificationService notificationService,
SendGridNotificationService sendGridNotificationService,
CompleteNotificationService completeService,
MultiChannelNotificationService multiChannelService
) {
this.notificationService = notificationService;
this.sendGridNotificationService = sendGridNotificationService;
this.completeService = completeService;
this.multiChannelService = multiChannelService;
}

@GetMapping("/all")
public String testAllImplem() {
multiChannelService.broadcastEmail("user@example.com",
"رسالة بث");
return "✅ إرسال عبر جميع المزودين - انظر إلى وحدة التحكم";
}

اختبر:

http://localhost:8080/api/test/all

وحدة التحكم:

=== إرسال عبر جميع المزودين ===
[Gmail] إرسال بريد إلكتروني إلى user@example.com: رسالة بث
[SendGrid] إرسال بريد إلكتروني إلى user@example.com: رسالة بث

✅ يتم استخدام كلا التنفيذين!


التمرين 5: الحقن عبر المُعَيّن (التبعيات الاختيارية)​

الهدف​

استخدام الحقن عبر المُعَيّن للتبعيات الاختيارية حقًا.

الخطوة 5.1: إنشاء LoggerService مع المُعَيّن​

الملف: service/LoggerService.java

package com.example.injection.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class LoggerService {

private SmsService smsService; // ← ليس 'final'، يمكن أن يكون null

// الحقن عبر المُعَيّن - اختياري
@Autowired(required = false) // ← required = false
public void setSmsService(SmsService smsService) {
this.smsService = smsService;
System.out.println("تم حقن خدمة الرسائل القصيرة في LoggerService");
}

public void logNotification(String message) {
System.out.println("[LOG] " + message);

if (smsService != null) {
System.out.println("[LOG] خدمة الرسائل القصيرة متاحة");
} else {
System.out.println("[LOG] خدمة الرسائل القصيرة غير متاحة");
}
}
}

النقاط الرئيسية:

  • @Autowired(required = false) → ليس إلزاميًا
  • التحقق if (smsService != null)

الخطوة 5.2: الاختبار​

أضف في TestController.java:

private final LoggerService loggerService;

@GetMapping("/setter")
public String testSetter() {
loggerService.logNotification("اختبار الحقن عبر المُعَيّن");
return "✅ تم اختبار الحقن عبر المُعَيّن - انظر إلى وحدة التحكم";
}

اختبر:

http://localhost:8080/api/test/setter

وحدة التحكم (مع SmsService):

تم حقن خدمة الرسائل القصيرة في LoggerService
[LOG] اختبار الحقن عبر المُعَيّن
[LOG] خدمة الرسائل القصيرة متاحة

ملخص: مقارنة الطرق​

جدول المقارنة​

النوعالصياغةإلزامي؟غير قابل للتغيير؟الاستخدام
المُنشئpublic Service(Dep d)✅ نعم✅ نعم (final)موصى به للتبعيات الإلزامية
المُعَيّن@Autowired(required=false)❌ لا❌ لاالتبعيات الاختيارية
الحقل@Autowired private Dep d⚠️ نعم❌ لا❌ يجب تجنبه
Optional<T>Optional<Dep> d❌ لا✅ نعمالتبعيات الاختيارية الحديثة
List<T>List<Dep> all✅ نعم✅ نعمجميع التنفيذات

الكود النهائي الكامل​

يجب أن يحتوي مشروعك الآن على:

✅ GmailService (@Primary)
✅ SendGridService
✅ NotificationService (المُنشئ + Gmail)
✅ SendGridNotificationService (المُنشئ + @Qualifier)
✅ CompleteNotificationService (Optional<SmsService>)
✅ SimpleSmsService (اختياري)
✅ MultiChannelNotificationService (List<EmailService>)
✅ LoggerService (المُعَيّن + required=false)

التقييم الذاتي​

أجب عن الأسئلة التالية:

  1. لماذا يُفضل الحقن عبر المُنشئ؟
  2. ما الفرق بين @Primary و @Qualifier؟
  3. كيف تدير تبعية اختيارية حقًا؟
  4. كيف تحقن جميع التنفيذات لواجهة معينة؟
  5. متى تستخدم Optional<T> مقابل @Autowired(required=false)؟

إذا كنت تستطيع الإجابة على جميع هذه الأسئلة، فقد أتقنت حقن التبعيات! ✅


تحدي إضافي 🎯​

أنشئ نظام إشعارات متعدد القنوات:

  1. واجهة NotificationChannel
  2. تنفيذات: EmailChannel, SmsChannel, PushChannel
  3. خدمة NotificationOrchestrator التي:
    • تحقن جميع القنوات
    • تسمح بتفعيل/تعطيل القنوات
    • ترسل على جميع القنوات النشطة

الوقت المقدر: 30 دقيقة


الخطوة التالية​

لقد أتقنت الآن حقن التبعيات! انتقل إلى TP2 لتتعلم عن Beans والنطاقات.

➡️ TP5.2: Beans والنطاقات

⬅️ العودة إلى الوحدة 5