【问题标题】:Jenkins Pipeline NotSerializableException: groovy.json.internal.LazyMapJenkins Pipeline NotSerializableException:groovy.json.internal.LazyMap
【发布时间】:2016-10-18 06:52:52
【问题描述】:

已解决:感谢来自 S.Richmond 的 below answer。我需要取消设置 all 存储的 groovy.json.internal.LazyMap 类型的映射,这意味着在使用后将变量 envServersobject 无效。

附加:搜索此错误的人可能有兴趣改用 Jenkins 管道步骤 readJSON - 查找更多信息 here


我正在尝试使用 Jenkins Pipeline 从用户那里获取作为 json 字符串传递给作业的输入。然后 Pipeline 使用 slurper 解析它,然后我挑选出重要信息。然后它将使用该信息以不同的作业参数并行运行 1 个作业。

直到我在"## Error when below here is added" 下添加代码之前,脚本都可以正常运行。甚至低于该点的代码也将自行运行。但是当结合我得到以下错误。

我应该注意到触发的作业被调用并且确实运行成功但是发生了以下错误并且使主作业失败。因此,主作业不会等待触发作业的返回。我可以尝试/赶上build job:,但是我希望主要工作等待触发的工作完成。

有人可以帮忙吗?如果您需要更多信息,请告诉我。

干杯

def slurpJSON() {
return new groovy.json.JsonSlurper().parseText(BUILD_CHOICES);
}

node {
  stage 'Prepare';
  echo 'Loading choices as build properties';
  def object = slurpJSON();

  def serverChoices = [];
  def serverChoicesStr = '';

  for (env in object) {
     envName = env.name;
     envServers = env.servers;

     for (server in envServers) {
        if (server.Select) {
            serverChoicesStr += server.Server;
            serverChoicesStr += ',';
        }
     }
  }
  serverChoicesStr = serverChoicesStr[0..-2];

  println("Server choices: " + serverChoicesStr);

  ## Error when below here is added

  stage 'Jobs'
  build job: 'Dummy Start App', parameters: [[$class: 'StringParameterValue', name: 'SERVER_NAME', value: 'TestServer'], [$class: 'StringParameterValue', name: 'SERVER_DOMAIN', value: 'domain.uk'], [$class: 'StringParameterValue', name: 'APP', value: 'application1']]

}

错误:

java.io.NotSerializableException: groovy.json.internal.LazyMap
    at org.jboss.marshalling.river.RiverMarshaller.doWriteObject(RiverMarshaller.java:860)
    at org.jboss.marshalling.river.RiverMarshaller.doWriteObject(RiverMarshaller.java:569)
    at org.jboss.marshalling.river.BlockMarshaller.doWriteObject(BlockMarshaller.java:65)
    at org.jboss.marshalling.river.BlockMarshaller.writeObject(BlockMarshaller.java:56)
    at org.jboss.marshalling.MarshallerObjectOutputStream.writeObjectOverride(MarshallerObjectOutputStream.java:50)
    at org.jboss.marshalling.river.RiverObjectOutputStream.writeObjectOverride(RiverObjectOutputStream.java:179)
    at java.io.ObjectOutputStream.writeObject(Unknown Source)
    at java.util.LinkedHashMap.internalWriteEntries(Unknown Source)
    at java.util.HashMap.writeObject(Unknown Source)
...
...
Caused by: an exception which occurred:
    in field delegate
    in field closures
    in object org.jenkinsci.plugins.workflow.cps.CpsThreadGroup@5288c

【问题讨论】:

标签: json jenkins groovy jenkins-pipeline


【解决方案1】:

请改用JsonSlurperClassic

