Трубопровод Jenkins NotSerializableException: groovy.json.internal.LazyMap

80

Решено : благодаря приведенному ниже ответу от С.Ричмонда. Мне нужно было отключить все сохраненные карты этого groovy.json.internal.LazyMapтипа, что означало обнуление переменных envServersи objectпосле использования.

Дополнительно : люди, ищущие эту ошибку, могут быть заинтересованы в использовании readJSONвместо этого шага конвейера Jenkins - дополнительную информацию можно найти здесь .


Я пытаюсь использовать 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
Sunvic
источник
Просто столкнулся с этим сам. Вы уже добились каких-либо успехов?
S.Richmond

Ответы:

71

Я сам столкнулся с этим сегодня и с помощью некоторой грубой силы выяснил, как решить эту проблему и, возможно, почему.

Наверное, лучше всего начать с того, почему:

У Дженкинса есть парадигма, в которой все задания могут быть прерваны, приостановлены и возобновлены посредством перезагрузки сервера. Для этого конвейер и его данные должны быть полностью сериализуемыми - IE должен иметь возможность сохранять состояние всего. Точно так же он должен иметь возможность сериализовать состояние глобальных переменных между узлами и подзаданиями в сборке, что, как я думаю, происходит для вас и меня, и почему это происходит только в том случае, если вы добавляете этот дополнительный шаг сборки.

По какой-то причине JSONObject по умолчанию не сериализуемы. Я не разработчик Java, поэтому, к сожалению, не могу сказать больше по этой теме. Существует множество ответов о том, как это исправить, хотя я не знаю, насколько они применимы к Groovy и Jenkins. См. Этот пост для получения дополнительной информации.

Как это исправить:

Если вы знаете как, вы можете каким-то образом сделать JSONObject сериализуемым. В противном случае вы можете решить эту проблему, убедившись, что глобальные переменные не относятся к этому типу.

Попробуйте отключить objectvar или обернуть его в метод, чтобы его область видимости не была глобальной.

С. Ричмонд
источник
2
Спасибо, это ключ, который мне нужен, чтобы решить эту проблему. Хотя я уже попробовал ваше предложение, это заставило меня снова взглянуть, и я не подумал, что сохраняю части карты в других переменных - они вызывали ошибки. Так что мне тоже нужно было отключить их. Внесу поправки в свой вопрос, чтобы включить правильные изменения в код. Cheers
Sunvic
1
Это просматривается ~ 8 раз в день. Не могли бы вы привести более подробный пример реализации этого решения?
Jordan Stefanelli
1
Нет простого решения, поскольку это зависит от того, что вы сделали. Информация, представленная здесь, а также решение, которое @Sunvic добавил в верхней части своего сообщения, достаточно, чтобы привести к решению для своего собственного кода.
S.Richmond
1
Приведенное ниже решение с использованием JsonSlurperClassic устраняет ту же проблему, что и у меня, вероятно, здесь должен быть одобренный выбор. У этого ответа есть достоинства, но это не правильное решение для данной конкретной проблемы.
Quartz
@JordanStefanelli Я разместил код своего решения. Смотрите мой ответ ниже
Нильс Эль-Химуд
127

JsonSlurperClassicВместо этого используйте .

Поскольку Groovy 2.3 ( примечание: Jenkins 2.7.1 использует Groovy 2.4.7 ) JsonSlurperвозвращает LazyMapвместо HashMap. Это делает новую реализацию JsonSlurper не потокобезопасной и не сериализуемой. Это делает его непригодным для использования вне функций @NonDSL в сценариях конвейерного DSL.

Однако вы можете вернуться к тому, 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прежде чем его можно будет вызвать.

luka5z
источник
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
13
JsonSluperClassic .. Это имя многое говорит о текущем состоянии разработки программного обеспечения
Маркос Бриганте
1
Большое спасибо за это подробное объяснение. Вы сэкономили мне много времени. Это решение работает как шарм в моем конвейере Дженкинса.
Сатиш Пракасам
16

EDIT: как указано в комментариях @Sunvic , приведенное ниже решение не работает как есть для массивов JSON.

Я справился с этим, используя, JsonSlurperа затем создав новый HashMapиз ленивых результатов. HashMapесть Serializable.

Я считаю , что это требовало от белого списка как new HashMap(Map)и JsonSlurper.

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

В целом, я бы рекомендовал просто использовать плагин Pipeline Utility Steps , так как у него есть readJSONшаг, который может поддерживать файлы в рабочей области или текст.

