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

تبعيات Gradle والمهام المخصصة

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

تبعيات Gradle والمهام المخصصة

تتمحور قوة Gradle حول نظامين متكاملين: نموذج إدارة التبعيات — المبني على تهيئات مسمّاة — ورسم بياني للمهام يتيح لك تأليف وتمديد وأتمتة كل جانب من جوانب عملية البناء. في هذا الدرس ستتقن كلا النظامين بالعمق المطلوب في مشاريع الإنتاج.

تهيئات التبعيات — الصورة الكاملة

في Gradle، التهيئة هي دلو مسمّى يجمع تبعيات لغرض محدد (التجميع، تشغيل الاختبارات، توليد معالجات التعليقات التوضيحية، وغيرها). تسجّل ملحقات Java وApplication/Library عدة تهيئات تلقائيًا.

أهم التهيئات في مشروع Java حديث:

  • implementation — التبعية موجودة في مسار تجميع هذه الوحدة لكنها لا تُكشف للمستهلكين الذين يعتمدون على هذه الوحدة. استخدمها لغالبية تبعياتك.
  • api (ملحق المكتبة فقط) — مثل implementation لكنها تكشف التبعية للمستهلكين. استخدمها باعتدال: الإفراط في api يُجبر على إعادة تجميع غير ضرورية عبر رسم مشروع بالكامل.
  • compileOnly — موجودة أثناء التجميع وغائبة عن مسار التشغيل. مثال كلاسيكي: Lombok، وواجهات برمجة Jakarta التوضيحية عندما يوفّرها حاوي التشغيل.
  • runtimeOnly — غائبة وقت التجميع وموجودة وقت التشغيل. مثال نموذجي: مشغّلات JDBC، وربطات SLF4J مثل logback-classic.
  • testImplementation — مثل implementation لكن مقيّدة بمجموعة مصادر الاختبار.
  • testCompileOnly / testRuntimeOnly — معادلات مقيّدة بالاختبار للحالات أعلاه.
  • annotationProcessor — معالجات التعليقات التوضيحية التي يشغّلها javac؛ لا تُوضع على مسار التطبيق نهائيًا.
لماذا يهم الفرق بين implementation وapi؟ تتابع Gradle أي ملفات jar تظهر على مسار التجميع للمشاريع الفرعية. إن استخدمت api، فأي تغيير في تلك التبعية العابرة يُفجّر إعادة تجميع كل وحدة فرعية. في بناء متعدد الوحدات الكبير قد يُضيف ذلك دقائق على البنيات التدريجية. الزم implementation افتراضيًا ولا ترقِّ إلى api إلا حين يكون النوع جزءًا من سطح واجهتك البرمجية العامة.

تعريف التبعيات

تُعرَّف التبعيات داخل كتلة dependencies { } في build.gradle (Groovy) أو build.gradle.kts (Kotlin DSL). الشكل القياسي هو إحداثيات Maven: group:artifact:version.

