Jenkins流水线NotSerializableException错误:groovy.json.internal.LazyMap

116

已解决:感谢S.Richmond提供下面的答案。我需要取消设置groovy.json.internal.LazyMap类型的所有存储映射,这意味着在使用后将变量envServersobject置为null。

附加信息:寻找此错误的人可能有兴趣使用Jenkins管道步骤readJSON - 在此处查找更多信息。


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

在添加下面的代码"## Error when below here is added"之前,脚本将正常运行。即使下面那个点以下的代码也会单独运行。但是当组合时,我会得到下面的错误。

我应该注意到触发的作业被调用并且成功运行,但是下面的错误会发生并使主要工作失败。由于这个原因,主要工作不等待触发的作业的返回。我可以在build job:周围使用try/catch,但是我希望主要工作等待触发的作业完成。

有人能在这里提供帮助吗?如果您需要更多信息,请让我知道。

谢啦

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

我自己也遇到了这个问题。你有进一步的进展吗? - S.Richmond
4
好的,以下是您需要翻译的内容:https://github.com/jenkinsci/pipeline-plugin/blob/master/TUTORIAL.md#serializing-local-variables - Christophe Roussy
不错的问题 - 我投了赞成票,但是有一个小建议:你不需要编辑你的问题来链接答案,这就是勾选标记的作用(它还会分散注意力,使最高评分的答案更加突出)。 - Rhubarb
13个回答

183

请使用JsonSlurperClassic代替。

自Groovy 2.3(注意:Jenkins 2.7.1使用Groovy 2.4.7)起,JsonSlurper返回LazyMap而不是HashMap。这使得新的JsonSlurper实现不是线程安全的和不可序列化的。因此,在流水线DSL脚本的@NonDSL函数之外无法使用它。

但是,您可以退回到groovy.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"]
    ...
}    

提示:在调用之前仍需要批准JsonSlurperClassic


2
请问如何批准JsonSlurperClassic - mybecks
7
Jenkins管理员需要导航到“管理Jenkins » 进程脚本批准”页面。 - luka5z
很不幸,我只得到了 hudson.remoting.ProxyException: org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed: Script1.groovy: 24: unable to resolve class groovy.json.JsonSlurperClassic - dvtoever
21
JsonSlurperClassic.. 这个名称很好地说明了当前软件开发的状态。 - Marcos Brigante
1
非常感谢您提供的详细解释。您节省了我很多时间。这个解决方案在我的Jenkins流水线中运行得非常好。 - Sathish Prakasam
显示剩余3条评论

92

今天我自己遇到了这个问题,通过一些尝试,我找到了解决方法并可能知道原因。

最好先从原因开始:

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

由于某种原因,默认情况下JSONObject是不可序列化的。我不是Java开发人员,所以对此话题无法多加评论。虽然有很多答案可以告诉你如何正确地修复它,但我不知道它们对Groovy和Jenkins的适用性如何。请参见此帖子获取更多信息。

如何解决:

如果您知道如何做到这一点,可能可以使JSONObject可序列化。否则,您可以通过确保没有全局变量属于该类型来解决它。

尝试取消设置object变量或将其包装在方法中,以便其范围不是节点全局的。


2
谢谢,这就是我需要解决问题的线索。虽然我已经尝试了你的建议,但它让我再次仔细查看,我没有考虑到我将地图的部分存储在其他变量中——这些导致了错误。所以我还需要取消它们。我会修正我的问题,包括代码的正确更改。谢谢。 - Sunvic
2
这个页面每天被访问大约8次。你们能否提供一个更详细的实现这个解决方案的例子? - Jordan Stefanelli
1
没有简单的解决方案,因为它取决于你做了什么。这里提供的信息以及@Sunvic在他的帖子顶部添加的解决方案应该足以让人们找到自己代码的解决方案。 - S.Richmond
2
下面的解决方案使用JsonSlurperClassic修复了我遇到的完全相同的问题,可能应该在这里被批准选择。这个答案有优点,但对于这个特定的问题来说并不是正确的解决方案。 - Quartz
1
我正在从Stage块中检索Junit结果。在@S.Richmond的帮助下,我能够理解管道的工作原理以及触发异常的原因。然后,我将currentBuild代码移动到管道块之外的@NonCPS独立方法中。立即开始工作,没有异常。(我知道我不应该仍然感谢@S.Richmond提供的非常好的解释)所以我移动了。 - Paulo Oliveira
显示剩余4条评论

20

编辑:如评论中 @Sunvic 指出的那样,下面的解决方案对于 JSON 数组不起作用。

我通过使用 JsonSlurper 并从惰性结果创建一个新的 HashMap 来处理此问题。 HashMapSerializable 的。

我相信这需要同时将 new HashMap(Map)JsonSlurper列入白名单中。

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

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


1
对我没用 - 一直出现错误 Could not find matching constructor for: java.util.HashMap(java.util.ArrayList)。文档建议它应该输出一个列表或映射 - 如何配置以返回映射? - Sunvic
@Sunvic 很好的发现,我们一直在解析对象数据,而不是 JSON 数组。你是在尝试解析一个 JSON 数组吗? - mkobit
啊,是的,这是一个JSON数组,没错。 - Sunvic
无论是这个答案还是下面关于Jenkins的内容,都提到了RejectedEception,因为Jenkins在沙箱环境中运行groovy。 - yiwen
@yiwen 我提到它需要管理员白名单,但也许可以进一步澄清这意味着什么? - mkobit
问题在于当使用HashMap或者JsonSlurperClassic时,Jenkins进程中的审批界面应该会询问管理员是否批准它们的使用。但是该界面并没有显示出来,因此无法进行批准。 - yiwen

16

如果你的JSON不是来自文件怎么办?在我的情况下,我使用httpRequest与Bitbucket Server REST API交互,响应以JSON格式返回。 - Gene Pavlovsky
如果你的JSON不是来自文件,那该怎么办呢?在我的情况下,我使用httpRequest来与Bitbucket Server REST API进行交互,响应以JSON格式返回。 - undefined
1
读取JSON文本:foo_json,“foo_json”是一个字符串变量。 - Fran
哦,是的,这个工作得非常好。还有一个选项readPojo: true,如果没有它,一些字符串在调试时看起来有点奇怪(但除此之外,代码似乎运行良好)。我发现唯一的缺点是,在单元测试中(我正在使用JenkinsPipelineUnit),像readJSON这样的步骤必须被模拟,我只是添加了一个(不太)模拟函数,它使用JsonSlurper来解析JSON - 这使测试通过了。 - Gene Pavlovsky

11

这是被询问的详细答案。

对我来说,取消设置(set)有效:

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

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


5
您可以使用以下功能将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);

或者更好的方法是,如先前评论中所提到的,使用readJSON步骤:

Map serializableMap = readJSON text: jsonText

5
从 @mkobit 给出的答案稍微推广一下,这样就可以解码数组和映射了:

一个稍微更通用的形式是从 @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出现问题。


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

注意:特别是要避免使用Groovy的XmlSlurper和JsonSlurper进行Pipeline XML或JSON解析!强烈建议使用命令行工具或脚本。

i. Groovy实现在Pipeline使用中更为复杂,因此更加脆弱。

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

iii. xmllint和xmlstartlet是提供XPath的命令行工具。

iv. jq为JSON提供相同的功能。

v. 这些提取工具可以与curl或wget结合使用,从HTTP API中获取信息。

因此,这也解释了为什么此页面上提出的大多数解决方案都被Jenkins安全脚本插件的沙盒默认阻止。
Groovy的语言哲学更接近于Bash而不是Python或Java。这也意味着在本地Groovy中进行复杂和繁重的工作并不自然。
鉴于此,我个人决定使用以下内容:
sh('jq <filters_and_options> file.json')

请参阅jq手册使用jq选择对象stackoverflow帖子以获得更多帮助。

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

如果您决定仍然大部分时间使用Groovy语言,并启用了沙盒和清理功能(这并不容易,因为不是自然的),我建议您检查安全脚本插件版本的白名单,以了解您的可能性:脚本安全插件白名单


2

1
我犯了新手错误。从旧的管道插件,jenkins 1.6?移动了某人的代码到运行最新2.x jenkins的服务器上。
失败的原因是:“java.io.NotSerializableException: groovy.lang.IntRange” 我多次阅读此帖子以获取上述错误。 意识到: for (num in 1..numSlaves) { IntRange-不可序列化的对象类型。

简单重写成: for (num = 1; num <= numSlaves; num++)
世界一切都好了。
我很少使用Java或Groovy。
谢谢大家。

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