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

TP5.2 - Beans والنطاقات

الأهداف​

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

  1. ✅ إنشاء Beans باستخدام @Bean و@Configuration
  2. ✅ فهم الفرق بين Singleton وPrototype
  3. ✅ استخدام @PostConstruct و@PreDestroy
  4. ✅ إنشاء Beans مشروطة

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


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

  • ✅ إكمال الفصل 3 من الوحدة 5
  • ✅ إكمال TP5.1
  • ✅ Maven 3.9+ وJava 17+

التحضير: مشروع جديد​

إنشاء المشروع​

عبر Spring Initializr (https://start.spring.io/):

  • Project: Maven
  • Spring Boot: 3.2.0
  • Group: com.example
  • Artifact: beans-demo
  • Dependencies: Spring Web

الهيكل:

src/main/java/com/example/beans/
├── BeansDemoApplication.java
├── service/
│ ├── CounterService.java
│ ├── ReportGenerator.java
│ └── CacheService.java
├── config/
│ └── AppConfig.java
└── controller/
└── TestController.java

التمرين 1: Singleton مقابل Prototype​

الهدف​

فهم الفرق بين Bean Singleton (مثيل واحد) وPrototype (عدة مثيلات).

الخطوة 1.1: إنشاء CounterService (Singleton افتراضيًا)​

الملف: service/CounterService.java

package com.example.beans.service;

import org.springframework.stereotype.Service;

@Service // Singleton افتراضيًا
public class CounterService {

private int counter = 0;

public void increment() {
counter++;
System.out.println("Counter incrementé: " + counter);
}

public int getCounter() {
return counter;
}
}

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

  • @Service → Singleton افتراضيًا
  • counter مشترك بين جميع الاستخدامات

الخطوة 1.2: اختبار Singleton​

الملف: controller/TestController.java

package com.example.beans.controller;

import com.example.beans.service.CounterService;
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 CounterService counterService;

public TestController(CounterService counterService) {
this.counterService = counterService;
}

@GetMapping("/singleton")
public String testSingleton() {
counterService.increment();
return "Counter: " + counterService.getCounter() +
" (Singleton - نفس المثيل)";
}
}

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

ابدأ:

mvn clean install -DskipTests spring-boot:run

اختبر عدة مرات:

http://localhost:8080/api/test/singleton

النتيجة:

الطلب الأول: Counter: 1
الطلب الثاني: Counter: 2
الطلب الثالث: Counter: 3
...

✅ إنه نفس المثيل! العداد يستمر في الزيادة.


الخطوة 1.4: إنشاء ReportGenerator (Prototype)​

الملف: service/ReportGenerator.java

package com.example.beans.service;

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

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Service
@Scope("prototype") // ← مثيل جديد في كل مرة!
public class ReportGenerator {

private String reportId;
private List<String> data = new ArrayList<>();
private LocalDateTime createdAt;

public ReportGenerator() {
this.reportId = "REPORT-" + System.currentTimeMillis();
this.createdAt = LocalDateTime.now();
System.out.println("Nouvelle instance ReportGenerator créée: " + reportId);
}

public void addData(String item) {
data.add(item);
}

public String getReport() {
return String.format("Report %s créé à %s avec %d items",
reportId, createdAt, data.size());
}
}

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

  • @Scope("prototype") → مثيل جديد في كل حقن
  • كل مثيل لديه reportId وdata خاص به

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

أضف في TestController.java:

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

private final CounterService counterService;
private final ApplicationContext context; // ← لاسترجاع Prototypes

public TestController(CounterService counterService,
ApplicationContext context) {
this.counterService = counterService;
this.context = context;
}

// ... طريقة singleton ...

@GetMapping("/prototype")
public String testPrototype() {
// استرجاع 2 مثيلات
ReportGenerator report1 = context.getBean(ReportGenerator.class);
ReportGenerator report2 = context.getBean(ReportGenerator.class);

// تعديل report1
report1.addData("Item 1");
report1.addData("Item 2");

// التحقق من الاثنين
return "Report1: " + report1.getReport() + "<br>" +
"Report2: " + report2.getReport() + "<br>" +
"(إنها مثيلات مختلفة)";
}
}

