إعداد Spring والملفّات الشخصية

الـ Beans الشرطية (@Conditional)

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

الـ Beans الشرطية (@Conditional)

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

التجريد الجوهري: Condition و @Conditional

كل فحص شرطي للـ bean مبني على واجهة واحدة: org.springframework.context.annotation.Condition، التي تحتوي على تابع وحيد:

@FunctionalInterface public interface Condition { boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }

يمنحك ConditionContext وصولًا إلى BeanDefinitionRegistry وConfigurableListableBeanFactory وEnvironment وResourceLoader وClassLoader. هذا يعني أن الشرط المخصص يستطيع فحص أي شيء يعرفه Spring وقت بدء التشغيل.

تربط الشرط بأي تابع @Bean أو فئة @Configuration باستخدام @Conditional(YourCondition.class). إذا أعاد matches() القيمة false، يُتجاهل تعريف الـ bean صامتًا — لا خطأ ولا عنصر بديل.

التقييم يسبق الإنشاء. تُفحص الشروط خلال مرحلة تعريف الـ beans، لا عند أول طلب للـ bean. هذا يعني أن الشرط الذي يُعيد false يزيل الـ bean من السجل كليًا — لا وكيل (proxy)، ولا كائن كسول (lazy stub).

الشروط الجاهزة في Spring Boot

يأتي Spring Boot مزوّدًا بمجموعة غنية من الشروط الجاهزة في الحزمة org.springframework.boot.autoconfigure.condition، وهي اللبنات الأساسية لكل مبدئ ضبط تلقائي:

  • @ConditionalOnProperty — يسجّل الـ bean فقط عندما تمتلك خاصية معينة قيمة محددة.
  • @ConditionalOnClass / @ConditionalOnMissingClass — بناءً على وجود أو غياب فئة على مسار الفئات.
  • @ConditionalOnBean / @ConditionalOnMissingBean — بناءً على ما إذا كان bean آخر مسجّلًا بالفعل أم لا.
  • @ConditionalOnExpression — يُقيَّم مقابل تعبير SpEL.
  • @ConditionalOnWebApplication / @ConditionalOnNotWebApplication — بناءً على نوع التطبيق.
  • @ConditionalOnCloudPlatform — يطابق منصة نشر بعينها كـ Kubernetes أو Cloud Foundry.

مثال عملي: Bean مرتبط بعلامة ميزة

افترض أن خدمة الإشعارات ينبغي أن ترسل بريدًا إلكترونيًا حقيقيًا في الإنتاج، بينما تكتفي بالكتابة إلى الـ console في بيئة التطوير المحلية. استخدم @ConditionalOnProperty للتبديل بين التنفيذين بناءً على علامة:

# application.properties notifications.email.enabled=true
package com.example.notifications; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class NotificationConfig { @Bean @ConditionalOnProperty(name = "notifications.email.enabled", havingValue = "true") public NotificationService smtpNotificationService() { return new SmtpNotificationService(); } // matchIfMissing = true يعني أن الـ bean يُسجَّل حين تغيب الخاصية @Bean @ConditionalOnProperty( name = "notifications.email.enabled", havingValue = "true", matchIfMissing = false ) public NotificationService consoleNotificationService() { return new ConsoleNotificationService(); } }
استخدم matchIfMissing = true على الـ bean الاحتياطي. اجمعه مع @ConditionalOnMissingBean لضمان تسجيل تنفيذ واحد بالضبط حتى حين تغيب الخاصية. يظهر هذا النمط في كل أنحاء ضبط Spring Boot التلقائي.

@ConditionalOnMissingBean: نمط القابلية للتوسعة

أقوى نمط في ضبط Spring Boot التلقائي هو التراجع: قدّم إعدادًا افتراضيًا معقولًا، لكن دع مطوّر التطبيق يتجاوزه ببساطة بتعريف bean خاص به من النوع ذاته.

package com.example.cache; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class CacheAutoConfiguration { // يُسجَّل هذا الـ bean فقط إذا لم يُعرّف التطبيق CacheManager من قبل @Bean @ConditionalOnMissingBean(CacheManager.class) public CacheManager defaultCacheManager() { return new InMemoryCacheManager(); } }

إذا أعلن التطبيق عن CacheManager خاص به — مثل واحد مدعوم بـ Redis — يتراجع الضبط التلقائي صامتًا. لا حاجة للإقصاء الصريح ولا لعلامة خاصية. يأخذ الـ bean الصريح في التطبيق الأولوية لأن Spring يعالج فئات @Configuration الخاصة بالتطبيق قبل الضبط التلقائي.

كتابة شرط مخصص

حين لا تناسب أي حاشية جاهزة احتياجك، نفّذ واجهة Condition مباشرةً. إليك شرطًا يُنشّط bean فقط عند تشغيل التطبيق على نظام Linux:

package com.example.config; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.type.AnnotatedTypeMetadata; public class OnLinuxCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { String os = context.getEnvironment().getProperty("os.name", ""); return os.toLowerCase().contains("linux"); } }
package com.example.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @Configuration public class OsConfig { @Bean @Conditional(OnLinuxCondition.class) public FileWatcherService inotifyFileWatcher() { return new InotifyFileWatcherService(); // تنفيذ مبني على inotify في Linux } }

لإمكانية إعادة الاستخدام عبر المشاريع، لفّ الشرط في حاشية مركّبة (meta-annotation):

package com.example.config; import org.springframework.context.annotation.Conditional; import java.lang.annotation.*; @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(OnLinuxCondition.class) public @interface ConditionalOnLinux { }

الآن يمكن لأي تابع @Bean أو فئة @Configuration الاكتفاء بالحاشية @ConditionalOnLinux.

@ConditionalOnExpression للمنطق الديناميكي

حين يُعبَّر الشرط بأفضل صورة كصيغة بوليانية على الخصائص، استخدم SpEL:

@Bean @ConditionalOnExpression("'${app.mode}' == 'batch' and ${app.batch.workers:1} > 1") public BatchProcessorPool batchProcessorPool() { return new BatchProcessorPool(); }
أبقِ الشروط بسيطة وسريعة. تعمل الشروط بشكل متزامن خلال تحديث السياق (context refresh) قبل إنشاء أي bean. تجنّب عمليات I/O أو استدعاءات الشبكة أو الحسابات المكلفة داخل matches(). الشرط البطيء يُؤخّر كل عملية بدء تشغيل.

الترتيب وتسلسل التقييم

حين يفحص شرط ما وجود bean آخر (@ConditionalOnBean)، يُصبح الترتيب مهمًا. إذا كانت فئة التهيئة B تحتاج إلى التسجيل بعد الفئة A لترى beans الأخيرة، استخدم @AutoConfigureAfter في حالة الضبط التلقائي، أو @DependsOn للترتيب الصريح في كود التطبيق. بدون ترتيب، تصبح النتيجة غير محددة.

لفئات @Configuration العادية التي تتحكم فيها، فضّل @ConditionalOnMissingBean على @ConditionalOnBean لأنك تستطيع التفكير فيه دون القلق بشأن ترتيب التقييم: الشرط يعني أساسًا "إذا لم يسجّل أحد هذا النوع، سأقوم بذلك."

المقايضات وأفضل الممارسات

  • فضّل الشروط المبنية على الخصائص على تلك المبنية على الفئات في كود التطبيق — يسهل تجاوزها في الاختبارات بتعيين الخصائص.
  • تجنّب تكديس شروط كثيرة على bean واحد. إذا احتجت إلى ثلاثة شروط متحققة معًا، استخرج فئة @Configuration مخصصة وطبّق الشروط عليها ليُقرأ كسياسة متماسكة.
  • وثّق الشروط غير الواضحة بتعليق يشرح ما يجب أن يكون صحيحًا حتى يُسجَّل الـ bean. المطورون المستقبليون — وأنت أيضًا — سيشكرونك.
  • اختبر الـ beans الشرطية صراحةً. استخدم ApplicationContextRunner في اختبارات الوحدة للتحقق من الـ beans التي تُسجَّل في كل مزيج من الشروط دون تشغيل خادم كامل.
// الاختبار باستخدام ApplicationContextRunner (لا حاجة لـ @SpringBootTest) import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class NotificationConfigTest { private final ApplicationContextRunner runner = new ApplicationContextRunner() .withUserConfiguration(NotificationConfig.class); @Test void smtpBeanPresentWhenPropertyTrue() { runner.withPropertyValues("notifications.email.enabled=true") .run(ctx -> assertThat(ctx).hasSingleBean(SmtpNotificationService.class)); } @Test void smtpBeanAbsentWhenPropertyFalse() { runner.withPropertyValues("notifications.email.enabled=false") .run(ctx -> assertThat(ctx).doesNotHaveBean(SmtpNotificationService.class)); } }

الخلاصة

تمنح الـ beans الشرطية حاوي Spring القدرة على الضبط الذاتي بناءً على بيئة التشغيل. واجهة Condition هي نقطة التوسع الوحيدة؛ وكل حاشيات الضبط التلقائي في Spring Boot — @ConditionalOnProperty و@ConditionalOnMissingBean و@ConditionalOnClass وسائرها — هي تنفيذات قابلة للتركيب وإعادة الاستخدام لتلك الواجهة الواحدة. اكتب شروطًا مخصصة حين لا تناسب أي حاشية جاهزة، لفّها في meta-annotations لإعادة الاستخدام، واختبر كل مسار شرطي باستخدام ApplicationContextRunner لإبقاء تهيئتك موثوقة في جميع البيئات.