زمان خواندن : 15 minutes

شما قبلا این لحظه را تجربه کرده‌اید که یک ساده`./gradlew build`که صد بار در روز اجرا می‌کنید ناگهان به دلیل خطایی که تا به حال نبوده، قطع می‌شود؟ آن را به ۱۱ خطای متوالی ضرب کنید، یک مقدار مناسب ناسازگاری Docker Engine ۲۹ اضافه کنید، Gradle ۹ را بریزید که API‌هایی را که از ده سال قبل استفاده می‌کردیم محو می‌کند. این لاگ کامل است.


خط نمودار 1 — مهاجرت Gradle 9 (رشتهٔ خطا)

هدف : نمایش سری ناسازگاری‌ها

👉 واضح، متمرکز بر Gradle.
@startuml
left to right direction
title مهاجرت Gradle 9
rectangle "1
دایرکتوری گزارشات" as A
rectangle "2
mainClass" as B
rectangle "3
سازگاری منبع" 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
نسخه‌ها
اصلاح‌شده" as C #E3F2FD
rectangle "4
تجميع OK" as D #E8F5E9

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

@enduml

نمودار 3 — Docker 29 / Testcontainers

هدف : مشکل زمان اجرا.

👉 تاریخ واضح زمان اجرا
@startuml
left to right direction
title Docker 29 + Testcontainers

skinparam shadowing false
skinparam roundcorner 18
skinparam defaultFontSize 14

rectangle "1
ترکیب کن OK" as A #E8F5E9
rectangle "2
تست‌های Cucumber ناموفق" as B #FFEBEE
rectangle "3\nTestcontainers 1.32\nرد شده" as C #FFF3E0
rectangle "4
ارتقا
نسخ‌ها" as D #E3F2FD
rectangle "5
ساخت نهایی موفق" 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 "⚙️ Migration Gradle ۹" 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، یک build که بارگذاری نمی‌شود

پروژه`edster`این یک برنامه JHipster است که در سال 2024 تولید شده است. script ساخت ریشه`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

کاتلین

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'

ما حتی از بارگذاری اسکریپت build هم خارج نمی‌شویم. مشکل در`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`-- که در هیچ جای تعریف نشده است.

تصحیح(Empty)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"`

خطا ۳: 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
}

خطای ۴: ادعای سه‌عمله Groovy

اسکریپت قدیمی شامل بود:

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

در گرووی,"21" et "24"`همراه با رشته‌های truthy هستند. عبارت به(…​ == "17") || true || true`، که همیشه درست است — اما سینتکس برای ادعای دقیق مورد نظر معتبر نیست.

تصحیح :

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

خطا 5 : وابستگی ضمنی compileKotlin → `openApiGenerate

ساخت پیش می‌رود. کامپایل کاتلین موفق بود. سپس :

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

Gradle 9 وابستگی‌های ضمنی بین کارها را رد می‌کند. چون`compileKotlin`منابع تولید شده توسط را می‌خواند`openApiGenerate`، باید لینک را اعلام کرد.

تصحیحدر`build.gradle`:

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

Le `afterEvaluate`ضروری است زیرا`openApiGenerate`یک افزونه (پلاگین OpenAPI Generator) است و نه کاری که مستقیماً در مرحله پیکربندی قابل دسترسی است.

خطای 6 : generateGitProperties در FilterOutputStream.write() شکست می‌خورد

کامپایل جاوا موفق می‌شود. منابع پردازش می‌شوند. سپس :

> Task :generateGitProperties FAILED

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

افزونه`gradle-git-properties`نسخه 2.5.0 با Java 21 / Gradle 9 ناسازگار است. یک bug داخلی سعی می‌گیرد صدا بزند`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
1.20.6" as TC
participant "docker-java\n3.4.1" as DJ
participant "Docker Engine
29.4.1" as DE

TC -> DJ : new DockerClient()
DJ -> DE : GET /_ping\nUser-Agent: docker-java/3.4.1\nAPI-Version: 1.32
DE --> DJ : HTTP 400 Bad Request\nclient version 1.32 is too old.\nMinimum supported API version is 1.40
DJ --> TC : BadRequestException
TC --> TC : Could not find a valid\nDocker environment

