
Практическое руководство для миграции своего Java приложения (в особенности Spring Boot) на Kotlin. Основные ссылки на документацию: Kotlin Docs (на русском ссылки можно заменять на "ru", у меня работает только чз VPN).
Инициализация Gradle‑Kotlin проекта
- Создаем новый Gradle‑Kotlin проект: Create a Spring Boot project with Kotlin. Kts является расширением для Kotlin скриптов. Дополнительно: Understanding the Gradle Build. 
- Меняем версию Kotlin на последнюю и перегружаем/обновляем проект 
- Копируем без изменения ресурсы src/main/resources и src/test/resources 
Миграция базовых классов/интерфейсов
Преобразовывать классы Java в Kotlin в IDEA можно через конвертацию (Ctrl+Alt+Shift+K) или просто копируя Java код в Kotlin класс. После этого обычно требуется ручные правки.
- В Kotlin в одном файле можно создавать несколько публичных классов, поэтому, если классы небольшие, связанные между собой и их в проекте будет ограниченное количество, их помещяют в один общий с обопщающим названием 
- Наследовать можно только открытые и абстрактные классы, а по умолчанию Kotlin классы - final. Все подклассы делаем- open
- В Kotlin приходится заранее думать про Nullable and non‑nullable types. Чтобы не пропустить ошибку, после конвертации можно сначала убрать все Nullable? типы и затем добавлять - "?"только там, где он действительно требуется. Аннотация- @NonNullи !! operator принудительного преобразования нулевого типа в ненулевой для non‑nullable типов не нужены.
- Обычно, для улучшения читаемости и минимизации ошибок, если в конструкторае Kotlin несколько параметров, их принято распологать в отдельной строке: ставим курсор на любой из параметров и делаем Alt+Enter→Put parameters on separate lines. 
- Делаем везде, где возможно, реализацию методов в одну строку, тип возвращаемого значения, если он очевиден или неважен, опускаем и используем String templates — очень удобную фичу вставки значений прямо в строку (особенно часто используется в - toString)
- Интерфейсы Kotlin могут содержать свойства. Мой базовый интерфейс - HasId, от которого наследую все сущности JPA и все объекты DTO, выглядит так:
interface HasId {
    @get:Schema(accessMode = Schema.AccessMode.READ_ONLY)
    var id: Int?
    @JsonIgnore
    fun isNew() = id == null
    // doesn't work for hibernate lazy proxy
    fun id(): Int {
        Assert.notNull(id, "Entity must has id")
        return id!!
    }
}
- в - @Schemaдля springdoc по умолчанию прячем id поля вводимых объектов в Swagger. IDEA требует к ней «get/set use‑site target».
- В - id()после проверки на null делается принудительное приведение к non‑nullable type.
DTO
- Классы TO будем делать классами данных (по сути это так и есть, кроме того удобно сразу иметь сгенерированные - equals()/hashCode()/copy()). Для этого нам нужно объявлять все поля в конструкторе (синтаксис похоже на Java records, только поля будут изменяемые- var). При этом базовые классы и их поля придется открывать- open, а в наследуемых классах поля перекрывать- override. Также, для генерации конструктор без параметров, задаем всем полям в конструкторах значения по умолчанию. Lombok в Kotlin нет, копируем Java код без его аннотаций и по Alt+Enter добавляем- import.
- Строковые Non‑Null типы инициализируем - =""
- Чтобы не создавать лишних аннотации (только на поля, исключая геттеры и сеттеры) делаем к ним use‑site targets указатели - field:.
Entities
Entities классы не принято делать классами данных смотри Note (напомню, что классы автоматически открываются у нас через plugin.jpa). Кроме того в них, для правильной работы Hibernate, нельзя переопределять equals()/hashcode(), как это делают классы данных. К полям модели нужен доступ извне, делаем их public по умолчанию (см.свойства). 
- Переносим все поля в конструктор, добавляем к аннотациям - @field:, для конструктора без параметров делаем инициализацию по умолчанию, код методов причесываем в стиле Kotlin (put short branches on the same line as the condition, without braces)
- Мой базовый интерфейс - BaseEntityвыглядит так:
@MappedSuperclass
@Access(AccessType.FIELD)
abstract class BaseEntity(
    @field:Id
    @field:GeneratedValue(strategy = GenerationType.IDENTITY)
    override var id: Int? = null
) : HasId {
    //    https://stackoverflow.com/questions/1638723
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || javaClass != ProxyUtils.getUserClass(other)) return false
        val that = other as BaseEntity
        return id != null && id == that.id
    }
    override fun hashCode() = id ?: 0
    override fun toString() = "${javaClass.simpleName}:$id"
}- Для - BaseEntity.hashCodeиспользуем Elvis operator.
Repositories
- Дефолтные методы в - BaseRepositoryKotlin не видит, нужно добавить их поддержку: опция- -Xjvm-default=allи- @JvmDefaultWithCompatibility
Мой базовый интерфейс, от которого наследуются все репозитории:
@NoRepositoryBean
@JvmDefaultWithCompatibility
interface BaseRepository<T> : JpaRepository<T, Int?> {
    //    https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query.spel-expressions
    @Transactional
    @Modifying
    @Query("DELETE FROM #{#entityName} e WHERE e.id=:id")
    fun delete(id: Int): Int
    //  https://stackoverflow.com/a/60695301/548473 (existed delete code 204, not existed: 404)
    fun deleteExisted(id: Int) {
        if (delete(id) == 0) throw NotFoundException("Entity with id=$id not found")
    }
    fun getExisted(id: Int): T = findById(id).orElseThrow { NotFoundException("Entity with id=$id not found") }
}- Если мы не привязаны к - JpaRepository API, заменяем Java Optional на Kotlin nullable, переучиваемся работать с nullable еще раз :)
Залогиненный пользователь
- Для получения авторизованного пользователя из любого места приложения вместо companion objects сделал top-level functions 
- Для проверки в - authUserиспользуем Preconditions
- Коллизию имени с org.springframework.security.core.userdetails.User, разрешаем с помощью import as SecurityUser 
- AuthUser.userпри обновлении пользователя переприсваивается, делаем его- var
import org.springframework.security.core.userdetails.User as SecurityUser
fun safeAuthUser(): AuthUser? {
    val auth = SecurityContextHolder.getContext().authentication ?: return null
    val principal = auth.principal
    return if (principal is AuthUser) principal else null
}
fun authUser(): AuthUser = checkNotNull(safeAuthUser()) { "No authorized user found" }
class AuthUser(var user: User) : SecurityUser(user.email, user.password, user.roles) {
    fun id() = user.id()
    fun hasRole(role: Role) = user.hasRole(role)
    override fun toString() = "AuthUser:${user.id}[${user.email}]"
}Логирование
Наиболее красиво работать с логами через kotlin‑logging: подключаем kotlin‑logging‑jvm
Бины Spring
- Для - @Autowiredиспользуем отложенную инициализацию (- lateinit var)
- Все методы, которые переопределяются и проксируется Spring должны быть - open
Общие замечания
- Используем, где возможно универсальное выражение when, замену Java - Optionalна Kotlin Nulls и константы времени компиляции
- Используем Scope functions, выбор нужной можно первоначально делать по табличке Function selection 
- В Kotlin нет проверяемых исключений, - @Throwsтребуется только для вызова методов из Java
- Kotlin позволяет расширять класс путём добавления нового функционала, что позволяет делать код еще более объектно‑ориентированным (Extension Oriented Design). Старайтесь увидеть и применять этот дизайн везде, где к объекту добавляются новые свойства. Для всех расширений уровня проекта создал в корневом пакете - Extensions.kt
- Kotlin по умолчанию генерирует getter/setter для всех полей класса. В случае его переопределения будет конфликт, решается через запрет на автогенерацию через аннотацию @JvmField 
- Заменяем Java классы - Classна Kotlin KClass и Java коллекции на их Kotlin аналоги
- Если в функции несколько аргументов, заданных по умолчанию, можно менять только определенные, используя определенный именованный параметр 
Напоследок еще раз — статья не предназначена для чтения. Это скорее набор практический правил из курса Spring Boot REST API приложение на Kotlin (в рамках наших курсов «Из Middle в Senior», см. предыдущий пост по курсу "Работа с документами в Java") для миграции своего Java приложения (в особенности Spring Boot), поэтому обращайтесь к ней, когда решитесь на миграцию.
И да пребудет с вами сила:)!
PS: буду признателен за любые дополнения, замечания и комментарии к миграции кода на Kotlin.
Комментарии (8)
 - Joysi01.12.2023 11:04- Года 4 назад препятствием для переноса Spring Boot проекта была невозможность для Kotlin ставить аннотацию непосредственно на package (что было необходимо при работе с XML/JAXB). Сейчас это возможно?  - gkislin Автор01.12.2023 11:04- У меня не было такой необходимости. А зачем при XML/JAXB аннотация на package? На Java курсе Работа с документами в Java (https://habr.com/ru/articles/765332/) не было такой надобности. 
 
 - navrotski01.12.2023 11:04+1- Мне кажется стоило добавить о коллекциях и sequence в kotlin, о их влиянии на performance, и о том, что некоторые операции .collect() при совпадении ключа, в java вызовут ошибку, которая призовет к использованию другого, соответствующего коллектора (где можно указать, что делать в такой ситуации), а то время как в Котлин все отработает "молча", но не всегда так, как нужно автору кода. 
 По моему опыту, это частые подводные камни при подобном переходе. - gkislin Автор01.12.2023 11:04+1- Спасибо за комментарий! Ссылку на коллекции Kotlin добавил (https://kotlinlang.org/docs/collections-overview.html). По остальному - буду рад или ресурсам или подробному комментарию, тк на это натыкаешься уже в процессе работы, не на простой миграции.  - navrotski01.12.2023 11:04- Ну лично мне эта статья показалось очень иллюстративной 
 https://typealias.com/guides/when-to-use-sequences/
 
 
 
           
 
serious2monkeys
Средняя сложность тут с натяжкой, как мне кажется) Статья хорошая для начинающих в переходе с Java
gkislin Автор
Средний уровень Java для начинающих на Kotlin - так подразумевалось:)