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

от автора

Практическое руководство для миграции своего 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.


ссылка на оригинал статьи https://habr.com/ru/articles/777824/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *