أساسيات Spring Boot

الخادم المدمج

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

الخادم المدمج

من أبرز الفوارق بين تطبيق Java الويب التقليدي وتطبيق Spring Boot طريقة تشغيله. في النموذج التقليدي كنت تحزم شيفرتك كملف WAR وتنشره داخل خادم تطبيقات مثبّت بشكل مستقل — Tomcat أو JBoss أو WebLogic. في Spring Boot يُشحن الخادم بداخل ملف JAR الخاص بك. تشغّل java -jar myapp.jar فيبدأ Tomcat من تلقاء نفسه. يشرح هذا الدرس كيف يعمل ذلك بالضبط، ولماذا يهم، وكيف تضبطه أو تستبدله.

معنى "المدمج"

الخادم المدمج هو خادم تطبيقات تديره شيفرة تطبيقك لا العكس. يحقق Spring Boot ذلك بتضمين الخادم كتبعية عادية. حين يستدعي التابع main() التابعَ SpringApplication.run()، تقوم إطار العمل بما يلي:

  1. إنشاء سياق تطبيق Spring ApplicationContext.
  2. الكشف عن تبعية الخادم المدمج في مسار الفئات.
  3. إنشاء الخادم وتهيئته برمجيًا (مثل org.apache.catalina.startup.Tomcat).
  4. تسجيل DispatcherServlet لديه.
  5. تشغيل الخادم وربطه بمنفذ.

تجري عملية الإقلاع بالكامل داخل عملية JVM. حين تُنهي العملية يتوقف الخادم معها — لا سكريبت إيقاف منفصل، ولا خطوة إلغاء نشر.

الخيارات الثلاثة للخوادم المدمجة

يشحن Spring Boot تهيئة تلقائية لثلاثة خوادم مدمجة. تختار واحدًا بالتحكم في التبعية الموجودة في مسار الفئات:

  • Tomcat (الافتراضي) — ناضج وشائع الاستخدام ومُختبَر على نطاق واسع. يُسحب تلقائيًا بواسطة spring-boot-starter-web.
  • Jetty — خفيف الوزن، قوي تاريخيًا في التعامل مع الاتصالات طويلة الأمد (WebSocket وبث HTTP/2).
  • Undertow — إنتاجية عالية، نواة إدخال/إخراج غير متزامنة، بصمة ذاكرة منخفضة لكل اتصال. شائع لأحمال العمل التفاعلية حتى في مكدس سيرفلت.

للتبديل من Tomcat إلى Undertow تستبعد مُشغّل Tomcat وتضيف مُشغّل Undertow في ملف pom.xml:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency>

لا حاجة لأي تغييرات في الشيفرة — يكتشف Spring Boot الخادم الجديد في مسار الفئات ويوصّله بنفس الطريقة. هذه هي التهيئة التلقائية في عملها.

ضبط الخادم عبر application.properties

تقع أكثر إعدادات الخادم شيوعًا ضمن الفضاء server.* في ملف application.properties (أو application.yml):

# المنفذ (الافتراضي 8080؛ استخدم 0 ليختار نظام التشغيل منفذًا عشوائيًا متاحًا) server.port=8080 # مسار السياق — جميع نقاط النهاية ستكون تحت /api server.servlet.context-path=/api # حدود الاتصالات والخيوط (خاص بـ Tomcat) server.tomcat.threads.max=200 server.tomcat.threads.min-spare=10 server.tomcat.accept-count=100 server.tomcat.max-connections=8192 # حدود حجم الطلب server.tomcat.max-http-form-post-size=2MB server.tomcat.max-swallow-size=2MB # HTTPS — تفعيل SSL على المنفذ 8443 server.port=8443 server.ssl.key-store=classpath:keystore.p12 server.ssl.key-store-password=changeit server.ssl.key-store-type=PKCS12
حيلة المنفذ العشوائي: يؤدي ضبط server.port=0 إلى تكليف نظام التشغيل بتعيين أي منفذ متاح. هذا مفيد جدًا في اختبارات التكامل — تحصل كل جولة اختبار على منفذ فريد مما يُزيل عدم الاستقرار الناجم عن تعارض المنافذ. أدرج المنفذ الفعلي المختار في الاختبار باستخدام @LocalServerPort.

التخصيص البرمجي للخادم

تغطي الخصائص 80% من حالات الاستخدام. لما تبقى — موصّلات مخصصة وصفحات خطأ مخصصة وضغط Gzip مضبوط بما يتجاوز ما تتيحه الخصائص — تنفّذ واجهة WebServerFactoryCustomizer. النسخة العامة تعمل مع الخوادم الثلاثة؛ أما الواجهات الفرعية الخاصة بكل خادم فتتيح الوصول إلى واجهات برمجية من مورّد بعينه:

