Avatar ·

Как правильно использовать аннотации Spring (@Autowired, @Component) в Kotlin?

Всем привет! Я только начал изучать Kotlin и пытаюсь перенести свой старый проект на Spring с Java на этот язык. Столкнулся с проблемой: аннотации Spring, такие как @Autowired, не работают так, как я привык в Java.

В Java я обычно делал так:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    public String getUserName(Long id) {
        return userRepository.findById(id).map(User::getName).orElse("Unknown");
    }
}

Когда я попробовал написать то же самое на Kotlin:

@Service
class UserService {
    @Autowired
    private lateinit var userRepository: UserRepository
    
    fun getUserName(id: Long): String {
        return userRepository.findById(id).map { it.name }.orElse("Unknown")
    }
}

Компилятор ругается на lateinit var (якобы поле должно быть nullable или инициализировано), а приложение падает с NullPointerException. Пробовал использовать @Autowired на конструкторе — тоже не заработало. В интернете пишут, что в Kotlin нужно использовать constructor или field injection, но я запутался.

Подскажите, пожалуйста, как правильно внедрять зависимости через Spring в Kotlin? Нужны ли какие-то специальные плагины или настройки? И можно ли вообще использовать @Autowired как в Java?

<<>>

Основная проблема и её решение

В Kotlin аннотация @Autowired для полей работает, но есть нюансы. Главная проблема в вашем коде — использование lateinit var с @Autowired без указания цели аннотации. В Kotlin, в отличие от Java, аннотации могут применяться к разным элементам (полю, геттеру, параметру конструктора). По умолчанию @Autowired может примениться не туда, куда вы ожидаете.

Правильные подходы к DI в Spring + Kotlin

1. Внедрение через конструктор (рекомендуемый способ)

Это самый идиоматичный способ для Kotlin. Он использует constructor и не требует lateinit:

@Service
class UserService(
    private val userRepository: UserRepository
) {
    fun getUserName(id: Long): String {
        return userRepository.findById(id).map { it.name }.orElse("Unknown")
    }
}

Здесь private val делает поле неизменяемым и потокобезопасным. Spring автоматически внедрит зависимость через конструктор, если у компонента один конструктор (или вы явно пометили его @Autowired).

2. Внедрение через поле (если очень нужно)

Если вы настаиваете на field injection, используйте @Autowired с явным указанием цели AnnotationTarget.FIELD:

@Service
class UserService {
    @set:Autowired
    private lateinit var userRepository: UserRepository
    
    fun getUserName(id: Long): String {
        return userRepository.findById(id).map { it.name }.orElse("Unknown")
    }
}

Или используйте @Autowired напрямую на поле, но тогда вам нужно подключить плагин kotlin-spring в Gradle/Maven. Этот плагин автоматически открывает классы и методы (Spring требует их не-final).

3. Использование @Autowired для setter-метода

@Service
class UserService {
    private lateinit var userRepository: UserRepository
    
    @Autowired
    fun setUserRepository(repository: UserRepository) {
        this.userRepository = repository
    }
    
    fun getUserName(id: Long): String {
        return userRepository.findById(id).map { it.name }.orElse("Unknown")
    }
}

Важные настройки для Spring в Kotlin

  • Добавьте плагин kotlin-spring (он же kotlin-allopen): в Gradle это id("org.jetbrains.kotlin.plugin.spring"). Он автоматически делает все классы open, чтобы Spring мог создавать прокси.
  • Используйте kotlin-jpa плагин для работы с JPA-аннотациями.
  • Для конфигурации бинов используйте @Configuration и функции с @Bean — всё как в Java, но в Kotlin.

Почему lateinit var без @set:Autowired не работает?

В Kotlin @Autowired по умолчанию применяется к полю (target = FIELD). Но Spring ожидает, что аннотация будет на сеттере или конструкторе. Когда вы пишете @Autowired private lateinit var, Kotlin применяет аннотацию к полю, но Spring её не видит, потому что ожидает на сеттере. Решение — либо явно указать @set:Autowired, либо использовать constructor injection.

Рекомендация

Используйте внедрение через конструктор. Это даёт вам:

  • Неизменяемые поля (val вместо var)
  • Удобное тестирование (можно передать mock в конструктор)
  • Отсутствие NPE из-за lateinit
  • Чистый код без лишних аннотаций

Пример с несколькими зависимостями:

@Service
class OrderService(
    private val orderRepository: OrderRepository,
    private val paymentService: PaymentService,
    private val notificationService: NotificationService
) {
    fun processOrder(orderId: Long) {
        val order = orderRepository.findById(orderId).orElseThrow()
        paymentService.charge(order)
        notificationService.sendConfirmation(order)
    }
}

Этот код работает без единой аннотации @Autowired — Spring сам всё подхватит благодаря одному конструктору.

<<>>

Альтернативный подход с использованием @Autowired через primary constructor

Если вы привыкли к явному @Autowired и не хотите полагаться на неявное внедрение, можно использовать аннотацию прямо на конструкторе:

@Service
class UserService @Autowired constructor(
    private val userRepository: UserRepository
) {
    fun getUserName(id: Long): String {
        return userRepository.findById(id).map { it.name }.orElse("Unknown")
    }
}

Синтаксис @Autowired constructor явно указывает Spring, что этот конструктор должен использоваться для внедрения зависимостей. Это особенно полезно, если у вас несколько конструкторов (что редкость для сервисов).

Когда это может пригодиться?

  • Если вы работаете с устаревшим кодом, где много @Autowired.
  • Если вам нужно внедрить зависимости через конструктор, но при этом сохранить совместимость с Java-стилем.
  • При использовании фреймворков, которые требуют явного указания аннотации (например, некоторые версии Spring Boot с определёнными конфигурациями).

Важное замечание про lateinit

Избегайте lateinit var для Spring-зависимостей, если это возможно. lateinit создаёт риск NPE, если Spring не успеет проинициализировать поле (например, при неправильной конфигурации). Вместо этого используйте val в конструкторе — это гарантирует, что зависимость будет установлена до создания объекта.

Если же вы всё-таки вынуждены использовать lateinit (например, для циклических зависимостей), всегда проверяйте ::userRepository.isInitialized перед использованием.