thời gian đọc: 15 minutes

Bạn đã từng trải qua khoảnh khắc khi một điều đơn giản`./gradlew build`mà bạn chạy hàng trăm lần mỗi ngày bỗng nhiên bị treo với một lỗi chưa từng thấy? Hãy nhân điều đó với mười một lỗi liên tiếp, cộng thêm một liều lượng lớn sự không tương thích của Docker Engine 29, rắc thêm Gradle 9 làm biến mất các API mà chúng ta đã sử dụng suốt mười năm qua. Đây là toàn bộ nhật ký chi tiết.


Sơ đồ 1 — Di trú Gradle 9 (chuỗi lỗi)

Mục tiêu: hiển thị chuỗi các điểm không tương thích.

👉 Rõ ràng, tập trung vào Gradle.
@startuml
left to right direction
title Di cư Gradle 9
rectangle "1\nreportsDir" as A
rectangle "2\nmainClass" as B
rectangle "3\nsourceCompatibility" as C
rectangle "4\nGroovy assert" as D

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

@enduml

Biểu đồ 2 — Các plugin JHipster không tương thích

Mục tiêu: cô lập các plugin bị lỗi.

👉 Tầm nhìn riêng về các phụ thuộc.
@startuml
left to right direction
title Các plugin cần sửa
skinparam shadowing false
skinparam roundcorner 18

rectangle "1\nopenApiGenerate" as A #FFF3E0
rectangle "2\ngitProperties\n2.5.7" as B #FFF3E0
rectangle "3\nPhiên bản\nđã sửa" as C #E3F2FD
rectangle "4\nAssemble OK" as D #E8F5E9

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

@enduml

Sơ đồ 3 — Docker 29 / Testcontainers

Mục tiêu: lỗi runtime.

👉 Lịch sử rõ ràng của runtime.
@startuml
left to right direction
title Docker 29 + Testcontainers

skinparam shadowing false
skinparam roundcorner 18
skinparam defaultFontSize 14

rectangle "1\nAssemble OK" as A #E8F5E9
rectangle "2\nTests Cucumber THẤT BẠI" as B #FFEBEE
rectangle "3\nTestcontainers 1.32\nbị từ chối" as C #FFF3E0
rectangle "4\nNâng cấp\nphiên bản" as D #E3F2FD
rectangle "5\nBuild cuối cùng OK" as E #E8F5E9

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

@enduml

một cách khác để hình dung điều đó

@startuml

!option handwritten true

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

title Lộ trình Ổn định — Từ Gradle 9 đến Build Cuối cùng
left to right direction

rectangle "⚙️ Di chuyển Gradle 9" as A #E3F2FD
rectangle "🧩 Plugin JHipster" as B #FFF3E0
rectangle "🐳 Docker 29\nTestcontainers" as C #FCE4EC
rectangle "✅ Build Ổn định" as D #E8F5E9

note bottom of A
reportsDir
mainClass
sourceCompatibility
Groovy assert
end note

note bottom of B
openApiGenerate
gitProperties
các phiên bản được căn chỉnh
end note

note bottom of C
tests Cucumber thất bại
image Docker bị từ chối
nâng cấp libs
end note

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

@enduml

Bối cảnh: một dự án JHipster, một bản build không còn tải được nữa

Dự án`edster`là một ứng dụng JHipster được tạo vào năm 2024. Root build script`build.gradle`đã duy trì ổn định trong nhiều tháng. Sau đó, chúng tôi quyết định nâng cấp phiên bản:

Thành phần Phiên bản mục tiêu

Gradle

9.4.1

Java

21.0.11-tem (Eclipse Temurin)

JHipster

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

Spring Boot

3.4.5

Kotlin

2.3.0

Docker Engine

29.4.1 (API 1.54)

Việc thay đổi phiên bản Java được thực hiện thông qua SDKMAN:

sdk use java 21.0.11-tem

Lần khởi chạy đầu tiên:

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

Chúng ta thậm chí còn chưa thoát khỏi giai đoạn nạp build script. Vấn đề nằm ở`buildSrc/src/main/groovy/jhipster.cucumber-conventions.gradle`Vui lòng cung cấp văn bản cần dịch.

