وقت القراءة: 15 minutes

هل سبق لك أن عشت تلك اللحظة عندما يكون الأمر بسيطًا`./gradlew build` الذي تشغّله مئة مرة في اليوم يتوقف فجأة عن العمل بسبب خطأ لم يُرَ من قبل ? اضربه بـ أحد عشر خطأً متتاليًا، أضف جرعة جيدة من عدم توافق Docker Engine 29، رشّ Gradle 9 الذي يزيل واجهات برمجة التطبيقات التي كنا نستخدمها منذ عشر سنوات. هنا السجل الكامل.


مخطط 1 — ترحيل Gradle 9 (سلسلة الأخطاء)

الهدف: إظهار سلسلة عدم التوافق.

👉 واضح، يركّز على Gradle.
@startuml
left to right direction
title الترحيل إلى Gradle 9
rectangle "1\nمجلد التقارير" as A
rectangle "2\nmainClass" as B
rectangle "3\nتوافق المصدر" as C
rectangle "4\nتأكيد Groovy" as D

A --> B
B --> C
C --> D

@enduml

الرسم التوضيحي 2 — مكونات JHipster غير المتوافقة

الهدف: عزل المكونات الإضافية المعطوبة

👉 رؤية واضحة للاعتمادات
@startuml
left to right direction
title إضافات بحاجة إلى تصحيح
skinparam shadowing false
skinparam roundcorner 18

rectangle "1\nopenApiGenerate" as A #FFF3E0
rectangle "2\ngitProperties\n2.5.7" as B #FFF3E0
rectangle "3\nإصدارات\nمصححة" as C #E3F2FD
rectangle "4\nجمّع جيدًا" as D #E8F5E9

A --> B
B --> C
C --> D

@enduml

المخطط 3 — Docker 29 / Testcontainers

الهدف: مشكلة وقت التشغيل

👉 تاريخ واضح لوقت التشغيل.
@startuml
left to right direction
title دوكر 29 + Testcontainers
skinparam shadowing false
skinparam roundcorner 18
skinparam defaultFontSize 14

rectangle "1\nالتجميع سليم" as A #E8F5E9
rectangle "2\nاختبارات Cucumber فشل" as B #FFEBEE
rectangle "3\nTestcontainers 1.32\nمرفوض" as C #FFF3E0
rectangle "4\nترقية\nإصدارات" as D #E3F2FD
rectangle "5\nالبناء النهائي ناجح" as E #E8F5E9

A --> B
B --> C
C --> D
D --> E

@enduml

طريقة أخرى لتمثيله لنفسه
@startuml

!option handwritten true

skinparam backgroundColor #FEFEFE
skinparam shadowing false
skinparam defaultFontName Inter
skinparam defaultFontSize 14
skinparam ArrowThickness 2
skinparam RoundCorner 18

title خارطة طريق الاستقرار — Gradle 9 نحو البناء النهائي
left to right direction

rectangle "⚙️ ترحيل Gradle 9" as A #E3F2FD
rectangle "🧩 إضافات JHipster" as B #FFF3E0
rectangle "🐳 Docker 29\nTestcontainers" as C #FCE4EC
rectangle "✅ بناء مستقر" as D #E8F5E9

note bottom of A
reportsDir
mainClass
sourceCompatibility
Groovy assert
end note

note bottom of B
openApiGenerate
gitProperties
versions alignées
end note

note bottom of C
tests Cucumber KO
image Docker rejetée
upgrade libs
end note

A -[#42A5F5]-> B : corrections build.gradle
B -[#FB8C00]-> C : assemble OK
C -[#E53935]-> D : tests réparés

@enduml

المشهد: مشروع JHipster، بناء لم يعد يتم تحميله

المشروع`edster`هو تطبيق JHipster تم إنشاؤه في 2024. نص البناء الجذري`build.gradle`ظل مستقرًا لمدة أشهر. ثم قررنا ترقية النسخة :

مكوّن النسخة المستهدفة

Gradle

9.4.1

جافا

21.0.11-tem (Eclipse Temurin)

JHipster

8.x (الإطار`tech.jhipster:jhipster-framework:8.11.0`)

سبرينغ بوت

3.4.5

Kotlin

2.3.0

محرك دوكر

29.4.1 (API 1.54)

التغيير في نسخة Java يتم عبر SDKMAN :

sdk use java 21.0.11-tem

الإطلاق الأول :

$ ./gradlew build --no-daemon

FAILURE: Build failed with an exception.

* Where:
Build file '/home/.../edster/build.gradle' line: 18

* What went wrong:
An exception occurred applying plugin request [id: 'jhipster.cucumber-conventions']
> Failed to apply plugin 'jhipster.cucumber-conventions'.
   > Could not create task ':cucumberTest'.
      > Could not create task ':consoleLauncherTest'.
         > Could not set unknown property 'reportsDir' for task ':consoleLauncherTest'

لا نكاد ننتقل حتى من تحميل سكريبت البناء. المشكلة تكمن في`buildSrc/src/main/groovy/jhipster.cucumber-conventions.gradle`.

خطأ 1 : المتغير الوهمي `reportsDir

السكريبت`jhipster.cucumber-conventions.gradle`يحتوي :

tasks.register('consoleLauncherTest', JavaExec) {
    dependsOn(testClasses)
    String cucumberReportsDir = file("$buildDir/reports/tests")
    outputs.dir(reportsDir)           // <-- reportsDir n'existe PAS
    classpath = sourceSets["test"].runtimeClasspath
    main = "org.junit.platform.console.ConsoleLauncher"
    // ...
}

المتغير المحدد هو`cucumberReportsDir`. المستعملة هي`reportsDir`-- التي لا تُعرّف في أي مكان.

تصحيح:`outputs.dir(cucumberReportsDir)`

الخطأ 2 : main = "…​" لم يعد موجودًا تحت Gradle 9

إعادة تشغيل فورية :

Could not set unknown property 'main' for task ':consoleLauncherTest' of type org.gradle.api.tasks.JavaExec.

Gradle 9 يحذف الخاصية`main`لمصلحة`mainClass`على المهام`JavaExec`.

تصحيح:`mainClass = "org.junit.platform.console.ConsoleLauncher"`

خطأ 3 : sourceCompatibility لم يعد خاصية للمشروع

خطأ جديد :

Could not set unknown property 'sourceCompatibility' for root project 'edster' of type org.gradle.api.Project.

النص الجذري كان:

sourceCompatibility=17
targetCompatibility=17

Gradle 9 يحذف هذه الخصائص على مستوى الجذر. يجب أن تعيش داخل كتلة`java`.

تصحيح :

java {
    sourceCompatibility = JavaVersion.VERSION_21
    targetCompatibility = JavaVersion.VERSION_21
}

خطأ 4 : تأكيد Groovy بثلاث معاملات

البرنامج القديم كان يحتوي :

assert System.properties["java.specification.version"] == "17" || "21" || "24"

في جروفي,"21" et "24"`strings truthy. التعبير يحل إلى(…​ == "17") || true || true`, ما هو دائمًا صحيح — لكن الصياغة غير صالحة للمطالبة الصارمة المطلوبة.

تصحيح:

assert System.properties["java.specification.version"] in ["17", "21", "23", "24"]

خطأ 5 : الاعتماد الضمني compileKotlin → `openApiGenerate

البناء يتقدم. تم تجميع Kotlin بنجاح. ثم :

Task ':compileKotlin' uses this output of task ':openApiGenerate'
without declaring an explicit or implicit dependency.

Gradle 9 يرفض الاعتماديات الضمنية بين المهام. لأن`compileKotlin`يقرأ المصادر المولدة من`openApiGenerate`, يجب إعلان الرابط.

تصحيحفي`build.gradle`[Nothing]

afterEvaluate {
    tasks.named("compileKotlin").configure {
        dependsOn(tasks.named("openApiGenerate"))
    }
}

Le `afterEvaluate`ضروري لأن`openApiGenerate`هي إضافة (مكوّن OpenAPI Generator) وليس مهمة يمكن الوصول إليها مباشرة خلال مرحلة الإعداد.

خطأ 6 : generateGitProperties ينكسر على `FilterOutputStream.write()

تنجح ترجمة Java. تم معالجة الموارد. ثم:

> Task :generateGitProperties FAILED

No signature of method: java.io.FilterOutputStream.write() is applicable for argument types: (Integer) values: [103]

الإضافة`gradle-git-properties`version 2.5.0 غير متوافق مع Java 21 / Gradle 9. عطل داخلي يحاول استدعاء`write(int)`باستخدام Groovy على تدفق الذي أغلقه

تصحيحفي`gradle.properties`:

gitPropertiesPluginVersion=2.5.7

أخيرًا: يدخل Testcontainers إلى المشهد

حتى الآن، كان مجرد "build script debugging" صرفًا. كل خطأ كان عدم توافق Gradle 9 أو Java 21 في سكريبتات البناء. بعد الست تصحيحات أعلاه،`./gradlew assemble`ينجح بنجاح.

لكن`./gradlew build`ينفذ أيضًا الاختبارات، وتستخدم اختبارات Cucumber Testcontainers لتشغيل PostgreSQL مؤقت.

Could not find a valid Docker environment.

EnvironmentAndSystemPropertyClientProviderStrategy: failed with exception BadRequestException
(Status 400: {"message":"client version 1.32 is too old. Minimum supported API version is 1.40"}
UnixSocketClientProviderStrategy: failed with exception BadRequestException
(Status 400: {"message":"client version 1.32 is too old. Minimum supported API version is 1.40"}

Testcontainers غير قادر على التواصل مع Docker. استراتيجيتين مختلفتين (متغيرات البيئة + مقبس يونكس) تفشلان على نفس الخطأ.

@startuml
skinparam backgroundColor #FEFEFE
skinparam handwritten false

actor "Testcontainers\n1.20.6" as TC
participant "docker-java\n3.4.1" as DJ
participant "Docker محرك\n29.4.1" as DE

TC -> DJ : جديد DockerClient()
DJ -> DE : GET /_ping\nUser-Agent: docker-java/3.4.1\nAPI-Version: 1.32
DE --> DJ : HTTP 400 طلب غير صالح\nإصدار العميل 1.32 قديم جداً.\nالحد الأدنى المدعوم API الإصدار هو 1.40
DJ --> TC : BadRequestException
TC --> TC : لم يتم العثور على البيئة valid\nDocker

note right of DE
Docker يفرض المحرك 29.x
API الحد الأدنى = 1.40
(القيمة الافتراضية القديمة : 1.24)
end note

note left of DJ
docker-java 3.4.1 يرسل
hardcoded API النسخة 1.32
في الرأس HTTP
end note

@enduml

المرحلة 1 : مسار docker-java

أول رد فعل: عميل Docker Java المستخدم من قبل Testcontainers يرسل نسخة API`1.32`في Handshake HTTP الخاص به، لكن Docker Engine 29.4.1 يتطلب أدنى ما هو`1.40`. المشكلة على مستوى العميل، وليس على مستوى الديمون.

التحقق من إصدار docker-java الذي حله Testcontainers 1.20.6 :

$ ./gradlew dependencies --configuration testRuntimeClasspath | grep docker-java

+--- com.github.docker-java:docker-java-api:3.4.1
+--- com.github.docker-java:docker-java-transport:3.4.1

docker-java 3.4.1، يعود إلى Testcontainers 1.20.6. أحدث نسخة عامة من docker-java هي 3.5.1. نحاول إجبار ترقية الإصدار عبر`resolutionStrategy`في الكتلة`configurations` de build.gradle:

configurations {
    all {
        resolutionStrategy.eachDependency { details ->
            if (details.requested.group == "com.github.docker-java"
                && details.requested.name.startsWith("docker-java")) {
                details.useVersion("3.5.1")
            }
        }
    }
}

إعادة تشغيل البناء. نفس الخطأ :`client version 1.32 is too old`.

التحقق من ذاكرة التخزين المؤقت Gradle: تم إعادة تنزيل ملفات JAR بنجاح، لكن الخطأ لا يزال مستمرًا. docker-java 3.5.1 يرسل دائمًا`1.32`. هذه النسخة ليست كافية لمحرك Docker 29.4.1.

عملية تنقية: مسح ذاكرة التخزين المؤقت لـ Gradle بالكامل، فقط للتأكد :

rm -rf ~/.gradle/caches
rm -rf /path/to/project/.gradle

إعادة التشغيل الكاملة بعد الحذف. نفس الخطأ.

المرحلة 2 : البحث على الويب وخيط أريادن جيتهاب

docker-java 3.5.1 غير كافٍ. لم يبقَ إلا البحث عما إذا كان شخص ما قد واجه نفس المشكلة بالضبط.

بحث مستهدف على القضايا testcontainers-java و docker-java:

site:github.com/testcontainers "client version 1.32 is too old"
site:github.com/docker-java "client version 1.32" Docker Engine 29

النتائج الفورية الأولى :

مسار التنقل واضح: محرك Docker 29.x رفع الحد الأدنى لإصدار الـ API له من`1.24`(عطل قديم) إلى`1.40`. Testcontainers 1.20.x يستخدم docker-java 3.4.x الذي يتفاوض على إصدار واجهة برمجة التطبيقات عبر الإرسال`1.32`. الديamon يرفض بلطف لكنه حازم.

فيissue #11491، يرد أحد المُحافظين :

(nothing) مرحبًا، لدي مشروع يعمل بالإصدار 2.0.3 على جهاز تشغيل GH مع المحرك 29.1 وهو يعمل. هل يمكنكم جميعًا من فضلكم التحقق من عدم وجود صراع بين الإصدارات؟ من فضلك، تذكر أن جميع الوحدات كانت مسبوقة بـ`testcontainers-. لذلك، بدءًا من الإصدار 2.x، انتقلنا من`postgresql to testcontainers-postgresql __

هذه الإجابة تحتويمعلومتان حاسمتان:

  1. Testcontainers 2.0.3+ يحل المشكلة

  2. تمالمُسمّون بالبادئة `testcontainers-

المرحلة 3 : فخ القطع الأثرية المعاد تسميتها

إعادة التسمية قاسية. في Testcontainers 2.x، لا يعمل أي من الأسماء القديمة تحت هذا الاسم:

الاسم القديم(1.x) اسم جديد (2.x)

org.testcontainers:postgresql

org.testcontainers:testcontainers-postgresql

org.testcontainers:jdbc

org.testcontainers:testcontainers-jdbc

org.testcontainers:junit-jupiter

org.testcontainers:testcontainers-junit-jupiter

org.testcontainers:testcontainers

غير متغير

المحاولة الأولى : تطبيق BOM Testcontainers 2.0.5 وأسماء artifacts الجديدة في`build.gradle`.

dependencies {
    testImplementation platform("org.testcontainers:testcontainers-bom:2.0.5")
    testImplementation "org.testcontainers:testcontainers-postgresql"
    testImplementation "org.testcontainers:testcontainers-jdbc"
    testImplementation "org.testcontainers:testcontainers-junit-jupiter"
    testImplementation "org.testcontainers:testcontainers"
}

./gradlew build

FAILURE: Could not find org.testcontainers:jdbc:2.0.5.
Could not find org.testcontainers:junit-jupiter:2.0.5.

لا يكفي BOM testcontainers-bomلماذا؟ لأن Spring Boot 3.4.5 يعرض BOM الخاص به لإدارة الاعتماديات (spring-boot-dependencies) الذياكسبعلى BOM الخاص بـ Testcontainers في الحل العابر. Spring Boot يحدّد`testcontainers` à 1.20.6، وبالتالي فإن جميع العناصر التي ليست صريحةً في BOM Spring Boot (مثل الجدد`testcontainers-*) يحلون نحو الأسماء القديمة`1.20.6-- لم تعد موجودة في هذا الإصدار.

En inspectant le dependencies --configuration testRuntimeClasspath, najd :

+--- org.testcontainers:testcontainers -> 1.20.6 (*)
|    \--- org.testcontainers:testcontainers:2.0.5 -> 1.20.6

Le →`يشير إلى الاستبدال : Testcontainers 2.0.5 هو مطلوب، لكن Spring Boot يفرض`1.20.6.

@startuml
skinparam backgroundColor #FEFEFE
skinparam handwritten false
skinparam componentStyle rectangle

left to right direction

component "build.gradle\n(المستهلك)" as BG #E3F2FD {
    rectangle "طلب\n`testcontainers-bom:2.0.5`" as REQ
    rectangle "طلب\n`testcontainers-postgresql`" as REQ2
}

component "Maven المركزية" as MC #FFF3E0

component "الإعلانات" as DEC #E8F5E9 {
    rectangle "`testcontainers-bom:2.0.5`\nثابت 2.0.5" as BOM2
    rectangle "`spring-boot-dependencies:3.4.5`\nثابت 1.20.6" as BOM1
}

component "الحل Gradle" as RES #FFEBEE {
    rectangle "النسخة الفائزة\n`testcontainers:1.20.6`" as WIN #EF9A9A
}

BG --> DEC : "يحلل BOMs"
BOM2 --> RES : "يقترح 2.0.5"
BOM1 --> RES : "يقترح 1.20.6"
WIN -> MC : "يحمل\ntestcontainers:1.20.6"

note bottom of WIN
Spring Boot BOM السيارة الرئيسية
الإضافة سبرنغ-بوت تطبق
إدارة تبعياتها
بعد حلّ المستهلك
end note

@enduml

المرحلة 4: إجبار الدقة

تصبح الاستراتيجية مزدوجة:

  1. إجبار docker-java على استخدام الإصدار 3.7.1 (المختبر كمتوافق مع Docker Engine 29)

  2. إجبار Testcontainers core على الإصدار 2.0.5 عبر تجاوز BOM الخاص بـ Spring Boot

@startuml
skinparam backgroundColor #FEFEFE
skinparam handwritten false
skinparam componentStyle rectangle

left to right direction

component "build.gradle\n(مع resolutionStrategy)" as BG #E8F5E9 {
    rectangle "BOM Testcontainers\n`2.0.5`" as BOM2
    rectangle "`resolutionStrategy\n.eachDependency`" as RS #66BB6A
}

component "تم حل النزاع" as RESOLVED #E3F2FD {
    rectangle "docker-java\n`3.7.1` (إجباري)" as DJ
    rectangle "testcontainers\n`2.0.5` (إجباري)" as TC #81C784
    rectangle "testcontainers-postgresql\n`2.0.5`" as TPG
    rectangle "testcontainers-jdbc\n`2.0.5`" as TJ
    rectangle "testcontainers-junit-jupiter\n`2.0.5`" as TJJ
}

component "BOM Spring Boot\n(ضمني, تم تجاوزه)" as BOM1 #FFEBEE

component "Maven Central" as MC #FFF3E0

BG --> RESOLVED : "BOM Testcontainers 2.0.5\nيمر مباشرة"
RS -> DJ : "فرض 3.7.1"
RS -> TC : "فرض 2.0.5"
RESOLVED --> MC : "تنزيل الـ JARs\nالمتوافقة Docker 29"
BOM1 -[#gray,dashed]-> RESOLVED : "تم تجاهله على testcontainers\nو docker-java"

note bottom of RS
ال `eachDependency` يعترض
الحل Gradle AVANT
أن BOM Spring Boot لا يفوز
هذا تدخل جراحي
end note

@enduml

التصحيح النهائيفي`build.gradle` :

configurations {
    all {
        resolutionStrategy.eachDependency { details ->
            if (details.requested.group == "com.github.docker-java"
                && details.requested.name.startsWith("docker-java")) {
                details.useVersion("3.7.1")
            }
            if (details.requested.group == "org.testcontainers"
                && details.requested.name == "testcontainers") {
                details.useVersion("2.0.5")
            }
        }
    }
}

dependencies {
    testImplementation platform("org.testcontainers:testcontainers-bom:2.0.5")
    testImplementation "org.testcontainers:testcontainers-jdbc"
    testImplementation "org.testcontainers:testcontainers-junit-jupiter"
    testImplementation "org.testcontainers:testcontainers-postgresql"
    testImplementation "org.testcontainers:testcontainers"
    // ... autres dépendances Spring Boot
}

Le `resolutionStrategy.eachDependency`الطريقة الوحيدة لتجاوز BOM`spring-boot-dependencies`تم إدارته بواسطة إضافة Spring Boot. اختبار المساواة على`details.requested.name == "testcontainers"`هو متعمد أن يكون مقيدًا: نحن نُجبر فقط الوحدة الأساسية. الوحدات`testcontainers-*`يتبعون BOM من Testcontainers 2.0.5 الذي أعلنا صراحةً.

النتيجة النهائية

$ ./gradlew build --no-daemon

> Task :consoleLauncherTest
[2 containers found]
[2 containers started]
[2 containers successful]

BUILD SUCCESSFUL in 1m 16s

Testcontainers 2.0.5 + docker-java 3.7.1 تنجحان أخيرًا في إنشاء حاويات PostgreSQL الخاصة بهما عبر Docker Engine 29.4.1. الخطأ الوظيفي النهائي (HTTP 500 في اختبار Cucumber) هو مشكلة تطبيقية totally مستقل عن سكريبت البناء.

جدول ملخص التصحيحات

(empty) ملف مشكلة تصحيح

1

buildSrc/src/main/groovy/jhipster.cucumber-conventions.gradle

متغير`reportsDir`غير موجودة

outputs.dir(cucumberReportsDir)

2

buildSrc/src/main/groovy/jhipster.cucumber-conventions.gradle

`main`Gradle 9 معطل

mainClass

3

build.gradle

`sourceCompatibility`مستوى أعلى ممنوع Gradle 9

كتلة`java { sourceCompatibility = JavaVersion.VERSION_21 }`

4

build.gradle

إدعاء Groovy غير صالح من حيث بناء الجملة

assert …​ in ["17", "21", "23", "24"]

5

build.gradle

الاعتماد الضمني`compileKotlin` → openApiGenerate

afterEvaluate { tasks.named("compileKotlin").dependsOn("openApiGenerate") }

6

gradle.properties

`gitPropertiesPluginVersion`غير متوافق مع Java 21

2.5.7

7

ذاكرة التخزين المؤقت Gradle

docker-java 3.5.1 تم اختباره، تم حلّه لكنه غير كافٍ

تنظيف + مرحلة docker-java 3.7.1

8

build.gradle

Testcontainers 1.20.6 غير متوافق مع محرك دوكير 29

إجبار Testcontainers 2.0.5 + البادئة`testcontainers-*`+ resolutionStrategy مقابل Spring Boot BOM

خاتمة

إذا فشل البناء JHipster/Gradle بعد ترقية الإصدار إلى Gradle 9 + Docker Engine 29، فإن الأخطاء تنقسم إلى فئتين:

أخطاء سكريبت البناء(الست الأولى) : Gradle 9 يزيل خصائصًا وصيغًا كانت تعمل لسنوات (sourceCompatibility=, main =, reportsDir). إنها هجرات ميكانيكية بمجرد أن تعرف واجهة برمجة التطبيقات الجديدة.

أخطاء وقت تشغيل Docker(الأخيران) : محرك Docker 29.4.1 يستبعد عملاء API أقل من 1.40. Testcontainers 1.20.6 (مفروض من قبل Spring Boot 3.4.5) يستخدم docker-java 3.4.1 الذي يرسل 1.32. فقط Testcontainers 2.0.5 + docker-java 3.7.1 يحلّ المشكلة، مع إعادة تسمية القطع الفنية وفرض النسخة عبر`resolutionStrategy.eachDependency`كطريقة وحيدة لتجاوز BOM الضمني لـ Spring Boot.

البرنامج النصي لبناء Gradle في جذر المشروع أصبح نظيفًا الآن.`./gradlew build`يمر على Java 21، Gradle 9.4.1، Spring Boot 3.4.5 ومحرك Docker 29.4.1.

مقالات ذات صلة