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

الإعدادات الآمنة من حيث النوع باستخدام @ConfigurationProperties

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

الإعدادات الآمنة من حيث النوع باستخدام @ConfigurationProperties

في الدرس السابق رأيت كيف تقرأ قيمًا فردية باستخدام @Value. يُجدي هذا الأسلوب مع خاصية أو اثنتين، لكن حين تحتاج ميزة ما إلى عشرات الإعدادات المترابطة — مهلة الانتظار وعدد مرات إعادة المحاولة وعنوان URL الأساسي وبيانات الاعتماد — يُفضي حقن كل منها على حدة إلى كود مُبعثَر يصعب صيانته. يعالج Spring Boot هذا الأمر بـ @ConfigurationProperties: آلية تربط مجموعةً كاملة من الخصائص بفئة Java واحدة قوية النوع.

لماذا نفضّل @ConfigurationProperties على @Value

تأمّل حقن خمسة إعدادات لخادم البريد باستخدام @Value:

@Value("${mail.host}") private String host; @Value("${mail.port}") private int port; @Value("${mail.username}") private String username; @Value("${mail.password}") private String password; @Value("${mail.timeout-ms}") private long timeoutMs;

كل تعليق توضيحي هش: خطأ مطبعي في المفتاح يجتاز المُترجم لكنه يُطلق استثناءً وقت التنفيذ. لا يوجد إكمال تلقائي في بيئة التطوير عبر المجموعة، ولا تحقق من صحة البيانات، ولا مكان واحد ترى فيه جميع الإعدادات معًا. يُعالج @ConfigurationProperties كل هذه المشكلات.

الإعداد الأساسي

أولًا، أعلن فئة Java عادية مُزيَّنة بـ @ConfigurationProperties وبادئة مشتركة. في Spring Boot 3 تحتاج أيضًا إلى تعليمها بـ @Component أو تسجيلها عبر @EnableConfigurationProperties.

