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

إنشاء الوحدات واستخدامها

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

إنشاء الوحدات واستخدامها

قدّم الدرس السابق السبب وراء نظام وحدات منصّة Java (JPMS). هذا الدرس تطبيقي بامتياز: ستُنشئ ملفات JAR معيارية (modular JARs)، وتكتب وتقرأ تصريحات module-info.java، وتفهم الفرق الجوهري بين مسار الوحدات (module path) وبين مسار الكلاسات القديم (classpath). هذه هي الميكانيكيات التي ستتعامل معها يوميًا في أي مشروع مبني على JPMS.

تشريح ملف module-info.java

كل وحدة تُعرَّف بملف واحد في جذر شجرة المصدر: module-info.java. يُصرّح هذا الملف باسم الوحدة وما تكشفه وما تعتمد عليه. إليك مثالًا واقعيًا لوحدة مكتبة:

// src/com.example.payments/module-info.java module com.example.payments { // الحزم التي تجعلها هذه الوحدة مرئية للوحدات الأخرى exports com.example.payments.api; exports com.example.payments.model; // حزمة داخلية — غير مُصدَّرة؛ مخفيّة عن الجميع // com.example.payments.internal تبقى خاصّة // تبعية وقت الترجمة والتشغيل requires com.example.logging; // وقت الترجمة فقط (معالجات التعليقات التوضيحية، مولّدات الكود) requires static com.example.annotations; // إعادة تصدير تبعية حتى لا يضطر المستدعون لتصريحها requires transitive com.example.currency; // فتح للانعكاس العميق (مثل Jackson وHibernate) opens com.example.payments.model to com.fasterxml.jackson.databind; }

التوجيهات الرئيسية ومعانيها:

  • exports — يجعل الأنواع العامّة (public) في حزمة ما متاحةً للكود في الوحدات الأخرى. الحزم غير المُصدَّرة مخفيّة تمامًا في وقت الترجمة والتشغيل، حتى وإن كانت أنواعها public.
  • requires — يُصرّح بتبعية وقت الترجمة والتشغيل على وحدة أخرى.
  • requires static — اختياري في وقت التشغيل؛ يُستخدم لمعالجات التعليقات التوضيحية أو الواجهات البرمجية المطلوبة فقط أثناء الترجمة.
  • requires transitive — ينقل التبعية ضمنيًا لأي وحدة تعتمد على وحدتك. إن كانت واجهتك البرمجية تُعيد أنواعًا من com.example.currency، فالمستدعون يحتاجون تلك الأنواع أيضًا، لذا تُعيد تصديرها.
  • opens … to — يمنح صلاحية انعكاس عميق مؤهَّل (متجاوزًا التغليف) لوحدة محدّدة. استخدم هذا للأطر التي تعتمد على الانعكاس في وقت التشغيل.
التغليف هو الفائدة الرئيسية. قبل JPMS، كان بإمكان أي كلاس في أي مكان على مسار الكلاسات الوصول إلى أي كلاس آخر بالاسم — بما في ذلك واجهات Sun الداخلية. مع الوحدات، فقط ما تُصدّره صراحةً يكون مرئيًا. هذا مُطبَّق بواسطة JVM، وليس مجرّد اتفاقية.

هيكل المشروع لبناء متعدّد الوحدات

يضع المشروع متعدّد الوحدات النموذجي كل وحدة في مجلدها الخاص، وكل مجلد يحتوي على module-info.java الخاص به:

my-project/ ├── com.example.payments/ │ ├── src/ │ │ └── com/example/payments/ │ │ ├── api/PaymentService.java │ │ ├── model/Payment.java │ │ └── internal/PaymentProcessor.java │ └── module-info.java ├── com.example.logging/ │ ├── src/ │ │ └── com/example/logging/ │ │ └── Logger.java │ └── module-info.java └── com.example.app/ ├── src/ │ └── com/example/app/ │ └── Main.java └── module-info.java

تُجمَّع كل وحدة في ملف JAR معياري مستقل. اسم الوحدة (في module-info.java) واسم ملف JAR مستقلّان — لكن الاتفاقية أن يتطابقا، ويُنتج Maven/Gradle القطع الصحيحة تلقائيًا.

الترجمة والتعبئة اليدوية لملف JAR المعياري

