Русский
Русский
English
Статистика
Реклама

Groovy

Конфигурация многомодульных проектов

26.08.2020 14:06:49 | Автор: admin

Предыстория


Иногда, когда я прокрастинирую, я занимаюсь уборкой: чищу стол, раскладываю вещи, прибираюсь в комнате. По сути, привожу окружающую среду в порядок это заряжает энергией и настраивает на рабочий лад. С программированием у меня та же ситуация, только я чищу проект: провожу рефакторинги, делаю различные инструменты и всячески стараюсь упростить жизнь себе и коллегам.

Некоторое время назад мы в команде Android решили сделать один из наших проектов Кошелек многомодульным. Это привело как к ряду преимуществ, так и проблем, одна из которых необходимость конфигурировать каждый модуль заново. Конечно, можно просто копировать конфигурацию из модуля в модуль, но если мы захотим что-то поменять, то придется перебрать все модули.

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



Первая итерация вынос версий библиотек


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

Подход заключается в том, что надо выносить версии библиотек в отдельные глобальные свойства проекта, тогда они становятся доступны по всему проекту, что помогает использовать их многократно. Обычно это делается в файле build.gradle на уровне проекта, но иногда эти переменные выносят в отдельный .gradle файл и подключают в основном build.gradle.

Скорее всего, вы уже видели такой код в проекте. В нем нет никакой магии, это просто одно из расширений Gradle под названием ExtraPropertiesExtension. Если кратко, то это просто Map<String, Object>, доступный по имени ext в объектe project, а все остальное работа как будто с объектом, блоки конфигурации и прочее магия Gradle. Примеры:
.gradle .gradle.kts
// creationext {  dagger = '2.25.3'  fabric = '1.25.4'  mindk = 17}// usageprintln(dagger)println(fabric)println(mindk)

// creationval dagger by extra { "2.25.3" }val fabric by extra { "1.25.4" }val minSdk by extra { 17 }// usageval dagger: String by extra.propertiesval fabric: String by extra.propertiesval minSdk: Int by extra.properties


Что мне нравится в этом подходе: он крайне простой и помогает версиям не разъезжаться. Но у него есть минусы: надо следить, чтобы разработчики использовали версии из этого набора, и это не сильно упрощает создание новых модулей, потому что всё равно приходится копировать еще много чего.

Кстати, подобного эффекта можно добиться, используя gradle.properties вместо ExtraPropertiesExtension, только будьте осторожны: ваши версии можно будет переопределить при сборке с помощью -P флагов, а если вы обращаетесь к переменной просто по имени в groovy-cкриптах, то gradle.properties заменят и их. Пример с gradle.properties и переопределением:

// grdle.propertiesoverriden=2// build.gradleext.dagger = 1ext.overriden = 1// module/build.gradleprintln(rootProject.ext.dagger)   // 1println(dagger)                   // 1println(rootProject.ext.overriden)// 1println(overriden)                // 2

Вторая итерация project.subprojects


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

allprojects {    repositories {        google()        jcenter()    }}

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