package com.example.demo.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "mail") public class MailProperties { private String host; private int port = 25; // قيمة افتراضية private String username; private String password; private long timeoutMs = 5_000; // الـ getters والـ setters القياسية (مطلوبة للربط) public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public String getUsername() { return username; } public void setUsername(String u) { this.username = u; } public String getPassword() { return password; } public void setPassword(String p) { this.password = p; } public long getTimeoutMs() { return timeoutMs; } public void setTimeoutMs(long t) { this.timeoutMs = t; } }

ثم في ملف application.properties (أو application.yml):

mail.host=smtp.example.com mail.port=587 mail.username=noreply@example.com mail.password=${MAIL_PASSWORD} mail.timeout-ms=10000
الربط المرن (Relaxed Binding): يُعالج Spring Boot كلًّا من timeout-ms وtimeoutMs وTIMEOUT_MS وtimeout_ms باعتبارها اسمًا واحدًا للحقل ذاته. ما عليك سوى التوحيد داخل الملف الواحد؛ إذ يُطبّع الإطار الاسم عند وقت الربط.

استخدام الـ Bean الخاص بالخصائص

احقن الفئة تمامًا كأي bean في Spring:

@Service public class MailService { private final MailProperties mail; public MailService(MailProperties mail) { this.mail = mail; } public void send(String to, String subject, String body) { // يستخدم mail.getHost() و mail.getPort() إلخ. } }

لأن فئة الخصائص bean حقيقية، تحصل مجانًا على حقن المنشئ وقابلية الاختبار والتنقل عبر بيئة التطوير.

استخدام Records في Java (Spring Boot 3.x)

يدعم Spring Boot 3 الربط مباشرةً بـ Java records. الـ Records غير قابلة للتغيير بطبيعتها، وهو ما يجعلها مثاليةً للإعدادات التي يجب ألا تتغير بعد بدء التشغيل:

package com.example.demo.config; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "mail") public record MailProperties( String host, int port, String username, String password, long timeoutMs ) {}
// سجّلها في @SpringBootApplication أو في فئة @Configuration @SpringBootApplication @EnableConfigurationProperties(MailProperties.class) public class DemoApplication { ... }
فضّل الـ Records للإعدادات الثابتة. تُجبرك الـ Record على تقديم كل قيمة عند الإنشاء ولا يمكن تغييرها لاحقًا بطريق الخطأ. هذا هو الأسلوب الموصى به في كود Spring Boot 3 الجديد. إن احتجت قيمًا افتراضية أو حقولًا اختيارية، استخدم أسلوب الفئة القابلة للتغيير.

الخصائص المتداخلة

يمكن تداخل مجموعات الإعدادات إلى أي عمق. تأمّل خدمة تتحدث إلى API خارجية بسياسة إعادة المحاولة الخاصة بها:

@ConfigurationProperties(prefix = "payment") @Component public class PaymentProperties { private String baseUrl; private Retry retry = new Retry(); // getters / setters ... public static class Retry { private int maxAttempts = 3; private long backoffMs = 500; // getters / setters ... } }
payment.base-url=https://api.payments.io payment.retry.max-attempts=5 payment.retry.backoff-ms=1000

التحقق من الصحة باستخدام Bean Validation

أضف @Validated إلى الفئة واستخدم تعليقات Bean Validation القياسية على الحقول. يُشغّل Spring Boot التحقق عند بدء التشغيل ويفشل بسرعة مع رسالة خطأ واضحة إذا انتُهك قيد ما — بدلًا من الانفجار وقت التنفيذ داخل منطق العمل.

import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Positive; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; @Component @ConfigurationProperties(prefix = "mail") @Validated public class MailProperties { @NotBlank private String host; @Positive private int port; @NotBlank private String username; // ... }
غياب spring-boot-starter-validation. يتطلب Bean Validation على خصائص الإعدادات وجود spring-boot-starter-validation في الـ classpath. إن زيّنت الحقول بـ @NotBlank لكنك نسيت إضافة الـ starter، تُتجاهل التعليقات التوضيحية صمتًا عند بدء التشغيل ولا تحصل على أي تحقق. تحقق دائمًا من وجود التبعية.

إكمال تلقائي في بيئة التطوير عبر بيانات وصفية

أضف معالج التعليقات التوضيحية إلى ملف pom.xml ليتمكن IDE من توليد الإكمال التلقائي والتوثيق لبوادئك المخصصة:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency>

يكتب المعالج عند الترجمة ملف META-INF/spring-configuration-metadata.json. تقرأ IntelliJ IDEA وVS Code هذا الملف لتقديم إكمال اسم الخاصية وتوثيق مضمّن داخل application.properties.

@ConfigurationProperties مقابل @Value — دليل القرار

  • استخدم @Value لخاصية واحدة معزولة — مثل علامة ميزة أو مهلة بسيطة لا تنتمي إلى مجموعة منطقية.
  • استخدم @ConfigurationProperties حين تشترك خاصيتان أو أكثر في بادئة منطقية، خاصةً إذا حُقنت المجموعة في عدة beans، أو احتاجت قيمًا افتراضية، أو وجب التحقق منها عند بدء التشغيل.
  • لا تخلط الأسلوبين لنفس المجموعة المفاهيمية — اختر أحدهما والتزم به.

الخلاصة

يُحوّل @ConfigurationProperties فضاء الأسماء المسطّح من مفاتيح وقيم إلى كائن منظَّم آمن النوع. تكسب إكمالًا تلقائيًا في بيئة التطوير وربطًا مرنًا من أي مصدر — ملف خصائص أو YAML أو متغير بيئة أو وسيطة سطر أوامر — وتحققًا عند بدء التشغيل ومكانًا واحدًا لتوثيق كل إعداد تحتاجه الميزة. في الدرس القادم سترى كيف تُتيح Profiles في Spring استبدال مجموعات كاملة من هذه الخصائص بحسب البيئة.