note right of DE
  Docker Engine 29.x impose
  API minimum = 1.40
  (défaut ancien : 1.24)
end note

note left of DJ
  docker-java 3.4.1 envoie
  hardcoded API version 1.32
  dans l'en-tête HTTP
end note

@enduml

مرحله 1 : مسیر docker-java

اولین واکنش: کلاینت Docker Java استفاده شده توسط Testcontainers نسخه API را ارسال می‌کند`1.32`در handshake HTTP آن، اما Docker Engine 29.4.1 حداقل را می‌طلبد`1.40`.مشکل در سمت کلاینت است، نه در daemon.

بررسی نسخه 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(Note: No output)

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 Engine 29.4.1 کافی نیست.

عمل تطهیر: خالی کردن کامل کش Gradle، فقط برای اطمینان :

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

بارگیری کامل پس از پاک‌سازی. همان خطا

فاز 2: جستجوی وب و مسیرbreadcrumb GitHub

docker-java 3.5.1 کافی نیست. کاری که مانده فقط جستجو است تا ببینیم آیا کسی این مشکل دقیق را تجربه کرده است.

جستجوی هدفمند بر روی issues 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 Engine 29.x حداقل نسخه API خود را بالاتر برده از`1.24`(معایب قدیمی) به`1.40`Testcontainers 1.20.x از docker-java 3.4.x استفاده می‌کند که با ارسال negotiated API version`1.32`. daemon به صورت مهذب اما قطعی رد می‌کند.

در Issue #11491، یک نگهدارنده پاسخ می‌دهد:

_ سلام، من پروژه‌ای دارم که با نسخه 2.0.3 روی یک رانر GH با موتور 29.1 در حال اجرا است و کار می‌کند. لطفاً همگی بررسی کنید که هیچ نسخه‌ای در تعارض نباشد. لطفاً به یاد داشته باشید که تمام ماژول‌ها با …​ پیشوند گذاشته شده‌اند`testcontainers-. بنابراین، از نسخهٔ ۲.x شروع کردیم که ما از`postgresql to testcontainers-postgresql _

این پاسخ شاملdo etelaat hayati :

  1. Testcontainers 2.0.3+ مشکل را حل می‌کند

  2. ماژول‌ها بودندتغییر نام شده با پیشوند `testcontainers-

مرحله ۳: جالبه artefact‌های باز نام‌گذاری

تغییر نام سخت است. در 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 و نام‌هایartifact جدید در`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.

بوم testcontainers-bom کافی نیست.چرا؟ چون Spring Boot 3.4.5 BOM خود مدیریت وابستگی‌ها را نشان می‌دهد (spring-boot-dependencies) کهاو برنده می‌شوددر BOM Testcontainers در حل transitive. Spring Boot رفع می‌کند`testcontainers` à 1.20.6, و بنابراین تمام آرتیفکت‌های که به‌صورت صریح در BOM Spring Boot نیستند (مثل جدید`testcontainers-*) به سمت نام‌های قدیمی حل می‌شوند`1.20.6-- که در این نسخه وجود ندارند.

در بررسی`dependencies --configuration testRuntimeClasspath`, به شرح زیر:

+--- 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
(مصرف‌کننده)" as BG #E3F2FD {
    rectangle "درخواست
`testcontainers-bom:2.0.5" as REQ
    rectangle "درخواست
`testcontainers-postgresql" as REQ2
}

component "مخزن مرکزی Maven" as MC #FFF3E0

component "بیانیه‌ها" as DEC #E8F5E9 {
    rectangle "`testcontainers-bom:2.0.5`\nfixe 2.0.5" as BOM2
    rectangle "spring-boot-dependencies:3.4.5`
ثابت 1.20.6" as BOM1
}

component "حل Gradle" as RES #FFEBEE {
    rectangle "نسخهٔ برنده
`testcontainers:1.20.6" as WIN #EF9A9A
}

BG --> DEC : "BOMها را تحلیل می‌کند"
BOM2 --> RES : "پیشنهاد 2.0.5"
BOM1 --> RES : "پیشنهاد 1.20.6"
WIN -> MC : "دانلود
testcontainers:1.20.6"

note bottom of WIN
  Spring Boot BOM prime car
  le plugin spring-boot applique
  ses dépendances de gestion
  après la résolution du consommateur
end note

@enduml

مرحله ۴ : اجبار به حل

استراتژی دوگانه می‌شود :

  1. دocker-java را به نسخه ۳.۷٫۱ اجبار کنید (که به‌عنوان سازگار با Docker Engine ۲۹ آزمایش شده)

  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
(با resolutionStrategy)" as BG #E8F5E9 {
    rectangle "بوم Testcontainers
`2.0.5" as BOM2
    rectangle "resolutionStrategy\n.eachDependency" as RS #66BB6A
}

component "اختلاف حل شد" as RESOLVED #E3F2FD {
    rectangle "docker-java
`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
`2.0.5" as TJ
    rectangle "testcontainers-junit-jupiter
`2.0.5" as TJJ
}

component "BOM Spring Boot
(ضمنی, بازنویسی)" as BOM1 #FFEBEE

component "موان سنترال" as MC #FFF3E0

BG --> RESOLVED : "BOM Testcontainers 2.0.5
مستقیماً می‑گذراند"
RS -> DJ : "نیرو 3.7.1"
RS -> TC : "نیروی 2.0.5"
RESOLVED --> MC : "دانلود کن فایل‌های JAR\nسازگار با Docker 29"
BOM1 -[#gray,dashed]-> RESOLVED : "نادیده گرفته شده در testcontainers\net docker-java"

note bottom of RS
  Le `eachDependency` intercepte
  la résolution Gradle AVANT
  que le BOM Spring Boot ne gagne.
  C'est une intervention chirurgicale.
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"`به صورت عمدی محدود است: ما فقط ماژول core را اجبار می‌کنیم. ماژول‌ها`testcontainers-*`که بوم 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) یک مشکل برنامه‌ای است که به‌طور کامل مستقل از اسکریپت ساخت است.

جدول خلاصه تصحیح‌ها

# فایل مشکل تصحیح

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`top-level ممنوع Gradle ۹

بلوک`java { sourceCompatibility = JavaVersion.VERSION_21 }`

4

build.gradle

سنتکس assertion Groovy نامعتبر

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

5

build.gradle

وابستگی ضمنی`compileKotlin`→openApiGenerate

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

6

gradle.properties

`gitPropertiesPluginVersion`جاوا ۲۱ ناصازگار

2.5.7

7

کش Gradle

docker-java 3.5.1 آزمایش شده، حل‌شده اما ناکافی

حذف + عبور docker-java 3.7.1

8

build.gradle

Testcontainers 1.20.6 ناسازگار با موتور Docker 29

اجبار Testcontainers 2.0.5 + پیشوند`testcontainers-*`+ resolutionStrategy مقابل Spring Boot BOM

نتیجه

اگر build JHipster/Gradle شما پس از ارتقا به Gradle 9 + Docker Engine 29 خراب شود، خطاها به دو دسته تقسیم می‌شوند:

خطاهای اسکریپت ساخت(۶ اول) : Gradle 9 ویژگی‌ها و سینتکس‌هایی را حذف می‌کند که سال‌ها کار می‌کردند (sourceCompatibility=, main =, reportsDir). اینها مهاجرات مکانیکی هستند پس از آشنایی با API جدید.

خطاهای زمان اجرا Docker(دو آخرین) : Docker Engine 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 مشکل را حل می‌کند، با تغییر نام artifact‌ها و اجبار نسخه از طریق`resolutionStrategy.eachDependency`به عنوان تنها راه برای دور زدن BOM ضمنی Spring Boot.

اسکریپت ساخت Gradle در ریشه پروژه اکنون پاک است.`./gradlew build`بر Java 21، Gradle 9.4.1، Spring Boot 3.4.5 و Docker Engine 29.4.1 می‌گذرد

مقالات مرتبط