Pull to refresh

Мигрируем Java Spring Boot приложение на Kotlin

Level of difficultyMedium
Reading time6 min
Views6.3K

Практическое руководство для миграции своего Java приложения (в особенности Spring Boot) на Kotlin. Основные ссылки на документацию: Kotlin Docs (на русском ссылки можно заменять на "ru", у меня работает только чз VPN).

Инициализация Gradle‑Kotlin проекта

Миграция базовых классов/интерфейсов

Преобразовывать классы 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!!
    }
}

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"
}

Repositories

Мой базовый интерфейс, от которого наследуются все репозитории:

@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") }
}

Залогиненный пользователь

  • Для получения авторизованного пользователя из любого места приложения вместо 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

Общие замечания

Напоследок еще раз — статья не предназначена для чтения. Это скорее набор практический правил из курса Spring Boot REST API приложение на Kotlin (в рамках наших курсов «Из Middle в Senior», см. предыдущий пост по курсу "Работа с документами в Java") для миграции своего Java приложения (в особенности Spring Boot), поэтому обращайтесь к ней, когда решитесь на миграцию.

И да пребудет с вами сила:)!

PS: буду признателен за любые дополнения, замечания и комментарии к миграции кода на Kotlin.

Tags:
Hubs:
Total votes 10: ↑7 and ↓3+4
Comments8

Articles