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

خطافات دورة حياة الحبة

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

خطافات دورة حياة الحبة

يُدير Spring كل حبة (bean) منذ لحظة إنشائها حتى لحظة إيقاف تشغيل سياق التطبيق. على امتداد هذه الرحلة يوفر الحاوي نقطتي ربط محددتين تنفيذٍ يمكن لكودك من خلالهما أن يعمل: فور اكتمال البناء وحقن التبعيات (ما بعد البناء)، وقُبيل إزالة الحبة من السياق (ما قبل التدمير). إنّ معرفة كيفية استخدام هذه الخطافات بشكل صحيح — وأي واجهة برمجية تختار — أمرٌ ضروري لكتابة حبات تبدأ بشكل موثوق وتُنظّف أثرها عند الانتهاء.

لماذا تهمّ خطافات دورة الحياة

تخيّل حبةً تُغلّف تجمّع اتصالات بقاعدة البيانات، أو تفتح مقبضَ ملف، أو يجب أن تُسجّل نفسها مع خدمة خارجية عند الإطلاق. لا يصحّ تنفيذ أيٍّ من ذلك داخل المُنشئ (constructor) — ففي تلك اللحظة لم يحقن Spring تبعيات الحبة بعد. يجب أن يظل المُنشئ خفيف الوزن وخاليًا من الآثار الجانبية. يمنحك @PostConstruct لحظةً آمنةً مضمونةً لتشغيل التهيئة بعد توصيل جميع التبعيات.

الحاجة المقابلة موجودة عند الإغلاق: يجب استنزاف تجمّعات الاتصالات، وإغلاق المقابس، وإلغاء تسجيل التسجيلات الخارجية. يمنحك @PreDestroy لحظةً يمكن التنبؤ بها للقيام بكل ذلك قبل خروج JVM.

@PostConstruct — التهيئة بعد التوصيل

@PostConstruct هو تعليق توضيحي من Jakarta EE (في حزمة jakarta.annotation) تدعمه Spring دون إعداد إضافي. يُوضع على أي دالة عامة أو محمية أو على مستوى الحزمة، لا تأخذ وسيطات وتُعيد void. تستدعي Spring تلك الدالة مرة واحدة فورًا بعد الانتهاء من حقن جميع التبعيات.

import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class ReportScheduler { @Value("${reports.outputDir}") private String outputDir; private java.nio.file.Path resolvedPath; @PostConstruct public void init() { // آمن: تم حقن @Value قبل تنفيذ هذه الدالة resolvedPath = java.nio.file.Path.of(outputDir).toAbsolutePath().normalize(); if (!java.nio.file.Files.exists(resolvedPath)) { try { java.nio.file.Files.createDirectories(resolvedPath); } catch (java.io.IOException e) { throw new IllegalStateException("Cannot create output dir: " + resolvedPath, e); } } System.out.println("ReportScheduler ready. Output: " + resolvedPath); } }
لماذا لا يُنفَّذ هذا داخل المُنشئ؟ إذا حاولتَ قراءة outputDir داخل المُنشئ ستجدها null — تحقن Spring قيم الحقول بعد انتهاء المُنشئ. @PostConstruct هي أبكر لحظة آمنة لاستخدام أي قيمة محقونة.

@PreDestroy — التنظيف قبل الإيقاف

تُعلّم @PreDestroy، من الحزمة jakarta.annotation ذاتها، دالةً ستستدعيها Spring قُبيل تدمير الحبة — عادةً أثناء ApplicationContext.close() أو عند إغلاق JVM إذا سجّل السياق خطافَ إغلاق. تنطبق نفس قواعد التوقيع: لا وسيطات، ونوع إعادة void.

import jakarta.annotation.PreDestroy; import org.springframework.stereotype.Component; @Component public class ConnectionManager { private java.sql.Connection connection; @PostConstruct public void openConnection() throws java.sql.SQLException { connection = java.sql.DriverManager.getConnection( "jdbc:h2:mem:demo", "sa", ""); System.out.println("Connection opened"); } @PreDestroy public void closeConnection() { try { if (connection != null && !connection.isClosed()) { connection.close(); System.out.println("Connection closed cleanly"); } } catch (java.sql.SQLException e) { System.err.println("Error closing connection: " + e.getMessage()); } } }
لا تُستدعى @PreDestroy على حبات النطاق النموذجي (prototype). لا تتتبع Spring نُسَخ النموذجي بعد تسليمها، لذا ليس بمقدورها استدعاء دوال التدمير عليها. إذا كنت بحاجة إلى تنظيف لحبة نموذجية فيجب عليك إدارة دورة حياتها يدويًا أو استخدام نمط الغلاف (wrapper pattern).

InitializingBean و DisposableBean — أسلوب الواجهة

قبل أن تصبح @PostConstruct و@PreDestroy سائدتين، كانت Spring توفر نقطتَي الربط ذاتيهما عبر واجهتين في org.springframework.beans.factory:

  • InitializingBean — تُعلن عن afterPropertiesSet() التي تُستدعى بعد اكتمال الحقن.
  • DisposableBean — تُعلن عن destroy() التي تُستدعى قبل تدمير الحبة.
import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Service; @Service public class CacheService implements InitializingBean, DisposableBean { private java.util.Map<String, Object> store; @Override public void afterPropertiesSet() { store = new java.util.concurrent.ConcurrentHashMap<>(); System.out.println("CacheService: in-memory store initialised"); } @Override public void destroy() { store.clear(); System.out.println("CacheService: store cleared on shutdown"); } public void put(String key, Object value) { store.put(key, value); } public Object get(String key) { return store.get(key); } }

التعليق التوضيحي أم الواجهة: أيهما تختار؟

الآليتان متكافئتان وظيفيًا عند استخدامهما على حبات مفردة (singleton). يكمن الفرق العملي في مستوى الترابط:

  • افضّل @PostConstruct / @PreDestroy في كل مشروع جديد تقريبًا. هذه تعليقات توضيحية قياسية من Jakarta EE — تُترجم حبتك وتعمل حتى دون Spring في مسار الفئات، مما يُسهّل اختبارها بمعزل.
  • استخدم الواجهات فقط عند كتابة كود البنية التحتية على مستوى الإطار الذي يستهدف Spring API عمدًا، أو عند صيانة كود قديم يستخدمهما بالفعل.
تدعم Spring أيضًا الإعدادات الافتراضية على مستوى XML (default-init-method) وخصائص init-method / destroy-method لكل حبة في تعريفات @Bean. لدالة @Bean يمكنك كتابة @Bean(initMethod = "start", destroyMethod = "stop") — مفيد عندما لا تملك الفئة ولا يمكنك إضافة تعليقات توضيحية إليها (مثل مكتبة طرف ثالث).

ترتيب التنفيذ الدقيق

عندما تكون الآليات الثلاث موجودة على نفس الحبة تستدعيها Spring بترتيب محدد:

  1. المُنشئ (Constructor)
  2. حقن التبعيات (الحقول والمُعيِّنات)
  3. دالة @PostConstruct
  4. InitializingBean.afterPropertiesSet()
  5. دالة @Bean(initMethod = ...)

عند الإغلاق ينعكس الترتيب لخطافات التدمير:

  1. دالة @PreDestroy
  2. DisposableBean.destroy()
  3. دالة @Bean(destroyMethod = ...)

نادرًا ما تخلط الآليات على حبة واحدة في الممارسة، لكن فهم الترتيب يُساعد عند وراثة فئة أساسية تُطبّق InitializingBean بالفعل وتريد إضافة @PostConstruct في فئة فرعية.

تسجيل خطاف الإغلاق

في تطبيقات Spring المستقلة (غير العاملة في حاوي servlet) يجب عليك تسجيل خطاف إغلاق JVM صراحةً حتى تُشغَّل استدعاءات @PreDestroy عند خروج العملية. أبسط طريقة هي استخدام ConfigurableApplicationContext:

import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class App { public static void main(String[] args) { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); ctx.registerShutdownHook(); // يضمن تشغيل @PreDestroy عند خروج JVM // ... منطق التطبيق ... } }

تفعل Spring Boot ذلك تلقائيًا. في سياق Spring العادي أنت مسؤول عن استدعاء registerShutdownHook() أو استدعاء ctx.close() صراحةً.

نمط عملي: تدفئة ذاكرة التخزين المؤقت

تتمثّل إحدى حالات الاستخدام الكلاسيكية لـ @PostConstruct في التحميل المسبق للبيانات كي لا يتحمّل الطلب الأول تكلفة الإطلاق البارد:

import jakarta.annotation.PostConstruct; import org.springframework.stereotype.Service; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @Service public class ProductCatalog { private final ProductRepository repo; private final ConcurrentHashMap<Long, Product> cache = new ConcurrentHashMap<>(); public ProductCatalog(ProductRepository repo) { this.repo = repo; } @PostConstruct void warmUp() { List<Product> featured = repo.findFeatured(); featured.forEach(p -> cache.put(p.getId(), p)); System.out.printf("Warmed up %d featured products%n", featured.size()); } public Product find(long id) { return cache.computeIfAbsent(id, repo::findById); } }

الخلاصة

@PostConstruct و@PreDestroy هما خطافا دورة الحياة القياسيان والمحمولان لحبات Spring. يمنحانك وصولًا آمنًا موقوتًا بدقة لتشغيل التهيئة بعد الحقن والتنظيف قبل التدمير. واجهتا InitializingBean / DisposableBean سابقتان للتعليقات التوضيحية ولا تزالان صالحتين لكنهما تربطان حبتك بـ Spring API. للفئات التابعة لطرف ثالث التي لا يمكنك توصيف تعليقاتها، استخدم خصائص initMethod / destroyMethod الخاصة بـ @Bean. دائمًا سجّل خطاف الإغلاق في التطبيقات المستقلة. في الدرس القادم ستكتشف كيف يُتيح لك التهيئة الكسولة (lazy initialization) تأجيل هذا التسلسل الكامل حتى تُحتاج الحبة فعلًا.