عندما يغلي Gradle 9، Docker 29 و Testcontainers طحالهم في مرق: تصحيح أخطاء بناء JHipster
نُشر في 25 April 2026
- المشهد: مشروع JHipster، بناء لم يعد يتم تحميله
- خطأ 1 : المتغير الوهمي `reportsDir
- الخطأ 2 :
main = "…"لم يعد موجودًا تحت Gradle 9 - خطأ 3 :
sourceCompatibilityلم يعد خاصية للمشروع - خطأ 4 : تأكيد Groovy بثلاث معاملات
- خطأ 5 : الاعتماد الضمني
compileKotlin→ `openApiGenerate - خطأ 6 :
generateGitPropertiesينكسر على `FilterOutputStream.write() - أخيرًا: يدخل Testcontainers إلى المشهد
- المرحلة 1 : مسار docker-java
- المرحلة 2 : البحث على الويب وخيط أريادن جيتهاب
- المرحلة 3 : فخ القطع الأثرية المعاد تسميتها
- المرحلة 4: إجبار الدقة
- النتيجة النهائية
- جدول ملخص التصحيحات
- خاتمة
وقت القراءة: 15 minutes
هل سبق لك أن عشت تلك اللحظة عندما يكون الأمر بسيطًا`./gradlew build` الذي تشغّله مئة مرة في اليوم يتوقف فجأة عن العمل بسبب خطأ لم يُرَ من قبل ? اضربه بـ أحد عشر خطأً متتاليًا، أضف جرعة جيدة من عدم توافق Docker Engine 29، رشّ Gradle 9 الذي يزيل واجهات برمجة التطبيقات التي كنا نستخدمها منذ عشر سنوات. هنا السجل الكامل.
مخطط 1 — ترحيل Gradle 9 (سلسلة الأخطاء)
الهدف: إظهار سلسلة عدم التوافق.
@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
الهدف: عزل المكونات الإضافية المعطوبة
@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
النتائج الفورية الأولى :
-
testcontainers-java#11210—
[Bug]: client version 1.32 is too old. Minimum supported API version is 1.44 -
testcontainers-java#11212—
[Bug]: Docker 29.0.0 could not find a valid Docker environment -
testcontainers-java#11491—
[Bug]: Incompatibility in Ubuntu Github runners with Docker Engine 29.1.*
مسار التنقل واضح: محرك 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 __
هذه الإجابة تحتويمعلومتان حاسمتان:
-
Testcontainers 2.0.3+ يحل المشكلة
-
تمالمُسمّون بالبادئة `testcontainers-
المرحلة 3 : فخ القطع الأثرية المعاد تسميتها
إعادة التسمية قاسية. في Testcontainers 2.x، لا يعمل أي من الأسماء القديمة تحت هذا الاسم:
| الاسم القديم(1.x) | اسم جديد (2.x) |
|---|---|
|
|
|
|
|
|
|
غير متغير |
المحاولة الأولى : تطبيق 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: إجبار الدقة
تصبح الاستراتيجية مزدوجة:
-
إجبار docker-java على استخدام الإصدار 3.7.1 (المختبر كمتوافق مع Docker Engine 29)
-
إجبار 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 |
|
متغير`reportsDir`غير موجودة |
|
2 |
|
`main`Gradle 9 معطل |
|
3 |
|
`sourceCompatibility`مستوى أعلى ممنوع Gradle 9 |
كتلة`java { sourceCompatibility = JavaVersion.VERSION_21 }` |
4 |
|
إدعاء Groovy غير صالح من حيث بناء الجملة |
|
5 |
|
الاعتماد الضمني`compileKotlin` → |
|
6 |
|
`gitPropertiesPluginVersion`غير متوافق مع Java 21 |
|
7 |
ذاكرة التخزين المؤقت Gradle |
docker-java 3.5.1 تم اختباره، تم حلّه لكنه غير كافٍ |
تنظيف + مرحلة docker-java 3.7.1 |
8 |
|
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.