أساسيات Spring Boot

دورة حياة التطبيق والـ Runners

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

دورة حياة التطبيق والـ Runners

يمرّ كل تطبيق Spring Boot بدورة حياة محدّدة بدقة منذ اللحظة التي يُستدعى فيها SpringApplication.run() حتى خروج JVM. إنّ فهم هذه الدورة يُمكّنك من الربط بالمرحلة الصحيحة تمامًا — سواء أردت تشغيل فحص قاعدة بيانات قبل أول طلب، أو تهيئة بيانات أوّلية، أو تنفيذ إيقاف تشغيل نظيف. يتناول هذا الدرس تسلسل الإقلاع الداخلي وواجهتَي runner — CommandLineRunner وApplicationRunner — اللتان تُعدّان الطريقة الموصى بها لتنفيذ الكود بعد اكتمال تهيئة سياق التطبيق بالكامل.

ما الذي يحدث داخل SpringApplication.run()؟

عندما تستدعي main التابع SpringApplication.run(MyApp.class, args)، يجري التسلسل التالي (بصورة مبسّطة):

  1. مرحلة الإقلاع: يُحمّل Spring الـ SpringApplicationRunListeners ويُطلق حدث starting().
  2. تحضير البيئة: يتمّ دمج application.properties / .yml ومتغيرات البيئة وسيطات سطر الأوامر في كائن Environment واحد.
  3. إنشاء السياق: يُنشأ النوع المناسب من ApplicationContext (AnnotationConfigServletWebServerApplicationContext لتطبيق الويب، وAnnotationConfigApplicationContext لغيره).
  4. تحميل تعريفات الـ Bean: تُعالَج فئات @Configuration وتُسجَّل جميع تعريفات الـ Bean.
  5. تحديث السياق: يُنشأ كل bean وتُحقن التبعيات وتُنفَّذ الدوال المُعلَّمة بـ @PostConstruct ويُشغَّل الخادم المُدمج.
  6. تنفيذ الـ Runners: تُستدعى جميع الـ CommandLineRunner وApplicationRunner beans بالترتيب.
  7. الجاهزية: يُنشر الحدث ApplicationReadyEvent ويبدأ التطبيق بمعالجة الطلبات.
الفكرة الأساسية: تُنفَّذ الـ Runners بعد أن يكون الخادم المُدمج قد انطلق فعلًا والسياق قد اكتمل تحديثه. هذا يعني أنّك تستطيع بأمان حقن أي bean أو فتح اتصالات قاعدة بيانات أو استدعاء REST endpoints داخل runner. أنت لست في المسار الحرج للإقلاع — التطبيق بات حيًّا.

CommandLineRunner

CommandLineRunner واجهة وظيفية ذات تابع واحد. يستقبل تابعها run(String... args) سيطات سطر الأوامر الخام تمامًا كما مُرّرت إلى main().

package com.example.demo; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component public class DataSeedRunner implements CommandLineRunner { private final UserRepository userRepository; public DataSeedRunner(UserRepository userRepository) { this.userRepository = userRepository; } @Override public void run(String... args) throws Exception { if (userRepository.count() == 0) { userRepository.save(new User("admin", "admin@example.com")); System.out.println("Seeded admin user."); } } }

نظرًا لكونه @Component عاديًا فهو يشارك في حقن التبعيات كأي bean آخر. يستقبل المُنشئ UserRepository — دون الحاجة إلى حقن حقل.

ApplicationRunner

ApplicationRunner شبه مطابق تقريبًا لكنه يستقبل كائن ApplicationArguments بدلًا من مصفوفة String[] خام. هذا أكثر ملاءمةً عندما تُمرّر سيطات منظَّمة من سطر الأوامر.