Пример конфигурации модулей через project.subprojects
subprojects { project ->    afterEvaluate {        final boolean isAndroidProject =            (project.pluginManager.hasPlugin('com.android.application') ||                project.pluginManager.hasPlugin('com.android.library'))        if (isAndroidProject) {            apply plugin: 'kotlin-android'            apply plugin: 'kotlin-android-extensions'            apply plugin: 'kotlin-kapt'                        android {                compileSdkVersion rootProject.ext.compileSdkVersion                                defaultConfig {                    minSdkVersion rootProject.ext.minSdkVersion                    targetSdkVersion rootProject.ext.targetSdkVersion                                        vectorDrawables.useSupportLibrary = true                }                compileOptions {                    encoding 'UTF-8'                    sourceCompatibility JavaVersion.VERSION_1_8                    targetCompatibility JavaVersion.VERSION_1_8                }                androidExtensions {                    experimental = true                }            }        }        dependencies {            if (isAndroidProject) {                // android dependencies here            }                        // all subprojects dependencies here        }        project.tasks            .withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile)            .all {                kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()            }    }}


Теперь для любого модуля с подключенным плагином com.android.application или com.android.library мы можем настраивать что угодно: подключаемые плагины, конфигурации плагинов, зависимости.

Все было бы отлично, если бы не пара проблем: если мы захотим в модуле переопределить какие-то параметры, заданные в subprojects, то у нас это не получится, потому что конфигурация модуля происходит до применения subprojects (спасибо afterEvaluate). А еще если мы захотим не применять это автоматическое конфигурирование в отдельных модулях, то в блоке subprojects начнет появляться много дополнительных проверок. Поэтому я стал думать дальше.

Третья итерация buildSrc и plugin


До этого момента я уже несколько раз слышал про buildSrc и видел примеры, в которых buildSrc использовали как альтернативу первому шагу из этой статьи. А еще я слышал про gradle pluginы, поэтому стал копать в этом направлении. Все оказалось очень просто: у Gradle есть документация по разработке кастомных плагинов, в которой все написано.

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

Код плагина
import org.gradle.api.JavaVersionimport org.gradle.api.Pluginimport org.gradle.api.Projectclass ModulePlugin implements Plugin<Project> {    @Override    void apply(Project target) {        target.pluginManager.apply("com.android.library")        target.pluginManager.apply("kotlin-android")        target.pluginManager.apply("kotlin-android-extensions")        target.pluginManager.apply("kotlin-kapt")        target.android {            compileSdkVersion Versions.sdk.compile            defaultConfig {                minSdkVersion Versions.sdk.min                targetSdkVersion Versions.sdk.target                javaCompileOptions {                    annotationProcessorOptions {                        arguments << ["dagger.gradle.incremental": "true"]                    }                }            }            // resources prefix: modulename_            resourcePrefix "${target.name.replace("-", "_")}_"            lintOptions {                baseline "lint-baseline.xml"            }            compileOptions {                encoding 'UTF-8'                sourceCompatibility JavaVersion.VERSION_1_8                targetCompatibility JavaVersion.VERSION_1_8            }            testOptions {                unitTests {                    returnDefaultValues true                    includeAndroidResources true                }            }        }        target.repositories {            google()            mavenCentral()            jcenter()                        // add other repositories here        }        target.dependencies {            implementation Dependencies.dagger.dagger            implementation Dependencies.dagger.android            kapt Dependencies.dagger.compiler            kapt Dependencies.dagger.androidProcessor            testImplementation Dependencies.test.junit                        // add other dependencies here        }    }}


Теперь конфигурация нового проекта выглядит как applyplugin:ru.yandex.money.module и все. Можно вносить свои дополнения в блок android или dependencies, можно добавлять плагины или настраивать их, но главное, что новый модуль конфигурируется одной строкой, а его конфигурация всегда актуальна и продуктовому разработчику больше не надо думать про настройку.

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

Важный момент: если вы используете android gradle plugin ниже 4.0, то некоторые вещи очень сложно сделать в kotlin-скриптах по крайней мере, блок android проще конфигурировать в groovy-скриптах. Там есть проблема с тем, что некоторые типы недоступны при компиляции, а groovy динамически типизированный, и ему это не важно =)

Дальше standalone plugin или монорепо


Конечно же, третий шаг это еще не всё. Нет предела совершенству, поэтому есть варианты, куда двигаться дальше.

Первый вариант standalone plugin для gradle. После третьего шага это уже не так сложно: надо создать отдельный проект, перенести туда код и настроить публикацию.

Плюсы: плагин можно шарить между несколькими проектами, что упростит жизнь не в одном проекте, а в экосистеме.

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

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

Итого


В новом проекте делай сразу третий шаг buildSrc и plugin проще будет всем, тем более, что код я приложил. А второй шаг project.subprojects используй для того, чтобы подключать общие модули между собой.

Если у тебя есть что добавить или возразить, пиши в комментарии или ищи меня в соцсетях.
Подробнее..

Code evaluation как средство отладки

15.02.2021 14:04:42 | Автор: admin

Господа разработчики java приложений. Сегодня вашему вниманию представляется простой способ использования code evaluation, реализация которого позволит исполнять произвольный код в работающем приложении, что в свою очередь позволит сэкономить массу времени на CI/CD.

Зачем мне это нужно?

Если вам приходится разрабатывать в условиях микросервисной архитектуры, особенно в большой компании, то скорее всего вам знакома ситуация, когда посмотреть как "по-боевому" работает приложение можно только на стендах на которых есть интеграции с другими микросервисами. И поскольку далеко не все вещи можно проверить локально на заглушках, для того чтобы проверить ту или иную гипотезу, приходится пушить новый код (не факт что работающий корректно) в репозиторий... прогонять его через CI/CD... и только потом по логам понять что где-то что-то пошло не так. И хорошо если логи сразу покажут в чём вы ошиблись, потому как иначе этот процесс пуша и прогона по пайплайнам может стать вашим круговоротом сансары.

Исполнение динамически введённого кода поможет решить эту проблему.

Как это работает?

Как мы с вами знаем, groovy это полностью совместимый с Java язык программирования с динамической компиляцией. Эти две особенности groovy и помогут нам в том, что бы реализовать code evaluation для java приложений. О том как добавить поддержку groovy в java проект вы сможете легко найти сами. А я приведу пример, как реализовать code evaluation (в некотором смысле аналогичный тому, который вы можете видеть во время дебага в вашей IDE).

1) Создадим groovy класс, а в нём шаблонную строку в которую поместим класс и плейсхолдер для динамически введённого кода. Пример такой строки:

def EXPRESSION_CLASS_TEMPLATE = """package dev.toliyansky.eval.serviceclass ExpressionClass implements java.util.function.Supplier<Object> {def get() {%s}}"""

Примечание: package должен быть такой же как и у класса в котором вы этот код будете писать.

Почему мы имплементируем Supplier будет описано ниже.

2) Динамически скомпилируем и загрузим этот класс.

Кусок кода ниже может быть размещён например в REST контроллере, который получает code как тело запроса.

def finalClassCode = String.format(EXPRESSION_CLASS_TEMPLATE, code)def supplier = groovyClassLoader.parseClass(finalClassCode)                                .getDeclaredConstructor()                                .newInstance() as Supplier<Object>def result = supplier.get()

В первой строчке заменяем %s на код который хотим динамически ввести и исполнить в рантайме.

Во второй строке компилируем и загружаем класс из строки, которую получили на предыдущем шаге. Тут важно заметить что созданный инстанс класса приводим к Supplier для того, чтобы далее в третьей строчке можно было сделать вызов метода в котором должен исполниться динамически введённый код. Supplier<Object> в данном случае идеально нам подходит.

Вот собственно и всё.

Пример использования code evaluation

Допустим у вас web приложение в kubernetes. Вы написали какую-то новую фичу, закоммитились, прошли код ревью, прогнали код через CI/CD и вот POD с вашим приложением наконец поднимается, вы заходите в логи и видите что фича работает не так как ожидалось. Допустим, для примера, что вы забыли в конструкторе что-то проинициализировать и поэтому остальная бизнес логика не отрабатывает с банальным NullPointerException.

Имея в своём арсенале HTTP роут с исполнением динамического кода, можно спокойно обратиться к applicationContext, вытащить нужный бин и ручками проинициализировать забытую переменную. После чего потыкать в инициирование отработки бизнес кода и проверить результат, минуя весь CI/CD. Таким банальным примером всё не ограничивается. При желании можно даже в райнтайме переопределить метод класса с бизнес логикой и играться до тех пор пока не отладите код и потом уже зная как оно себя ведёт коммититься и прогонять пайплайны.

Готовое решение для web приложений на spring boot

Если вы не хотите возиться с добавлением groovy в ваш java проект и писать все эти контроллеры, обёртки, разбираться с динамикой, а просто хотите добавить в одну строчку зависимость в ваш проект и чтобы работало из коробки, то тогда специально для вас презентую маленький проект который всё это умеет - evaluator-spring-boot-starter

Это, как можно догадаться из названия, spring boot starter. Подключение данного стартера добавляет в ваше web приложение роут http://host:port/eval отдающий WEB-UI, в котором можно ввести код который хотите динамически исполнить, а результат выведется в окне рядом. Всё это приправлено подсветкой синтаксиса, и прочими удобствами. Если же, например, ваше приложение не имеет выходного роута из кластера и вы можете только лишь использовать curl или wget из терминала POD, то тогда можно использовать роут http://host:port/eval/groovy и отправлять код как параметр GET запроса или как тело POST запроса.

Впрочем, всё это более-менее подробно описано в readme проекта.

Скриншот WEB-UI evaluator-spring-boot-starterСкриншот WEB-UI evaluator-spring-boot-starter

В итоге

  • Продемонстрировано как code evaluation может сэкономить время на отладке приложения

  • Продемонстрировано как реализовать code evaluation в java проекте

  • Продемонстрировано готовое решение в виде spring boot starter.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru