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

التهيئة الكسولة

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

التهيئة الكسولة

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

لكنّ "المتحمّسة افتراضيًا" لا تعني "المتحمّسة دائمًا". بعض الحبوب تكون مكلفة الإنشاء، أو تتصل بموارد خارجية، أو تمثّل مسارات اختيارية حقيقية لا تُستخدَم فعلًا في كثير من مثيلات التطبيق. لهذه الحالات تقدّم Spring التهيئة الكسولة (lazy initialization): لا تُنشأ الحبّة حتى يطلبها شيء ما لأول مرة. يتناول هذا الدرس متى يصحّ هذا المقايضة، وكيف تطبّقها، والمفاجآت التي يجب توقّعها.

المتحمّسة مقابل الكسولة — الفرق الجوهري

فكّر في الجدول الزمني لكلّ منهما:

  • المتحمّسة: تحديث ApplicationContext ← إنشاء جميع الحبوب الأحادية وحقن التبعيات وتشغيل توابع @PostConstruct ← التطبيق جاهز.
  • الكسولة: تحديث ApplicationContext ← تسجيل تعريف الحبّة دون إنشاء مثيل ← عند أول طلب للحبّة ← يحدث الإنشاء على خيط ذلك الطلب.

يظلّ الحاوي يتحقّق من صحة تعريف الحبّة (وجود الفئة وعدم الغموض في المُنشئ) عند بدء التشغيل. ما يُؤجَّل هو عملية new الفعلية وتوصيل التبعيات واستدعاءات دورة الحياة.

تعريف حبّة كسولة باستخدام @Lazy

ضع التعليق التوضيحي @Lazy على أي @Component أو @Service أو @Repository أو تابع @Bean ليُعامله Spring باعتباره كسولًا:

import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @Service @Lazy public class ReportGeneratorService { public ReportGeneratorService() { // تخيّل أن هذا يفتح اتصالًا بقاعدة بيانات التقارير System.out.println("تهيئة ReportGeneratorService"); } public byte[] generatePdf(long reportId) { // ... عمل مكلف ... return new byte[0]; } }

بوجود @Lazy، لن تظهر رسالة المُنشئ أثناء بدء تشغيل السياق. تظهر فقط حين تستدعي حبّة أخرى أو طلب ما تابعًا من ReportGeneratorService لأول مرة.

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

@Lazy على نقطة الحقن

إذا أقدمت حبّة متحمّسة على حقن حبّة كسولة، فعليك تعليم نقطة الحقن بـ @Lazy أيضًا — وإلا أنشأ Spring الحبّة الكسولة عند بدء التشغيل لإرضاء التبعية المتحمّسة:

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @Service public class AdminDashboardService { private final ReportGeneratorService reportGenerator; @Autowired public AdminDashboardService(@Lazy ReportGeneratorService reportGenerator) { this.reportGenerator = reportGenerator; // يستقبل وكيلًا لا الحبّة الحقيقية } public byte[] downloadReport(long id) { return reportGenerator.generatePdf(id); // تُنشأ الحبّة الحقيقية هنا عند أول استدعاء } }

الوكيل الذي يُدرجه Spring شفّاف تمامًا: تستدعي توابعه بشكل طبيعي، وتعمل فحوصات المساواة، ويشارك بشكل صحيح في المعاملات وتوجيهات AOP.

التهيئة الكسولة العالمية

أضاف Spring Boot 2.2+ علامةً عالمية بسطر واحد:

# application.properties spring.main.lazy-initialization=true

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

استخدم التهيئة الكسولة العالمية أثناء التطوير لا في الإنتاج. إن فعّلتها عالميًا، ضع @Lazy(false) على حبوب البنية التحتية الحرجة (أغلفة مصدر البيانات، إعداد الأمان، مؤشرات الصحة) لإجبارها على التهيئة المتحمّسة والحفاظ على التحقق عند بدء التشغيل للمسارات الهامة.

متى يكون @Lazy مناسبًا فعلًا

التهيئة الكسولة ليست حيلة أداء تُطبّقها في كل مكان. بل هي الأداة الصحيحة في حالات بعينها:

  • الموارد الاختيارية المكلفة: خدمة تفتح اتصالًا بنظام قديم يستخدمه فقط وحدة تقارير المشرف. كثير من مثيلات التطبيق في مجموعة قابلة للتوسيع الأفقي قد لا تستخدمه أبدًا.
  • سرعة التطوير والاختبار: الوضع الكسول العالمي يُقلّص وقت بدء التشغيل أثناء التطوير التكراري. احتفظ بالملف الشخصي للإنتاج متحمّسًا.
  • حلّ التبعية الدائرية (الملاذ الأخير): كسر التبعية الدائرية بـ @Lazy يمكن أن يُؤجّل الإنشاء بما يكفي لحلّ مشكلة الدجاجة والبيضة. غير أنّ هذا مؤشر على مشكلة في التصميم — يُفضَّل إعادة هيكلة الكود للقضاء على الدورة.
  • التكاملات الاختيارية: حبّة تُغلّف SDK طرف ثالث قد لا يكون مُعدًّا في جميع بيئات النشر. اقرنها بـ @ConditionalOnProperty للتحكم الأنظف.

متى لا تستخدم @Lazy

تجنّب التهيئة الكسولة للحبوب التي يجب التحقق منها عند بدء التشغيل. تجمّعات اتصال قواعد البيانات، وفلاتر الأمان، ومستهلكو Kafka، وتسجيلات المهام المجدولة يجب أن تكون متحمّسة دائمًا. تأجيل إنشائها يُخفي سوء الإعداد ويُؤخّر الأعطال إلى وقت التشغيل — ربما تحت ضغط حركة المرور الإنتاجية.

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

الحبوب النموذجية (Prototype) والتهيئة الكسولة

الحبوب ذات النطاق النموذجي كسولة بطبيعتها — فالحاوي لا يُنشئها مسبقًا أبدًا لعدم وجود مثيل واحد يُنشئه مسبقًا. إضافة @Lazy إلى حبّة نموذجية لا يُضيف أثرًا. السلوك الذي تراه عمليًا:

import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; @Component @Scope("prototype") // كسولة بالفعل — @Lazy ستكون مكرّرة هنا public class ReportRequest { // يُنشأ مثيل جديد عند كل استدعاء getBean() }

التحقق من السلوك الكسول

أثناء التطوير يمكنك التحقق بسرعة من كون الحبّة كسولة فعلًا بمراقبة سجلّ بدء التشغيل. مع مستوى DEBUG للتسجيل على org.springframework، يُسجّل Spring إنشاء كل حبّة. سيظهر سطر سجلّ إنشاء الحبّة الكسولة بعد سجلّ تحديث السياق، مرتبطًا بأول استدعاء تابع لا ببدء التشغيل. فحص سريع للتحقق:

import org.springframework.boot.CommandLineRunner; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; @Component public class StartupProbe implements CommandLineRunner { private final ApplicationContext ctx; public StartupProbe(ApplicationContext ctx) { this.ctx = ctx; } @Override public void run(String... args) { boolean exists = ctx.containsBean("reportGeneratorService"); boolean initialised = ctx.containsSingleton("reportGeneratorService"); System.out.println("التعريف مسجَّل: " + exists); // true System.out.println("المثيل مُنشأ: " + initialised); // false (إذا لا تزال كسولة) } }

containsBean يتحقق من وجود تعريف مسجَّل؛ أما containsSingleton فيتحقق من إنشاء مثيل أحادي. الحبّة الكسولة تُظهر true / false حتى تُستخدم لأول مرة.

الخلاصة

التهيئة المتحمّسة تكشف المشكلات عند بدء التشغيل وهي الافتراض الصحيح لكل شيء تقريبًا. استخدم @Lazy بانتقائية — على الحبوب المكلفة أو الاختيارية حقًا أو التي تُحتاج فقط في مسارات كود بعينها. حين تحقن حبّة كسولة في حبّة متحمّسة، علّم نقطة الحقن بـ @Lazy أيضًا كي يُدرج Spring وكيلًا بدلًا من إجبار الإنشاء المبكر. العلامة العالمية spring.main.lazy-initialization=true مساعدة للإنتاجية في التطوير المحلي، لا تحسينًا للإنتاج. احتفظ بحبوب البنية التحتية الحرجة متحمّسةً لكي يظهر سوء الإعداد عند بدء التشغيل لا أمام المستخدمين.