waktu membaca : 15 minutes

Anda pernah mengalami momen ketika sesuatu yang sederhana`./gradlew build`yang Anda jalankan seratus kali sehari tiba-tiba mengalami crash pada kesalahan yang belum pernah terlihat? Kalikan dengan sebelas kesalahan berturut-turut, tambahkan dosis incompatibilitas Docker Engine 29, taburkan Gradle 9 yang menghilangkan API yang telah kita gunakan selama sepuluh tahun. Inilah catatan lengkapnya.


Diagram 1 — Migrasi Gradle 9 (rantai kesalahan)

Tujuan: menampilkan rangkaian ketidaksesuaian

👉 Jelas, berfokus pada Gradle.
@startuml
left to right direction
title Migrasi Gradle 9
rectangle "1
reportsDir" as A
rectangle "2\nmainClass" as B
rectangle "3
sourceCompatibility" as C
rectangle "4\nGroovy assert" as D

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

@enduml

Diagram 2 — Plugin JHipster yang tidak kompatibel

Tujuan : mengisolasi plugin yang rusak.

👉 Visi jelas tentang dependensi.
@startuml
left to right direction
title Plugins yang perlu diperbaiki
skinparam shadowing false
skinparam roundcorner 18

rectangle "1\nopenApiGenerate" as A #FFF3E0
rectangle "2
gitProperties
2.5.7" as B #FFF3E0
rectangle "3
Versi yang diperbaiki" as C #E3F2FD
rectangle "4
Mengasambel OK" as D #E8F5E9

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

@enduml

Diagram 3 — Docker 29 / Testcontainers

Tujuan : masalah runtime.

👉 Riwayat jelas dari runtime
@startuml
left to right direction
title Docker 29 + Testcontainers

skinparam shadowing false
skinparam roundcorner 18
skinparam defaultFontSize 14

rectangle "1\nSusun OK" as A #E8F5E9
rectangle "2
Ujian Cucumber GAGAL" as B #FFEBEE
rectangle "3
Testcontainers 1.32
ditolak" as C #FFF3E0
rectangle "4
Upgrade
versi" as D #E3F2FD
rectangle "5\nBuild akhir OK" as E #E8F5E9

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

@enduml

cara lain untuk membayangkannya
@startuml

!option handwritten true

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

title Peta jalan stabilisasi — Gradle 9 ke build final
left to right direction

rectangle "⚙️ Migrasi Gradle 9" as A #E3F2FD
rectangle "🧩 Plugin JHipster" as B #FFF3E0
rectangle "🐳 Docker 29
Testcontainers" as C #FCE4EC
rectangle "✅ Build Stable" 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

Situasi: sebuah proyek JHipster, sebuah build yang tidak lagi memuat

Proyek`edster`adalah sebuah aplikasi JHipster yang dihasilkan pada tahun 2024. Skrip build akar`build.gradle`est tetap stabil selama beberapa bulan. Lalu kami memutuskan untuk meningkatkan versinya:

Komponen

Versi target

Gradle

9.4.1

Java

21.0.11-tem (Eclipse Temurin)

JHipster

8.x (kerangka kerja`tech.jhipster:jhipster-framework:8.11.0`)

Spring Boot

3.4.5

Kotlin

2.3.0

Docker Engine

29.4.1 (API 1.54)

Perubahan versi Java melalui SDKMAN :

sdk use java 21.0.11-tem

Peluncuran pertama:

$ ./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'

Kami bahkan tidak bisa meluncur dari pemuatan build script. Masalahnya ada di`buildSrc/src/main/groovy/jhipster.cucumber-conventions.gradle`.

