Переупаковка пакетов в Gradle

    В своей статье хочу рассказать об очередной хитрости, которую можно довольно просто реализовать с помощью Gradle — переупаковке пакетов библиотек. Каждый, кто хоть чуть чуть работал с этой системой сборки, знает, что она автоматически умеет решать конфликты разных версий библиотек, а при желании можно повлиять на это, например зафорсить конкретную версию какой-нибудь библиотеки:


    configurations.all {
        resolutionStrategy {
            force "org.ow2.asm:asm:7.2"
        }
    }

    К сожалению, это не всегда помогает решить проблему конфликта версий. Например, есть известная проблема, что некоторые устройства htc в прошивке уже имеют библиотеку gson и если ваша версия gson-а отличается от встроенной, то могут возникнуть проблемы, так как ClassLoader загрузит в память только один класс и в данном случае это будет системный.


    Такая проблема также может возникнуть и при разработке библиотек. Если вы подключите в свой проект 2 библиотеки, использующие одну и ту же стороннюю библиотеку разных версий, например 1 и 2, то Gradle разрулит и возьмет самую новую версию, вторую. Но если в этой сторонней библиотеке нет обратной совместимости и вторая версия не может быть просто так использована вместо первой, то будут проблемы, которые наверняка будет очень сложно отследить по стектрейсу. Библиотека, ожидающая первую версию, получит классы второй и просто упадет.


    Я столкнулся с конфликтом версий при написании градл плагина, в нем используется библиотека asm, которая и конфликтовала. После написания плагина, я проверил его работоспособность на тестовом проекте: все отлично, проверил на pet project-е, тоже все хорошо, но когда подключил к реальному рабочему проекту с кучей сторонних зависимостей, столкнулся с проблемой.



    Решение проблемы под катом.


    Все же работало, что пошло не так?


    Выведем полный стектрейс ошибки:



    Видим, что ошибка в конструкторе класса библиотеки asm ClassVisitor на 79 строке. Заглянем туда, но при попытке открыть ClassVisitor, студия предложила 2 варианта



    В моем плагине используется asm версии 7.2, значит идем туда и на 79 строке видим следующее:



    Это явно не то, что нам нужно. Теперь идем в ClassVisitor 6 версии:



    Как раз наш IllegalArgumentException без сообщения. Мой плагин использует ASM api 7 версии Opcodes.ASM7, а в 6 версии библиотеки этого api еще не существует, поэтому и вылетает IllegalArgumentException в конструкторе. Можно сделать вывод, что плагин получает не вернную версию библиотеки.


    Херня вопрос, подумал я и сделал так:


    configurations.all {
        resolutionStrategy {
            force "org.ow2.asm:asm:7.2"
        }
    }

    К моему сожалению это не дало абсолютно никакого эффекта. Я так и не смог выяснить точную причину, почему не получается зафорсить версию asm-а, хотя команда ./gradlew app:dependencies показывает, что версия заменилась на 7.2. Если у кого-то есть мысли или предположения, буду рад услышать мнение.


    Проблему надо как-то решать


    Началась череда гугления и углубления в работу градла. В итоге, пошел на сайт asm-а, может они что-то знаю по этому поводу. Оказалось, что действительно знают, ответ на мой вопрос оказался в разделе FAQ. Говорят подменить пакет asm-а на другой, даже предлагают для этого утилиту. Ок, попробуем. Нужно лишь подключить плагин и сделать небольшую настройку:


    apply plugin: 'org.anarres.jarjar'
    ...
    dependencies {
        implementation fileTree(dir: 'build/jarjar', include: ['*.jar'])
        implementation jarjar.repackage('asm') {
            from 'org.ow2.asm:asm:7.2'
            classRename "org.objectweb.asm.**", "stater.org.objectweb.asm.@1"
        }
    }

    build/jarjar в данном случае директория, в которую будет сгенерирован jar файл библиотеки asm с переупакованными пакетами, поэтому нужно открыть доступ зависимостей в эту директорию через fileTree. Теперь библиотека будет доступна с импортом stater.org.objectweb.asm.* вместо org.objectweb.asm.*. У этого плагина есть еще различные настройки, но в моем примере хватило просто смены пакетов.


    Далее идем по всему проекту и меняем везде импорты с org.objectweb.asm на
    stater.org.objectweb.asm. На мой взгляд очень удобная утилита, в разы проще чем делать это руками, тем более что при обновлении библиотеки, мы просто меняем from 'org.ow2.asm:asm:7.2' на новую версию и переупакованный jar-ник с новой версией сгенерится на автомате.


    Если у вас просто проект (не библиотека), то этого вам будет достаточно, чтобы разрешить неразрешимые конфликты, типо gson-а, упомянутого в начале статьи. Но если вы, как и я, пишите библиотеку, то это не все.


    Проблему с переупаковкой мы решили, но теперь asm подключен к проекту не через зависимость от удаленного maven репозитория, а через локальный jar файл, который просто потеряется при деплое вашей библиотеки и будет такая ошибка NoClassDefFoundError. Но эту проблему решить довольно просто:


    1. В нашем gradle файле создаем новую конфигурацию:


      configurations {
          extraLibs
          implementation.extendsFrom(extraLibs)
      }

    2. Далее меняем


      implementation fileTree(dir: 'build/jarjar', include: ['*.jar'])

      на


      extraLibs fileTree(dir: 'build/jarjar', include: ['*.jar'])

    3. Переопределяем таску, которая отвечает за сбор вашего итогового jar файла и записываем все библиотеки с нашей новой конфигурацией в итоговый jar-ник:


      jar {
        from {
          configurations.extraLibs.collect { it.isDirectory() ? it : zipTree(it) }
        }
      }


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


    А если просто подключить jar файл конфликтующей библиотеки к плагину без переупаковки?


    Плохая идея, ни к чему хорошему она не приведет. В процессе сборки проекта есть такая интересная таска check...DuplicateClasses, которая просто зарубит файлы с одинаковыми пакетами. То есть файлы полученные из jar файла подключенной библиотеки и файлы из этой же библиотеки, подключенной через удаленный репозиторий. В итоге будет такая ошибка:



    На этом все. Спасибо всем, кто дочитал!


    Тулза для переупаковки
    Плагин с примером

    • +10
    • 1,7k
    • 6
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 6

      +1
      Спасибо за статью. Тоже решал данную проблему лет 5 назад, но в рамках android и пользовался jarjar вручную. Сегодня, конечно, сама проблема уже не актуальна если говорить о android.
      В Вашем случае другая ситуация, но по той же причине.
      Я так и не смог выяснить точную причину, почему не получается зафорсить версию asm-а, хотя команда ./gradlew app:dependencies показывает, что версия заменилась на 7.2
      То, с чем Вы столкнулись- перегрузка классов. Если Ваше приложение\плагин имеет классы, которые по именам и пакетам совпадают с теми, которые уже содержит рантайм (в Вашем случае- Gradle)- они не будут грузиться в память так как они там уже есть. Иными словами- конфликты классов решаются в сторону рантайма и, как итог, Ваш плагин обращается к классам Gradle, а не к классам Вашей библиотеки.

      Зафорсить не получится потому, что байткод Gradle уже собран с зависимостью от asm 6 (по ссылке уже 7.1). Тут либо самому собирать Gradle из исходников c нужной зависимостью в надежде ничего не сломать, либо искать версию Gradle с asm 7.2 из коробки (если такая есть), либо пользоваться той версией asm, что поставляется с Gradle.

      Ну или можно поступить так, как Вы- перепаковать либу или подвергнуть ее обфускации
        0
        Но в таком случае как вы объясните то, что плагин корректно работает на тестовых и pet проектах с точно такой же версией Gradle, как и на проекте где плагин не работает?
        В рабочем проекте есть robolectric, kryo, они и используют asm другой версии (возможно какая-нибудь еще либа). Я думаю тут конфликт версий asm-а связан именно со сторонними библиотеками, а не а с градлом.
          +1
          Сложно сказать. Надо сравнивать конфигурации Ваших проектов и что в них происходит. Если даже дело не в самом Gradle- могу предположить, что какой-то плагин, который уже содержит asm, подключается раньше Вашего.

          Быстрый гуглеж подсказывает, что это весьма вероятно. Например, вот так подключается asm в kotlin-android-extensions. И, судя по всему, по этой зависимости подключается версия asm с патчами для IntelliJ.

          Я не уверен, но, возможно, это может стать решением и для Вас
        0
        >Я столкнулся с конфликтом версий при написании градл плагина, в нем используется библиотека asm, которая и конфликтовала.
        А что, плагин разве не имеет своего класслоадера?
          0

          Интересный вопрос. Судя по всему нет, нашел это:


          It’s important to understand that a Gradle plugin does not run in its own, isolated classloader.
            0
            Как-то это довольно странно. Мавеновский плагин, чисто для примера. имеет свой собственный класслоадер. Если в API плагина ничего от упомянутых asm и т.п. не торчит (а оно по-идее не должно) — то это должно радикально решать подобные проблемы.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое