Jenkinsfile中变量作用域行为奇怪

63
当我运行以下 Jenkins 流水线脚本时:
def some_var = "some value"

def pr() {
    def another_var = "another " + some_var
    echo "${another_var}"
}

pipeline {
    agent any

    stages {
        stage ("Run") {
            steps {
                pr()
            }
        }
    }
}

我遇到了这个错误:

groovy.lang.MissingPropertyException: No such property: some_var for class: groovy.lang.Binding
如果从 `some_var` 中移除 `def`,它可以正常工作。有人能解释一下导致这种行为的作用域规则吗?
2个回答

158

TL;DR

  • 使用def在主脚本体中定义的变量无法从其他方法中访问。
  • 没有使用def定义的变量可以直接被任何方法访问,甚至来自不同的脚本。这是一种不好的做法。
  • 使用def@Field 注释定义的变量可以直接从同一脚本中定义的方法中访问。

Explanation

Groovy编译脚本时实际上将所有内容移动到一个类中,该类大致如下:

class Script1 {
    def pr() {
        def another_var = "another " + some_var
        echo "${another_var}"
    }
    def run() {
        def some_var = "some value"
        pipeline {
            agent any
            stages {
                stage ("Run") {
                    steps {
                        pr()
                    }
                }
            }
        }
    }
}

你可以看到,some_var 明显超出了 pr() 的作用域,因为它是另一个方法中的局部变量。

当你定义一个变量时不带def,实际上将该变量放入脚本的绑定中(称为绑定变量)。 因此,当Groovy执行pr()方法时,首先尝试查找名称为some_var的局部变量,如果不存在,则尝试在绑定中查找该变量(因为您没有使用def定义过)。

绑定变量被认为是一种不好的做法,因为如果加载多个脚本(load步骤),则绑定变量将在所有这些脚本中可访问,因为Jenkins为所有脚本共享同一个绑定。一个更好的选择是使用@Field注释。 这样,您可以使一个变量在一个脚本内的所有方法中都可用,而无需将其暴露给其他脚本。

import groovy.transform.Field

@Field 
def some_var = "some value"

def pr() {
    def another_var = "another " + some_var
    echo "${another_var}"
}
//your pipeline

当Groovy将此脚本编译成类时,它将看起来像这样

class Script1 {
    def some_var = "some value"

    def pr() {
        def another_var = "another " + some_var
        echo "${another_var}"
    }
    def run() {
        //your pipeline
    }
}

4
感谢您详细说明并可视化生成的脚本。还要感谢您对“@Field”的建议。您的回答超出了我的预期! - haridsv
1
很好的回答,声明变量(使用{out}def和@field)在并行阶段内部是否存在从一个阶段到另一个阶段的访问限制?在流水线脚本中声明在阶段/步骤内部的变量的可见性如何? - Kanagavelu Sugumar
1
请解释一下在环境块内声明的变量的作用域和可访问性,以及它如何等同于Java类变量。 - Kanagavelu Sugumar
如何将一个变量的作用域限定在一个阶段内,以便在该阶段的后续步骤中也能使用? - mike01010
这对于我的流程步骤可能调用的库也适用吗?如果我有一个自定义库加载,那么在该库中调用的函数是否能够访问使用@FIELD声明的'some_var'变量? - undefined

9

@Vitalii Vitrenko的回答非常好!
我尝试了该程序进行验证。还添加了一些测试案例。

import groovy.transform.Field

@Field  
def CLASS_VAR = "CLASS"
def METHOD_VAR = "METHOD"
GLOBAL_VAR = "GLOBAL"

def testMethod() {
    echo  "testMethod starts:" 
    def testMethodLocalVar = "Test_Method_Local_Var"
    testMethodGlobalVar = "Test_Metho_Global_var"
    echo "${CLASS_VAR}"
    // echo "${METHOD_VAR}" //can be accessed only within pipeline run method
    echo "${GLOBAL_VAR}"
    echo "${testMethodLocalVar}"
    echo "${testMethodGlobalVar}"
    echo  "testMethod ends:" 
}

pipeline {
    agent any
    stages {
         stage('parallel stage') {
             parallel {
                 stage('parallel one') {
                     agent any
                     steps {
                        echo  "parallel one" 
                        testMethod()
                        echo "${CLASS_VAR}"
                        echo "${METHOD_VAR}"
                        echo "${GLOBAL_VAR}"
                        echo "${testMethodGlobalVar}"
                        script {
                            pipelineMethodOneGlobalVar = "pipelineMethodOneGlobalVar"
                            sh_output = sh returnStdout: true, script: 'pwd' //Declared global to access outside the script
                        }
                        echo "sh_output ${sh_output}"
                     }
                 }
                 stage('parallel two') {
                     agent any
                     steps {
                         echo  "parallel two"
                        //  pipelineGlobalVar = "new"      //cannot introduce new variables here
                        //  def pipelineMethodVar = "new"  //cannot introduce new variables here
                         script { //new variable and reassigning needs scripted-pipeline
                             def pipelineMethodLocalVar = "new";
                             pipelineMethodLocalVar = "pipelineMethodLocalVar reassigned";
                             pipelineMethodGlobalVar = "new" //no def keyword
                             pipelineMethodGlobalVar = "pipelineMethodGlobalVar reassigned"

                             CLASS_VAR = "CLASS TWO"
                             METHOD_VAR = "METHOD TWO"
                             GLOBAL_VAR = "GLOBAL TWO"
                         }
                        //  echo "${pipelineMethodLocalVar}" only script level scope, cannot be accessed here
                         echo "${pipelineMethodGlobalVar}"
                         echo "${pipelineMethodOneGlobalVar}"
                         testMethod()
                     }
                 }
             }
         }
         stage('sequential') {
             steps {
                 script {
                     echo "sequential"
                 }
             }
         }
     }
}

观察结果:

  1. 变量声明的六种情况

    a. 管道之前/上有三种类型(带有def、不带def、带有def和@field)

    b. 在脚本化管道内部(带有def、不带def)

    c. 局部于方法外管道(带有def)

  2. 新变量声明和重新分配需要在管道内使用脚本。

  3. 在管道之外声明的所有变量都可以在阶段之间访问

  4. 带有def关键字的变量通常特定于某个方法,如果它是在脚本内部声明的,则在其外部将无法使用。因此需要在脚本中声明全局变量(不带def)以便在脚本外部使用。


1
谢谢,还有很好的补充! 在我的情况下,当使用@Field时,Jenkins会抛出异常。我已经从管道传递了一个带有“def”的变量到外部方法。 - artegen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接