Kesalahan 1 : variabel fantom `reportsDir

skrip`jhipster.cucumber-conventions.gradle`mengandung :

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"
    // ...
}

Variabel yang didefinisikan adalah`cucumberReportsDir`. Yang digunakan adalah`reportsDir`-- yang tidak ditentukan di mana-mana.

Perbaikan : outputs.dir(cucumberReportsDir)

Error 2 : main = \"…​\" tidak ada lagi di Gradle 9

Ulang segera:

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

Gradle 9 menghapus properti`main`untuk keuntungan`mainClass`pada tugas-tugas`JavaExec`.

Perbaikan:`mainClass = "org.junit.platform.console.ConsoleLauncher"`

Kesalahan 3 : sourceCompatibility tidak lagi merupakan properti proyek

Kesalahan baru :

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

Skrip root memiliki:

sourceCompatibility=17
targetCompatibility=17

Gradle 9 menghapus properti-properti ini pada level root. Mereka harus berada di dalam sebuah blok`java`.

Koreksi:

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

Kesalahan 4 : pernyataan Groovy dengan tiga operand

Skrip lama mengandung:

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

Di Groovy,"21" et "24"`adalah string truthy. Ekspresi menyelesaikan ke(…​ == "17") || true || true`, yang selalu benar — tetapi sintaksnya tidak valid untuk pernyataan ketat yang diinginkan.

Koreksi:

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

Error 5: dependensi implisit compileKotlin → `openApiGenerate

Build sedang berjalan. Kompilasi Kotlin berhasil. Lalu :

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

Gradle 9 menolak dependensi implisit antara tugas. Karena`compileKotlin`membaca sumber yang dihasilkan oleh`openApiGenerate`, harus mendeklarasikan tautan.

perbaikandalam`build.gradle`:

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

Le `afterEvaluate`diperlukan karena`openApiGenerate`adalah sebuah ekstensi (plugin OpenAPI Generator) dan bukan sebuah tugas yang dapat diakses langsung selama fase konfigurasi.

Error 6: generateGitProperties gagal pada `FilterOutputStream.write()

Compilasi Java berhasil. Sumber daya diproses. Kemudian :

> Task :generateGitProperties FAILED

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

plugin`gradle-git-properties`Versi 2.5.0 tidak kompatibel dengan Java 21 / Gradle 9. Bug internal mencoba memanggil`write(int)`via Groovy pada aliran yang ditutup

Perbaikandi`gradle.properties` :

gitPropertiesPluginVersion=2.5.7

Akhirnya: Testcontainers masuk ke panggung

Sampai sekarang, ini hanya 'build script debugging' murni. Setiap kesalahan adalah ketidakcocokan Gradle 9 atau Java 21 dalam skrip build. Setelah enam perbaikan di atas, le`./gradlew assemble`berhasil lolos.

Tapi`./gradlew build`menjalankan juga tes, dan tes Cucumber menggunakan Testcontainers untuk menjalankan PostgreSQL sementara.

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 tidak mampu berkomunikasi dengan Docker. Dua strategi berbeda (variabel lingkungan + soket Unix) gagal pada kesalahan yang sama.

@startuml
skinparam backgroundColor #FEFEFE
skinparam handwritten false

actor "Testcontainers
1.20.6" as TC
participant "docker-java
3.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

Tahap 1 : jalur docker-java

Refleks pertama : klien Docker Java yang digunakan oleh Testcontainers mengirim versi API`1.32`dalam handshake HTTP-nya, tetapi Docker Engine 29.4.1 memerlukan minimum`1.40`. Masalahnya ada di sisi klien, bukan pada daemon.

Pemeriksaan versi docker-java yang diselesaikan oleh 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, yang berasal dari Testcontainers 1.20.6. Versi publik terbaru dari docker-java adalah 3.5.1. Mari kita coba memaksa peningkatan versi melalui`resolutionStrategy`dalam blok`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")
            }
        }
    }
}

Ulangi build. Kesalahan yang sama :`client version 1.32 is too old`.

Verifikasi cache Gradle: JARs telah di-download ulang dengan baik, tetapi kesalahan tetap ada. docker-java 3.5.1 masih mengirim`1.32`. Versi ini TIDAK cukup untuk Docker Engine 29.4.1.