由于 Groovy 2.3(注意:Jenkins 2.7.1 使用 Groovy 2.4.7JsonSlurper 返回 LazyMap 而不是 HashMap。这使得 JsonSlurper 的新实现 not 线程安全且 not 可序列化。这使得它无法在管道 DSL 脚本中的 @NonDSL 函数之外使用。

但是,您可以回退到支持旧 behaviorgroovy.json.JsonSlurperClassic,并且可以在管道脚本中安全使用。

示例

import groovy.json.JsonSlurperClassic 


@NonCPS
def jsonParse(def json) {
    new groovy.json.JsonSlurperClassic().parseText(json)
}

node('master') {
    def config =  jsonParse(readFile("config.json"))

    def db = config["database"]["address"]
    ...
}    

ps。您仍然需要批准 JsonSlurperClassic 才能调用它。

【讨论】:

  • 你能告诉我如何批准JsonSlurperClassic吗?
  • Jenkins 管理员需要导航到 Manage Jenkins » In-process Script Approval。
  • 不幸的是我只收到hudson.remoting.ProxyException: org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed: Script1.groovy: 24: unable to resolve class groovy.json.JsonSlurperClassic
  • JsonSluperClassic.. 这个名字说明了很多关于软件开发的当前状态
  • 非常感谢您的详细解释。你节省了我很多时间。这个解决方案在我的詹金斯管道中就像一个魅力。
【解决方案2】:

我今天自己遇到了这个问题,通过一些蛮力我已经弄清楚了如何解决它以及可能的原因。

最好从原因开始:

Jenkins 有一个范例,所有作业都可以通过服务器重新启动来中断、暂停和恢复。为了实现这一点,管道及其数据必须是完全可序列化的 - IE 它需要能够保存所有内容的状态。同样,它需要能够序列化构建中节点和子作业之间的全局变量的状态,这就是我认为发生在你我身上的事情,以及为什么它只有在你添加额外的构建步骤时才会发生。

无论出于何种原因,默认情况下 JSONObject 是不可序列化的。我不是 Java 开发人员,所以遗憾的是我不能就这个话题说更多。尽管我不知道它们对 Groovy 和 Jenkins 的适用性如何,但有很多关于如何正确解决此问题的答案。 See this post 了解更多信息。

你如何解决它:

如果你知道怎么做,你可能会以某种方式使 JSONObject 可序列化。否则,您可以通过确保没有全局变量属于该类型来解决它。

尝试取消设置您的 object var 或将其包装在一个方法中,使其范围不是节点全局。

【讨论】:

  • 谢谢,这是我解决此问题所需的线索。虽然我已经尝试过你的建议,但它让我再看一遍,我没有考虑到我将地图的一部分存储在其他变量中——这些都是导致错误的原因。所以我也需要取消设置它们。将修改我的问题以包括对代码的正确更改。干杯
  • 每天查看约 8 次。你们介意提供一个更详细的示例来说明如何实施此解决方案吗?
  • 没有简单的解决方案,因为这取决于你做了什么。这里提供的信息以及@Sunvic 在他的帖子顶部添加的解决方案足以引导人们找到他们自己的代码的解决方案。
  • 下面的解决方案,使用 JsonSlurperClassic 修复了我遇到的完全相同的问题,应该是这里认可的选择。这个答案有其优点,但它不是这个特定问题的正确解决方案。
  • 我在 Stage 块中从 currentBuild 检索 Junit 结果。在@S.Richmond 的帮助下,我能够了解管道的工作原理以及触发异常的原因。然后,我将currentBuild 代码移到了管道块之外,成为@NonCPS 独立方法。立即开始工作,无一例外。 (我知道我不应该仍然要感谢@S.Richmond 的非常好的解释)所以我移动了
【解决方案3】:

编辑:正如 @Sunvic 在 cmets 中指出的那样,以下解决方案不适用于 JSON 数组。

我通过使用JsonSlurper 来处理这个问题,然后从惰性结果中创建一个新的HashMapHashMapSerializable

我认为这需要将 new HashMap(Map)JsonSlurper 列入白名单。

@NonCPS
def parseJsonText(String jsonText) {
  final slurper = new JsonSlurper()
  return new HashMap<>(slurper.parseText(jsonText))
}

总的来说,我建议只使用Pipeline Utility Steps plugin,因为它有一个readJSON step,可以支持工作区中的文件或文本。

【讨论】:

  • 对我不起作用 - 不断收到错误 Could not find matching constructor for: java.util.HashMap(java.util.ArrayList)。文档建议它应该输出一个列表或地图 - 你如何配置返回地图?
  • @Sunvic 很好,我们一直在解析的数据总是对象,而不是 JSON 数组。您是否尝试解析 JSON 数组?
  • 啊,是的,它是一个 JSON 数组,就是这样。
  • 在 Jenkins 上,这个答案和下面的答案都引发了 RejectedEception,因为 Jenkins 在沙盒环境中运行 groovy
  • @yiwen 我提到它需要管理员白名单,但也许答案可以澄清这意味着什么?
【解决方案4】:

我想对其中一个答案投赞成票:我建议只使用 Pipeline Utility Steps 插件,因为它有一个 readJSON 步骤,可以支持工作区中的文件或文本:https://jenkins.io/doc/pipeline/steps/pipeline-utility-steps/#readjson-read-json-from-files-in-the-workspace

script{
  def foo_json = sh(returnStdout:true, script: "aws --output json XXX").trim()
  def foo = readJSON text: foo_json
}

这不需要任何白名单或其他东西。

【讨论】:

    【解决方案5】:

    这是所要求的详细答案。

    未设置对我有用:

    String res = sh(script: "curl --header 'X-Vault-Token: ${token}' --request POST --data '${payload}' ${url}", returnStdout: true)
    def response = new JsonSlurper().parseText(res)
    String value1 = response.data.value1
    String value2 = response.data.value2
    
    // unset response because it's not serializable and Jenkins throws NotSerializableException.
    response = null
    

    我从解析的响应中读取值,当我不再需要该对象时,我取消设置它。

    【讨论】:

      【解决方案6】:

      来自@mkobit 的答案的稍微更通用的形式,它允许对数组和地图进行解码:

      import groovy.json.JsonSlurper
      
      @NonCPS
      def parseJsonText(String json) {
        def object = new JsonSlurper().parseText(json)
        if(object instanceof groovy.json.internal.LazyMap) {
            return new HashMap<>(object)
        }
        return object
      }
      

      注意:请注意,这只会将* LazyMap 对象转换为 HashMap。任何嵌套的 LazyMap 对象仍将存在并继续导致 Jenkins 出现问题。

      【讨论】:

        【解决方案7】:

        您可以使用以下函数将 LazyMap 转换为常规的 LinkedHashMap(它将保持原始数据的顺序):

        LinkedHashMap nonLazyMap (Map lazyMap) {
            LinkedHashMap res = new LinkedHashMap()
            lazyMap.each { key, value ->
                if (value instanceof Map) {
                    res.put (key, nonLazyMap(value))
                } else if (value instanceof List) {
                    res.put (key, value.stream().map { it instanceof Map ? nonLazyMap(it) : it }.collect(Collectors.toList()))
                } else {
                    res.put (key, value)
                }
            }
            return res
        }
        
        ... 
        
        LazyMap lazyMap = new JsonSlurper().parseText (jsonText)
        Map serializableMap = nonLazyMap(lazyMap);
        
        

        或者更好地使用之前 cmets 中注意到的 readJSON 步骤:

        Map serializableMap = readJSON text: jsonText
        

        【讨论】:

          【解决方案8】:

          管道插件的实现方式对不平凡的 Groovy 代码有相当严重的影响。此链接说明了如何避免可能出现的问题:https://github.com/jenkinsci/pipeline-plugin/blob/master/TUTORIAL.md#serializing-local-variables

          在您的具体情况下,我会考虑将 @NonCPS 注释添加到 slurpJSON 并返回 map-of-maps 而不是 JSON 对象。不仅代码看起来更简洁,而且效率更高,尤其是在 JSON 很复杂的情况下。

          【讨论】:

            【解决方案9】:

            根据 Jenkins 博客 (Pipeline scalability best practice) 上发布的最佳实践,强烈建议使用命令行工具或脚本进行此类工作:

            陷阱:尤其要避免使用 Groovy 的 Pipeline XML 或 JSON 解析 XmlSlurper 和 JsonSlurper!强烈喜欢命令行工具或 脚本。

            我。 Groovy 实现很复杂,因此在 Pipeline 使用中更加脆弱。

            二。 XmlSlurper 和 JsonSlurper 在管道中可能会带来很高的内存和 CPU 成本

            三。 xmllint 和 xmlstartlet 是通过 xpath 提供 XML 提取的命令行工具

            四。 jq 为 JSON 提供相同的功能

            v.这些提取工具可以耦合到 curl 或 wget 以从 HTTP API 获取信息

            因此,它解释了为什么此页面上提出的大多数解决方案默认被 Jenkins 安全脚本插件的沙箱阻止。

            Groovy 的语言哲学比 Python 或 Java 更接近 Bash。此外,这意味着在原生 Groovy 中进行复杂繁重的工作并不自然。

            鉴于此,我个人决定使用以下内容:

            sh('jq <filters_and_options> file.json')
            

            请参阅 jq ManualSelect objects with jq * post 以获得更多帮助。

            这有点违反直觉,因为 Groovy 提供了许多不在默认白名单中的通用方法。

            如果你决定在你的大部分工作中使用 Groovy 语言,启用沙箱并且清理(这并不容易,因为不自然),我建议你检查你的安全脚本插件版本的白名单,以了解你的可能性:Script security plugin whitelists

            【讨论】:

              【解决方案10】:

              这篇文章中的其他想法很有帮助,但并不是我想要的全部 - 所以我提取了适合我需要的部分并添加了一些我自己的 magix...

              def jsonSlurpLaxWithoutSerializationTroubles(String jsonText)
              {
                  return new JsonSlurperClassic().parseText(
                      new JsonBuilder(
                          new JsonSlurper()
                              .setType(JsonParserType.LAX)
                              .parseText(jsonText)
                      )
                      .toString()
                  )
              }
              

              是的,正如我在自己的 git 提交代码中所指出的那样,“非常低效,但系数很小:JSON slurp 解决方案”(我对此表示同意)。 我需要解决的方面:

              1. 完全摆脱 java.io.NotSerializableException 问题,即使 JSON 文本定义了嵌套容器
              2. 适用于地图和数组容器
              3. 支持 LAX 解析(对我来说最重要的部分)
              4. 易于实现(即使使用笨拙的嵌套构造函数来避免 @NonCPS

              【讨论】:

                【解决方案11】:

                我的菜鸟错误。从旧的管道插件 jenkins 1.6 中移动了某人的代码?到运行最新 2.x jenkins 的服务器。

                因此失败:“java.io.NotSerializableException: groovy.lang.IntRange” 针对上述错误,我多次阅读和阅读这篇文章。 实现: for (num in 1..numSlaves) { IntRange - 不可序列化的对象类型。

                改写成简单的形式: for (num = 1; num

                世界上一切都很好。

                我不经常使用 java 或 groovy。

                谢谢大家。

                【讨论】:

                  【解决方案12】:

                  我在off docs for Jenkins pipeline找到了更简单的方法

                  工作示例

                  import groovy.json.JsonSlurperClassic 
                  
                  
                  @NonCPS
                  def jsonParse(def json) {
                      new groovy.json.JsonSlurperClassic().parseText(json)
                  }
                  
                  @NonCPS
                  def jobs(list) {
                      list
                          .grep { it.value == true  }
                          .collect { [ name : it.key.toString(),
                                        branch : it.value.toString() ] }
                  
                  }
                  
                  node {
                      def params = jsonParse(env.choice_app)
                      def forBuild = jobs(params)
                  }
                  

                  由于 Workflow 的限制 - 即 JENKINS-26481 - 实际上不可能使用 Groovy 闭包或依赖于闭包的语法,因此您不能 > 执行 Groovy 标准,即在列表上使用 .collectEntries 并生成步骤作为结果条目的值。您也不能对 For 循环使用标准 > Java 语法——即“for (String s: strings)”——而必须使用老式的基于计数器的 for 循环。

                  【讨论】:

                  • 建议使用 Jenkins 管道步骤 readJSON - 查找更多信息 here