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

دورة حياة الـ Bean

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

دورة حياة الـ Bean

يمرّ كل كائن يديره حاوي Spring بدورة حياة محدّدة المعالم: يُنشأ الكائن أولًا، ثم تُعبَّأ تبعياته، ثم تُشغَّل استدعاءات التهيئة، ثم يخدم الكائنُ التطبيقَ، وأخيرًا تُنظّف الموارد عبر استدعاءات الإتلاف. إن فهمت كل مرحلة، أمكنك التدخّل في اللحظة المناسبة تمامًا — سواء أردتَ فتح مجمّع اتصالات، أو التحقق من الإعدادات، أو تفريغ ذاكرة التخزين المؤقت قبل الإيقاف.

المراحل الأربع في لمحة

  1. الإنشاء (Instantiation) — يُنشئ الحاوي الكائن الخام باستخدام الدالة البانية (أو factory method).
  2. تعبئة التبعيات (Dependency Population) — يحقن Spring جميع التبعيات: وسائط الدالة البانية، واستدعاءات الـ setter، وحقن الحقل عبر @Autowired.
  3. التهيئة (Initialisation) — بعد تعبئة جميع التبعيات، تُشغَّل استدعاءات التهيئة. يمكن لكودك الآن استخدام المتعاونين المحقونين بالكامل.
  4. الإتلاف (Destruction) — عند إغلاق السياق (إيقاف التطبيق، تفكيك بيئة الاختبار، إلخ)، تُحرِّر استدعاءات الإتلاف الموارد.
لماذا يهمّ الترتيب: تعمل استدعاءات التهيئة بعد الحقن، وليس أثناء الإنشاء. إن حاولتَ استخدام حقل محقون داخل الدالة البانية فستكون قيمته null لأن Spring لم تضبطه بعد.

المرحلة الأولى — الإنشاء

ينشئ Spring نسخة الـ bean باستخدام الانعكاس (Reflection). بالنسبة لـ bean مفردة (singleton — الوضع الافتراضي) يحدث هذا مرةً واحدة عند بدء سياق التطبيق. أما بالنسبة لـ bean من نوع prototype فيحدث في كل مرة يُطلَب فيها. في هذه اللحظة يكون الكائن مجرد كائن Java عادي — لم تُضبط له أي تبعيات.

@Component public class ReportService { private final DataSource dataSource; // ينادي Spring هذه الدالة البانية أولًا (المرحلتان 1 و2 مدمجتان لحقن الدالة البانية) public ReportService(DataSource dataSource) { this.dataSource = dataSource; } }

يدمج حقن الدالة البانية المرحلتين 1 و2 في خطوة واحدة، وهو أحد أسباب تفضيله: لن يظهر الكائن أبدًا في حالة تهيئة جزئية.

المرحلة الثانية — تعبئة التبعيات

بعد الإنشاء (في حالة حقن الـ setter أو الحقل)، يفحص Spring تعريف الـ bean ويحلّ كل تبعية من الحاوي، ثم يستدعي الـ setter المناسب أو يكتب مباشرةً في الحقل. هنا تُعالَج @Autowired و@Value و@Inject.

@Component public class NotificationService { // يُعبَّأ بواسطة Spring بعد الإنشاء (المرحلة الثانية) @Autowired private MailSender mailSender; @Value("${notifications.max-retries:3}") private int maxRetries; }

المرحلة الثالثة — التهيئة

بمجرد حقن جميع التبعيات، يستدعي Spring استدعاءات التهيئة بهذا الترتيب:

  1. التوابع الموسومة بـ @PostConstruct (الموصى بها)
  2. التابع afterPropertiesSet() إن كان الـ bean ينفّذ InitializingBean
  3. تابع init-method مخصص مُعلَن في @Bean(initMethod = "...")

@PostConstruct هو الخيار الأنظف: إنه توصيف Java معياري (jakarta.annotation.PostConstruct) مستقل عن Spring، مما يبقي الفئة محمولة وقابلة للاختبار.

import jakarta.annotation.PostConstruct; import org.springframework.stereotype.Component; @Component public class CacheWarmer { private final ProductRepository productRepository; private Map<Long, Product> cache; public CacheWarmer(ProductRepository productRepository) { this.productRepository = productRepository; } @PostConstruct public void warmUp() { // آمن استخدام productRepository هنا — الحقن مكتمل cache = productRepository.findAll() .stream() .collect(Collectors.toMap(Product::getId, p -> p)); System.out.println("Cache warmed: " + cache.size() + " products loaded."); } }
استخدم @PostConstruct من أجل: فتح الموارد (مجمّعات الخيوط ومجمّعات الاتصالات)، وتحميل الإعدادات في الذاكرة، والتحقق من ربط التبعيات المطلوبة بصورة صحيحة، أو تعبئة ذاكرات التخزين المؤقت. احرص على سرعة التنفيذ — فـ @PostConstruct البطيء يؤخّر بدء تشغيل السياق.

المرحلة الرابعة — الإتلاف

عند إغلاق ApplicationContext الخاص بـ Spring — عند إيقاف JVM، أو عند استدعاء context.close() في اختبار، أو عند رفع تطبيق ويب — تعمل استدعاءات الإتلاف لجميع beans المفردة بترتيب عكسي للتسجيل:

  1. التوابع الموسومة بـ @PreDestroy (الموصى بها)
  2. التابع destroy() إن كان الـ bean ينفّذ DisposableBean
  3. تابع destroy-method مخصص مُعلَن في @Bean(destroyMethod = "...")
import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import org.springframework.stereotype.Component; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @Component public class AsyncTaskRunner { private ExecutorService executor; @PostConstruct public void start() { executor = Executors.newFixedThreadPool(4); System.out.println("AsyncTaskRunner started."); } @PreDestroy public void stop() throws InterruptedException { System.out.println("Shutting down AsyncTaskRunner..."); executor.shutdown(); if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { executor.shutdownNow(); } } }
لا يُتلِف Spring الـ beans من نوع Prototype. ينشئ الحاوي هذه الـ beans ويحقن تبعياتها، لكنه لا يحتفظ بمرجع إليها بعد تسليمها. لن يُستدعى @PreDestroy أبدًا على bean من نوع prototype. إن كانت الـ bean من نوع prototype تحتجز موارد، فعليك إدارة دورة حياتها يدويًا.

استخدام توابع Init وDestroy مع @Bean

عند دمج فئة خارجية لا تملك صلاحية تعديل مصدرها، استخدم السمتين initMethod وdestroyMethod على @Bean:

import com.zaxxer.hikari.HikariDataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class InfrastructureConfig { @Bean(initMethod = "getConnection", destroyMethod = "close") public HikariDataSource dataSource() { HikariDataSource ds = new HikariDataSource(); ds.setJdbcUrl("jdbc:postgresql://localhost:5432/shop"); ds.setUsername(System.getenv("DB_USER")); ds.setPassword(System.getenv("DB_PASS")); ds.setMaximumPoolSize(10); return ds; } }
اكتشاف تابع الإتلاف تلقائيًا: يستنتج Spring تلقائيًا close() أو shutdown() بوصفه تابع الإتلاف للـ beans المُعادة من توابع @Bean، لذا كثيرًا ما لا تحتاج إلى تحديد destroyMethod صراحةً. يمكنك تعطيل هذا السلوك بضبط destroyMethod = "".

دورة الحياة الكاملة — مثال شامل واحد

تتعمّد الفئة التالية تمرين جميع المراحل الأربع حتى تتمكن من مراقبة الترتيب في سجلاتك:

import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.DisposableBean; import org.springframework.stereotype.Component; @Component public class LifecycleDemoBean implements InitializingBean, DisposableBean { public LifecycleDemoBean() { System.out.println("[1] Constructor called — raw object created"); } // الحقول/الـ setters الموسومة بـ @Autowired تُعبَّأ هنا (المرحلة الثانية) @PostConstruct public void postConstruct() { System.out.println("[3a] @PostConstruct — first init callback"); } @Override public void afterPropertiesSet() { System.out.println("[3b] afterPropertiesSet() — second init callback"); } @PreDestroy public void preDestroy() { System.out.println("[4a] @PreDestroy — first destroy callback"); } @Override public void destroy() { System.out.println("[4b] destroy() — second destroy callback"); } }

الخرج المتوقع في السجل عند بدء التشغيل واستدعاء context.close():

[1] Constructor called — raw object created [3a] @PostConstruct — first init callback [3b] afterPropertiesSet() — second init callback [4a] @PreDestroy — first destroy callback [4b] destroy() — second destroy callback

الخلاصة

تمرّ دورة حياة الـ bean في Spring بأربع مراحل متمايزة: الإنشاء (الدالة البانية)، وتعبئة التبعيات (الحقن)، والتهيئة (@PostConstructafterPropertiesSet() ← init مخصص)، والإتلاف (@PreDestroydestroy() ← destroy مخصص). عمليًا، الجأ إلى @PostConstruct و@PreDestroy — فهما يُبقيان الـ beans محمولة وسهلة القراءة. استخدم @Bean(initMethod, destroyMethod) عند دمج فئات خارجية. وتذكّر أن الـ beans من نوع prototype لا تشملها إدارة Spring للإتلاف إطلاقًا.