import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer; import org.springframework.stereotype.Component; @Component public class TomcatTuning implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> { @Override public void customize(TomcatServletWebServerFactory factory) { // تخصيص الموصّل: السماح بأحرف خاصة في سلاسل الاستعلام factory.addConnectorCustomizers(connector -> connector.setProperty("relaxedQueryChars", "|{}[]") ); // تفعيل ضغط Gzip factory.addConnectorCustomizers(connector -> { connector.setProperty("compression", "on"); connector.setProperty("compressibleMimeType", "text/html,application/json,application/javascript"); connector.setProperty("compressionMinSize", "1024"); }); } }
فضّل استخدام application.properties للإعدادات القياسية. احتفظ بـ WebServerFactoryCustomizer للأشياء التي ليس لها خاصية مقابلة حقًا. المخصّصات التي تستخدم الواجهة الخاصة بالخادم (مثل TomcatServletWebServerFactory) ستتعطل إذا استبدلت الخادم لاحقًا؛ أما ConfigurableServletWebServerFactory العامة فهي محمولة.

الإيقاف الأنيق

بشكل افتراضي، حين يستقبل Spring Boot إشارة إيقاف (SIGTERM) يوقف الخادم فورًا — تُقطع الطلبات الجارية. منذ Spring Boot 2.3 يمكنك تفعيل الإيقاف الأنيق الذي يسمح للطلبات النشطة بالاكتمال قبل خروج العملية:

# application.properties spring.lifecycle.timeout-per-shutdown-phase=30s server.shutdown=graceful

مع server.shutdown=graceful يتوقف الخادم عن قبول اتصالات جديدة فور وصول الإشارة لكنه يكمل معالجة الطلبات الجارية بالفعل حتى حد المهلة timeout-per-shutdown-phase. هذا ضروري للنشر المتدرج بدون توقف في Kubernetes أو خلف موازن حمل.

الإيقاف الأنيق لا يُجدي إذا كان تجمّع الخيوط ممتلئًا. إذا كانت جميع خيوط Tomcat مشغولة بطلبات بطيئة حين تصل SIGTERM، ستحجب تلك الطلبات الإيقافَ حتى انتهاء المهلة ثم تُقتل على أي حال. اضبط حجم تجمّع الخيوط والمهل الزمنية لتعكس مدد الطلبات الواقعية.

ملف JAR القابل للتنفيذ وآلية عمله

حين تشغّل mvn package (أو ./gradlew bootJar) ينشئ إضافة البناء في Spring Boot ملف fat JAR — أرشيف واحد يحتوي على فئاتك المُجمَّعة وجميع ملفات JAR للتبعيات (بما فيها JAR الخادم) ومشغّل مخصص. يفهم المشغّل ملفات JAR المتداخلة (فأداة تحميل الفئات القياسية في Java لا تفهمها)، يحمّلها جميعًا ثم يستدعي التابع main() لفئة @SpringBootApplication.

# بناء ملف fat JAR mvn clean package -DskipTests # تشغيله — يبدأ Tomcat المدمج على المنفذ 8080 java -jar target/myapp-0.0.1-SNAPSHOT.jar # تجاوز المنفذ عند وقت التشغيل دون تغيير الخصائص java -jar target/myapp-0.0.1-SNAPSHOT.jar --server.port=9090

هيكل ملف JAR الموسّع داخل الأرشيف كالتالي:

myapp.jar ├── BOOT-INF/ │ ├── classes/ <-- شيفرتك المُجمَّعة │ └── lib/ <-- جميع ملفات JAR للتبعيات (بما فيها tomcat-embed-core) ├── META-INF/ │ └── MANIFEST.MF <-- Main-Class: org.springframework.boot.loader.JarLauncher └── org/springframework/boot/loader/ <-- فئات المشغّل

نشر WAR: متى لا تزال بحاجة إليه

بعض المنظمات تشغّل خادم Tomcat أو JBoss مشتركًا تديره العمليات. في هذه الحالة تنتج WAR بدلًا من JAR. غيّر نوع التعبئة في pom.xml، ومدّد SpringBootServletInitializer، وضع نطاق الخادم المدمج على provided:

// src/main/java/com/example/MyApp.java import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; @SpringBootApplication public class MyApp extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(MyApp.class); } public static void main(String[] args) { SpringApplication.run(MyApp.class, args); } }
كلا النمطين من قاعدة شيفرة واحدة: لأن main() لا يزال موجودًا، يعمل المشروع ذاته كملف JAR قابل للتنفيذ في التطوير ويمكن نشره كملف WAR في الإنتاج. نطاق provided على مُشغّل Tomcat يعني استبعاده من WAR (الحاوية توفّره) مع بقائه في مسار الفئات عند التشغيل محليًا عبر main().

الخلاصة

يدمج Spring Boot Tomcat (أو Jetty أو Undertow) كمكتبة داخل ملف JAR، مما يجعل java -jar وحدة النشر الكاملة. تضبط الخادم عبر فضاء الخصائص server.* للإعدادات الشائعة وعبر WebServerFactoryCustomizer للضبط المتقدم. الإيقاف الأنيق مع مهلة مُهيَّأة ضرورة لأي حمل إنتاجي يتوقع إعادة التشغيل دون توقف. حين تفرض القيود المؤسسية استخدام حاوية خارجية، يتطلب التبديل إلى تعبئة WAR ثلاثة تغييرات صغيرة فقط مع الإبقاء على شجرة المصدر ذاتها.