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

التبعيات والمستودعات في Maven

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

التبعيات والمستودعات في Maven

إعلان تبعية في ملف pom.xml أمر بسيط — كتلة XML واحدة وسيتولّى Maven تحليل كل شيء وتنزيله وربطه. التعقيد الحقيقي يكمن في الفهم: ما الذي يجلبه Maven بالنيابة عنك؟ وكيف يختار الإصدار حين تتعارض مكتبتان؟ ولماذا قد يُضخّم النطاق الخاطئ حجم حزمة الإنتاج بصمت أو يُعطل مستهلكًا من المكتبة؟ تحوّل هذه الدرس تلك المعلومات الخلفية إلى إدارة تبعيات احترافية ومقصودة.

كيف يحلّ Maven التبعيات

عند إعلان تبعية يبحث Maven في سلسلة مستودعات بالترتيب حتى يعثر على القطعة المطلوبة:

  1. المستودع المحلي~/.m2/repository على جهازك. بمجرد تنزيل القطعة تُخزَّن هنا للأبد (حتى تحذفها أو تشغّل mvn dependency:purge-local-repository).
  2. Maven Central — المستودع العام الافتراضي. لا يحتاج إلى أي إعداد.
  3. مستودعات بعيدة إضافية — تُعلَن في pom.xml أو settings.xml (مثل JitPack أو Spring Milestones أو Nexus/Artifactory الخاص بشركتك).
Maven Central ليس مجرد خادم تنزيل. يفرض قواعد نشر صارمة: كل قطعة يجب أن تأتي بـ POM وملف JAR للمصادر وملف JAR لـ Javadoc وتوقيعات GPG. هذا الصرامة هو ما يجعل Central حجر الأساس الموثوق في نظام Java البيئي.

الإحداثيات — groupId:artifactId:version — تُعرّف قطعةً بشكل فريد. يُحوّلها Maven إلى مسار في أي مستودع: com/google/guava/guava/32.1.3-jre/guava-32.1.3-jre.jar.

نطاقات التبعية (Dependency Scopes)

يتحكّم العنصر <scope> في ثلاثة أشياء في آنٍ واحد: مسارات الفئات التي تظهر عليها التبعية (تجميع / اختبار / تشغيل)، وهل تُضمَّن في الأداة الناتجة، وهل تنتقل إلى مستهلكي مكتبتك كتبعية انتقالية. النطاقات الستة هي:

  • compile (الافتراضي) — متاحة في وقت التجميع والتشغيل والاختبار. تُضمَّن في JAR/WAR المُعبَّأ وتنتقل إلى المستهلكين كتبعية انتقالية. استخدمها للواجهات البرمجية التي تكشفها أو الأنواع الظاهرة في توقيعاتك العامة.
  • provided — متاحة في وقت التجميع والاختبار لكن غير مُضمَّنة ولا تنتقل. توفّرها بيئة التشغيل (الحاوية أو JDK). المثال الكلاسيكي: jakarta.servlet-api في WAR يُنشر على Tomcat.
  • runtime — غير مطلوبة للتجميع لكن ضرورية في وقت التشغيل. تُضمَّن لكن تُستبعد من مسار تجميع الكود حتى لا تعتمد الكود بطريقة خاطئة على تفاصيل التنفيذ. مشغّلات JDBC تنتمي هنا.
  • test — متاحة فقط في مسارَي تجميع الاختبار وتشغيله. لا تُضمَّن ولا تنتقل أبدًا. JUnit وMockito وAssertJ وTestcontainers.
  • system — مثل provided لكنك تحدد مسارًا صريحًا عبر <systemPath>. تجنّبه: غير محمول وأُهمل فعليًا.
  • import — صالح فقط في <dependencyManagement> مع type=pom. يدمج Bill of Materials (BOM) في إدارة التبعيات. يُغطّى في الدرس التاسع.
<dependencies> <!-- نطاق compile (الافتراضي) --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>32.1.3-jre</version> </dependency> <!-- provided: تُوفّر الحاوية servlet API --> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>6.0.0</version> <scope>provided</scope> </dependency> <!-- runtime: مشغّل JDBC غير مطلوب وقت التجميع --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.3.0</version> <scope>runtime</scope> </dependency> <!-- test: متاحة فقط أثناء تجميع وتشغيل الاختبارات --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.2</version> <scope>test</scope> </dependency> </dependencies>
انضباط النطاق أهم ما يكون لمطوّري المكتبات. إن نشرت مكتبة بتبعية compile-scope على SLF4J ورثها كل مستهلك انتقاليًا. إن نشرت بـ provided scope (مع توثيق المتطلب) يختار المستهلكون ربط SLF4J الخاص بهم. النطاق جزء من عقد مكتبتك.

التبعيات الانتقالية

