إنشاء الوحدات واستخدامها
إنشاء الوحدات واستخدامها
قدّم الدرس السابق السبب وراء نظام وحدات منصّة Java (JPMS). هذا الدرس تطبيقي بامتياز: ستُنشئ ملفات JAR معيارية (modular JARs)، وتكتب وتقرأ تصريحات module-info.java، وتفهم الفرق الجوهري بين مسار الوحدات (module path) وبين مسار الكلاسات القديم (classpath). هذه هي الميكانيكيات التي ستتعامل معها يوميًا في أي مشروع مبني على JPMS.
تشريح ملف module-info.java
كل وحدة تُعرَّف بملف واحد في جذر شجرة المصدر: module-info.java. يُصرّح هذا الملف باسم الوحدة وما تكشفه وما تعتمد عليه. إليك مثالًا واقعيًا لوحدة مكتبة:
التوجيهات الرئيسية ومعانيها:
exports— يجعل الأنواع العامّة (public) في حزمة ما متاحةً للكود في الوحدات الأخرى. الحزم غير المُصدَّرة مخفيّة تمامًا في وقت الترجمة والتشغيل، حتى وإن كانت أنواعهاpublic.requires— يُصرّح بتبعية وقت الترجمة والتشغيل على وحدة أخرى.requires static— اختياري في وقت التشغيل؛ يُستخدم لمعالجات التعليقات التوضيحية أو الواجهات البرمجية المطلوبة فقط أثناء الترجمة.requires transitive— ينقل التبعية ضمنيًا لأي وحدة تعتمد على وحدتك. إن كانت واجهتك البرمجية تُعيد أنواعًا منcom.example.currency، فالمستدعون يحتاجون تلك الأنواع أيضًا، لذا تُعيد تصديرها.opens … to— يمنح صلاحية انعكاس عميق مؤهَّل (متجاوزًا التغليف) لوحدة محدّدة. استخدم هذا للأطر التي تعتمد على الانعكاس في وقت التشغيل.
هيكل المشروع لبناء متعدّد الوحدات
يضع المشروع متعدّد الوحدات النموذجي كل وحدة في مجلدها الخاص، وكل مجلد يحتوي على module-info.java الخاص به:
تُجمَّع كل وحدة في ملف JAR معياري مستقل. اسم الوحدة (في module-info.java) واسم ملف JAR مستقلّان — لكن الاتفاقية أن يتطابقا، ويُنتج Maven/Gradle القطع الصحيحة تلقائيًا.
الترجمة والتعبئة اليدوية لملف JAR المعياري
فهم العملية اليدوية يساعدك في تشخيص مشكلات أدوات البناء. لنفترض أنك تُجمّع com.example.logging أولًا (بلا تبعيات)، ثم com.example.payments الذي يعتمد عليه:
module-info.java مع ملفات مصدر الوحدة في استدعاء javac واحد. تجميعه منفردًا أو بترتيب خاطئ يُسبّب أخطاء "module not found" مُربكة في وقت الترجمة.
مسار الوحدات مقابل مسار الكلاسات — الفرق الجوهري
هذا أهم مفهوم يجب إتقانه. كلٌّ من مسار الوحدات ومسار الكلاسات طريقة لإخبار JVM بمكان الكود، لكنّهما يتصرّفان بشكل مختلف جذريًا:
- مسار الكلاسات (
-classpath/-cp): قائمة مسطّحة من ملفات JAR والمجلدات. كل الكود على مسار الكلاسات ينتمي إلى الوحدة مجهولة الاسم (unnamed module). لا يوجد تغليف: كل نوع عامّ في كل ملف JAR متاح من كل مكان آخر. هكذا عملت Java منذ عام 1995. - مسار الوحدات (
--module-path/-p): قائمة من المجلدات أو ملفات JAR التي يبحث فيها JVM عن الوحدات المسمّاة. كل ملف JAR يحتوي علىmodule-info.classيُعامَل كوحدة مسمّاة بقواعد التغليف الخاصة بها. ملفات JAR التي لا تحتوي علىmodule-info.classتصبح وحدات تلقائية (automatic modules).
يُحدّد خيار --module الوحدة الجذر وكلاس main الخاص بها بصيغة اسمالوحدة/الاسمالكاملللكلاس. يحلّ JVM رسم الوحدات من هناك، محمّلًا فقط ما هو مطلوب.
الوحدات التلقائية — جسر ملفات JAR القديمة
ملفات JAR القديمة (بلا module-info.class) الموضوعة على مسار الوحدات تصبح وحدات تلقائية. يشتقّ JVM أسماءها من أسماء ملفات JAR (الشُرَط تصبح نقاطًا، لواحق الإصدار تُحذف — مثلًا jackson-databind-2.17.0.jar يصبح jackson.databind). الوحدات التلقائية:
- تُصدّر جميع حزمها لجميع الوحدات الأخرى.
- تستطيع قراءة الوحدة مجهولة الاسم (أي الكود لا يزال على مسار الكلاسات).
- يمكن التصريح بها في بنود
requiresللوحدات المسمّاة.
تنشر كثير من المكتبات إدخال Automatic-Module-Name صريحًا في MANIFEST.MF الخاص بها لضمان اسم وحدة ثابت قبل إضافة دعم كامل للوحدات. استخدم دائمًا ذلك الاسم على الاسم المشتقّ عند وجوده.
التشغيل مع Maven (عملي)
عند استخدام Maven، تكتشف إضافة المُجمِّع تلقائيًا module-info.java وتتحوّل إلى وضع الترجمة المدرك للوحدات. تحتاج فقط لضمان أن إصدار Java هو 9 أو أعلى:
في مشاريع Maven متعدّدة الوحدات، كل وحدة هي وحدة Maven فرعية مستقلة. يمرّر Maven الإخوة المُجمَّعين تلقائيًا على --module-path أثناء الترجمة. في وقت التشغيل (مثلًا عبر exec-maven-plugin)، مرّر الوحدة الرئيسية صراحةً:
التصديرات المؤهَّلة والفتح المستهدَف
أحيانًا تريد أن تكون حزمة مرئية فقط لوحدات موثوقة محدّدة — مثلًا مساعد اختبار لا يجب أن يصل إليه إلا وحدة الاختبار:
هذا ما يُسمى التصدير المؤهَّل / الفتح المؤهَّل. يمنحك تحكّمًا دقيقًا: تبقى المساعدات الداخلية غير مرئية للكود الخارجي، بينما تظل متاحة حيثما تكون هناك حاجة حقيقية لها.
التكامل مع ServiceLoader
يدعم JPMS نمط ServiceLoader بشكل أصيل، ليحلّ بشكل نظيف محلّ الفحص اليدوي لمسار الكلاسات:
في وقت التشغيل، يكتشف ServiceLoader.load(PaymentGateway.class) جميع المزوّدين في رسم الوحدات دون أن يعرف المستهلك اسم كلاس التنفيذ. هكذا يربط JDK نفسه خدماته القابلة للتوسيع (مثل java.sql.Driver).
الخلاصة
تُعرَّف الوحدة بملف module-info.java الذي يُصرّح بتوجيهات exports وrequires وopens وprovides/uses. يُطبّق مسار الوحدات التغليف؛ مسار الكلاسات لا يفعل — فهم أي آلية نشطة لكل ملف JAR هو أهم مهارة تشخيصية في مشروع JPMS. تصبح ملفات JAR القديمة وحدات تلقائية حين توضع على مسار الوحدات. يتعامل Maven مع الترجمة المدركة للوحدات بشفافية بمجرّد وجود module-info.java. التصديرات والفتح المؤهَّلان يتيحان كشف تفاصيل التنفيذ للمستهلكين الموثوقين فقط.