فهم العملية اليدوية يساعدك في تشخيص مشكلات أدوات البناء. لنفترض أنك تُجمّع com.example.logging أولًا (بلا تبعيات)، ثم com.example.payments الذي يعتمد عليه:

# الخطوة 1 — تجميع وحدة logging javac -d out/com.example.logging \ com.example.logging/module-info.java \ com.example.logging/src/com/example/logging/Logger.java # الخطوة 2 — تعبئتها في ملف JAR معياري jar --create \ --file=mods/com.example.logging.jar \ --module-version=1.0 \ -C out/com.example.logging . # الخطوة 3 — تجميع وحدة payments مع تحديد مسار الوحدات javac --module-path mods \ -d out/com.example.payments \ com.example.payments/module-info.java \ $(find com.example.payments/src -name "*.java") # الخطوة 4 — تعبئتها jar --create \ --file=mods/com.example.payments.jar \ --module-version=1.0 \ -C out/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).
# تشغيل بمسار الكلاسات القديم — لا تغليف للوحدات java -cp mods/com.example.logging.jar:mods/com.example.payments.jar \ com.example.app.Main # تشغيل بمسار الوحدات — تطبيق كامل لتغليف JPMS java --module-path mods \ --module com.example.app/com.example.app.Main

يُحدّد خيار --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 أو أعلى:

<properties> <maven.compiler.release>17</maven.compiler.release> </properties>

في مشاريع Maven متعدّدة الوحدات، كل وحدة هي وحدة Maven فرعية مستقلة. يمرّر Maven الإخوة المُجمَّعين تلقائيًا على --module-path أثناء الترجمة. في وقت التشغيل (مثلًا عبر exec-maven-plugin)، مرّر الوحدة الرئيسية صراحةً:

<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <configuration> <executable>java</executable> <arguments> <argument>--module-path</argument> <argument>${project.build.directory}/dependency</argument> <argument>--module</argument> <argument>com.example.app/com.example.app.Main</argument> </arguments> </configuration> </plugin>

التصديرات المؤهَّلة والفتح المستهدَف

أحيانًا تريد أن تكون حزمة مرئية فقط لوحدات موثوقة محدّدة — مثلًا مساعد اختبار لا يجب أن يصل إليه إلا وحدة الاختبار:

module com.example.payments { exports com.example.payments.api; // فقط وحدة الاختبار ترى هذه الحزمة exports com.example.payments.testkit to com.example.payments.test; // السماح لوحدة Jackson بالانعكاس في كلاسات النموذج opens com.example.payments.model to com.fasterxml.jackson.databind; }

هذا ما يُسمى التصدير المؤهَّل / الفتح المؤهَّل. يمنحك تحكّمًا دقيقًا: تبقى المساعدات الداخلية غير مرئية للكود الخارجي، بينما تظل متاحة حيثما تكون هناك حاجة حقيقية لها.

التكامل مع ServiceLoader

يدعم JPMS نمط ServiceLoader بشكل أصيل، ليحلّ بشكل نظيف محلّ الفحص اليدوي لمسار الكلاسات:

// وحدة المزوّد تُصرّح بالخدمة التي تقدّمها module com.example.payments.stripe { requires com.example.payments; provides com.example.payments.api.PaymentGateway with com.example.payments.stripe.StripeGateway; } // وحدة المستهلك تُصرّح بما تستخدمه module com.example.app { requires com.example.payments; uses com.example.payments.api.PaymentGateway; }

في وقت التشغيل، يكتشف ServiceLoader.load(PaymentGateway.class) جميع المزوّدين في رسم الوحدات دون أن يعرف المستهلك اسم كلاس التنفيذ. هكذا يربط JDK نفسه خدماته القابلة للتوسيع (مثل java.sql.Driver).

الخلاصة

تُعرَّف الوحدة بملف module-info.java الذي يُصرّح بتوجيهات exports وrequires وopens وprovides/uses. يُطبّق مسار الوحدات التغليف؛ مسار الكلاسات لا يفعل — فهم أي آلية نشطة لكل ملف JAR هو أهم مهارة تشخيصية في مشروع JPMS. تصبح ملفات JAR القديمة وحدات تلقائية حين توضع على مسار الوحدات. يتعامل Maven مع الترجمة المدركة للوحدات بشفافية بمجرّد وجود module-info.java. التصديرات والفتح المؤهَّلان يتيحان كشف تفاصيل التنفيذ للمستهلكين الموثوقين فقط.