حين تعتمد على spring-webmvc فإنها بدورها تعتمد على spring-core وspring-beans وغيرها. يقرأ Maven POM كل تبعية ويحلّ الرسم البياني الكامل بشكل تعاودي. هذا هو حل التبعيات الانتقالية — تعلن قطعة واحدة ويسحب Maven شجرة قطع بصمت.

ينتقل النطاق عبر الرسم البياني وفق هذه القواعد: التبعية الانتقالية ذات نطاق compile تبقى compile؛ وذات نطاق runtime تبقى runtime؛ أما ذات نطاق test أو provided فلا تنتقل إطلاقًا. لهذا السبب لا تتسرّب JUnit أبدًا إلى مسارات الفئات في الإنتاج.

لفحص الرسم البياني المحلول كاملًا:

mvn dependency:tree # فلترة قطعة محددة mvn dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind

حل تعارض الإصدارات: قاعدة "الأقرب يفوز"

يحتوي الرسم البياني دائمًا تقريبًا على نفس القطعة بإصدارات متعددة (تبعية الماسة Diamond Dependency). يطبّق Maven قاعدة الأقرب يفوز: الإصدار المُعلَن الأقرب إلى جذر مشروعك في رسم التبعيات يفوز. إن تساوت المسافتان يفوز الأول المُعلَن في POM.

قاعدة الأقرب يفوز حتمية لكنها قد تفاجئك. إن احتاجت المكتبة A إلى Jackson 2.17 واحتاجت المكتبة B إلى Jackson 2.15، وكانت B مُعلَنة أولًا على العمق 2 بينما تسحب A إلى Jackson على العمق 3، ستحصل على 2.15 — وهو قد يكون غير متوافق مع استدعاءات API للمكتبة A.

لا تقبل أبدًا الإصدار الذي تختاره قاعدة "الأقرب يفوز" بصمت. شغّل mvn dependency:tree وmvn dependency:analyze بانتظام. حين يكون إصدار انتقالي خاطئًا تجاوزه صراحةً في <dependencyManagement>: أعلن القطعة بالإصدار الذي تحتاجه فيثبّت Maven كل إشارة في الشجرة على ذلك الإصدار بغض النظر عن العمق.
<!-- تثبيت تبعية انتقالية على إصدار محدد --> <dependencyManagement> <dependencies> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.17.0</version> </dependency> </dependencies> </dependencyManagement>

استبعاد التبعيات الانتقالية غير المرغوبة

أحيانًا تجرّ مكتبة تبعية انتقالية لا تريدها — تنفيذ تسجيل منافس، أو إصدار قديم ضعيف لا يمكنك تجاوزه عالميًا، أو قطعة تتعارض مع بيئة تشغيلك. استخدم <exclusions> لقطعها من الرسم البياني عند موضع الإعلان:

<dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> <version>3.7.0</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-reload4j</artifactId> </exclusion> </exclusions> </dependency>

إضافة مستودع بعيد

بعض القطع غير موجودة على Maven Central (إصدارات Spring التجريبية، بناءات JitPack من GitHub، مستودع شركتك الخاص). أعلنها في POM أو في ~/.m2/settings.xml:

<repositories> <repository> <id>spring-milestones</id> <url>https://repo.spring.io/milestone</url> </repository> </repositories>
فضّل Central والمستودعات الخاصة الموثّقة على المستودعات العامة العشوائية. هجمات الخلط بين التبعيات (dependency confusion) تستبدل قطعة داخلية بأخرى خبيثة عامة. استخدم <mirrorOf> في settings.xml لتوجيه كل حركة المرور عبر بروكسي شركتك (Nexus / Artifactory) الذي يمكنه فحص القطع قبل أن تصل إلى أجهزة المطوّرين.

أوامر التشخيص العملية

  • mvn dependency:tree — الرسم البياني المحلول الكامل مع النطاقات ومصادر الإصدارات.
  • mvn dependency:analyze — يُشير إلى التبعيات المستخدمة غير المُعلَنة والمُعلَنة غير المستخدمة.
  • mvn dependency:resolve -Dclassifier=sources — تنزيل ملفات JAR المصادر للتنقل بها في بيئة التطوير.
  • mvn versions:display-dependency-updates — عرض التبعيات المُعلَنة التي يتوفر لها إصدارات أحدث.

الخلاصة

إدارة التبعيات في Maven تنقسم إلى ثلاث طبقات: النطاقات تُحدّد متى وأين تكون التبعية مرئية؛ والحل الانتقالي يؤتمت رسم التبعيات لكنه يُدخل تعارضات في الإصدارات؛ والمستودعات تحدّد أين يبحث Maven عن القطع. إتقان dependency:tree وفهم قاعدة الأقرب يفوز واستخدام <dependencyManagement> لتثبيت الإصدارات — هذه هي المهارات التي تُميّز مطوّرًا يلصق الإحداثيات فحسب عن مطوّر يمتلك بناءه بالفعل.