// build.gradle.kts — كتلة تبعيات واقعية لخدمة Spring Boot dependencies { // الإطار الأساسي — على مسار التجميع والتشغيل، غير مكشوف للمستهلكين implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-data-jpa") // وقت التجميع فقط — Lombok يولّد الكود؛ الملف غير مطلوب وقت التشغيل compileOnly("org.projectlombok:lombok:1.18.32") annotationProcessor("org.projectlombok:lombok:1.18.32") // وقت التشغيل فقط — مشغّل JDBC؛ لا كود Spring يستدعيه مباشرة بالاسم runtimeOnly("org.postgresql:postgresql:42.7.3") // تبعيات الاختبار testImplementation("org.springframework.boot:spring-boot-starter-test") testRuntimeOnly("org.junit.platform:junit-platform-launcher") }
استخدم قائمة الفواتير (BOM) لمزامنة الإصدارات عبر عائلة إطار عمل. استوردها مرة واحدة عبر platform(...) واحذف سلاسل الإصدار من التبعيات الفردية. BOM الخاصة بـ Spring Boot هي المثال القياسي.
dependencies { implementation(platform("org.springframework.boot:spring-boot-dependencies:3.3.0")) // لا حاجة لإصدار — تحدّده BOM تلقائيًا implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.core:jackson-databind") }

حل التبعيات واستراتيجيات تعارض الإصدارات

عندما يحتوي رسم التبعيات على نفس المكتبة بإصدارين مختلفين، تطبّق Gradle اختيار الإصدار التفاؤلي افتراضيًا: تختار أعلى إصدار مطلوب. يختلف هذا عن استراتيجية Maven الأقرب-يفوز وعادةً يُنتج نتيجة أكثر صحة.

يمكنك تجاوز سلوك الحل عند الحاجة:

// إجبار إصدار محدد لتبعية عابرة (استخدم باعتدال) configurations.all { resolutionStrategy { force("com.google.guava:guava:33.2.1-jre") } } // فشل البناء إن بقي أي تعارض إصدار دون حل — مفيد في عمليات التدقيق الصارمة configurations.all { resolutionStrategy.failOnVersionConflict() }

المهام المخصصة — الأساسيات

كل إجراء في بناء Gradle هو مهمة. للمهمة مدخلات ومخرجات ومجموعة من الإجراءات. يستخدم نظام البناء التدريجي في Gradle المدخلات والمخرجات لتحديد ما إذا كانت المهمة محدَّثة ويمكن تخطّيها كليًا — وهذا المصدر الرئيسي لميزة سرعة Gradle على Maven في المشاريع الكبيرة.

أبسط طريقة لتعريف مهمة مخصصة هي استخدام واجهة برمجة tasks.register، التي تُنشئ المهمة بشكل كسول (لا تُهيَّأ إلا حين يحتاجها شيء ما فعليًا):

// tasks.register مُفضَّل على tasks.create القديم (الفوري) tasks.register("printBuildInfo") { group = "build info" description = "Prints project coordinates to the console." doLast { println("Project: ${project.name}") println("Version: ${project.version}") println("Group: ${project.group}") } }

شغّلها بـ ./gradlew printBuildInfo. حقلا group وdescription يجعلان المهمة قابلة للاكتشاف عبر ./gradlew tasks --all.

المهام المكتوبة — المنهج الاحترافي

تمتد معظم المهام الحقيقية من نوع مدمج مثل Copy أو Jar أو JavaExec أو Exec أو Zip. تحصل المهام المكتوبة على دعم البناء التدريجي تلقائيًا عند تعريف المدخلات والمخرجات بشكل صحيح.

import org.gradle.api.tasks.Copy // تحزيم ملفات الإعداد في ملف ضغط منفصل للنشر tasks.register<Zip>("packageConfig") { group = "distribution" description = "Zips all environment config files for deployment." from(layout.projectDirectory.dir("src/main/config")) include("*.yml", "*.properties") // المخرجات — تتبّعها Gradle لفحوصات UP-TO-DATE archiveFileName.set("config-${project.version}.zip") destinationDirectory.set(layout.buildDirectory.dir("dist")) } // مهمة JavaExec تشغّل أداة ترحيل قاعدة بيانات tasks.register<JavaExec>("migrateDb") { group = "database" description = "Runs Flyway migrations against the local database." classpath = sourceSets["main"].runtimeClasspath mainClass.set("org.flywaydb.commandline.Main") args("migrate", "-url=jdbc:postgresql://localhost/mydb") // هذه المهمة لا تكون UP-TO-DATE أبدًا — تعمل دائمًا عند استدعائها outputs.upToDateWhen { false } }

تبعيات المهام والترتيب

تشكّل المهام رسمًا بيانيًا موجّهًا لا دوريًا (DAG). تتحكم في العلاقات بثلاث آليات:

  • dependsOn — يضمن تشغيل المهمة المسمّاة قبل هذه المهمة ويُشغّلها فعليًا.
  • mustRunAfter — يُلزم ترتيبًا عندما تكون كلتا المهمتين مجدوَلتين لكن لا يُفعّل المهمة الأخرى.
  • finalizedBy — يشغّل مهمة بعد هذه المهمة حتى لو فشلت (مفيد للتنظيف / توليد تقرير الاختبار).
tasks.register("integrationTest") { group = "verification" description = "Runs integration tests against a live database." // ضمان بناء ملف jar قبل بدء اختبارات التكامل dependsOn(tasks.named("jar")) // تشغيل مولّد التقرير دائمًا بعد ذلك، حتى عند الفشل finalizedBy(tasks.named("generateIntegrationReport")) doLast { println("Running integration tests...") } } tasks.named("check") { // إضافة integrationTest لدورة حياة check القياسية دون استبدالها dependsOn(tasks.named("integrationTest")) }
تجنّب tasks.create (التسجيل الفوري). يهيّئ المهمة فورًا عند وقت التهيئة — في كل مرة يُقيَّم فيها سكربت البناء — حتى لو لن تعمل المهمة أبدًا. مع عشرات المهام المخصصة يُهدر ذلك ثوانٍ في كل استدعاء لـ Gradle. استخدم دائمًا tasks.register (الكسول) بدلًا من ذلك.

البنيات التدريجية والتخزين المؤقت

لكي تكون المهام المخصصة تدريجية، عرِّف مدخلاتك ومخرجاتك بشكل صريح باستخدام تعليقات خاصيات Gradle التوضيحية أو واجهة برمجة inputs/outputs:

import org.gradle.api.DefaultTask import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.* abstract class GenerateVersionFile : DefaultTask() { @get:Input abstract val version: Property<String> @get:OutputFile abstract val outputFile: RegularFileProperty @TaskAction fun generate() { outputFile.get().asFile.writeText("version=${version.get()}\n") } } tasks.register<GenerateVersionFile>("generateVersion") { version.set(project.version.toString()) outputFile.set(layout.buildDirectory.file("generated/version.properties")) }

مع وجود @Input و@OutputFile، ستتخطى Gradle المهمة إن لم تتغيّر سلسلة الإصدار منذ آخر بناء — ومع تفعيل ذاكرة التخزين المؤقت للبناء (org.gradle.caching=true في gradle.properties)، يمكنها حتى استعادة المخرجات من ذاكرة تخزين بعيدة مشتركة بين عوامل CI، مما يُنتج أوقات إعادة بناء شبه صفرية للعمل غير المتغيّر.

الخلاصة

تمنحك تهيئات التبعيات (implementation وapi وcompileOnly وruntimeOnly وannotationProcessor) تحكمًا دقيقًا فيما يظهر على كل مسار وما يُكشف للمستهلكين. أما المهام المخصصة — المسجَّلة بكسل عبر tasks.register، والمكتوبة لدعم التدريج، والمرتبطة عبر dependsOn/finalizedBy — فتتيح لك توسيع البناء بأي أتمتة مع إبقائه سريعًا. عرِّف المدخلات والمخرجات بدقة وستتولى آليتا التدريج والتخزين المؤقت في Gradle الباقي.