تحتاج تطبيقات Spring Boot إلى طريقة منظمة لإدارة القيم التي تختلف بين بيئات التطوير والاختبار والإنتاج. وتتيح المنصة تشغيل artifact واحد في هذه البيئات عبر جلب الإعدادات من ملفات الخصائص، ومتغيرات البيئة، وخصائص النظام، ووسائط سطر الأوامر، بدلاً من تضمينها داخل الشيفرة. وترى JetBrains أن الاستراتيجية الجيدة يجب أن تفصل الإعدادات عن التطبيق، وتمنع بدء التشغيل عند غياب القيم المطلوبة أو عدم صلاحيتها، وتسمح بتجاوز القيم الافتراضية لكل بيئة، مع إبقاء الأسرار في نظام مخصص لإدارتها.
قسّم الإعدادات قبل اختيار طريقة تخزينها
يقترح الدليل التعامل مع إعدادات التطبيق ضمن ثلاث فئات واضحة. الأولى هي القيم الافتراضية الآمنة وغير الحساسة، مثل عناوين خدمات الطرف الثالث، ومهلات الانتظار، وحدود إعادة المحاولة، ويمكن الاحتفاظ بها مع التطبيق. والثانية إعدادات النشر التي تحدد البيئة، مثل مضيف قاعدة البيانات، وأسماء قوائم الانتظار، وعناوين الخدمات الخارجية، ويُفضل تمريرها عبر منصة النشر. أما الفئة الثالثة فهي الأسرار، وتشمل كلمات المرور ومفاتيح API والشهادات والمفاتيح الخاصة، ويجب تخزينها في نظام مخصص للأسرار.
ينبغي أن يكون أي إعداد افتراضي صالحاً وآمناً في كل بيئة يمكن استخدامه فيها. ولا ينبغي تضمين عناوين قواعد البيانات أو بيانات الاعتماد في الشيفرة. وإذا لم توجد قيمة افتراضية آمنة لإعداد مطلوب، فمن الأفضل التحقق من وجوده أثناء بدء التطبيق بدلاً من السماح له بالعمل بإعداد ناقص.
استخدم الربط النوعي للإعدادات المترابطة
يمكن لتطبيقات Spring الوصول إلى الإعدادات عبر Environment أو @Value أو @ConfigurationProperties. وتناسب Environment الحالات التي تتطلب حل أسماء الخصائص ديناميكياً أو الوصول المباشر إلى مصادر الإعدادات داخل البنية التحتية. أما @Value فيلائم القيم المعزولة، لكن توزيع تعبيراته داخل عدة مكونات يجعل أسماء الخصائص أصعب في الاكتشاف والتحقق وإعادة الهيكلة.
عند وجود مجموعة مترابطة من القيم، يوصي الدليل باستخدام @ConfigurationProperties، لأنها توفر ربطاً يحافظ على الأنواع وتحويلاً مناسباً، وتدعم الربط المرن بين أسماء الخصائص وأعضاء Java، والتحقق على مستوى المجموعة، إضافة إلى الإكمال والتنقل داخل بيئة التطوير من خلال البيانات الوصفية التي يولدها spring-boot-configuration-processor. ويمكن تمرير قيم مثل عنوان الخدمة ومهلة الاتصال من متغيرات البيئة، مع تعريف قيمة افتراضية مثل 3 ثوانٍ لمهلة الانتظار عند الحاجة.
يُسجل هذا النوع من الإعدادات عبر @ConfigurationPropertiesScan، ما يسمح بحقنه في مكونات Spring الأخرى. وبما أن الإعدادات تُحدد عادة عند بدء التطبيق ولا تتغير طوال دورة حياته، فإن استخدام Java records هو الخيار المفضل في معظم الحالات؛ إذ يوفر عدم القابلية للتغيير ويمنع تعديل القيم عن طريق الخطأ عبر setters. كما يترجم Spring Boot أسماء kebab-case مثل base-url إلى الحقل baseUrl تلقائياً.
تعامل مع المكتبات الخارجية وتحقق مبكراً
عندما تكون فئة الإعدادات تابعة لمكتبة خارجية ولا يمكن تعديل مصدرها لإضافة @ConfigurationProperties، يمكن تعريفها كـ bean ووضع التعليمة على طريقة إنشائها. وإذا كانت الفئة الخارجية غير قابلة للتغيير أو لا تدعم الربط عبر setters، فمن الأفضل إنشاء فئة إعدادات خاصة بالتطبيق ثم استخدامها لبناء الكائن الخارجي. هذا الأسلوب يقلل اقتران بنية إعدادات التطبيق بالبنية الداخلية للمكتبة.
يجب اكتشاف أخطاء الإعداد عند بدء التشغيل. ويمكن إضافة @Validated إلى bean من نوع @ConfigurationProperties، ثم استخدام قيود Jakarta Bean Validation مثل @NotBlank و@NotNull و@Min و@Max و@Valid. ومع وجود spring-boot-starter-validation، تؤدي أخطاء الربط أو التحقق إلى إيقاف بدء التطبيق. ويشمل ذلك القيم المطلوبة، والنطاقات الرقمية، والمجموعات المتداخلة، والقيود الخاصة بالتطبيق.
عند الحاجة إلى التمييز بين غياب القيمة وقيمة Java الافتراضية، يُفضل استخدام الأنواع المغلفة. فمثلاً يسمح Integer مع @NotNull باكتشاف قيمة مفقودة، بينما قد يخفي النوع البدائي int غياب القيمة خلف الرقم 0.
افهم أولوية مصادر الخصائص
عندما تظهر الخاصية نفسها في أكثر من مصدر، يعتمد Spring Boot القيمة القادمة من المصدر الأعلى أولوية. وبصورة مبسطة، تبدأ الأولوية المنخفضة من application.properties أو application.yaml، ثم ملفات الإعداد الخاصة بالملف التعريفي، ثم متغيرات نظام التشغيل، وخصائص Java، وصولاً إلى وسائط سطر الأوامر ذات الأولوية الأعلى.
فهم هذا الترتيب ضروري عند تشخيص اختلاف القيمة الفعلية عن القيمة المتوقعة. كما يحول Spring Boot أسماء الخصائص إلى أسماء مناسبة لمتغيرات البيئة؛ فـ app.payment-timeout تصبح APP_PAYMENT_TIMEOUT، بينما تتحول spring.datasource.url إلى SPRING_DATASOURCE_URL.
ما الذي يتغير عملياً بحسب بنية التطبيق؟
لا توجد استراتيجية واحدة تناسب كل التطبيقات. في التطبيق الأحادي، يمكن الاحتفاظ بالقيم الافتراضية المشتركة داخل التطبيق، واستخدام ملفات خاصة بالملفات التعريفية عند الضرورة، وتمرير تجاوزات النشر عبر متغيرات البيئة، مع وضع الأسرار في مدير مخصص.
أما التطبيقات التي تعمل داخل حاويات مثل Kubernetes، فيمكن الاحتفاظ بالقيم الافتراضية المعقولة داخل التطبيق وتمرير إعدادات النشر عبر ConfigMaps، مع فصل الأسرار في نظام مستقل. وفي بنية الخدمات المصغرة، يمكن النظر في Spring Cloud Config Server لتوحيد الإعدادات وإدارتها وحوكمتها وإصدار نسخ منها، لكن ذلك لا يلغي الحاجة إلى نظام مخصص للأسرار.
احمِ الأسرار وراجع القيمة الفعلية
لا ينبغي تخزين كلمات المرور أو مفاتيح API أو الشهادات أو المفاتيح الخاصة في مستودع الشيفرة. ومن الخيارات المذكورة HashiCorp Vault وAWS Secrets Manager وGoogle Cloud Secret Manager وAzure Key Vault أو خدمة مكافئة. كما يجب منع ظهور الأسرار في السجلات ورسائل الأخطاء والبيانات الوصفية للإعدادات ونقاط الإدارة المتاحة للعامة.
في البيئات غير الإنتاجية، قد يساعد endpoint الخاص بـ Actuator env في تحديد مصدر الخاصية الفعلية، لكنه لا ينبغي أن يكون مكشوفاً للعامة لأن الإعدادات قد تتضمن معلومات حساسة. وتوفر IntelliJ IDEA مؤشرات داخل المحرر تعرض القيمة المحلولة ومصدرها، وتوضح ما إذا كانت قد تجاوزت بقيمة من متغير بيئة أو خاصية نظام، كما تتيح الانتقال بين تعريفات الخصائص وأعضاء @ConfigurationProperties واستخداماتها. بهذه الأدوات يصبح فصل الإعدادات والتحقق منها ومراجعة تأثير أولوية المصادر جزءاً من دورة التطوير، لا خطوة لاحقة عند ظهور مشكلة في النشر.