أدوات البناء والوحدات

نظام وحدات منصة Java (JPMS)

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

نظام وحدات منصة 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:

module com.example.payments { }

الوحدة التي لا تحتوي على توجيهات لا تزال موجودة في رسم الوحدات؛ لكنها لا تكشف أي حزمة ولا تُعلن عن أي اعتمادية سوى java.base التي تتطلبها كل وحدة ضمنيًا.

اصطلاح تسمية الوحدات: استخدم اصطلاح DNS العكسي الذي تستخدمه بالفعل للحزم، وفي الغالب يتطابق اسم الوحدة مع حزمتها الجذرية. فالوحدة المسمّاة com.example.payments تحتوي عادةً على حزم مثل com.example.payments.api وcom.example.payments.model وهكذا.

توجيه requires

يُعلن requires أن هذه الوحدة تعتمد على وحدة أخرى مُسمّاة. سيرفض المترجم والـ JVM الإطلاق إن كانت أي وحدة مطلوبة غائبة عن مسار الوحدات.

module com.example.payments { requires java.net.http; // وحدة JDK — واجهة HTTP Client requires com.fasterxml.jackson.databind; // طرف ثالث على مسار الوحدات requires org.slf4j; }

ثمة ثلاثة أشكال لـ requires:

  • requires M — اعتماد في وقت التصريف والتشغيل على الوحدة M.
  • requires transitive M — اعتماد على M يُعاد تصديره: أي وحدة تعتمد على وحدتك تقرأ M تلقائيًا أيضًا. استخدمه عندما تظهر أنواع من M في واجهتك البرمجية العامة (معاملات، أنواع إرجاع، أنواع حقول). إن أهملت transitive حين تكون ضرورية، سيحصل المستدعون على خطأ في التصريف.
  • requires static M — اعتماد في وقت التصريف فحسب؛ لا يلزم وجود M في وقت التشغيل. يُستخدم للتكاملات الاختيارية ومعالجات التعليقات التوضيحية (annotation processors).
module com.example.payments.api { // مستهلكو هذه الوحدة يحتاجون java.sql لأن أنواعنا تمتد منها requires transitive java.sql; // معالج تعليقات لا يُحتاج إلا في وقت التصريف requires static lombok; }

توجيه exports

exports هو حارس البوابة. الحزم المُصدَّرة فقط هي المرئية للوحدات الأخرى — و"مرئية" هنا تعني أن المترجم والـ JVM يُطبِّقان ذلك، وليس مجرد اصطلاح تسمية. أي حزمة غير مُصدَّرة هي خاصة بالوحدة فعليًا بغض النظر عن كون فئاتها معلنة public.

module com.example.payments { requires java.net.http; requires com.fasterxml.jackson.databind; requires org.slf4j; // واجهة API العامة — متاحة لجميع الوحدات exports com.example.payments.api; exports com.example.payments.model; // التطبيق الداخلي — غير مُصدَّر؛ غير مرئي للوحدات الأخرى // com.example.payments.internal مقصودة الغياب هنا }

إن حاول كود في وحدة أخرى استخدام فئة من com.example.payments.internal، أصدر المترجم خطأً. وفي وقت التشغيل يطرح الـ JVM IllegalAccessException. هذا هو التغليف القوي — ذا معنى حقيقي أخيرًا.

التصدير المحدود (Qualified exports)

أحيانًا تريد كشف حزمة لوحدة موثوقة واحدة فقط (وحدة شقيقة، وحدة اختبار، إطار عمل) دون عرضها على العالم. استخدم التصدير المحدود:

module com.example.payments { exports com.example.payments.api; exports com.example.payments.internal to com.example.payments.tests; // وحدة الاختبار فقط }
صمّم لأدنى مساحة سطحية ممكنة. صدِّر فقط الحزم التي تشكّل واجهتك البرمجية العامة المقصودة. إن وجدت نفسك تُصدِّر حزمة "داخلية" لوحدات غير مترابطة كثيرة، فذلك رائحة تصميم — انقل الأنواع المشتركة إلى وحدة API مخصصة.

مثال متعدد الحزم من الواقع العملي

إليك مثالًا عمليًا لوحدة payments تعتمد على Jackson للتسلسل JSON وتكشف واجهة برمجية نظيفة مع إخفاء تفاصيل نقل HTTP الداخلية:

// src/com.example.payments/module-info.java module com.example.payments { // اعتماديات وقت التشغيل requires java.net.http; requires com.fasterxml.jackson.databind; requires org.slf4j; // transitive: واجهتنا البرمجية تُعيد أنواع java.time requires transitive java.base; // ضمني، لكن صريح للتوضيح // حزم الواجهة البرمجية العامة exports com.example.payments.api; exports com.example.payments.model; // com.example.payments.http — داخلية، غير مُصدَّرة // com.example.payments.cache — داخلية، غير مُصدَّرة }

قد تحتوي الحزمة com.example.payments.api على واجهة مثل:

package com.example.payments.api; import com.example.payments.model.Payment; import java.util.concurrent.CompletableFuture; public interface PaymentGateway { CompletableFuture<Payment> submit(Payment payment); CompletableFuture<Payment> status(String paymentId); }

ويعيش تطبيق HTTP في com.example.payments.http — غير مرئي للمستهلكين. يبرمجون وفق واجهة PaymentGateway؛ وتفاصيل التطبيق مخفية حقًا.

opens والوصول بالانعكاس

يحجب التغليف القوي الوصول الانعكاسي أيضًا، مما يكسر أطر العمل التي تستخدم الانعكاس (Spring وHibernate وJackson). يُخفّف توجيه opens هذا للانعكاس فحسب:

module com.example.payments { exports com.example.payments.api; exports com.example.payments.model; // السماح لـ Jackson بالفحص الانعكاسي لفئات النموذج في وقت التشغيل opens com.example.payments.model to com.fasterxml.jackson.databind; }
لا تفتح كل شيء. فتح شامل من شكل 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 من بناء صور تشغيل مُحسَّنة. الدرس التالي يُطبِّق هذه المفاهيم بهيكلة مشروع حقيقي كمجموعة من الوحدات المُسمَّاة المتعاونة.