Трубопровід Дженкінса NotSerializableException: groovy.json.internal.LazyMap


80

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

Додатково : Люди, які шукають цю помилку, можуть зацікавитись скористатися readJSONнатомість кроком конвеєра Дженкінса - знайдіть більше інформації тут .


Я намагаюся використовувати конвеєр Jenkins, щоб взяти введення від користувача, яке передається на роботу як рядок json. Потім трубопровід аналізує це, використовуючи шланг, і я вибираю важливу інформацію. Потім він буде використовувати цю інформацію для запуску 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

Просто натрапив на це сам. Ви ще досягли подальшого прогресу?
S.Richmond

Відповіді:


71

Я сьогодні натрапив на це, і через деяку грубу силу я зрозумів, як це вирішити, і потенційно чому.

Можливо, найкраще почати з того, чому:

Дженкінс має парадигму, коли всі роботи можна перервати, призупинити та відновити за допомогою перезавантаження сервера. Щоб досягти цього, конвеєр та його дані повинні бути повністю серіалізуються - IE він повинен мати можливість зберегти стан усього. Подібним чином він повинен мати можливість серіалізувати стан глобальних змінних між вузлами та підзадачами у збірці, що, на мою думку, відбувається і для вас, і для мене, і чому це відбувається, лише якщо ви додасте цей додатковий крок збірки.

З якоїсь причини JSONObject не можна серіалізувати за замовчуванням. Я не розробник Java, тому я не можу сказати набагато більше на цю тему, на жаль. Існує безліч відповідей про те, як можна це правильно виправити, хоча я не знаю, наскільки вони застосовні до Груві та Дженкінса. Детальніше про це див. У цій публікації .

Як це виправити:

Якщо ви знаєте як, ви можете якось зробити серіалізацію JSONObject якось. В іншому випадку ви можете вирішити це, переконавшись, що жодні глобальні змінні не мають такого типу.

Спробуйте зняти свій objectvar або обернути його методом, щоб область його дії не була глобальною.


2
Дякую, це ключ, який мені потрібен, щоб вирішити це. Хоча я вже випробував вашу пропозицію, це змусило мене знову зазирнути, і я не вважав, що зберігаю частини карти в інших змінних - це спричиняло помилки. Тож мені потрібно було і їх зняти. Внесу зміни до мого питання, включивши правильні зміни до коду. Привітання
Sunvic

1
Це переглядається ~ 8 разів на день. Не могли б ви, хлопці, надати більш детальний приклад того, як реалізувати це рішення?
Йорданія Стефанеллі

1
Не існує простого рішення, оскільки це залежить від того, що ви зробили. Наведеної тут інформації, а також рішення, яке @Sunvic додав у верхній частині свого допису, може бути достатнім, щоб навести когось на рішення для власного коду.
S.Richmond

1
Наведене нижче рішення, використовуючи JsonSlurperClassic, вирішило ту саму проблему, яка була і у мене, мабуть, тут має бути схваленим вибором. Ця відповідь має переваги, але вона не є правильним рішенням для цієї конкретної проблеми.
Кварц

@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"]
    ...
}    

ps. Вам все одно потрібно буде схвалити, JsonSlurperClassicперш ніж його можна буде викликати.


2
Підкажіть, будь ласка, як схвалити JsonSlurperClassic?
mybecks

7
Адміністратору Дженкінса потрібно буде перейти до розділу Керування Дженкінсом »Затвердження сценарію в процесі.
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 Arrays.

Я впорався з цим, використовуючи, 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крок, який може підтримувати як файли в робочій області, так і текст.


1
Мені не вдалося - постійно з’являлася помилка Could not find matching constructor for: java.util.HashMap(java.util.ArrayList). Документація передбачає, що слід виплюнути список або карту - як ви налаштовуєте повернення карти?
Sunvic

@Sunvic Хороший улов, дані, які ми аналізуємо, завжди є об’єктами, а не масивами JSON. Ви намагаєтесь проаналізувати масив JSON?
mkobit

Ах так, це масив JSON, це буде все.
Sunvic

І ця відповідь, і нижче, про Дженкінса, підняли RejectedEception, оскільки Дженкінс пробігається в пісочниці env
yiwen

@yiwen Я згадав, що для цього потрібен адміністративний білий список, але, можливо, відповідь можна пояснити, що це означає?
mkobit

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
}

Для цього НЕ потрібно жодного білого списку або додаткових матеріалів.


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 все ще будуть там і надалі спричинятимуть проблеми з Дженкінсом.


2

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

У вашому конкретному випадку я міг би розглянути можливість додавання @NonCPSанотацій slurpJSONта повернення map-of map замість об'єкта JSON. Не тільки код виглядає чистішим, але й ефективнішим, особливо якщо цей JSON складний.


2

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

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

i. Реалізації Groovy є складними і, як результат, більш крихкими при використанні трубопроводів.

ii. XmlSlurper та JsonSlurper можуть нести високу вартість пам'яті та процесора в конвеєрах

iii. xmllint та xmlstartlet - це інструменти командного рядка, що пропонують витяг XML за допомогою xpath

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

v. Ці інструменти вилучення можуть бути пов’язані між собою для завивання або wget для отримання інформації з HTTP API

Отже, це пояснює, чому більшість рішень, запропонованих на цій сторінці, за замовчуванням заблоковані пісочницею плагіна сценарію безпеки Jenkins.

Мовна філософія Groovy ближча до Bash, ніж Python чи Java. Крім того, це означає, що не природно робити складну та важку роботу в рідному Groovy.

Враховуючи це, я особисто вирішив використати наступне:

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

Див. Інструкцію jq та Вибір об’єктів за допомогою 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)

1

Помилка нуба з мого боку. Переміщено чийсь код зі старого плагіна конвеєра, jenkins 1.6? на сервер, на якому встановлено найновіші версії 2.x jenkins.

Помилка з цієї причини: "java.io.NotSerializableException: groovy.lang.IntRange" Я продовжував читати та читати цю публікацію кілька разів для вищезазначеної помилки. Здійснено: для (число в 1..numSlaves) {IntRange - несеризуючий тип об’єкта.

Переписав у простій формі: for (num = 1; num <= numSlaves; num ++)

Зі світом все добре.

Я не часто використовую java або groovy.

Спасибі, хлопці.


0

Я знайшов більш простий спосіб поза документацією для конвеєра Дженкінса

Приклад роботи

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)" - і замість цього потрібно використовувати стару школу на основі лічильника для циклів.


1
Рекомендую замість цього використовувати крок конвеєра Jenkins readJSON - докладніше тут .
Sunvic
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.