package com.example.demo; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; @Component public class StartupCheckRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { // تحقق ما إذا كانت --dry-run قد مُرّرت: java -jar app.jar --dry-run boolean dryRun = args.containsOption("dry-run"); // الوصول إلى السيطات غير الخيارية: java -jar app.jar migrate for (String nonOption : args.getNonOptionArgs()) { System.out.println("Non-option arg: " + nonOption); } // الحصول على قيمة --env=prod if (args.containsOption("env")) { String env = args.getOptionValues("env").get(0); System.out.println("Running in environment: " + env); } System.out.println("Startup check complete. Dry-run=" + dryRun); } }

يُميّز ApplicationArguments بين السيطات الخيارية (المسبوقة بـ -- كـ --env=prod) والسيطات غير الخيارية (السلاسل النصية العادية كـ migrate). يُزيل هذا الحاجة إلى تحليل String[] يدويًا.

التحكم في ترتيب التنفيذ

عند وجود عدة Runners، يُنفّذها Spring Boot وفق الترتيب الذي تُحدّده التعليقة التوضيحية @Order (القيمة الأدنى = الأولوية الأعلى) أو الواجهة Ordered.

import org.springframework.core.annotation.Order; @Component @Order(1) public class SchemaCheckRunner implements CommandLineRunner { @Override public void run(String... args) { System.out.println("Step 1: verify DB schema"); } } @Component @Order(2) public class DataSeedRunner implements CommandLineRunner { @Override public void run(String... args) { System.out.println("Step 2: seed reference data"); } }
أفضل ممارسة: استخدم @Order لجعل التبعية صريحةً في الكود بدلًا من الاعتماد على ترتيب تسجيل الـ Bean، الذي لا يكون مضمونًا ويتغير بصمت مع نمو قاعدة الكود.

الـ Runners مقابل @PostConstruct مقابل ApplicationListener

يوفّر Spring عدة خطاطيف. إليك متى تستخدم كل منها:

  • @PostConstruct — يُنفَّذ مباشرةً بعد بناء bean واحدة، قبل اكتمال تهيئة الـ beans الأخرى التي تعتمد عليه. استخدمه للإعداد المحلي لـ bean (تهيئة ذاكرة تخزين مؤقتة داخلية، التحقق من خاصية تهيئة). لا تستخدمه لأي شيء يتطلب تشغيلًا كاملًا لـ beans أخرى كبدء خيط خلفية يلمس قاعدة البيانات.
  • CommandLineRunner / ApplicationRunner — يُنفَّذان بعد اكتمال تحديث السياق بالكامل وانطلاق الخادم. استخدمهما لمهام الإقلاع التي تلمس beans متعددة أو العالم الخارجي: ترحيل قواعد البيانات، تسخين الكاش، فحوصات الصحة، أوامر نمط CLI.
  • ApplicationListener<ApplicationReadyEvent> — يُطلق في نفس نقطة الـ Runners لكن عبر نظام الأحداث. مفيد عندما يقع منطق الإقلاع في طبقة بنية تحتية لا ينبغي لها معرفة واجهات Runner. الـ Runners أبسط بوجه عام وهي المُفضَّلة لكود طبقة التطبيق.

نمط عملي: تهيئة البيانات المشروطة

نمط شائع في الواقع هو تهيئة البيانات فقط عند تفعيل Spring profile معيّن، لتجنّب الإدراج العرضي للبيانات في بيئة الإنتاج:

import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; @Component @Profile("dev") // لا يُنفَّذ إلا عند تفعيل profile اسمه 'dev' public class DevDataSeeder implements CommandLineRunner { private final ProductRepository productRepository; public DevDataSeeder(ProductRepository productRepository) { this.productRepository = productRepository; } @Override public void run(String... args) { productRepository.saveAll(SampleData.products()); System.out.println("Dev data seeded."); } }
انتبه للاستثناءات داخل الـ Runners. إذا رمى runner استثناءً غير محدود (unchecked)، فإنّ Spring Boot يلتقطه ويُسجّل الخطأ ويستدعي System.exit(1). هذا ما تريده عادةً لفحص إقلاع حرج، لكنه مُفاجئ إذا كان الاستثناء عرضيًا. عالج الأخطاء المتوقعة بشكل صريح ودع الحالات غير القابلة للاسترداد فقط تنتشر.

اختبار الـ Runners

يمكنك استبعاد الـ Runners من اختبارات معيّنة باستبعاد فئة الـ bean أو استخدام @SpringBootTest مع مسح مكوّنات انتقائي. أو اختبر الـ runner مباشرةً بإنشائه مع تبعيات وهمية (mock):

import org.junit.jupiter.api.Test; import static org.mockito.Mockito.*; class DataSeedRunnerTest { @Test void seedsAdminWhenEmpty() throws Exception { UserRepository repo = mock(UserRepository.class); when(repo.count()).thenReturn(0L); DataSeedRunner runner = new DataSeedRunner(repo); runner.run(); // لا حاجة لسيطات لهذا المنطق verify(repo, times(1)).save(any(User.class)); } }

الخلاصة

تسلسل إقلاع Spring Boot محدّد ومُنظَّم. CommandLineRunner هو الواجهة المثلى عندما تحتاج فقط إلى تشغيل كود بعد الإقلاع ولا تحتاج إلى سيطات سطر أوامر منظَّمة. ApplicationRunner هو الاختيار الأفضل عندما تريد تحليل سيطات بأسلوب --option=value بنظافة. كلاهما beans Spring عادية: احقن ما تحتاجه، وعلّم بـ @Order عندما يهمّ الترتيب، وقيّد بـ @Profile لحماية بيئة الإنتاج. في الدرس القادم ستتناول نظام التسجيل المُدمج في Spring Boot وكيفية ضبطه دون لمس ملف logback.xml.