Tindakan penyucian: membersihkan sepenuhnya cache Gradle, hanya untuk memastikan :

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

Restart lengkap setelah purge. Kesalahan yang sama.

Tahap 2 : penelusuran web dan breadcrumb GitHub

docker-java 3.5.1 tidak mencukupi. Hanya perlu mencari apakah seseorang telah mengalami masalah ini.

Pencarian terarah pada isu testcontainers-java dan 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

Hasil segera pertama :

breadcrumb jelas: Docker Engine 29.x telah meningkatkan versi minimum API-nya menjadi`1.24`(bug lama) ke`1.40`. Testcontainers 1.20.x menggunakan docker-java 3.4.x yang negoiasi versi API dengan mengirim`1.32`. Daemon menolak dengan sopan tapi tegas.

Dalam isu #11491, seorang pemelihara menjawab:

_ Hai, saya memiliki proyek yang berjalan dengan versi 2.0.3 di GH runner dengan mesin 29.1 dan ini berfungsi. Bisakah kalian semua mohon memeriksa bahwa tidak ada konflik versi? Mohon, ingat semua modul telah diawali dengan`testcontainers-. Jadi, mulai dengan versi 2.x, kita beralih dari`postgresql to testcontainers-postgresql _

Jawaban ini berisidua informasi kritis:

  1. Testcontainers 2.0.3+ menyelesaikan masalah

  2. Modul telahdiberi nama ulang dengan awalan `testcontainers-

Tahap 3: jerat artefak yang diubah namanya

Penggantian nama ini brutal. Di Testcontainers 2.x, tidak ada nama lama yang lagi berfungsi dengan nama itu:

Nama lama (1.x)

Nama baru (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

tidak berubah

Percobaan pertama: menerapkan BOM Testcontainers 2.0.5 dan nama-nama artifact baru di`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 tidak cukup.Kenapa ? Karena Spring Boot 3.4.5 mengekspos BOM manajemen dependensi-nya sendiri (spring-boot-dependencies) yangmenangpada BOM Testcontainers dalam resolusi transitif. Spring Boot memperbaiki`testcontainers` à 1.20.6, dan karena itu semua artefak yang tidak secara eksplisit berada dalam BOM Spring Boot (seperti yang baru`testcontainers-*) mengarah ke nama-nama lama`1.20.6-- yang tidak lagi ada dalam versi ini.

Saat memeriksa`dependencies --configuration testRuntimeClasspath`, ada :

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

Le →`menampilkan penggantian : Testcontainers 2.0.5 diminta, tapi Spring Boot memaksa`1.20.6.

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

left to right direction

component "build.gradle
(konsumen)" as BG #E3F2FD {
    rectangle "permintaan\n`testcontainers-bom:2.0.5" as REQ
    rectangle "permintaan
`testcontainers-postgresql" as REQ2
}

component "Maven Central" as MC #FFF3E0

component "Pernyataan" as DEC #E8F5E9 {
    rectangle "testcontainers-bom:2.0.5`\ndiperbaiki 2.0.5" as BOM2
    rectangle "spring-boot-dependencies:3.4.5`
tetap 1.20.6" as BOM1
}

component "Resolusi Gradle" as RES #FFEBEE {
    rectangle "Versi pemenang\n`testcontainers:1.20.6" as WIN #EF9A9A
}

BG --> DEC : "menganalisis BOMs"
BOM2 --> RES : "menyatakan 2.0.5"
BOM1 --> RES : "menawarkan 1.20.6"
WIN -> MC : "download\ntestcontainers: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

Tahap 4 : memaksakan resolusi

Strategi menjadi ganda:

  1. Memaksa docker-java pada versi 3.7.1 (diuji sebagai kompatibel dengan Docker Engine 29)

  2. Memaksa Testcontainers core pada versi 2.0.5 dengan melewati BOM Spring Boot

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

left to right direction

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

component "Konflik terpecahkan" as RESOLVED #E3F2FD {
    rectangle "docker-java
