NoSuchMethodError при тестовом запуске с gradle

Я пытаюсь экспериментально перенести java-проект MAVEN на gradle. Одна из проблем, с которой я столкнулся, — это невозможность выполнить модульные тесты из-за ошибки NoSuchMethodError, возникающей во время выполнения (во время выполнения). Я вызываю метод FileUtils.write().

Я изменил свой код, чтобы отследить путь к классу, доступный в ClassLoader, который загрузил класс FileUtils, и у меня есть следующее:

C:/Sdk/gradle-2-7/lib/commons-io-1.4.jar
C:/Users/<me>/.gradle/caches/modules-2/files-2.1/commons-io/commons-io/2.4/b1b6ea3b7e4aa4f492509a4952029cd8e48019ad/commons-io-2.4.jar

Из этого я вижу, что во время тестового запуска в пути к классам есть 2 версии commons-io, и та, которая идет с gradle, является первой и, следовательно, имеет более высокий приоритет.

Какова основная причина? Как это можно исправить?

На самом деле, я ожидаю, что никакие JAR-файлы не будут доступны в пути к классам, кроме явно объявленных в зависимостях моего проекта gradle.


Обновление: кажется, у меня есть представление о первопричине - тестируемый проект является «плагином gradle», и чтобы скомпилировать его в gradle, я должен указать следующее в build.gradle:

dependencies {
    compile gradleApi()
}

И это просачивается, чтобы захватить все зависимости gradle в мой проект. Хотя я до сих пор не вижу способа исправить это:

  • Я не могу удалить его, потому что проект не компилируется
  • Я не могу что-то исключить, потому что gradle не поддерживает это для gradleApi() (см. http://gradle.1045684.n5.nabble.com/exclude-some-dependencies-from-gradleApi-dependency-td5712103.html).
  • Я не могу добавить только те gradle jar, которые мне действительно нужны, в качестве зависимостей - я не вижу способа явно ссылаться на них в зависимостях компиляции, я не вижу общедоступного репозитория, содержащего эти артефакты. Примечание: для сборки MAVEN я вручную загрузил их в локальный репозиторий MAVEN.

person Xtra Coder    schedule 20.10.2015    source источник
comment
Проблема в том, что вы пытаетесь использовать FileUtils.write, но в более старой версии (1.4) нет этого метода (класс «коллизия»)?   -  person Ethan    schedule 21.10.2015
comment
Это проблема, которая происходит, но основная проблема немного отличается - библиотека, на которую я ссылаюсь в своем плагине, использует более новую версию commons-io, тогда как среда выполнения, предоставляемая gradle, предоставляет более старую. Итак... gradle должен каким-то образом обеспечить разделение ClassLoaders во время тестирования, чтобы «собственный код» и «код плагина в модульном тесте» сосуществовали с разными версиями сторонних библиотек.   -  person Xtra Coder    schedule 21.10.2015
comment
Здесь Gradle поступает правильно и защищает вас от некорректных тестов. Когда ваш плагин загружается, он столкнется с тем же несоответствием jar, что и ваши тесты. Я бы попытался использовать Java8 или Groovy, чтобы обойти эту библиотеку.   -  person Ethan    schedule 21.10.2015
comment
Я бы не согласился с тем, что Gradle поступает здесь правильно. Как во время тестирования, так и во время выполнения внутренние ссылки (в данном случае на commons-io) классов в целевом плагине должны быть изолированы от внутренних ссылок gradle. MAVEN делает это как для тестирования, так и для выполнения кода плагина через специальные загрузчики классов.   -  person Xtra Coder    schedule 21.10.2015
comment
Но когда плагин выполняется, он будет выполняться со всеми внутренними jar-файлами, загруженными первыми в пути к классам. Так что он ведет себя так, как ваш плагин.   -  person Ethan    schedule 22.10.2015
comment
Я думаю, вы не понимаете правильную идею, которая реализована в Maven, но не реализована в Gradle. При создании внешних плагинов Maven загружает их классы через специальный загрузчик классов — путь к классам заполнен JAR-файлами, на которые есть ссылки в POM этого плагина. Этот загрузчик классов имеет в качестве родителя загрузчик классов Maven по умолчанию. Таким образом, ссылки локального плагина разрешаются первыми, а ссылки Maven по умолчанию — следующими. Проблема коллизий версий просто так не проявляется.   -  person Xtra Coder    schedule 22.10.2015


Ответы (2)


TL;DR

Не используйте commons-io:2.1, либо используйте Java8, Groovy, либо создайте помощника для использования вместо commons-io.

Пояснение

Основная причина заключается в том, что gradleApi() включает в себя commons-io:1.4, и когда тест выполняется, вы получаете два jar-файла commons-io в пути к классам, а версия 1.4 оказывается более ранней, поэтому вы получаете NoSuchMethodError.

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

Более конкретный пример: у вас есть jar-файл, содержащий расширение, которое используют другие плагины. Это может быть что-то вроде «failBuildOnQualityError». Затем каждый из отдельных плагинов (используя разные координаты) попытается использовать это расширение, чтобы увидеть, не должны ли они завершить сборку, когда checkstyle, findbugs, jacoco и т. д. обнаруживают проблемы.

person Ethan    schedule 22.10.2015
comment
Don't use commons-io:2.1 говорит не использовать никакую библиотеку с использованием commons-io:2.1, что, следовательно, означает - переписать всю эту библиотеку либо с помощью Java8, Groovy,... что, конечно, неприемлемо. - person Xtra Coder; 28.10.2015
comment
Альтернативой является создание основного класса, который затем внутри javaexec загружает совершенно отдельный загрузчик классов, куда вы можете добавить любую банку, которую хотите. - person Ethan; 29.10.2015

Хорошо, мой первоначальный диагноз «Gradle не изолирует путь к классам плагина от собственного пути к классам» неверен. На самом деле основная причина...

dependencies {
    compile gradleApi()
}

... поглощение многих зависимостей (включая commons-io:1.4). И правильное решение (по крайней мере, работоспособное для меня) - явно включать только необходимые банки:

versions = [
    gradle: "2.8",
    groovy: "2.4.4",
]

dependencies {
    compile "org.codehaus.groovy:groovy-all:${versions.groovy}"
    compile "org.gradle:gradle-base-services:${versions.gradle}"
    compile "org.gradle:gradle-base-services-groovy:${versions.gradle}"
    compile "org.gradle:gradle-core:${versions.gradle}"
    compile files("lib/gradle-platform-jvm-${versions.gradle}.jar")
}

Примечания:

  • на основные зависимости gradle следует ссылаться через общедоступные идентификаторы артефактов (а не через прямые ссылки на файлы JAR). Таким образом, gradle во время выполнения использует связанные POM для создания транзитивных зависимостей во время выполнения. В противном случае вы получите NoClassDefFoundError во время выполнения модульных тестов, потому что другие внутренние компоненты Gradle не находятся в пути выполнения к классам.

  • похоже, что не все артефакты Gradle доступны в общедоступных репозиториях (jcenter). Если в вашем плагине вы собираетесь строить зависимости от других «родных» задач (например, «jar») — на их реализацию нужно ссылаться напрямую через файл JAR.

person Xtra Coder    schedule 04.11.2015