اختبر:

http://localhost:8080/api/test/prototype

النتيجة:

Report1: Report REPORT-1234567890 créé à 2025-11-28T10:00:00 avec 2 items
Report2: Report REPORT-1234567891 créé à 2025-11-28T10:00:01 avec 0 items

✅ مثيلات مختلفة! كل واحدة لديها حالتها الخاصة.


التمرين 2: دورة الحياة مع @PostConstruct و@PreDestroy​

الهدف​

تنفيذ كود بعد الحقن وقبل التدمير.

الخطوة 2.1: إنشاء CacheService مع Hooks دورة الحياة​

الملف: service/CacheService.java

package com.example.beans.service;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class CacheService {

private Map<String, String> cache;

// المنشئ
public CacheService() {
System.out.println("1. Constructeur CacheService appelé");
// ⚠️ لا تقم بالتهيئة هنا إذا كانت تعتمد على Beans أخرى!
}

// يتم استدعاؤه بعد حقن جميع التبعيات
@PostConstruct
public void init() {
System.out.println("2. @PostConstruct - Initialisation du cache");
cache = new HashMap<>();
cache.put("key1", "value1");
cache.put("key2", "value2");
System.out.println(" Cache initialisé avec " + cache.size() + " éléments");
}

// يتم استدعاؤه قبل تدمير Bean
@PreDestroy
public void cleanup() {
System.out.println("3. @PreDestroy - Nettoyage du cache");
if (cache != null) {
System.out.println(" Sauvegarde de " + cache.size() + " éléments");
cache.clear();
}
System.out.println(" Cache nettoyé");
}

public void put(String key, String value) {
cache.put(key, value);
System.out.println("Ajouté au cache: " + key + " = " + value);
}

public String get(String key) {
return cache.get(key);
}

public int size() {
return cache.size();
}
}

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

أضف في TestController.java:

private final CacheService cacheService;

@GetMapping("/lifecycle")
public String testLifecycle() {
cacheService.put("test", "valeur");
return "Cache size: " + cacheService.size() + " - Voir console pour le cycle de vie";
}

ابدأ التطبيق:

الكونسول عند البدء:

1. Constructeur CacheService appelé
2. @PostConstruct - Initialisation du cache
Cache initialisé avec 2 éléments

اختبر:

http://localhost:8080/api/test/lifecycle

الكونسول:

Ajouté au cache: test = valeur

أوقف التطبيق (Ctrl+C):

الكونسول عند الإيقاف:

3. @PreDestroy - Nettoyage du cache
Sauvegarde de 3 éléments
Cache nettoyé

✅ دورة حياة كاملة تم اختبارها!


التمرين 3: إنشاء Beans باستخدام @Bean و@Configuration​

الهدف​

إنشاء Beans للفئات التي لا تتحكم فيها (المكتبات الخارجية).

الخطوة 3.1: إنشاء التكوين​

الملف: config/AppConfig.java

package com.example.beans.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.format.DateTimeFormatter;
import java.util.Random;

@Configuration
public class AppConfig {

// Bean 1: DateTimeFormatter (فئة Java القياسية)
@Bean
public DateTimeFormatter dateFormatter() {
System.out.println("Création du Bean dateFormatter");
return DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
}

// Bean 2: Random (فئة Java القياسية)
@Bean
public Random randomGenerator() {
System.out.println("Création du Bean randomGenerator");
return new Random();
}

// Bean 3: Bean مخصص مع تبعية
@Bean
public ReportFormatter reportFormatter(DateTimeFormatter dateFormatter) {
System.out.println("Création du Bean reportFormatter");
// Spring يحقن dateFormatter تلقائيًا!
return new ReportFormatter(dateFormatter);
}
}

الخطوة 3.2: إنشاء ReportFormatter​

الملف: service/ReportFormatter.java

package com.example.beans.service;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class ReportFormatter {

private final DateTimeFormatter dateFormatter;

public ReportFormatter(DateTimeFormatter dateFormatter) {
this.dateFormatter = dateFormatter;
}

public String formatReport(String title, String content) {
String timestamp = LocalDateTime.now().format(dateFormatter);
return String.format("=== %s ===\nDate: %s\n%s",
title, timestamp, content);
}
}

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

