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

الفصل 4: النطاقات والتكوين

مقدمة​

تعلم كيفية إنشاء Beans، ولكن هل تعرف كم عدد النسخ التي ينشئها Spring؟ نسخة واحدة لكل التطبيق؟ نسخة جديدة في كل مرة؟

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

ما ستتعلمه​

  1. نطاقات Beans (Singleton، Prototype، إلخ)
  2. متى تستخدم أي نطاق
  3. التكوين باستخدام @Configuration
  4. ملفات التعريف (@Profile) للتطوير/الاختبار/الإنتاج
  5. @ComponentScan

المدة: 45 دقيقة


نطاقات Beans​

ما هو النطاق؟​

النطاق = مدة الحياة وعدد النسخ من Bean

سؤال: عندما ينشئ Spring UserService، كم عدد النسخ التي ينشئها؟

  • واحدة فقط لكل التطبيق؟ (Singleton)
  • واحدة جديدة في كل حقن؟ (Prototype)
  • واحدة لكل طلب HTTP؟ (Request)
  • واحدة لكل جلسة مستخدم؟ (Session)

النطاق يحدد الإجابة!


النطاقات الخمسة المتاحة​

النطاقالوصفالنسخالاستخدام
@Singletonنسخة واحدة لكل التطبيق1افتراضي - الخدمات، المستودعات
@Prototypeنسخة جديدة في كل حقنعدةكائنات ذات حالة
@RequestScopeنسخة واحدة لكل طلب HTTP1/طلبكائنات مرتبطة بطلب
@SessionScopeنسخة واحدة لكل جلسة مستخدم1/جلسةسلة التسوق، تفضيلات المستخدم
@ApplicationScopeنسخة واحدة لتطبيق الويب1مشابه لـ Singleton للويب

1. نطاق Singleton (افتراضي)​

المبدأ​

ينشئ Spring نسخة واحدة فقط من Bean
جميع المكونات تشارك نفس النسخة

الكود​

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

private int counter = 0;

public void incrementCounter() {
counter++;
}

public int getCounter() {
return counter;
}
}

اختبار​

@RestController
public class TestController {

private final UserService userService;

public TestController(UserService userService) {
this.userService = userService;
}

@GetMapping("/test")
public String test() {
userService.incrementCounter();
return "Counter: " + userService.getCounter();
}
}

النتيجة:

الطلب 1: Counter: 1
الطلب 2: Counter: 2
الطلب 3: Counter: 3
...

لماذا؟ إنها نفس النسخة لجميع الطلبات!


التصور​

الجميع يشاركون نفس النسخة!


متى تستخدم Singleton؟​

✅ الخدمات بدون حالة (stateless)

@Service
public class EmailService {
public void send(String to, String message) {
// لا توجد متغيرات مثيلة قابلة للتعديل
// فقط طرق تعمل مع المعلمات
}
}

✅ المستودعات

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// لا توجد حالة
}

✅ المتحكمات

@RestController
public class UserController {
// لا توجد حالة (التبعيات نهائية)
}

القاعدة: إذا لم يكن لدى Bean حالة قابلة للتغيير، استخدم Singleton (افتراضي)!


2. نطاق Prototype​

المبدأ​

ينشئ Spring نسخة جديدة في كل مرة نطلب فيها Bean
كل مكون لديه نسخته الخاصة

الكود​

import org.springframework.context.annotation.Scope;

@Service
@Scope("prototype") // ← نسخة جديدة في كل مرة
public class ShoppingCart {

private List<Product> items = new ArrayList<>();

public void addProduct(Product product) {
items.add(product);
}

public List<Product> getItems() {
return items;
}
}

اختبار​

@RestController
public class CartController {

private final ApplicationContext context;

public CartController(ApplicationContext context) {
this.context = context;
}

@GetMapping("/test-prototype")
public String testPrototype() {
// الحصول على نسختين
ShoppingCart cart1 = context.getBean(ShoppingCart.class);
ShoppingCart cart2 = context.getBean(ShoppingCart.class);

// تعديل cart1
cart1.addProduct(new Product("Laptop"));

// التحقق من cart2
return "Cart1: " + cart1.getItems().size() +
", Cart2: " + cart2.getItems().size();
}
}

