Gradle构建Springboot项目,自定义打包、Docker镜像构建--草稿

本文最后更新于:2024-01-14 17:44 星期日

Gradle构建Springboot项目,自定义打包、Docker镜像构建–草稿

参考文档

下图 IDEA Gradle 窗口 ,com.bmuschko.docker-spring-boot-application 插件默认Tasks分组位Docker,一共四个 dockerBuildImage\dockerCreateDockerFile\dockerSysncBuildContext\dcokerPushImage

  • dockerBuildIamge : 构建docekr镜像
  • dockerCreateDockerFile : 构建dockerfile,下面代码使用了自定义dockerfile
  • dockerSysncBuildContext 将 class 代码、资源文件、第三方依赖复制到 build\docker目录先,dockerfile文件也生成在此目录
  • dockerPushimage· : 推送docker镜像,暂时未正常使用过。
  • 自定义task , 由于 dockerPushimage 未摸清楚怎么使用,为了节省时间,直接自定义两个task
    • pushDockerImage : 推送Image到远程仓库
    • saveDockerImagesBylocal : 将宿主机Docker镜像保存到本地文件。

Idea Gradle 窗口

build.gradle 文件配置,基于kotlin语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage
import com.bmuschko.gradle.docker.tasks.image.DockerPushImage
import com.bmuschko.gradle.docker.tasks.image.Dockerfile
import groovy.xml.XmlSlurper
import groovy.xml.slurpersupport.GPathResult
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
import org.springframework.boot.gradle.tasks.bundling.BootJar
import java.util.*
import java.util.jar.JarFile

plugins {
java
`maven-publish`
id("org.springframework.boot") version "3.1.5"
id("io.spring.dependency-management") version "1.1.4"
id("com.bmuschko.docker-spring-boot-application") version "9.4.0"
}

// 加载配置文件
val properties = Properties().apply {
load(rootProject.file("gradle.properties").inputStream())
load(rootProject.file("my.properties").inputStream())
load(rootProject.file("docker.properties").inputStream())
}

// 处理 annotationProcessor
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
// 设置jar仓库地址
repositories {
mavenLocal()
mavenCentral()
// 自定义了私库配置信息
val nexusUrlList = properties["nexus.url"] as? List<*>
nexusUrlList?.forEach { i ->
maven {
url = uri(i.toString())
logger.info(url.toString())
val isHttps = url.toString().startsWith("https://")
credentials {
username = properties["nexus.user"].toString()
password = properties["nexus.pwd"].toString()
// http 需要此配置=false
isAllowInsecureProtocol = !isHttps
}
}
}
}
// 项目依赖
dependencies {

compileOnly("org.springframework.boot:spring-boot-configuration-processor")
implementation("org.springframework.boot:spring-boot-starter-web")

compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")

testImplementation("org.springframework.boot:spring-boot-starter-test")

}

group = properties.getProperty("group")
version = properties.getProperty("version")
description = properties.getProperty("description")

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
// kotlin 语法定义的方法
fun addRepository(repositories: RepositoryHandler, url: String, username: String, password: String, isAllowInsecure: Boolean) {
repositories.maven {
this.url = uri(url)
credentials {
this.username = username
this.password = password
isAllowInsecureProtocol = isAllowInsecure
}
}
}
// 推送jar的定义,这里使用的私有仓库,很具Jar的后缀,分类推送到不同的仓库地址
publishing {
publications.create<MavenPublication>("maven") {
from(components["java"])
}
val nexusUrlList = properties["nexus.url"] as? List<*>
val nexusUser = properties["nexus.user"].toString()
val nexusPwd = properties["nexus.pwd"].toString()
repositories {
nexusUrlList?.forEach { url ->
logger.info(url.toString())
val urlStr = url.toString()
when {
version.toString().endsWith("SNAPSHOT") && urlStr.contains("SNAPSHOT".lowercase()) ->
addRepository(this, urlStr, nexusUser, nexusPwd, !urlStr.startsWith("https://"))

!version.toString().endsWith("SNAPSHOT") && !urlStr.contains("SNAPSHOT".lowercase()) ->
addRepository(this, urlStr, nexusUser, nexusPwd, !urlStr.startsWith("https://"))

else -> {
logger.error("无合适的仓库推送")
}
}
}
}
}

