مشروع التخرّج: تطبيق جافا حقيقي

تغليف التطبيق وتشغيله

15 دقيقة الدرس 9 من 13

تغليف التطبيق وتشغيله

كتابة الكود الصحيح ليست سوى نصف العمل. يجب أن يكون تطبيق Java الاحترافي قابلًا للبناء بشكل موثوق، وقابلًا للنقل، وقابلًا للتنفيذ على أي جهاز مستهدف — سواء كان حاسوب زميل أو خادم إنتاج. يغطّي هذا الدرس خط أنابيب التغليف الكامل: التجميع باستخدام Maven أو Gradle، وإنتاج ملفات JAR قابلة للتنفيذ، وإدارة بيئة التشغيل، وتشغيل الأداة النهائية محليًا وعلى الخادم.

فهم دورة حياة البناء

يُنمذج كلٌّ من Maven وGradle عمليات البناء كتسلسل من المراحل. فهم دورة الحياة يمنع الخطأ الشائع المتمثل في تشغيل هدف خاطئ في وقت خاطئ.

مراحل دورة حياة Maven (بالترتيب):

  1. validate — يتحقّق من صحة المشروع وتوفّر جميع المعلومات المطلوبة.
  2. compile — يُجمّع الكود المصدري إلى target/classes.
  3. test — يُشغّل اختبارات الوحدة عبر Surefire؛ يفشل البناء إذا فشل أي اختبار.
  4. package — يُجمّع الكلاسات المُجمَّعة في ملف JAR (أو WAR) داخل target/.
  5. verify — يُشغّل فحوصات التكامل (مثل إضافة Failsafe).
  6. install — يُثبّت الأداة في مستودع Maven المحلي (~/.m2).
  7. deploy — يرفع الأداة إلى مستودع بعيد (Nexus، Artifactory، GitHub Packages).
كل مرحلة تتضمّن جميع المراحل السابقة. تشغيل mvn package يُشغّل تلقائيًا validate وcompile وtest أولًا. إذا أردت التجميع فقط بدون اختبارات، استخدم mvn package -DskipTests — لكن افعل ذلك فقط على جهاز المطوّر، وليس أبدًا في بيئة CI.

بناء Fat JAR قابل للتنفيذ باستخدام Maven

ملف JAR القياسي يحتوي على كلاساتك فقط. لتشغيله في أي مكان بدون classpath منفصل، تحتاج إلى fat JAR (يُسمى أيضًا uber JAR) — كلاساتك بالإضافة إلى كل اعتمادية مُجمَّعة في ملف واحد. إضافة Maven Shade هي الطريقة القياسية لإنتاجه.

<!-- pom.xml — أضِف داخل <build><plugins> --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.2</version> <executions> <execution> <phase>package</phase> <goals><goal>shade</goal></goals> <configuration> <createDependencyReducedPom>false</createDependencyReducedPom> <transformers> <transformer implementation= "org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.example.app.Main</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin>

بعد إضافة هذه الإضافة، شغّل:

mvn clean package # ينتج target/myapp-1.0.0-shaded.jar java -jar target/myapp-1.0.0-shaded.jar

البناء باستخدام Gradle

المكافئ في Gradle هو إضافة shadowJar (com.github.johnrengelman.shadow). أضِفها إلى build.gradle:

plugins { id 'java' id 'com.github.johnrengelman.shadow' version '8.1.1' } jar { manifest { attributes 'Main-Class': 'com.example.app.Main' } }

ثم ابنِ وشغّل:

./gradlew clean shadowJar # ينتج build/libs/myapp-all.jar java -jar build/libs/myapp-all.jar
استخدم clean قبل كل بناء للإصدار. يمكن أن تتسلّل ملفات الكلاس القديمة من تجميع سابق إلى JAR دون أن تلاحظ، مما يتسبب في أخطاء دقيقة لا تتكرر من عملية سحب جديدة. اجعل clean package (أو clean shadowJar) عادة راسخة في CI.

ملف MANIFEST.MF

نقطة دخول JAR القابل للتنفيذ مُعلَنة في META-INF/MANIFEST.MF داخل الأرشيف. السطر الحاسم هو:

Main-Class: com.example.app.Main

إذا كان هذا السطر مفقودًا أو يشير إلى كلاس بدون طريقة public static void main(String[] args)، ستُلقي JVM خطأ java.lang.NoSuchMethodError: main أو "Main manifest attribute not found". كلتا الإضافتين المذكورتين تكتبان هذا الملف تلقائيًا عند تعريف mainClass.

إخراج الإعدادات إلى خارج التطبيق في وقت التشغيل

يجب أن يعمل JAR المبني مرة واحدة في بيئات التطوير والاختبار والإنتاج بدون إعادة تجميع. أخرج جميع القيم الخاصة بالبيئة:

// قراءة خاصية النظام (تُمرَّر عبر -D في سطر الأوامر) String dbUrl = System.getProperty("db.url", "jdbc:sqlite:local.db"); // قراءة متغيّر البيئة (مفضّل للحاويات) String apiKey = System.getenv("API_KEY"); if (apiKey == null) { throw new IllegalStateException("API_KEY environment variable is required"); }

مرّر خصائص النظام في وقت التشغيل:

java -Ddb.url=jdbc:mysql://prod-host/mydb \ -Ddb.user=appuser \ -jar target/myapp-1.0.0-shaded.jar
لا تضمّ بيانات الاعتماد داخل JAR أبدًا. أي قيمة مُضمَّنة في الكود المصدري أو داخل الأرشيف مرئية لأي شخص يفكّ ضغط الملف بـ jar tf أو unzip. استخدم متغيّرات البيئة أو مدير الأسرار (AWS Secrets Manager، HashiCorp Vault) لجميع الإعدادات الحساسة.

تحميل ملف Properties عند بدء التشغيل

للإعدادات غير الحساسة، تحميل ملف .properties عند بدء التشغيل أنظف من قائمة طويلة من أعلام -D:

import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Properties; public class AppConfig { private static final Properties props = new Properties(); static { // ابحث عن config.properties بجانب JAR أولًا، // ثم ارجع إلى الافتراضي من classpath Path external = Path.of("config.properties"); try { if (Files.exists(external)) { try (InputStream in = Files.newInputStream(external)) { props.load(in); } } else { try (InputStream in = AppConfig.class .getResourceAsStream("/config.properties")) { if (in != null) props.load(in); } } } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static String get(String key, String defaultValue) { return props.getProperty(key, defaultValue); } }

أعلام JVM للضبط في الإنتاج

الإعدادات الافتراضية لـ JVM مُعايَرة لأجهزة المطوّرين. في الإنتاج، اضبط على الأقل:

java \ -Xms256m \ # الكومة الأولية (خصّص مسبقًا لتجنّب ارتفاعات GC عند الإقلاع) -Xmx512m \ # الحد الأقصى للكومة (يمنع النموّ غير المحدود؛ اضبط حسب الخادم) -XX:+UseG1GC \ # جامع القمامة G1 — الأفضل افتراضيًا لمعظم أعباء العمل -XX:+HeapDumpOnOutOfMemoryError \ # التقاط مقطع الكومة للتحليل اللاحق -XX:HeapDumpPath=/var/log/myapp/ \ -jar myapp-shaded.jar

التشغيل كخدمة خلفية (systemd على Linux)

على خادم Linux، غلّف JAR في وحدة systemd ليبدأ عند التشغيل ويُعيد الإقلاع عند الفشل:

# /etc/systemd/system/myapp.service [Unit] Description=My Java Application After=network.target [Service] User=appuser WorkingDirectory=/opt/myapp ExecStart=/usr/bin/java \ -Xms256m -Xmx512m \ -Dspring.profiles.active=prod \ -jar /opt/myapp/myapp-shaded.jar Restart=on-failure RestartSec=5 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target
# تمكين الخدمة وتشغيلها sudo systemctl daemon-reload sudo systemctl enable myapp sudo systemctl start myapp sudo journalctl -u myapp -f # تابع السجلات مباشرة

اختبار الدخان للأداة المُغلَّفة

قبل النشر، تحقّق دائمًا من أن JAR يعمل من دليل نظيف — وليس من الدليل العمل لبيئة التطوير المتكاملة:

# انسخ JAR إلى دليل مؤقت بدون كود مصدري mkdir /tmp/smoke-test cp target/myapp-1.0.0-shaded.jar /tmp/smoke-test/ cp config.properties /tmp/smoke-test/ cd /tmp/smoke-test # شغّل بأعلام مكافئة للإنتاج API_KEY=test-key java -Xmx256m -jar myapp-1.0.0-shaded.jar # تحقّق من كود الخروج echo "Exit: $?"

يكتشف هذا أكثر أخطاء التغليف شيوعًا: مورد أو ملف تُتيحه بيئة التطوير المتكاملة على classpath أثناء التطوير لكنه غائب من JAR المُجمَّع.

الخلاصة

الأداة الجاهزة للإنتاج تُبنى بـ mvn clean package (إضافة Shade) أو ./gradlew clean shadowJar. يُعلن MANIFEST.MF عن نقطة الدخول؛ جميع الإعدادات الخاصة بالبيئة مُخرَجة عبر خصائص النظام أو متغيّرات البيئة أو ملف properties مصاحب. تُكمل أعلام كومة JVM ووحدة systemd قصة النشر. اختبر JAR دخانيًا من دليل نظيف قبل كل إصدار للكشف عن الموارد التي تعمل فقط على classpath مبكرًا.