النتيجة:

Cart1: 1, Cart2: 0

لماذا؟ إنها نسخ مختلفة!


التصور​

كل مكون لديه نسخته الخاصة!


متى تستخدم Prototype؟​

✅ كائنات ذات حالة

@Scope("prototype")
public class ReportGenerator {
private String title;
private List<Data> data;
// حالة خاصة بكل تقرير
}

✅ كائنات مؤقتة

@Scope("prototype")
public class FileProcessor {
private File currentFile;
// معالجة ملف معين
}

⚠️ انتباه: Prototype مع Singleton = مشكلة!


مشكلة: Prototype في Singleton​

@Service  // Singleton
public class OrderService {

private final ShoppingCart cart; // Prototype

// ❌ مشكلة!
public OrderService(ShoppingCart cart) {
this.cart = cart;
// cart يتم حقنه مرة واحدة فقط (عند بدء التشغيل)
// حتى لو كان ShoppingCart هو Prototype!
}
}

النتيجة: يتصرف Prototype مثل Singleton!

الحل: استخدام ObjectProvider أو ApplicationContext.getBean() للحصول على نسخة جديدة في كل مرة.


3. نطاق Request (ويب)​

المبدأ​

نسخة جديدة لكل طلب HTTP
توجد النسخة طوال مدة الطلب
تُدمر بعد الاستجابة

الكود​

import org.springframework.web.context.annotation.RequestScope;

@Component
@RequestScope // ← نسخة جديدة لكل طلب
public class RequestLogger {

private String requestId;
private LocalDateTime startTime;

@PostConstruct
public void init() {
this.requestId = UUID.randomUUID().toString();
this.startTime = LocalDateTime.now();
System.out.println("الطلب " + requestId + " بدأ");
}

@PreDestroy
public void cleanup() {
Duration duration = Duration.between(startTime, LocalDateTime.now());
System.out.println("الطلب " + requestId + " انتهى في " +
duration.toMillis() + "ms");
}
}

الاستخدام​

@RestController
public class UserController {

private final RequestLogger logger;

public UserController(RequestLogger logger) {
this.logger = logger;
}

@GetMapping("/users")
public List<User> getUsers() {
logger.log("استرجاع المستخدمين");
// ... منطق
}
}

النتيجة:

الطلب 1:
الطلب abc-123 بدأ
الطلب abc-123 انتهى في 45ms

الطلب 2:
الطلب def-456 بدأ
الطلب def-456 انتهى في 32ms

كل طلب لديه نسخته الخاصة!


4. نطاق Session (ويب)​

المبدأ​

نسخة واحدة لكل جلسة مستخدم
مشتركة بين جميع الطلبات لنفس المستخدم
تُدمر عندما تنتهي الجلسة

الكود​

import org.springframework.web.context.annotation.SessionScope;

@Component
@SessionScope // ← نسخة لكل جلسة مستخدم
public class UserPreferences {

private String theme = "light";
private String language = "fr";

public void setTheme(String theme) {
this.theme = theme;
}

public String getTheme() {
return theme;
}
}

الاستخدام: سلة التسوق، التفضيلات، التوثيق


مقارنة النطاقات​

جدول ملخص​

النطاقالنسخمدة الحياةالاستخدام الرئيسي
Singleton1 لكل التطبيقبدء التشغيل → الإيقافالخدمات، المستودعات (افتراضي)
Prototype1 لكل طلبالإنشاء → GCكائنات ذات حالة
Request1 لكل طلب HTTPالطلب → الاستجابةالسجلات، سياق الطلب
Session1 لكل جلسة مستخدمبدء الجلسة → نهايتهاسلة التسوق، التفضيلات

التصور الزمني​

Singleton:     [════════════════════════════════] (كل التطبيق)

Prototype: [===] [===] [===] [===] [===] (عند الطلب)

Request: [req1] [req2] [req3] [req4] (لكل طلب)

Session: [═══ جلسة المستخدم 1 ═══]
[═══════ جلسة المستخدم 2 ═══════]

التكوين باستخدام @Configuration​

فئة تكوين كاملة​

