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

تجريد البيئة في Spring

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

تجريد البيئة في Spring

تحتاج كل تطبيقات Spring غير البسيطة إلى قراءة قيم من العالم الخارجي: عناوين URL لقواعد البيانات، وأعلام الميزات، ومفاتيح API، وفترات المهلة. تُجمّع Spring كل ذلك خلف واجهة موحّدة واحدة تُسمّى Environment. بدلًا من تشتيت استدعاءات System.getenv() وSystem.getProperty() وProperties.load() عبر قاعدة الكود، تتفاعل مع كائن واحد يعرف كيف يبحث في مصادر متعددة بترتيب أولوية محدد.

يركّز هذا الدرس على ماهية Environment، ومصادر بياناتها، وبشكل حاسم — أي مصدر يفوز حين تظهر نفس المفتاح في أكثر من مكان.

واجهة Environment

تمتد org.springframework.core.env.Environment من واجهتين فرعيتين:

  • PropertyResolver — البحث عن قيم الخصائص بالمفتاح، وحل العناصر النائبة ${...}، وإجراء تحويل الأنواع.
  • EnvironmentCapable — يعرض أسماء البروفايل النشطة والافتراضية.

في معظم الأكواد تستخدم الواجهة الفرعية الأغنى ConfigurableEnvironment، التي تعرض إضافةً إلى ذلك القائمة المرتبة لمصادر الخصائص حتى تتمكن من إضافتها أو إزالتها أو إعادة ترتيبها برمجيًا.

نادرًا ما تحتاج إلى حقن Environment مباشرةً. كلٌّ من @Value و@ConfigurationProperties وapplication.properties مدعومة من نفس كائن Environment تحت الغطاء. حقنه مباشرةً مفيد أكثر في كود البنية التحتية، وفئات الشروط، وفي الاختبارات.

PropertySources — من أين تأتي القيم

تحتفظ Environment بقائمة مرتبة من نوع MutablePropertySources. كل إدخال هو PropertySource<?> — وعاء مُسمَّى يعرف كيف يُجيب على سؤال: "هل لديك المفتاح X، وإن كان الأمر كذلك فما قيمته؟"

تملأ Spring Boot 3 المصادر التالية تلقائيًا، مرتبةً من الأولوية الأعلى إلى الأدنى:

  1. وسيطات سطر الأوامر (--server.port=9090)
  2. متغير البيئة SPRING_APPLICATION_JSON أو خاصية النظام (JSON مضمَّن)
  3. متغيرات بيئة نظام التشغيل
  4. خصائص نظام JVM (-Dspring.profiles.active=prod)
  5. ملفات التطبيق الخاصة بالبروفايل (application-{profile}.properties / .yml)
  6. ملفات خصائص التطبيق (application.properties / application.yml)
  7. الخصائص الافتراضية المضبوطة عبر SpringApplication.setDefaultProperties()
القاعدة العامة: كلما كانت القيمة أقرب للعملية الجارية (سطر الأوامر، متغير بيئة نظام التشغيل)، كانت أولويتها أعلى. أما القيم المضمَّنة في الملفات المحزومة داخل ملف JAR فتقع في الأسفل — فهي قيم افتراضية، لا قيم تتجاوز الأولوية.

حل الخصائص برمجيًا

احقن Environment كأي حبّة (bean) أخرى:

import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; @Component public class DataSourceInfo { private final Environment env; public DataSourceInfo(Environment env) { this.env = env; } public void printConfig() { // getProperty ترجع null إذا غاب المفتاح String url = env.getProperty("spring.datasource.url"); // getProperty مع قيمة افتراضية int poolSize = env.getProperty("app.pool.size", Integer.class, 10); // getRequiredProperty تطرح MissingRequiredPropertiesException عند الغياب String apiKey = env.getRequiredProperty("app.payment.api-key"); System.out.println("url=" + url + ", pool=" + poolSize); } }

تُجري التحميلة الزائدة getProperty(String, Class<T>) تحويل النوع تلقائيًا عبر ConversionService في Spring، لذا تحصل على Integer من قيمة نصية دون أي تحليل يدوي.

فحص قائمة PropertySources

حوّل Environment إلى ConfigurableEnvironment لرؤية قائمة المصادر أو تعديلها في وقت التشغيل:

import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; // داخل @Bean أو ApplicationRunner ConfigurableEnvironment configEnv = (ConfigurableEnvironment) environment; MutablePropertySources sources = configEnv.getPropertySources(); for (PropertySource<?> source : sources) { System.out.printf("%-45s (%s)%n", source.getName(), source.getClass().getSimpleName()); }

تشغيل هذا على تطبيق Spring Boot نموذجي يطبع شيئًا كالتالي:

commandLineArgs (SimpleCommandLinePropertySource) systemEnvironment (SystemEnvironmentPropertySource) systemProperties (PropertiesPropertySource) Config resource 'application-prod.properties' (OriginTrackedMapPropertySource) Config resource 'application.properties' (OriginTrackedMapPropertySource)

القائمة مرتبة — أول مصدر يحتوي على مفتاح هو الفائز. هذه هي الآلية الدقيقة التي تقف وراء نظام الأولوية.

إضافة PropertySource مخصص

أحيانًا تحتاج إلى حقن قيم من موقع غير قياسي: قاعدة بيانات، خزنة أسرار (vault)، خادم تهيئة عن بُعد. الخطاف الصحيح هو ApplicationContextInitializer أو EnvironmentPostProcessor (خاص بـ Spring Boot)، وكلاهما يعمل قبل إنشاء أي حبّة (bean).

import org.springframework.boot.env.EnvironmentPostProcessor; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; import org.springframework.boot.SpringApplication; import java.util.Map; // سجّل في META-INF/spring.factories أو META-INF/spring/ // org.springframework.boot.env.EnvironmentPostProcessor=com.example.VaultPostProcessor public class VaultPostProcessor implements EnvironmentPostProcessor { @Override public void postProcessEnvironment(ConfigurableEnvironment env, SpringApplication application) { // محاكاة جلب الأسرار من vault Map<String, Object> secrets = Map.of( "app.payment.api-key", fetchFromVault("payment-key") ); // addFirst = أعلى أولوية؛ addLast = أدنى أولوية env.getPropertySources().addFirst( new MapPropertySource("vault", secrets) ); } private String fetchFromVault(String path) { // التنفيذ الحقيقي يستدعي خلفية الأسرار return "sk-live-secret"; } }
يهم الفرق بين addFirst و addLast. إن استدعيت addLast، يجلس مصدرك المخصص أسفل systemEnvironment — فمتغير بيئة نظام التشغيل بنفس المفتاح سيتجاوز أولويته صامتًا. استخدم addFirst فقط للمصادر التي يجب أن تكون الأعلى سلطةً (مثل مدير أسرار مركزي). استخدم addLast للقيم الافتراضية الاحتياطية.

حل العناصر النائبة وتحويل الأنواع

يستطيع Environment حل العناصر النائبة المتداخلة ${...} في أي سلسلة نصية تمررها:

# application.properties base.url=https://api.example.com orders.url=${base.url}/orders notifications.url=${base.url}/notifications

عند استدعاء env.getProperty("orders.url")، تُوسّع Spring ${base.url} بشكل تكراري قبل إعادة النتيجة. آلية العناصر النائبة القابلة للتركيب هذه هي ما يجعل الخصائص الأساسية المشتركة مفيدة عبر البروفايلات المختلفة.

الاستعلام عن البروفايلات النشطة

تُعدّ Environment أيضًا المصدر الموثوق لقائمة البروفايلات النشطة — الكائن ذاته الذي تتفاعل معه للخصائص يتحكم كذلك في الحبّات (beans) المقيّدة بالبروفايل:

String[] active = env.getActiveProfiles(); // مثال: ["prod", "eu-west"] String[] defaults = env.getDefaultProfiles(); // ["default"] ما لم يُعاد التعريف boolean isProd = env.acceptsProfiles( org.springframework.core.env.Profiles.of("prod") );

تقبل مصنع Profiles.of() (متاح منذ Spring 5.1) تعابير البروفايل بما في ذلك النفي (!dev) والتقاطع (prod & eu-west)، مما يمنحك منطقًا شرطيًا دقيقًا في كود البنية التحتية دون اللجوء إلى @Conditional.

المزالق الشائعة

  • قراءة الخصائص مبكرًا جدًا. الوصول إلى env.getProperty() داخل @PostConstruct يعمل بشكل جيد، لكن فعل ذلك في مُهيّئ ثابت أو مُنشئ ApplicationContextInitializer قد يحدث قبل تحميل جميع المصادر.
  • الربط المرن مقابل البحث المباشر. الربط المرن في Spring Boot (مثل تحويل APP_DB_URL إلى app.db.url) ينطبق عند استخدام @ConfigurationProperties. استدعاء env.getProperty("APP_DB_URL") مباشرةً يستخدم المطابقة الدقيقة فقط — لن يجد مفتاح النقطة.
  • مراسي YAML. صياغة YAML للمراسي &anchor / *alias يحلّها مُحلّل YAML قبل أن تراها Spring — إنها ليست مرادفة لمراجع Spring ${placeholder}.

الخلاصة

تجريد Environment هو النظرة الموحّدة في Spring لجميع مصادر التهيئة. يحتفظ بقائمة مرتبة من MutablePropertySources؛ أول مصدر يوفّر مفتاحًا هو الفائز. تملأ Spring Boot هذه القائمة مسبقًا بوسيطات سطر الأوامر، ومتغيرات بيئة نظام التشغيل، وخصائص نظام JVM، وملفات الخصائص بترتيب أولوية محدد جيدًا. يمكنك حقن Environment مباشرةً لحل الخصائص مع تحويل الأنواع، وفحص قائمة المصادر أو تعديلها بالتحويل إلى ConfigurableEnvironment، وإضافة مصادر مخصصة (مثل من vault) عبر EnvironmentPostProcessor. الكائن ذاته يعرض البروفايلات النشطة أيضًا، مما يجعله المصدر الوحيد للحقيقة لجميع قرارات التهيئة في وقت التشغيل.