// tasks 相关配置
tasks {
// 重写springboot 的打包方式。资源文件和第三方jar外置。保证可以使用 Java -jar 启动,保留了相对位置
named<BootJar>("bootJar") {
copy {
from("src/main/resources")
into("build/libs/resources")
}
// 第三方jar的目录整理
val classpathFilesByIot = configurations.runtimeClasspath.get().files
val classpathIot = StringBuilder()
classpathIot.append("./resources/")
classpathFilesByIot.forEach { file ->
copy {
val mainAttributes = JarFile(file).manifest?.mainAttributes
if (project.group.toString().equals(mainAttributes?.getValue("Group"))) {
from(file)
into("build/libs/lib")
classpathIot.append(" ./lib/${file.name}")
} else {
from(file)
into("build/libs/lib/third-party")
classpathIot.append(" ./lib/third-party/${file.name}")
}
}
exclude(file.name)
}
exclude("*xml*")
exclude("*.properties")
manifest {
attributes(
"Class-Path" to classpathIot,
"Group" to project.group.toString(),
)
}
}
withType<Test> {
useJUnitPlatform()
}
withType<JavaCompile>() {
options.encoding = "UTF-8"
}
withType<Javadoc>() {
options.encoding = "UTF-8"
}
// 跳过test task
named("test") {
onlyIf { false }
}
// 自定义dockerfile
"dockerCreateDockerfile"(Dockerfile::class) {
dependsOn("bootJar")
// 获取dockerfile 默认配置信息
val originalInstructions = instructions.get().toMutableList()
// 清空默认信息
originalInstructions.clear();
// 自定义dockerfile
// 安装curl 命令
val run = "apt-get update" +
" && apt-get install -y --no-install-recommends curl" +
" && rm -rf /var/lib/apt/lists/* " +
" && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone";
val baseImage = Dockerfile.FromInstruction(Dockerfile.From("eclipse-temurin:17.0.9_9-jre"))
val workDir = "/app"
originalInstructions.add(baseImage)
originalInstructions.add(Dockerfile.WorkDirInstruction(workDir))
originalInstructions.add(Dockerfile.RunCommandInstruction(run))
val jarFileName = project.name + "-" + project.version + ".jar"
originalInstructions.add(Dockerfile.EnvironmentVariableInstruction("IOT_PLUGIN_DOCKER_CONTAINER", jarFileName))
originalInstructions.add(Dockerfile.EnvironmentVariableInstruction("LANG", "C.UTF-8"))
originalInstructions.add(Dockerfile.EnvironmentVariableInstruction("LOG_LEVEL", "debug"))
originalInstructions.add(Dockerfile.CopyFileInstruction(Dockerfile.CopyFile("libs${File.separator}$jarFileName", "$workDir/$jarFileName")))
originalInstructions.add(Dockerfile.CopyFileInstruction(Dockerfile.CopyFile("libs${File.separator}lib", "$workDir/lib")))
originalInstructions.add(Dockerfile.CopyFileInstruction(Dockerfile.CopyFile("libs${File.separator}resources", "$workDir/resources")))
originalInstructions.add(Dockerfile.VolumeInstruction("/var/run/docker.sock", "$workDir/logs", "$workDir/resources", "$workDir/data"))
originalInstructions.add(Dockerfile.ExposePortInstruction(8080))
originalInstructions.add((Dockerfile.EntryPointInstruction("java", "-jar", "$workDir/$jarFileName")))
instructions.set(originalInstructions)
}
// 设置docker build dockerfile的路径
"dockerBuildImage"(DockerBuildImage::class) {
inputDir.set(project.layout.buildDirectory.asFile.get())
dockerFile.set(project.layout.buildDirectory.asFile.get().resolve("docker${File.separator}Dockerfile"))
}
// 管理文件复制到 build/docker目录先,由于自定义了dockerfile的jar信息,所有全部排除
"dockerSyncBuildContext"(Sync::class) {
exclude("**")
}
// 推送 docker镜像到远程仓库
// 主要命令 docker login / docker tag / docker push
create("pushDokcerImage") {
group = "docker"
// 定义了此task依赖 task :dockerBuildImage
dependsOn("dockerBuildImage")
doLast {
val commandLogin = listOf("bash", "-c", "docker login -u admin -p admin http://localhost:8082")
val process = ProcessBuilder(commandLogin)
.directory(project.layout.buildDirectory.asFile.get().resolve("docker"))
.redirectOutput(ProcessBuilder.Redirect.INHERIT).redirectError(ProcessBuilder.Redirect.INHERIT).start()
val exitCode = process.waitFor()
if (exitCode == 0) {
val commandTag = listOf("bash", "-c", "docker tag $imageName localhost:8082/$imageName")
ProcessBuilder(commandTag)
.directory(project.layout.buildDirectory.asFile.get().resolve("docker"))
.redirectOutput(ProcessBuilder.Redirect.INHERIT).redirectError(ProcessBuilder.Redirect.INHERIT).start()
val commandPush = listOf("bash", "-c", "docker push localhost:8082/$imageName")
val processPush = ProcessBuilder(commandPush)
.directory(project.layout.buildDirectory.asFile.get().resolve("docker"))
.redirectOutput(ProcessBuilder.Redirect.INHERIT).redirectError(ProcessBuilder.Redirect.INHERIT).start()
processPush.inputStream.bufferedReader().useLines { lines ->
lines.forEach { println(it) }
}
val exitCodePush = processPush.waitFor()
if (exitCodePush == 0) {
println("Command executed successfully.")
} else {
throw GradleException("Command failed with exit code2 $exitCodePush.")
}
} else {
throw GradleException("Command failed with exit code1 $exitCode.")
}
}
}
// 自定义task,保存docker镜像到文件,使用的是 docker save 命令,使用了gzip压缩
create("saveDockerImageByLocal") {
group = "docker"
// 依赖的task
dependsOn("dockerBuildImage")
doLast {
val command = listOf("bash", "-c", "docker save $imageName | gzip > ${imageName.replace(':', '-')}.tar.gz")
val process = ProcessBuilder(command)
.directory(project.layout.buildDirectory.asFile.get().resolve("docker"))
.redirectOutput(ProcessBuilder.Redirect.INHERIT).redirectError(ProcessBuilder.Redirect.INHERIT).start()
process.inputStream.bufferedReader().useLines { lines ->
lines.forEach { println(it) }
}
val exitCode = process.waitFor()
if (exitCode == 0) {
println("Command executed successfully.")
} else {
throw GradleException("Command failed with exit code $exitCode.")
}
}
}
// 由于 总是报错,更改了分组,标记为启用
"dockerPushImage"(DockerPushImage::class) {
group = "弃用"
}
// 由于 总是报错,更改了分组,标记为启用 ,此task,为Springboot插件的task,自定义功能不太理想
named<BootBuildImage>("bootBuildImage") {
group = "弃用"
}
}