Lỗi 1: biến ảo `reportsDir

Kịch bản`jhipster.cucumber-conventions.gradle`chứa :

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

Biến được định nghĩa là`cucumberReportsDir`. Cái được sử dụng là`reportsDir`-- cái mà không được định nghĩa ở bất cứ đâu.

Sửa lỗiVui lòng cung cấp nội dung cần dịch.outputs.dir(cucumberReportsDir)

Lỗi 2: main = "…​" không còn tồn tại trong Gradle 9

Nhắc nhở ngay lập tức:

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

Gradle 9 xóa bỏ thuộc tính`main`vì lợi ích của`mainClass`về các tác vụ`JavaExec`Vui lòng cung cấp văn bản tiếng Pháp cần dịch.

Sửa lỗiVui lòng cung cấp văn bản cần dịch.mainClass = "org.junit.platform.console.ConsoleLauncher"

Lỗi 3: sourceCompatibility không còn là một thuộc tính của dự án

Lỗi mới:

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

Script gốc có:

sourceCompatibility=17
targetCompatibility=17

Gradle 9 xóa bỏ các thuộc tính này ở cấp root. Chúng phải nằm trong một khối`java`Vui lòng cung cấp văn bản tiếng Pháp cần dịch.

Sửa lỗiVui lòng cung cấp văn bản cần dịch.

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

Lỗi 4: khẳng định Groovy ba toán hạng

Script cũ bao gồm:

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

Trong Groovy,"21" et "24"`là các chuỗi truthy. Biểu thức giải ra thành(…​ == "17") || true || true`, điều này luôn đúng — nhưng cú pháp không hợp lệ cho khẳng định nghiêm ngặt mong muốn.

Sửa lỗiVui lòng cung cấp văn bản cần dịch.

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

Lỗi 5: phụ thuộc ngầm định compileKotlin → `openApiGenerate

Quá trình build đang tiến triển. Biên dịch Kotlin thành công. Sau đó:

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

Gradle 9 từ chối các phụ thuộc ngầm định giữa các tác vụ. Vì`compileKotlin`đọc các nguồn được tạo bởi`openApiGenerate`, cần phải khai báo liên kết.

Sửa lỗitrong`build.gradle`Hãy nhập your_name vào trường tên.

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

Le `afterEvaluate`là cần thiết vì`openApiGenerate`là một tiện ích mở rộng (plugin OpenAPI Generator) chứ không phải là một tác vụ có thể truy cập trực tiếp trong giai đoạn cấu hình.

Lỗi 6: generateGitProperties bị lỗi tại `FilterOutputStream.write()

Quá trình biên dịch Java thành công. Các tài nguyên được xử lý. Sau đó:

> Task :generateGitProperties FAILED

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

Plugin`gradle-git-properties`phiên bản 2.5.0 không tương thích với Java 21 / Gradle 9. Một lỗi nội bộ cố gắng gọi`write(int)`via Groovy trên một luồng đã đóng nó.

Chỉnh sửatrong`gradle.properties`:

gitPropertiesPluginVersion=2.5.7

Cuối cùng: Testcontainers xuất hiện

Cho đến nay, đó hoàn toàn là "build script debugging". Mỗi lỗi đều là một sự không tương thích của Gradle 9 hoặc Java 21 trong các kịch bản build. Sau sáu lần sửa lỗi ở trên, cái`./gradlew assemble`vượt qua thành công.

Nhưng`./gradlew build`cũng thực thi các bài kiểm tra, và các bài kiểm tra Cucumber sử dụng Testcontainers để khởi chạy một PostgreSQL tạm thời.

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 không thể giao tiếp với Docker. Hai chiến lược khác nhau (biến môi trường + socket Unix) đều thất bại với cùng một lỗi.

@startuml
skinparam backgroundColor #FEFEFE
skinparam handwritten false

actor "Testcontainers\n1.20.6" as TC
participant "docker-java\n3.4.1" as DJ
participant "Docker Engine\n29.4.1" as DE

TC -> DJ : mới DockerClient()
DJ -> DE : GET /_ping\nUser-Agent: docker-java/3.4.1\nAPI-Version: 1.32
DE --> DJ : HTTP 400 Bad Request\nphiên bản khách hàng 1.32 là quá cũ.\nhỗ trợ tối thiểu API phiên bản là 1.40
DJ --> TC : BadRequestException
TC --> TC : Không thể tìm thấy môi trường \nDocker hợp lệ

note right of DE
Docker Engine 29.x impose
API minimum = 1.40
(Giá trị mặc định cũ : 1.24)
end note

note left of DJ
docker-java 3.4.1 gửi
hardcoded API version 1.32
trong tiêu đề HTTP
end note

@enduml

Giai đoạn 1: hướng tiếp cận docker-java

Phản xạ đầu tiên: Docker Java client được Testcontainers sử dụng sẽ gửi phiên bản API`1.32`trong handshake HTTP của nó, nhưng Docker Engine 29.4.1 yêu cầu tối thiểu là`1.40`Vấn đề nằm ở phía client, không phải ở daemon.

Kiểm tra phiên bản docker-java được giải quyết bởi 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, từ Testcontainers 1.20.6. Phiên bản công khai mới nhất của docker-java là 3.5.1. Hãy thử buộc nâng cấp phiên bản thông qua`resolutionStrategy`trong khối`configurations` de `build.gradle`Vui lòng cung cấp văn bản cần dịch.

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

Chạy lại build. Vẫn lỗi đó:`client version 1.32 is too old`Vui lòng cung cấp văn bản cần dịch.

Kiểm tra cache Gradle: các file JAR đã được tải lại thành công, nhưng lỗi vẫn còn. docker-java 3.5.1 vẫn gửi`1.32`. Do đó, phiên bản này KHÔNG đủ cho Docker Engine 29.4.1.

Hành động thanh lọc: xóa hoàn toàn bộ nhớ đệm Gradle, chỉ để cho chắc chắn:

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

Khởi động lại hoàn toàn sau khi xóa sạch. Vẫn lỗi tương tự.

Giai đoạn 2: tìm kiếm web và luồng điều hướng GitHub

docker-java 3.5.1` là không đủ. Giờ chỉ còn cách tìm xem có ai đã gặp chính xác vấn đề này chưa.

