Мигрируем Spring Boot REST API приложение на 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 : JpaRepository {
// 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
Общие замечания
Используем, где возможно универсальное выражение when, замену Java
Optional
на Kotlin Nulls и константы времени компиляцииИспользуем Scope functions, выбор нужной можно первоначально делать по табличке Function selection
В Kotlin нет проверяемых исключений,
@Throws
требуется только для вызова методов из JavaKotlin позволяет расширять класс путём добавления нового функционала, что позволяет делать код еще более объектно-ориентированным (Extension Oriented Design). Старайтесь увидеть и применять этот дизайн везде, где к объекту добавляются новые свойства. Для всех расширений уровня проекта создал в корневом пакете
Extensions.kt
Kotlin по умолчанию генерирует getter/setter для всех полей класса. В случае его переопределения будет конфликт, решается через запрет на автогенерацию через аннотацию @JvmField
Заменяем Java классы
Class
на Kotlin KClassЕсли в функции несколько аргументов, заданных по умолчанию, можно менять только определенные, используя определенный именованный параметр
Напоследок еще раз — статья не предназначена для чтения. Это скорее набор практический правил для миграции своего Java приложения (в особенности Spring Boot), поэтому обращайтесь к ней, когда решитесь на миграцию.
И да пребудет с вами сила:)!
PS: буду признателен за любые дополнения, замечания и комментарии к миграции кода на Kotlin.