// 定义了从XML文件中,获取镜像名字的信息
val xmlFile = file(project.layout.buildDirectory.asFile.get().resolve("${sourceSets.main.get().resources.srcDirs.first()}${File.separator}xml${File.separator}doccker.xml"))
if (!xmlFile.exists()) {
logger.error("找不到${xmlFile}文件")
}
val xml: GPathResult? = XmlSlurper().parseText(xmlFile.readText())
val idMy = xml?.getProperty("id").toString().replace("_", "-")
val versionMy = xml?.getProperty("version").toString().replace(".", "-")
val imageName = "$idMy:$versionMy"

// 参考 com.bmuschko.docker-spring-boot-application 插件文档
docker {
springBootApplication {
// 定义 docker 镜像的名字
images.add(imageName)
}
}


异常信息

执行> Task :dockerBuildImage 报错 Failure running command ([docker-credential-gcloud, get])

检查 ~/.docker/config.json 文件, 文档https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"auths": {
"127.0.0.1:8082": {}
},
"credsStore": "osxkeychain",
"credHelpers": { // 删除 credHelpers 配置项即可,或安装 gcloud
"asia.gcr.io": "gcloud",
"eu.gcr.io": "gcloud",
"gcr.io": "gcloud",
"marketplace.gcr.io": "gcloud",
"staging-k8s.gcr.io": "gcloud",
"us.gcr.io": "gcloud"
},
"experimental": "enabled",
"currentContext": "orbstack"
}

Task :dockerBuildImage


Gradle构建Springboot项目,自定义打包、Docker镜像构建--草稿
https://blog.dekun.wang/blog/dd130cd7.html
作者
Wang Dekun
发布于
2024-01-14 17:44 星期日
更新于
2024-01-15 09:14 星期一
许可协议