Tìm kiếm mục tiêu trên các issue của testcontainers-java và 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

Kết quả tức thì đầu tiên:

Luồng dẫn hướng đã rõ ràng: Docker Engine 29.x đã nâng phiên bản API tối thiểu của 1.24(lỗi cũ) tại`1.40`. Testcontainers 1.20.x sử dụng docker-java 3.4.x, phiên bản thương lượng phiên bản API bằng cách gửi`1.32`. Daemon từ chối một cách lịch sự nhưng kiên quyết.

Trong issue #11491, một người bảo trì trả lời:

_ Chào, tôi có một dự án đang chạy với phiên bản 2.0.3 trên GH runner với engine 29.1 và nó hoạt động. Mọi người vui lòng kiểm tra để đảm bảo không có xung đột phiên bản nhé. Vui lòng nhớ rằng tất cả các mô-đun đều đã được đặt tiền tố bằng`testcontainers-. Vì vậy, bắt đầu từ phiên bản 2.x, chúng tôi đã chuyển từ`postgresql to testcontainers-postgresql _

Câu trả lời này chứahai thông tin quan trọngVui lòng cung cấp văn bản cần dịch.

  1. Testcontainers 2.0.3+ giải quyết vấn đề này

  2. Các mô-đun đã đượcđược đổi tên với tiền tố `testcontainers-

Giai đoạn 3: cái bẫy của các artifact được đổi tên

Việc đổi tên diễn ra một cách triệt để. Trong Testcontainers 2.x, không còn tên cũ nào hoạt động dưới tên gọi đó nữa:

Tên cũ (1.x) Tên mới (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

không thay đổi

Lần thử đầu tiên: áp dụng BOM Testcontainers 2.0.5 và tên các artifact mới trong`build.gradle`Vui lòng cung cấp văn bản cần dịch.

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 của testcontainers-bom là không đủ.Tại sao? Bởi vì Spring Boot 3.4.5 cung cấp BOM quản lý phụ thuộc riêng của nó (spring-boot-dependencies) người màthắngtrên BOM của Testcontainers trong giải quyết chuyển tiếp. Spring Boot khắc phục`testcontainers` à 1.20.6, và do đó tất cả các artifact không nằm rõ ràng trong BOM Spring Boot (như các bản mới`testcontainers-*) phân giải về các tên cũ`1.20.6-- vốn không còn tồn tại trong phiên bản này.

Khi kiểm tra`dependencies --configuration testRuntimeClasspath`, ta thấy:

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

Le `→`cho biết sự thay thế: Testcontainers 2.0.5 được yêu cầu, nhưng Spring Boot bắt buộc`1.20.6`Vui lòng cung cấp văn bản cần dịch.

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

left to right direction

component "build.gradle\n(người tiêu dùng)" as BG #E3F2FD {
    rectangle "yêu cầu\n`testcontainers-bom:2.0.5`" as REQ
    rectangle "yêu cầu\n`testcontainers-postgresql`" as REQ2
}

component "Maven Central" as MC #FFF3E0

component "Khai báo" as DEC #E8F5E9 {
    rectangle "`testcontainers-bom:2.0.5`\ncố định 2.0.5" as BOM2
    rectangle "`spring-boot-dependencies:3.4.5`\ncố định 1.20.6" as BOM1
}

component "Giải quyết Gradle" as RES #FFEBEE {
    rectangle "phiên bản thắng\n`testcontainers:1.20.6`" as WIN #EF9A9A
}

BG --> DEC : "phân tích các BOMs"
BOM2 --> RES : "đề xuất 2.0.5"
BOM1 --> RES : "đề xuất 1.20.6"
WIN -> MC : "tải về\ntestcontainers:1.20.6"

note bottom of WIN
Spring Boot BOM xe chính
plugin spring-boot áp dụng các phụ thuộc quản lý của nó
sau khi giải quyết người tiêu dùng
end note

@enduml

Giai đoạn 4: bắt buộc phân giải

Chiến lược trở nên kép:

  1. Épôngage docker-java về phiên bản 3.7.1 (được kiểm tra là tương thích với Docker Engine 29)

  2. Épêcher Testcontainers core ở phiên bản 2.0.5 bằng cách bỏ qua BOM của Spring Boot

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

left to right direction

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

component "Xung đột đã được giải quyết" as RESOLVED #E3F2FD {
    rectangle "docker-java\n`3.7.1` (buộc)" as DJ
    rectangle "testcontainers\n`2.0.5` (buộc)" 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(ngầm, ghi đè)" as BOM1 #FFEBEE

component "Maven Central" as MC #FFF3E0

BG --> RESOLVED : "BOM Testcontainers 2.0.5\ntrực tiếp"
RS -> DJ : "ép buộc 3.7.1"
RS -> TC : "ép buộc 2.0.5"
RESOLVED --> MC : "tải các JARs\ntương thích Docker 29"
BOM1 -[#gray,dashed]-> RESOLVED : "bỏ qua trên testcontainers\nvà docker-java"

note bottom of RS
`eachDependency` chặn việc giải quyết Gradle AVANT
mà BOM Spring Boot không giành được.
Đây là một can thiệp phẫu thuật.
end note

@enduml

Chỉnh sửa cuối cùngtrong`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`là cách duy nhất để bỏ qua BOM`spring-boot-dependencies`được quản lý bởi plugin Spring Boot. Bài kiểm tra tính bằng nhau trên`details.requested.name == "testcontainers"`có chủ ý hạn chế: chúng ta chỉ bắt buộc module `core. Các module`testcontainers-*`tuân theo BOM của Testcontainers 2.0.5 mà chúng ta đã khai báo rõ ràng.

