نظام وحدات منصة Java (JPMS)
نظام وحدات منصة Java (JPMS)
قُدِّم نظام وحدات منصة Java (JPMS) في Java 9 ضمن مشروع Jigsaw، وقد غيّر بشكل جوهري طريقة هيكلة JDK نفسه، كما أتاح لمطوّري التطبيقات آلية من الدرجة الأولى للتعبير عن التغليف القوي والاعتماديات الصريحة على مستوى الحزمة التنفيذية. بعد عقدين من الاعتماد الكامل على الحزم ومسار الفئات (classpath)، حصلت Java أخيرًا على نظام وحدات يفرضه كل من المترجم والـ JVM.
لماذا وُجد JPMS: مشكلة مسار الفئات
قبل Java 9 كان مسار الفئات عبارة عن كومة مسطّحة وغير مرتّبة من ملفات JAR، مما أفرز مشكلات شهيرة صُمِّم JPMS لاستئصالها:
- فوضى JAR (JAR hell) — قد يعرّف ملفا JAR على مسار الفئات فئات في الحزمة ذاتها؛ فيختار الـ JVM أحدهما صامتًا، وتظهر الأخطاء في وقت التشغيل لا في وقت التصريف.
- غياب التغليف الموثوق — أي فئة
publicعلى مسار الفئات متاحة لكل شيء آخر دون استثناء، ولم يكن ثمة طريقة للإعلان عن كون فئة ما داخلية خاصة بالتطبيق. - غياب رسم الاعتماديات الصريح — لا يحمل ملف JAR قائمة يمكن قراءتها آليًا بالملفات الأخرى التي يحتاجها، وتضطر الأدوات والمطوّرون إلى استنتاج ذلك.
- JDK أحادي الكتلة — كانت كل تطبيقات Java تُشحَن مع JDK كامل بغض النظر عمّا تستخدمه فعليًا، مما يُعيق بناء صور خفيفة الوزن.
يعالج JPMS هذه الأربع من خلال استبدال مسار الفئات المسطّح برسم بياني للوحدات (module graph)، إذ يُعلن كل عقدة فيه بالضبط عما تحتاج وعما تكشف.
ملف module-info.java
الوحدة هي ملف JAR (أو مجلد) يحتوي على module-info.java في جذره. هذا الملف هو واصف الوحدة. يُصرّفه javac إلى module-info.class يقرأه الـ JVM عند الإطلاق. كل توجيه (directive) بداخله يتحقق منه المترجم ويُطبَّق في وقت التشغيل — ليس تنبيهًا ولا تعليقًا ولا اصطلاح Maven، بل إجراء مُلزِم.
أدنى واصف لوحدة اسمها com.example.payments:
الوحدة التي لا تحتوي على توجيهات لا تزال موجودة في رسم الوحدات؛ لكنها لا تكشف أي حزمة ولا تُعلن عن أي اعتمادية سوى java.base التي تتطلبها كل وحدة ضمنيًا.
com.example.payments تحتوي عادةً على حزم مثل com.example.payments.api وcom.example.payments.model وهكذا.
توجيه requires
يُعلن requires أن هذه الوحدة تعتمد على وحدة أخرى مُسمّاة. سيرفض المترجم والـ JVM الإطلاق إن كانت أي وحدة مطلوبة غائبة عن مسار الوحدات.
ثمة ثلاثة أشكال لـ requires:
requires M— اعتماد في وقت التصريف والتشغيل على الوحدةM.requires transitive M— اعتماد علىMيُعاد تصديره: أي وحدة تعتمد على وحدتك تقرأMتلقائيًا أيضًا. استخدمه عندما تظهر أنواع منMفي واجهتك البرمجية العامة (معاملات، أنواع إرجاع، أنواع حقول). إن أهملتtransitiveحين تكون ضرورية، سيحصل المستدعون على خطأ في التصريف.requires static M— اعتماد في وقت التصريف فحسب؛ لا يلزم وجودMفي وقت التشغيل. يُستخدم للتكاملات الاختيارية ومعالجات التعليقات التوضيحية (annotation processors).
توجيه exports
exports هو حارس البوابة. الحزم المُصدَّرة فقط هي المرئية للوحدات الأخرى — و"مرئية" هنا تعني أن المترجم والـ JVM يُطبِّقان ذلك، وليس مجرد اصطلاح تسمية. أي حزمة غير مُصدَّرة هي خاصة بالوحدة فعليًا بغض النظر عن كون فئاتها معلنة public.
إن حاول كود في وحدة أخرى استخدام فئة من com.example.payments.internal، أصدر المترجم خطأً. وفي وقت التشغيل يطرح الـ JVM IllegalAccessException. هذا هو التغليف القوي — ذا معنى حقيقي أخيرًا.
التصدير المحدود (Qualified exports)
أحيانًا تريد كشف حزمة لوحدة موثوقة واحدة فقط (وحدة شقيقة، وحدة اختبار، إطار عمل) دون عرضها على العالم. استخدم التصدير المحدود:
مثال متعدد الحزم من الواقع العملي
إليك مثالًا عمليًا لوحدة payments تعتمد على Jackson للتسلسل JSON وتكشف واجهة برمجية نظيفة مع إخفاء تفاصيل نقل HTTP الداخلية:
قد تحتوي الحزمة com.example.payments.api على واجهة مثل:
ويعيش تطبيق HTTP في com.example.payments.http — غير مرئي للمستهلكين. يبرمجون وفق واجهة PaymentGateway؛ وتفاصيل التطبيق مخفية حقًا.
opens والوصول بالانعكاس
يحجب التغليف القوي الوصول الانعكاسي أيضًا، مما يكسر أطر العمل التي تستخدم الانعكاس (Spring وHibernate وJackson). يُخفّف توجيه opens هذا للانعكاس فحسب:
opens com.example.foo (دون وحدة هدف) يمنح جميع الوحدات وصولًا انعكاسيًا عميقًا — مما يُلغي التغليف فعليًا. افضل دائمًا opens ... to المحدود نحو الإطار الذي يحتاجه فقط.
وضع module-info.java في مشروع Maven أو Gradle
في Maven الاصطلاح واضح: ضع module-info.java في src/main/java/ في الجذر لا داخل أي مجلد حزمة، ويلتقطه javac تلقائيًا. في مشاريع Maven متعددة الوحدات، يحمل كل artifact ملف module-info.java خاصًا به.
يُصرِّف الـ JVM رسم الوحدات ويُطبِّقه سواء كنت على مسار الوحدات (--module-path) أو مازلت على مسار الفئات. ملفات JAR التي تحتوي module-info.class هي وحدات مُسمَّاة (named modules)؛ أما تلك التي لا تحتوي عليه فتصبح وحدات غير مُسمّاة (unnamed modules) يمكنها رؤية كل شيء — وهي جسر للتوافق مع الكود القديم.
الخلاصة
يمنح JPMS مطوّري Java أداتين قويتين: requires للتعبير عن رسم الاعتماديات بصراحة، وexports لتعريف حدود واجهة برمجية دقيقة يُطبِّقها المترجم. معًا يُزيلان غموض مسار الفئات، ويُطبِّقان تغليفًا يتجاوز ما تستطيع الحزم تحقيقه وحدها، ويُمكِّنان الـ JVM من بناء صور تشغيل مُحسَّنة. الدرس التالي يُطبِّق هذه المفاهيم بهيكلة مشروع حقيقي كمجموعة من الوحدات المُسمَّاة المتعاونة.