أضف في TestController.java:

private final Random random;
private final ReportFormatter reportFormatter;

@GetMapping("/config-beans")
public String testConfigBeans() {
int randomNumber = random.nextInt(100);
String report = reportFormatter.formatReport(
"Test Report",
"Nombre aléatoire: " + randomNumber
);
return report.replace("\n", "<br>");
}

اختبر:

http://localhost:8080/api/test/config-beans

النتيجة:

=== Test Report ===
Date: 28/11/2025 10:30:45
Nombre aléatoire: 42

✅ Beans التي تم إنشاؤها عبر @Configuration تعمل!


التمرين 4: Beans مشروطة (إضافي)​

الهدف​

إنشاء Beans فقط في ظل ظروف معينة.

الخطوة 4.1: Bean شرطي بسيط​

أضف في AppConfig.java:

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;

@Configuration
public class AppConfig {

// ... Beans أخرى ...

// Bean يتم إنشاؤه فقط إذا تم تحديد الخاصية
@Bean
@ConditionalOnProperty(name = "app.feature.advanced", havingValue = "true")
public String advancedFeature() {
return "Feature Avancée Activée!";
}
}

الخطوة 4.2: تفعيل الميزة​

الملف: src/main/resources/application.properties

app.feature.advanced=true

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

أضف في TestController.java:

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

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

@Autowired(required = false) // ← اختياري!
private String advancedFeature;

@GetMapping("/conditional")
public String testConditional() {
if (advancedFeature != null) {
return "✅ " + advancedFeature;
} else {
return "❌ Feature avancée désactivée";
}
}
}

اختبر مع app.feature.advanced=true:

✅ Feature Avancée Activée!

اختبر مع app.feature.advanced=false:

❌ Feature avancée désactivée

ملخص​

ما الذي تعلمته​

المفهومالصياغةالاستخدام
Singleton@Service (افتراضي)خدمات بدون حالة، مثيل واحد
Prototype@Scope("prototype")كائنات بحالة، عدة مثيلات
@PostConstructطريقة مشروطةالتهيئة بعد الحقن
@PreDestroyطريقة مشروطةالتنظيف قبل التدمير
@Beanفي @Configurationفئات خارجية، تكوين معقد
Bean شرطي@ConditionalOnPropertyBeans حسب التكوين

مقارنة Singleton مقابل Prototype​

// Singleton (افتراضي)
@Service
public class SingletonService {
private int state = 0; // ⚠️ مشترك!
}

// Prototype
@Service
@Scope("prototype")
public class PrototypeService {
private int state = 0; // ✅ خاص بكل مثيل
}

متى تستخدم ماذا؟​

Singleton ✅ (افتراضي):

  • خدمات بدون حالة
  • Repositories
  • Controllers
  • 99% من الحالات!

Prototype ⚠️ (نادر):

  • كائنات بحالة قابلة للتغيير
  • كائنات مؤقتة
  • تقارير، معالجات الملفات

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

  1. ما الفرق بين Singleton وPrototype؟
  2. متى يتم استدعاء @PostConstruct و@PreDestroy؟
  3. لماذا تستخدم @Bean بدلاً من @Service؟
  4. كيف تنشئ Bean شرطي؟

تحدي إضافي 🎯​

أنشئ نظام ذاكرة تخزين مؤقت مع TTL (وقت الحياة):

  1. @Service @Scope("singleton") CacheManager
  2. طريقة put(key, value, ttlSeconds)
  3. طريقة get(key) التي تعيد null إذا انتهت الصلاحية
  4. @PostConstruct للتهيئة
  5. @PreDestroy لحفظ الذاكرة المؤقتة

إضافي: استخدم @Scheduled لتنظيف الإدخالات المنتهية الصلاحية تلقائيًا.


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

انتقل إلى TP3 لتعلم التكوين عبر ملفات التعريف!

➡️ TP5.3: التكوين عبر الملفات الشخصية

⬅️ TP السابق: الحقن المتقدم

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