Kết quả cuối cùng

$ ./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 cuối cùng đã có thể tạo các container PostgreSQL thông qua Docker Engine 29.4.1. Lỗi nghiệp vụ cuối cùng (HTTP 500 trong bài test Cucumber) là một vấn đề ứng dụng hoàn toàn độc lập với build script.

Bảng tổng hợp các chỉnh sửa

# Tệp Vấn đề Chỉnh sửa

1

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

Biến`reportsDir`không tồn tại

outputs.dir(cucumberReportsDir)

2

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

`main`Gradle 9 không được dùng nữa

mainClass

3

build.gradle

`sourceCompatibility`top-level bị cấm Gradle 9

Khối`java { sourceCompatibility = JavaVersion.VERSION_21 }`

4

build.gradle

Khẳng định Groovy không hợp lệ về mặt cú pháp

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

5

build.gradle

Phụ thuộc ngầm`compileKotlin`No source text was provided to translate. Please send the French text.openApiGenerate

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

6

gradle.properties

`gitPropertiesPluginVersion`không tương thích Java 21

2.5.7

7

Bộ nhớ đệm Gradle

docker-java 3.5.1 đã được kiểm thử, đã giải quyết nhưng chưa đủ

Dọn sạch + chuyển sang docker-java 3.7.1

8

build.gradle

Testcontainers 1.20.6 không tương thích với Docker Engine 29

Ép buộc Testcontainers 2.0.5 + tiền tố`testcontainers-*`+ resolutionStrategy so với Spring Boot BOM

Kết luận

Nếu bản build JHipster/Gradle của bạn bị lỗi sau khi nâng cấp lên Gradle 9 + Docker Engine 29, các lỗi sẽ được chia thành hai loại:

Lỗi build script(6 mục đầu tiên): Gradle 9 loại bỏ các thuộc tính và cú pháp đã hoạt động trong nhiều năm (sourceCompatibility=, main =, reportsDir). Đó là những cuộc di chuyển cơ học một khi chúng ta đã biết API mới.

Lỗi runtime Docker(2 cái cuối cùng): Docker Engine 29.4.1 loại bỏ các API client < 1.40. Testcontainers 1.20.6 (do Spring Boot 3.4.5 áp đặt) sử dụng docker-java 3.4.1 gửi bản 1.32. Chỉ có Testcontainers 2.0.5 + docker-java 3.7.1 mới giải quyết được vấn đề, cùng với việc đổi tên các artifact và ép buộc phiên bản thông qua`resolutionStrategy.eachDependency`như là cách duy nhất để ghi đè BOM ngầm định của Spring Boot.

Script build Gradle tại gốc của dự án hiện đã sạch.`./gradlew build`chuyển sang Java 21, Gradle 9.4.1, Spring Boot 3.4.5 và Docker Engine 29.4.1.

Bài viết liên quan