最近的工作中, 我写了许多Groovy代码. 我觉得我积累了一些值得分享的经验和心得. 因此我决定写这篇博客, 希望能帮助到正在或即将使用Groovy的开发者们.

我希望我的读者有一些编写 Jenkins 流水线的使用经验, 或是用过 GitLab, GitHub, Azure 它们提供的 CI/CD 功能.

在这篇博客中, 我主要介绍一些控制执行流程的方案,并提供一些我在实践中使用的代码片段.

注意: 本文中使用的是 Scripted Pipeline.

本文所有代码在: GitHub BlogCode

代码调试

非常推荐 Groovy Playground来进行调试 Groovy代码, 虽然语法结构不完全一致, 但是简单的执行以及查看结果会令开发过程效率倍增

https://onecompiler.com/groovy

1711859779592.png

控制流

在Groovy中, 控制流的灵活性是其强大功能之一. 顺序逻辑是我们最常见的, 我并不打算花时间在上面, 我将重点介绍并发和重试这两个方面.

针对这两种控制流, 我强类推荐使用 Blue Ocean, 就是为了它美观的展现形式

并发

通过使用 parallel 关键字来实现, 我给出两种实现方式

1711861460417.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
stage('run-parallel-branches') {
parallel(
a: {
echo "This is branch a"
},
b: {
echo "This is branch b"
}
)
}

// 我个人更加喜欢这一种, 可以方便的使用 for 循环以及 if 语句来控制并发逻辑
stage('looper-parallel-branches') {
def looper = [:]
for (int i = 0; i < 10; i++) {
looper["${i}"] = {
echo "This is branch ${i}"
}
}
parallel looper
}

重试

重试逻辑适用于想要确保某个任务正确执行, 又不希望重新执行整条流水线. Jenkins 中也提供了 retry 功能

以下的代码中, 会因为随机生成了偶数报错, 而我们可以通过 retry 功能直接进行重试

1711861660821.png

并且每次重试的执行记录也会存储在流水线日志中

1711861812746.png

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
stage('retry') {
testJob = { ->
// generate number between 40 and 99
def num = Math.abs( new Random().nextInt() % (99 - 40) ) + 40
if(num % 2 == 0) {
throw new Exception("Even number")
}
}

retryFunc = { job ->
waitUntil {
try {
job()
true
} catch (error) {
println error
input 'Retry the job ?'
false
}
}
}

def looper = [:]
for (int i = 0; i < 5; i++) {
looper["${i}"] = {
retryFunc(testJob)
}
}
parallel looper
}

设计方式

以下内容适合于想要优化现有的复杂流水线逻辑, 没写过流水线的读者就可以跳过了

依赖注入

在这里的依赖注入, 我想我更希望的是说控制反转, 将控制逻辑抽离, 还记得我在上面说到的并发执行方式吗? 如果我们的业务有很多流程, 比如某一步操作所有环境的命令1, 再下一步操作所有环境的命令2, 就可能产生以下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
stage('action 1') {
def looper = [:]
workNodes.each { node ->
looper["Action1 for ${node}"] = {
println "action1 on $node"
}
}
parallel looper
}

stage('action 2') {
def looper = [:]
workNodes.each { node ->
looper["Action2 for ${node}"] = {
println "action2 on $node"
}
}
parallel looper
}

是不是感觉一模一样的逻辑写了两次, 假如这种操作很多呢, 会带来代码中大量的循环, 而且这个循环是业务不相关的. 这里我给出一个改进的方案. 其最终效果是一致的:

1711867353521.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def actionRunner = { msg, nodes, action ->
def looper = [:]
nodes.each { node ->
looper["${msg} -- ${node}"] = {
stage("${msg} -- ${node}") {
action(node)
}
}
}
parallel looper
}
// 我们真正的业务代码应该只包括下面这些内容, 具有非常好的可读性和灵活性
// 不过, 前提是你理解了上面的控制逻辑
actionRunner("action1", workNodes, { node ->
println "action1 on $node"
})

actionRunner("action2", workNodes, { node ->
println "action2 on $node"
})

装饰器

我原本是一个熟练的 Python 程序员, 我发现在 Groovy 流水线中, 也有一部分逻辑也非常适合装饰器, 那就是重试逻辑, 我们在上面控制流过程中已经讲过了它的实现, 但我们其实有一种更加优雅的方案, 在你想为很多函数都添加类似功能的时候, 装饰器就是一个很好的选择.

以下代码可以在Groovy Playground中很好的使用, 但是无法在Jenkins流水线中使用. 我这里给出2种装饰器的编写方案, 你们选择一种就可以:

curry wrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// curry wrapper
def testWrapper(Closure job) {
def varFunc = { func, Object... args ->
println "args ${args}, ${args.getClass()}, ${args.size()}"
func.call(*args)
}
return varFunc.curry(job)
}

def xx = { a,b,c ->
println "a ${a}"
println "b ${b}"
println "c ${c}"
}
def func = testWrapper(xx)
func("aa", "bb", "cc")

/*
output:
args [aa, bb, cc], class [Ljava.lang.Object;, 3
a aa
b bb
c cc
*/

simple wrapper

这种方案比较好理解, 毕竟函数柯里化大家都不一定会考虑用

1
2
3
4
5
6
7
def testWrapper(Closure job) {
def varFunc = { Object... args ->
println "args ${args}, ${args.getClass()}, ${args.size()}"
job.call(*args)
}
return varFunc
}

很可惜的是, 这两个方案在 Jenkins 中都无法直接使用, 会报下面这个错误,

1712240532006.png

核心原因是Jenkins中Closure可变参数有问题, Object... args这里我们写了可变参数, 它还是只读了第一个参数

Jenkins wrapper

我这里给出一个替代方案, 使用wrapper包裹的函数, 传递参数时是使用一个数组, 我们在真正调用时将参数分别填好

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
def retryWrapper(Closure job) {
def func = { Object[] args ->
waitUntil {
try {
println "args ${args}, ${args.getClass()}, ${args.size()}"
job.call(*args)
true
} catch (error) {
println error
input 'Retry the job ?'
false
}
}
}
return func
}

testJob = retryWrapper({ arg1, arg2 ->
println("${arg1}, ${arg2}")
// generate number between 40 and 99
def num = Math.abs( new Random().nextInt() % (99 - 40) ) + 40
if(num % 2 == 0) {
throw new Exception("Even number")
}
})

def looper = [:]
for (int i = 0; i < 5; i++) {
looper["${i}"] = {
testJob(["arg1", i])
}
}
parallel looper

1712241220461.png

总结

这篇博客我主要想总结下最近遇到的问题以及我应对问题时准备的设计模式, 希望针对使用 Jenkins 的朋友有所帮助. 大佬们有更好的方案也请不吝赐教