@Configuration
public class AppConfig {

// Bean Singleton (افتراضي)
@Bean
public EmailService emailService() {
EmailService service = new EmailService();
service.setHost("smtp.gmail.com");
return service;
}

// Bean Prototype
@Bean
@Scope("prototype")
public ReportGenerator reportGenerator() {
return new ReportGenerator();
}

// Bean شرطي (موضح أدناه)
@Bean
@Profile("dev")
public DataSource devDataSource() {
return new H2DataSource();
}

@Bean
@Profile("prod")
public DataSource prodDataSource() {
return new PostgreSQLDataSource();
}
}

ملفات التعريف (@Profile)​

المشكلة​

لديك تكوينات مختلفة حسب البيئة:

التطوير:
- قاعدة H2 في الذاكرة
- سجلات مفصلة
- البريد الإلكتروني في وضع وهمي

الإنتاج:
- قاعدة PostgreSQL
- سجلات قليلة
- البريد الإلكتروني الحقيقي

كيف تدير ذلك بشكل نظيف؟


الحل: ملفات التعريف في Spring​

إنشاء Beans حسب ملف التعريف النشط:

@Configuration
public class DatabaseConfig {

@Bean
@Profile("dev") // ← نشط فقط في التطوير
public DataSource devDataSource() {
System.out.println("استخدام H2 (التطوير)");
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}

@Bean
@Profile("prod") // ← نشط فقط في الإنتاج
public DataSource prodDataSource() {
System.out.println("استخدام PostgreSQL (الإنتاج)");
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
config.setUsername("admin");
config.setPassword("secret");
return new HikariDataSource(config);
}

@Bean
@Profile("test") // ← نشط فقط في الاختبار
public DataSource testDataSource() {
System.out.println("استخدام H2 (الاختبار)");
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("test-schema.sql")
.build();
}
}

تفعيل ملف شخصي​

الطريقة 1: application.properties

spring.profiles.active=dev

الطريقة 2: متغير البيئة

export SPRING_PROFILES_ACTIVE=prod

الطريقة 3: وسيط JVM

java -jar app.jar --spring.profiles.active=prod

الطريقة 4: IDE (VS Code، IntelliJ)

"spring.profiles.active": "dev"

ملفات شخصية متعددة​

تفعيل ملفات شخصية متعددة:

spring.profiles.active=dev,debug,mock-email

Bean نشط لعدة ملفات شخصية:

@Bean
@Profile({"dev", "test"}) // ← نشط في التطوير أو الاختبار
public DataSource devOrTestDataSource() {
return new H2DataSource();
}

مثال كامل: التكوين حسب البيئة​

// واجهة مشتركة
public interface EmailService {
void send(String to, String message);
}

// تنفيذ DEV (وهمي)
@Service
@Profile("dev")
public class FakeEmailService implements EmailService {
@Override
public void send(String to, String message) {
System.out.println("بريد إلكتروني وهمي إلى " + to + ": " + message);
}
}

// تنفيذ PROD (حقيقي)
@Service
@Profile("prod")
public class RealEmailService implements EmailService {
@Override
public void send(String to, String message) {
// منطق إرسال حقيقي عبر SMTP
smtpClient.send(to, message);
}
}

في التطوير: لا يتم إرسال رسائل بريد إلكتروني حقيقية
في الإنتاج: يتم إرسال رسائل بريد إلكتروني حقيقية


ملفات التكوين حسب ملف التعريف​

الهيكل​

src/main/resources/
├── application.properties (مشترك)
├── application-dev.properties (التطوير فقط)
├── application-test.properties (الاختبار فقط)
└── application-prod.properties (الإنتاج فقط)

مثال​

application.properties (مشترك):

app.name=My Application
server.port=8080

application-dev.properties:

spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.show-sql=true
logging.level.root=DEBUG

application-prod.properties:

spring.datasource.url=jdbc:postgresql://prod-server:5432/mydb
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
spring.jpa.show-sql=false
logging.level.root=WARN

التفعيل:

spring.profiles.active=dev

يقوم Spring بتحميل application-dev.properties تلقائيًا!


@ComponentScan​

المشكلة​

افتراضيًا، يقوم Spring بمسح الحزمة الخاصة بالفئة الرئيسية والحزم الفرعية فقط.