mkobit
источник
1
Не вышло у меня - все время вылетает ошибка Could not find matching constructor for: java.util.HashMap(java.util.ArrayList). Документация предполагает, что он должен выдавать список или карту - как вы сконфигурируете возврат карты?
Sunvic
@Sunvic Хороший улов, данные, которые мы анализируем, всегда являются объектами, а не массивами JSON. Вы пытаетесь разобрать массив JSON?
mkobit
Ах да, это массив JSON, вот и все.
Sunvic
И этот ответ, и ниже, на Jenkins, вызвали RejectedEception, потому что Jenkins отлично работает в песочнице env
yiwen
@yiwen Я упоминал, что для этого требуется добавление администратора в белый список, но, возможно, ответ можно было бы уточнить, что это означает?
mkobit 08
8

Я хочу проголосовать за один из ответов: я бы рекомендовал просто использовать плагин 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
}

Это НЕ требует внесения в белый список или дополнительных материалов.

Regnoult
источник
6

Это подробный ответ, который был запрошен.

У меня отключение сработало:

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

Чуть более обобщенная форма ответа от @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.

TomDotTom
источник
2

Способ реализации плагина конвейера имеет довольно серьезные последствия для нетривиального кода Groovy. Эта ссылка объясняет, как избежать возможных проблем: https://github.com/jenkinsci/pipeline-plugin/blob/master/TUTORIAL.md#serializing-local-variables

В вашем конкретном случае я бы подумал о добавлении @NonCPSаннотации slurpJSONи возврате карты карт вместо объекта JSON. Код не только выглядит чище, но и более эффективен, особенно если этот JSON сложный.

Марцин Плонка
источник
2

В соответствии с лучшими практиками, опубликованными в блоге Jenkins ( передовая практика масштабируемости конвейера ), настоятельно рекомендуется использовать инструменты командной строки или сценарии для такого рода работы:

Попался: особенно избегайте синтаксического анализа Pipeline XML или JSON с использованием Groovy XmlSlurper и JsonSlurper! Сильно предпочитаю инструменты или сценарии командной строки.

я. Реализации Groovy сложны и, как следствие, более неустойчивы в использовании конвейера.

II. XmlSlurper и JsonSlurper могут нести высокую стоимость памяти и процессора в конвейерах.

iii. xmllint и xmlstartlet - это инструменты командной строки, предлагающие извлечение XML через xpath

iv. jq предлагает ту же функциональность для JSON

v. Эти инструменты извлечения могут быть связаны с curl или wget для получения информации из HTTP API.

Таким образом, это объясняет, почему большинство решений, предлагаемых на этой странице, по умолчанию блокируются песочницей плагина сценария безопасности Jenkins.

Философия языка Groovy ближе к Bash, чем к Python или Java. Кроме того, это означает, что выполнять сложную и тяжелую работу в родном Groovy неестественно.

Учитывая это, я лично решил использовать следующее:

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

См. Jq Manual и Select objects with jq stackoverflow post для получения дополнительной помощи.

Это немного противоречит интуиции, поскольку Groovy предоставляет множество универсальных методов, которых нет в белом списке по умолчанию.

Если вы решили использовать Groovy язык в любом случае большую часть своей работы, с песочницей включена и чистый (не легко , потому что не естественно), я рекомендую вам проверить белые списки для версии вашей безопасности скрипта плагина , чтобы знать , что ваши возможности: Script белые списки плагинов безопасности

вхамон
источник
2

Вы можете использовать следующую функцию для преобразования 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
Сергей П.
источник
1

Другие идеи в этом посте были полезны, но не совсем все, что я искал, поэтому я извлек части, которые соответствовали моим потребностям, и добавил некоторые из моих собственных магических средств ...

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)
Stevel
источник
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.

Спасибо ребята.

mpechner
источник
0

Я нашел более простой способ отключить документы для конвейера Jenkins

Пример работы

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)
}

Из-за ограничений в рабочем процессе - например, JENKINS-26481 - на самом деле невозможно использовать замыкания Groovy или синтаксис, который зависит от замыканий, поэтому вы не можете> использовать стандарт Groovy для использования .collectEntries в списке и генерации шагов как значений для итоговых записей. Вы также не можете использовать стандартный> синтаксис Java для циклов For - например, «for (String s: strings)» - и вместо этого должны использовать циклы for на основе счетчиков старой школы.

Кирилл К
источник
1
Рекомендую вместо этого использовать этап конвейера Jenkins readJSON - дополнительную информацию можно найти здесь .
Sunvic