Вызывал его в public class App extends Application Как я могу его переделать под такой же аналог Котлина class SampleApplication: Application() ,и смогу ли вызывать как прежде в Java классах таким методом?
Переход с Java на Kotlin при создании мобильного приложения
Почему нужен переход с Java на Kotlin при создании мобильного приложения. Какие-то детали будут полезны тем, кто занимается миграцией бэкенда.
Ксения Шеховцова
Разработчик компании «Философия.ИТ» (входит в Лигу Цифровой Экономики)
Мобильные приложения играют всё большую роль в ИТ-ландшафтах заказчиков и цифровизации экономики в целом. Логично, что предметные вопросы их разработки сегодня актуальны как никогда. В данном материале я расскажу, почему нужна миграция с Java на Kotlin при создании мобильного приложения. При этом какие-то детали будут полезны не только разработчикам под Android, но и тем, кто занимается миграцией бэкенда.
Зачем нужен переход на Kotlin
Стоит задуматься о переходе с Java на Kotlin, если вы постоянно пишите однотипный код и во время его исполнения сталкиваетесь с ошибками, на которые ранее мог бы указать компилятор, или в целом хотите повысить качество и читаемость исходников.
Kotlin сильно уменьшает количество кода, например, простой Java-объект (Plain Old Java Object), хранящий пять полей, займет примерно 96 строк кода. В то время как на Kotlin аналогичный Data-класс займёт всего одну строку, поскольку компилятор переопределит все нужные методы.
Кроме того, существует несколько серьёзных проблем, которые до сих пор есть в Java, но — внимание — уже решены в Kotlin! Вот некоторые из них:
Nullability — в Java мы не знаем, возможен ли null в значении данного объекта (и нельзя запретить ему принимать null), из-за чего может случиться всем известная ошибка NullPointerException, поскольку имело место обращение к «нулевому» объекту.
Raw types — это generic-типы в Java без указания типа-параметра, использование которых может привести к heap pollution — ситуации, когда переменная параметризованного типа хранит в себе объект, параметризованный другим типом. В результате может возникнуть ClassCastException в процессе исполнения программы — в Kotlin такие типы исключены.
Полноценные функциональные типы отсутствуют в Java в отличие от Kotlin.
Преимущества и дополнительные возможности Kotlin
Kotlin относится к объектно-ориентированным языкам программирования, сочетая в себе черты функциональных языков, что отличает его от Java, на котором не получится без проблем писать в функциональном стиле. Если вы разрабатываете под Android, то, скорее всего, выберете Kotlin, так как этот язык теперь официально поддерживается и продвигается Google, всё реже можно найти примеры кода на Java.
Огромное преимущество Kotlin – наличие Coroutines, которые легко позволяют управлять потоками при асинхронном программировании. В Java аналогичный код получается более сложным, плохо читаемым и громоздким.
У Kotlin есть множество возможностей, самые примечательные из которых:
Extension-функции или функции-расширения класса — делают код «чище», а в сочетании с автодополнением в IDE ускоряют разработку;
Data-классы хранят данные в лаконичном виде;
Лямбда-выражения и inline-функции — нужны для использования функций высшего порядка без потери производительности;
Object-классы позволяют с лёгкостью реализовать полезный паттерн проектирования Singleton;
Inline-функции с параметрами reified типа позволяют во многих случаях избавиться от рефлексии, которая может влиять на производительность (на старых версиях компиляторов).
Особенности миграции
Есть некоторые особенности, которые стоит учитывать при переходе.
Во-первых, в Kotlin нет привычных методов с ключевым словом static, для этого используется «companion object» в теле класса либо package level-функции. Аннотация @JvmStatic необходима тогда, когда мы хотим вызвать «companion object» — методы из Java таким же образом, каким бы вызывали любой другой static-метод в Java.
Во-вторых, Kotlin-файл может одновременно содержать константы, несколько классов, extension-функции, что разительно отличается от возможностей Java-файла, а это значит, что вам, скорее всего, придется поменять свой привычный способ организации кода и функций по классам.
В-третьих, в Kotlin существуют неизменяемые immutable-коллекции, которые более безопасно использовать при многопоточности, поэтому вы, вероятно, захотите переписать часть кода на них, а лучше вообще на Coroutines. При этом работать с коллекциями удобно в функциональном стиле.
Специальной подготовки непосредственно данных в БД не требуется. Если вы заменяете обычные Java-объекты на Data-классы, то необходимо проверить, что логика переопределенных вами ранее методов toString(), equlas() и hashCode() соответствует той, что по умолчанию будет сгенерирована в этих методах в Kotlin.
Необходимые шаги по переходу
Выделите основные участки кода, которые в первую очередь необходимо мигрировать. Выберите места, где максимально сократится код или значительно повысится читаемость и безопасность. Лучше начинать не с core-модулей, а с верхних слоев приложения: из-за того что Kotlin проектировался с учетом необходимости взаимодействия с языком Java, а не наоборот, лучше наследоваться от Java-классов, чем в Java наследоваться от Kotlin-классов.
Для упрощения перехода используйте встроенные средства конвертации Java-кода в Kotlin-код в IDE Intellij Idea или Android Studio, но не забывайте проверять полученный код.
Начинайте писать всю новую функциональность только на Kotlin и по возможности дорабатывайте старый код тоже на нём.
Постепенно углубляйте миграцию — переписывайте внутренние, абстрактные слои до тех пор, пока Java-кода не останется в проекте.
Возможные проблемы и уязвимости при переходе на Kotlin
Kotlin создавался как стопроцентно совместимый с Java — это значит, что проблемы при переходе будут минимальными.
Самая распространённая проблема связана с nullability, ведь когда мы вызываем Java-код из Kotlin-кода, то используем платформенные типы, которые не дают никакой информации о допустимости null-значений, а компилятор в свою очередь не делает дополнительной проверки на null. Так мы можем вызвать какой-нибудь метод на возвращённом нам объекте и получить на этапе исполнения кода описанную ранее NullPointerException.
Если в исходном проекте вы использовали популярный плагин Lombok для автоматической генерации методов (getters, setters и так далее), то вам придётся полностью от него отказаться, ведь Kotlin и так выполняет большинство функций Lombok, и вместе работать они не смогут. Полная замена Lombok может привести к масштабным изменениям в коде.
Если вы мигрируете проект, реализованный на Spring Boot, необходимо учитывать, что классы в Kotlin по умолчанию «закрыты» для наследования. Если не «открыть» классы с аннотациями типа @Configuration и @Service, это приведёт к ошибкам при запуске.
Кроме того, могут возникнуть ошибки при недостаточном понимании устройства языка, в редких случаях довольно, казалось бы, простой код ведет себя непредсказуемым образом. Рекомендую ознакомиться с Kotlin Puzzlers на Github, где вы увидите неочевидные головоломки в Kotlin и их решения. Это поможет лучше узнать язык и защитить себя от подобных проблем.
Пример реального кейса
На одном из проектов, где я выступала в роли backend-разработчика, необходимо было мигрировать высоконагруженную интеграционную платформу с Java на Kotlin. Отмечу, что особых проблем с переходом не возникало, несмотря на большой объем исходного кода.
Система сейчас успешно работает в продуктиве — за всё время развития и поддержки доля ошибок, связанных с Kotlin или взаимодействием Java и Kotlin, ничтожно мала. По состоянию на 2020 год все ключевые модули были переписаны, что составляло 60–70% от общего кода. Стоит также отметить, что переход на Kotlin происходил в начале развития этого языка программирования, задолго до пика популярности, что не помешало нам успешно использовать его в продуктиве.
Как с треском провалить миграцию с Java на Kotlin в Android приложении
С тех пор, как Google объявила об официальной поддержке Kotlin в Android, всё больше разработчиков хотят использовать его в своих новых и существующих проектах. Поскольку я также большой поклонник Kotlin, я не мог дождаться, когда смогу использовать Kotlin в своём рабочем проекте. В конце концов, Kotlin полностью совместим с Java, и все разработчики просто в восторге от этого. Так что же может пойти не так?
Ну, на самом деле многое может пойти не так. Меня просто пугает то, что на странице официальной документации Android «Начало работы с Kotlin» написано, что если вы хотите перенести существующее приложение на Kotlin, вы должны просто начать писать модульные тесты, а затем, после небольшого опыта работы с этим языком, вы должны писать новый код на Kotlin, а существующий Java-код просто конвертировать.
В этой статье я расскажу вам, что произошло бы, если бы я решил сразу же следовать этой стратегии на проде, а не попробовать её на своём маленьком проекте.
Познакомьтесь с моим маленьким проектом ??
У меня был проект, который я создал более года назад на Java, и мне нужно было немного его изменить и добавить один новый экран. Я подумал, что это отличная возможность проверить, действительно ли миграция существующего проекта на Kotlin очень проста, как все говорят. Исходя из рекомендаций официальной документации, я начал с написания на Kotlin модульных тестов. Новые классы я тоже писал на Kotlin и конвертировал некоторые существующие. Казалось, что всё замечательно, но через некоторое время я обнаружил одну неприятную проблему.
Kotlin + Lombok = ?
В моем приложении я использовал библиотеку Lombok для генерации геттеров и сеттеров. Lombok — это обработчик аннотаций для javac. К сожалению, компилятор kotlin использует javac без обработки аннотаций [источник]. Это означает, что методы, созданные при помощи Lombok, будут недоступны в Kotlin:
Но вы можете сказать: «Хорошо, это будет не очень круто, но в принципе можно вручную создать геттеры и сеттеры для тех полей, которые будут необходимы в коде Kotlin. В конце концов, не нужно этого делать во всём проекте сразу».
Я подумал так же. Но с реальной проблемой столкнулся тогда, когда захотел добавить новый экран в приложение.
Kotlin + Lombok + Dagger 2 = .
В моём приложении используется Dagger 2 для внедрения зависимостей. При создании нового экрана я обычно создаю структуру MVP: Activity, Presenter, Component, Module и Contract. Все зависимости для презентора внедряются с помощью Dagger. Activity вызывает DaggerSomeComponent.builder().(. ).build().inject(this) , чтобы внедрить презентор с необходимыми для себя зависимостями.
Использование Dagger 2 вместе с Kotlin — не проблема. Только перед этим нужно применить kapt-плагин, который создает необходимые самогенерируемые классы для Dagger.
И вот здесь всё начинает разваливаться
Без kapt-плагина я не мог использовать сгенерированные Dagger-классы в файлах Kotlin. Но после того, как я добавил этот плагин, все методы, созданные Lombok, исчезли!
До применения kapt-плагина:
После применения kapt-плагина:
И, к сожалению, решения этой проблемы нет. Вы можете применять либо только kapt-плагин, либо только Lombok. К счастью, поскольку это был всего лишь мой маленький проект, я просто удалил Lombok и сам написал геттеры и сеттеры. Но в этом проекте было всего около 50 сгенерированных методов. В проекте, который мы поддерживаем на работе, у нас их около тысячи. Удаление Lombok из этого приложения просто невозможно.
Также, очевидно, что отказ от kapt-плагина — не выход из ситуации. Без него вы не сможете использовать Kotlin в классах, где используется Dagger. В моем случае я должен был бы реализовать Activity, Component и Module на Java, и только Contract и Presenter могли быть написаны на Kotlin. А смешивание файлов Java и Kotlin — это определенно не здорово. Вместо плавного перехода с Java на Kotlin вы просто создали бы большой беспорядок.
Вот так выглядела бы эта ужасная полиглотная MVP-структура без kapt-плагина:
Но я всё же хочу перейти на Kotlin. Что мне делать?
Один из способов — использовать разные модули. Kotlin не увидит методы, которые Lombok будет генерировать в исходном коде, но будет видеть их в байт-коде.
Лично мне кажется, что это и есть самый предпочтительный путь. Если вы выводите Kotlin в отдельные зависимые модули для каждой фичи, вы уменьшаете риск таких проблем с совместимостью, с которыми столкнулся я, или более сложных, перечисленных в официальном гайде.
И это ещё не всё. В смешивании файлов Kotlin и Java без чёткого разделения есть много других недостатков. Это заставляет всех разработчиков, работающих с вами над проектом, знать оба языка. Также это уменьшает читаемость кода и может привести к увеличению времени сборки [источник].
Кратко
методы, сгенерированные Lombok, не видны в Kotlin;
использование kapt-плагина ломает Lombok;
без kapt-плагина вы не cможете использовать самогенерируемые классы для Dagger в Kotlin, что означает, что вам всё равно придётся писать новый код на Java;
способ решить эту проблему — вывести Kotlin в отдельные модули;
смешивание файлов Kotlin и Java в больших проектах без чёткого разделения может привести к неожиданным проблемам совместимости.
Из Java в Kotlin: туда и обратно
В статье рассмотрены проблемы и решения, которые возникли при добавлении Kotlin в существующий небольшой микросервис на Spring Boot, написанный изначально на Java. В рамках статьи не будут рассматриваться плюсы и минусы того или иного языка — здесь и так сломано много копий (некоторые статьи на habr: 1, 2, 3) и, как мне кажется, каждая команда должна это решать для себя. Рассматривается стандартный стек Spring WebMVC (не реактивный)
Краткое описание микросервиса, с которым будем работать
До перевода на Kotlin использовался такой стек:
Java 11
Spring Web MVC (в рамках Spring Boot)
Spring Data JPA
Map Struct
Lombook
Maven
С чего начать
Переводим тесты на Kotlin
Первая рекомендация, которую вы найдете, — начинайте с тестов. В рамках своего проекта — подтверждаем, что это самый правильный путь. Пока вы будете добавлять тесты на Kotlin в проект, вы пройдете через следующие этапы:
Настроите сборку совместно с Kotlin
Добавите требуемые библиотеки
Поймаете основные ошибки
Наконец-то напишите тесты на давно заброшенные участки кода
И при этом основной код, отвечающий за бизнес-логику затронут не будет. Начать с тестов нам очень помогло, даже хотя бы тем, что поймали пару ошибок в нашей существующей бизнес-логике.
Подключаем Kotlin в проект
Несколько слов о Gradle Kotlin DSL
Дополнительно к стандартному набору для сборки пакетов (Maven и Gradle на Groovy) теперь добавился еще один — Gradle Kotlin DSL. Это может быть хорошим вариантом для тех, кто хотел бы использовать Gradle, но смущала необходимость использовать Groovy.
Из плюсов данного решения — более корректные подсказки в IDEA (хотя, если честно, IDEA сильно тормозит при анализе build.gradle.kt файлов). Одним из главных неудобств, как мне кажется, является то, что не все возможности Gradle на Groovy реализованы или реализованы не зеркально, и поэтому многие подсказки со stackoverflow не будут работать. Поэтому мы остались на привычном нам Maven.
Maven
Если вы используете стек Spring-boot, то рекомендую для начала создать тестовый проект с нуля через https://start.spring.io/.
Ниже приведены 2 pom.xml файла, которые создает https://start.spring.io/ для Java и Kotlin.
Подробнее про kotlin-maven-plugin можно прочитать здесь.
Данная конфигурация подойдет только, если у вас в проекте не будет кода на Java. Если вы хотите использовать оба языка (что и происходит при миграции), то нужно добавить и корректно настроить maven-compiler-plugin. (Пример можно посмотреть здесь или здесь)
-Xjsr305=strict позволяет Kotlin корректно использоваться nullable-типы для некоторых api Spring (ссылка)
Про плагины All-open и No-arg будет подробнее рассказано нижe.
Kotlin plugins
Для изменения процесса компиляции в Kotlin используются compiler plugins.
По умолчанию spring-initializer добавляет 2 плагина: all-open и no-arg. Так же часто используются kapt и плагин для Lombok.
all-open
По умолчанию все классы и их члены в Kotlin имеют неявный модификатор final и, соответственно, они не могут быть наследованы и переопределены, что очень сильно ограничивает их использование в Spring. Чтобы решить эту проблему, используется plugin all-open (документация), который добавляет open к указанным аннотациями классам и членам классов. Для работы со spring используется уже преднастроенный plugin kotlin-spring.
Kotlin-spring работает со следующими аннотациями:
@Component
@Async
@Transactional
@Cacheable
@SpringBootTest
Если вы делаете свою аннотацию, то возможно вам стоит добавить plugin kotlin-allopen и настроить его для работы с этой аннотацией.
Обратите внимание, что в списке аннотаций нет аннотаций, относящихся к JPA и их нужно добавлять отдельно (см. статью от Haulmont).
No-arg
Плагин добавляет пустой конструктор к указанным аннотациями классам. Для jpa есть преднастроенный kotlin-jpa плагин. Он работает со следующими аннотациями: @Entity , @Embeddable , и @MappedSuperclass . При этом он не делает эти классы open , это требуется сделать руками с помощью плагина kotlin-allopen.
Kapt
Является адаптером для annotation processors (документация). Используется, например, mapstruct для генерации кода для маперов на kotlin
По опыту использования могу сказать, что довольно часто выдает ошибки и ломает сборку, особенно, если в проекте есть Lombok.
Плагин позволяет корректно вызывать сгенерированные lombok методы из kotlin кода. (Документация). Плагин находится еще в бете, и не поддерживает @Builder .
Работа со Spring
Spring уже хорошо оптимизирован для работы с Kotlin.
По выступлениям можно посмотреть, как и что добавлялось для поддержки Kotlin:
выступление 2020 года (видео и расшифровка)
выступление 2021 года (видео)
В этой части статьи приведены небольшие рекомендации и различия по использования Kotlin со Spring по сранению с Java.
Так как точка запуска в Kotlin является функция main, то запуск Spring-Boot приложения теперь выглядит так:
@SpringBootApplication class DemoKotlinApplication fun main(args: Array) < runApplication(*args) >
В Spring добавлены и добавляются функции расширения для удобства использования Kotlin, например, RestOperationsExtensions.kt. Поэтому стоит поискать новое API, адаптированное для Kotlin — возможно оно уже есть
Для Kotlin для внедрения зависимостей рекомендуется использовать val аргументы в конструкторе,
@Component class YourBean( private val mongoTemplate: MongoTemplate, private val solrClient: SolrClient )
Но возможны и другие варианты.
Если требуется внедрить зависимость для поля, то можно использовать latenit var
@Component class YourBean
Эта запись эквивалентна такой записи в Java:
@Component public class YourBean
Можно использовать инъекцию и через set-методы, но при этом поле становится Nullable
var hello: HelloService? = null @Autowired set(value)
Для конфигурации свойств требуется экранировать $: @Value(«\$»)
Поддерживается создания классов конфигураций через конструктор при использовании @ConstructorBinding
@ConfigurationProperties("test") @ConstructorBinding class TestConfig( val name:String )
Так же можно создавать через lateinit var
@ConfigurationProperties("test") class TestConfig
Для генерации мета-информации нужно настроить spring-boot-configuration-processor для работы с kapt
Работа с Hibernate
Основные ошибки и проблемы хорошо описаны в статье Haulmont.
Еще раз обращу внимание на необходимость корректной настройки плагинов no-args и all-open и на корректную реализацию методов hashCode и equals.
Остальные библиотеки
Jackson
Spring использует Jackson для сериализации/десериализации данных. Для работы с Kotlin требуется добавить зависимость Jackson Module Kotlin. После этого вы сможете работать с Kotlin-специфичными типами. При этом нет необходимости явно указывать типа объектов.
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue data class MyStateObject(val name: String, val age: Int) . val mapper = jacksonObjectMapper() val state = mapper.readValue(json) // or val state: MyStateObject = mapper.readValue(json) // or myMemberWithType = mapper.readValue(json)
MapStruct
MapStruct работает через annotation processor. Поэтому для его работы необходимо корректно настроить kapt. При этом если модели используют аннотации Lombok, то при генерации мапперов будут проблемы (см. Lombok)
Работа с Lombok-методами напрямую из Kotlin из коробки не работает. Для этого требуется использовать Lombok compiler plugin.
При этом возникают большие проблемы при совместной работе Kapt и Lombok. По умолчанию kapt начинает запускать все annotation processors и отключает их работу через javac. Для того чтобы Lombok продолжал работать, требуется явно это указать
При этом Lombok будет корректно работать с kapt только, если annotation processor, который используют kapt, не зависит от Lombok. В нашем случае это было не так, так как мы используем MapStruct, и поэтому пришлось в первую очередь всю доменную модель переводить на Kotlin. Так же одним из вариантов является использование Delombok. Эта проблема известная и уже поднималась на habr.
Mockito
Mockito некорректно работает с типами Kotlin из коробки. Поэтому Spring рекомендует использовать Mockk. Также для Mockito появился специальный модуль, добавляющий поддержку Kotlin — Mockito-Kotlin.
В нашем проекте мы использовали Mockito-Kotlin. Единственная проблема в нем, что нужно аккуратно следить, откуда что импортируется, так как, например, any() теперь будут в 2 местах — org.mockito.kotlin и org.mockito.Mockito
Логирование
При разработке enterprise-приложений привыкаешь ставить аннотацию @Slf4j от Lombok и получать готовый логгер. Так как Lombok с Kotlin не дружит, то приходится искать другие пути.
Можно пойти по пути создания своей надстройки — как в этой статье. Но для нас оказалась очень удобной библиотека — kotlin-logging, которая предоставляет возможность делать, например, вот такие вещи:
import mu.KotlinLogging private val logger = KotlinLogging.logger <> class FooWithLogging < val message = "world" fun bar() < logger.debug < "hello $message" >> >
Как видно, нам здесь не нужно указывать явно класс и пакет. А также поддерживается ленивое создание сообщения.
Выводы
Хочется закончить статью краткими выводами. Использование Java и Kotlin совместно в одном проекте требует дополнительныx настроек, но практически все решается и мы получаем возможность использовать 2 языка в одном проекте. Самой большой проблемой для нас оказалось невозможность полностью подружить Lombok и Kotlin.
Kotlin приносит много полезных идей. Но, как мне кажется, полностью удобство Kotlin начинаешь ценить при разработке реактивного кода. В данной статье это не рассмотрено, но это хорошо показано, например, в этой статье.
Использованные материалы
Документация по Kotlin
Выступление 2020 года о поддержке Kotlin в Spring (видео и расшифровка)
Выступление 2021 года о поддержке Kotlin в Spring (видео)