TP5.1 - حقن التبعيات المتقدم
الأهداف
في نهاية هذا التمرين، ستكون قادرًا على:
- ✅ استخدام الأنواع الثلاثة للحقن (المُنشئ، المُعَيّن، الحقل)
- ✅ الاختيار بين عدة تنفيذات باستخدام @Qualifier
- ✅ إدارة التبعيات الاختيارية
- ✅ فهم وحل التبعيات الدائرية
المدة المقدرة: 1 ساعة و30 دقيقة
المتطلبات الأساسية
- ✅ إكمال الفصول 1 و2 من الوحدة 5
- ✅ تثبيت Maven 3.9+
- ✅ تثبيت Java 17+
- ✅ VS Code مع إضافات Java
التحضير: إنشاء المشروع
الخطوة 1: إنشاء المشروع باستخدام Spring Initializr
عبر الموقع الإلكتروني:
-
اذهب إلى https://start.spring.io/
-
قم بتكوين:
- 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
-
التبعيات التي يجب إضافتها:
- Spring Web
-
انقر على GENERATE
-
فك الضغط وافتح في 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)
التقييم الذاتي
أجب عن الأسئلة التالية:
- لماذا يُفضل الحقن عبر المُنشئ؟
- ما الفرق بين @Primary و @Qualifier؟
- كيف تدير تبعية اختيارية حقًا؟
- كيف تحقن جميع التنفيذات لواجهة معينة؟
- متى تستخدم
Optional<T>مقابل@Autowired(required=false)؟
إذا كنت تستطيع الإجابة على جميع هذه الأسئلة، فقد أتقنت حقن التبعيات! ✅
تحدي إضافي 🎯
أنشئ نظام إشعارات متعدد القنوات:
- واجهة
NotificationChannel - تنفيذات:
EmailChannel,SmsChannel,PushChannel - خدمة
NotificationOrchestratorالتي:- تحقن جميع القنوات
- تسمح بتفعيل/تعطيل القنوات
- ترسل على جميع القنوات النشطة
الوقت المقدر: 30 دقيقة
الخطوة التالية
لقد أتقنت الآن حقن التبعيات! انتقل إلى TP2 لتتعلم عن Beans والنطاقات.