`3.7.1` (dipaksa)" as DJ
    rectangle "testcontainers\n`2.0.5` (dipaksa)" as TC #81C784
    rectangle "testcontainers-postgresql
`2.0.5" as TPG
    rectangle "testcontainers-jdbc\n`2.0.5" as TJ
    rectangle "testcontainers-junit-jupiter
`2.0.5" as TJJ
}

component "BOM Spring Boot
(implisit, dioverride)" as BOM1 #FFEBEE

component "Maven Central" as MC #FFF3E0

BG --> RESOLVED : "BOM Testcontainers 2.0.5
melewati langsung"
RS -> DJ : "kekuatan 3.7.1"
RS -> TC : "gaya 2.0.5"
RESOLVED --> MC : "unduh JARs\nkompatibel Docker 29"
BOM1 -[#gray,dashed]-> RESOLVED : "diabaikan pada 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

Koreksi finaldalam`build.gradle`(Empty output)

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`adalah satu-satunya cara untuk mengabaikan BOM`spring-boot-dependencies`Dikelola oleh plugin Spring Boot. Uji kesamaan pada`details.requested.name == "testcontainers"`adalah sengaja restriktif: kita hanya memaksa modul core. Modul-modul`testcontainers-*`mereka mengikuti BOM Testcontainers 2.0.5 yang telah kami deklarasikan secara eksplisit

Hasil akhir

$ ./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 akhirnya berhasil membuat containernya PostgreSQL melalui Docker Engine 29.4.1. Kesalahan bisnis akhir (HTTP 500 pada ujian Cucumber) adalah masalah aplikasi yang sepenuhnya independen dari skrip build.

Tabel ringkasan koreksi

# File Masalah korreksi

1

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

Variabel`reportsDir`tidak ada

outputs.dir(cucumberReportsDir)

2

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

`main`usang Gradle 9

mainClass

3

build.gradle

`sourceCompatibility`top-level dilarang Gradle 9

blok`java { sourceCompatibility = JavaVersion.VERSION_21 }`

4

build.gradle

Pernyataan sintaksis Groovy yang tidak valid

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

5

build.gradle

Ketergantungan implisit`compileKotlin`→openApiGenerate

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

6

gradle.properties

`gitPropertiesPluginVersion`tidak kompatibel Java 21

2.5.7

7

Cache Gradle

docker-java 3.5.1 diuji, diselesaikan, namun tidak cukup

Pembersihan + perjalanan docker-java 3.7.1

8

build.gradle

Testcontainers 1.20.6 tidak kompatibel dengan Docker Engine 29

Memaksa Testcontainers 2.0.5 + prefix`testcontainers-*`+ strategi resolusi vs Spring Boot BOM

Kesimpulan

Jika build JHipster/Gradle Anda gagal setelah upgrade ke Gradle 9 + Docker Engine 29, kesalahannya terbagi menjadi dua kategori :

Kesalahan skrip build(6 pertama) : Gradle 9 menghapus properti dan sintaksis yang berfungsi selama bertahun-tahun (sourceCompatibility=, main =, reportsDir). Mereka adalah migrasi mekanis setelah Anda mengenal API baru.

Kesalahan runtime Docker(kedua terakhir): Docker Engine 29.4.1 mengabaikan klien API < 1.40. Testcontainers 1.20.6 (dipaksa oleh Spring Boot 3.4.5) menggunakan docker-java 3.4.1 yang mengirimkan 1.32. Hanya Testcontainers 2.0.5 + docker-java 3.7.1 yang menyelesaikan masalah, dengan penamaan ulang artefak dan penaklukan versi via`resolutionStrategy.eachDependency`sebagai satu-satunya cara untuk mengatasi BOM implisit Spring Boot.

Skrip build Gradle di akar proyek sekarang bersih.`./gradlew build`melewati atas Java 21, Gradle 9.4.1, Spring Boot 3.4.5 dan Docker Engine 29.4.1

Artikel terkait