com.example.demo
├── DemoApplication.java ← الفئة الرئيسية
├── service/
│ └── UserService.java ✅ ممسوح
└── controller/
└── UserController.java ✅ ممسوح

com.external.library
└── ExternalService.java ❌ غير ممسوح

الحل: @ComponentScan​

@SpringBootApplication
@ComponentScan(basePackages = {
"com.example.demo", // الحزمة الرئيسية
"com.external.library" // حزمة خارجية
})
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

الآن سيتم مسح ExternalService!


تصفية المسح​

استبعاد بعض الفئات:

@ComponentScan(
basePackages = "com.example.demo",
excludeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {OldService.class, DeprecatedController.class}
)
)

تضمين فقط بعض التعليقات التوضيحية:

@ComponentScan(
basePackages = "com.example.demo",
includeFilters = @ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = Service.class // فقط @Service
),
useDefaultFilters = false
)

مثال كامل: تطبيق متعدد البيئات​

// الفئة الرئيسية
@SpringBootApplication
public class ECommerceApplication {
public static void main(String[] args) {
SpringApplication.run(ECommerceApplication.class, args);
}
}

// التكوين
@Configuration
public class AppConfig {

// البريد الإلكتروني حسب البيئة
@Bean
@Profile("dev")
public EmailService devEmailService() {
return new FakeEmailService();
}

@Bean
@Profile("prod")
public EmailService prodEmailService() {
return new SmtpEmailService();
}

// التخزين المؤقت حسب البيئة
@Bean
@Profile("prod")
@Scope("singleton")
public CacheManager redisCacheManager() {
return new RedisCacheManager();
}

@Bean
@Profile("dev")
@Scope("singleton")
public CacheManager simpleCacheManager() {
return new SimpleCacheManager();
}
}

// خدمة بنطاق مناسب
@Service
@Scope("singleton") // نسخة واحدة لكل التطبيق
public class OrderService {

private final EmailService emailService;
private final CacheManager cacheManager;

public OrderService(EmailService emailService,
CacheManager cacheManager) {
this.emailService = emailService;
this.cacheManager = cacheManager;
}
}

// Bean بحالة (Prototype)
@Component
@Scope("prototype") // نسخة جديدة في كل مرة
public class Invoice {
private Order order;
private BigDecimal total;

public void generate(Order order) {
this.order = order;
this.total = calculateTotal(order);
}
}

ملخص الفصل​

النطاقات​

@Scope("singleton")   // نسخة واحدة (افتراضي)
@Scope("prototype") // عدة نسخ
@RequestScope // نسخة لكل طلب HTTP
@SessionScope // نسخة لكل جلسة مستخدم

ملفات التعريف​

@Profile("dev")       // نشط في التطوير
@Profile("prod") // نشط في الإنتاج
@Profile({"dev","test"}) // نشط في التطوير أو الاختبار

التفعيل:

spring.profiles.active=dev

التكوين​

@Configuration        // فئة التكوين
@Bean // إنشاء Bean
@ComponentScan // مسح الحزم

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

✅ Singleton افتراضي (الخدمات بدون حالة)
✅ Prototype للكائنات ذات الحالة
✅ Request/Session للويب
✅ @Profile لإدارة التطوير/الاختبار/الإنتاج
✅ application-{profile}.properties للتكوين
✅ @ComponentScan للحزم الخارجية


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

  1. ما الفرق بين Singleton وPrototype؟
  2. متى تستخدم RequestScope؟
  3. كيف تقوم بتفعيل ملف شخصي في Spring؟
  4. ما الفرق بين application.properties وapplication-dev.properties؟
  5. ما فائدة @ComponentScan؟

تهانينا! 🎉​

لقد أكملت كل النظرية في الوحدة 5!

أنت الآن تفهم:

  • ✅ عكس التحكم (IoC)
  • ✅ حقن التبعيات (DI)
  • ✅ Beans في Spring
  • ✅ النطاقات والتكوين

الخطوة التالية: التطبيق العملي مع التدريبات العملية!


➡️ ابدأ التدريب العملي 1: الحقن المتقدم

⬅️ الفصل السابق: Beans

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