Spring Boot adalah framework backend paling matang di ekosistem JVM. Sejak versi 5, Spring mendukung Kotlin sebagai warga kelas satu — dengan ekstensi idiomatik, dukungan coroutines, dan integrasi tooling. Bab-bab sebelumnya membahas Ktor yang ringan; mulai dari chapter ini, kita akan beralih ke Spring Boot. Pada chapter ini kita akan memahami anotasi @SpringBootApplication, container IoC, dan konstruktor injection lewat model yang dapat dijalankan tanpa dependensi Spring nyata.
Anatomi Spring Boot #
Aplikasi Spring Boot dimulai dari satu class yang ditandai @SpringBootApplication. Anotasi ini sebenarnya adalah kombinasi tiga anotasi: @SpringBootConfiguration (konfigurasi bean), @EnableAutoConfiguration (auto-konfigurasi starter), dan @ComponentScan (pemindaian classpath untuk bean).
@SpringBootApplication
class MyApplication
fun main(args: Array<String>) {
runApplication<MyApplication>(*args)
}Saat runApplication<MyApplication>() dipanggil, Spring membuat container IoC, memindai classpath untuk class dengan anotasi @Component (atau turunannya: @Service, @Repository, @RestController, @Configuration), dan mendaftar instance sebagai bean.
Container IoC #
Container IoC (Inversion of Control) adalah jantung Spring. Container bertanggung jawab membuat instance, mengelola lifecycle (singleton, prototype), dan menyuntikkan dependensi. Saat sebuah class membutuhkan UserRepository, container mencari bean bertipe UserRepository dan menyuntikkan via konstruktor.
Pola IoC ini disebut juga Dependency Injection — dependensi tidak dibuat oleh class sendiri, tetapi disuntikkan dari luar. Hal ini membuat class lebih mudah diuji karena dependensi dapat dimock.
Konstruktor Injection #
Konstruktor injection adalah cara menyuntikkan dependensi yang direkomendasikan Spring. Dependensi dideklarasikan sebagai parameter konstruktor — kompilator memastikan dependensi tidak null saat objek dibuat.
@Service
class UserService(private val repo: UserRepository) {
fun getUser(id: Int): User = repo.findById(id) ?: throw NoSuchElementException("User $id")
}Pada Kotlin, anotasi @Autowired dapat dihilangkan jika class hanya punya satu konstruktor — Spring otomatis memakai konstruktor tersebut untuk injection. Hal ini menghasilkan kode yang lebih bersih dibanding Java.
Demo Spring Boot #
Karena Spring Boot nyata membutuhkan dependensi eksternal, kita akan memodelkannya sebagai container IoC sederhana. Model ini mendukung registrasi bean, lookup berdasarkan tipe, dan konstruktor injection manual.
// spring_boot_dengan_kotlin.kt
// Mendemonstrasikan konsep Spring Boot + Kotlin: @SpringBootApplication,
// constructor injection, dan data class sebagai bean. Pada kode produksi,
// anotasi diproses oleh Spring framework. Di sini kita memodelkannya.
// Mock anotasi Spring (di produksi: org.springframework.*)
@Target(AnnotationTarget.CLASS)
annotation class SpringBootApplication
@Target(AnnotationTarget.CLASS)
annotation class Component
@Target(AnnotationTarget.CLASS)
annotation class Service
@Target(AnnotationTarget.CLASS)
annotation class Repository
@Target(AnnotationTarget.CLASS)
annotation class RestController
@Target(AnnotationTarget.CLASS)
annotation class Configuration
// Data class sebagai bean — Spring mendukung data class sebagai bean
data class User(val id: Int, val nama: String, val email: String)
// Constructor injection: dependensi dideklarasikan di constructor
interface UserRepository {
fun findById(id: Int): User?
fun findAll(): List<User>
fun save(user: User): User
}
@Repository
class InMemoryUserRepository : UserRepository {
private val users = mutableListOf<User>()
private var nextId = 1
override fun findById(id: Int): User? = users.firstOrNull { it.id == id }
override fun findAll(): List<User> = users.toList()
override fun save(user: User): User {
val saved = if (user.id == 0) user.copy(id = nextId++) else user
val idx = users.indexOfFirst { it.id == saved.id }
if (idx >= 0) users[idx] = saved else users.add(saved)
return saved
}
}
@Service
class UserService(private val repo: UserRepository) {
fun getUser(id: Int): User = repo.findById(id)
?: throw NoSuchElementException("User $id tidak ditemukan")
fun listUsers(): List<User> = repo.findAll()
fun register(nama: String, email: String): User =
repo.save(User(0, nama, email))
}
// SpringApplicationContext: container IoC sederhana
class SpringApplicationContext {
internal val beans = mutableMapOf<String, Any>()
inline fun <reified T : Any> register(bean: T) {
register(T::class, bean)
}
fun <T : Any> register(cls: kotlin.reflect.KClass<T>, bean: T) {
beans[cls.simpleName ?: "anon"] = bean
}
inline fun <reified T : Any> getBean(): T = getBean(T::class)
@Suppress("UNCHECKED_CAST")
fun <T : Any> getBean(cls: kotlin.reflect.KClass<T>): T {
val key = cls.simpleName ?: error("Bean tanpa nama")
return beans[key] as? T ?: error("Bean $key tidak terdaftar")
}
}
@SpringBootApplication
class MyApplication
fun main() {
// Bootstrap container secara manual (Spring memakai @Autowired otomatis)
val ctx = SpringApplicationContext()
val repo = InMemoryUserRepository()
ctx.register<UserRepository>(repo)
val service = UserService(repo)
ctx.register<UserService>(service)
// Seed data
repo.save(User(0, "Budi", "[email protected]"))
repo.save(User(0, "Siti", "[email protected]"))
println("=== Spring Boot (mock) berjalan ===")
println("Beans terdaftar: ${ctx.beansInContext()}")
println("\n=== Register user baru ===")
val andi = service.register("Andi", "[email protected]")
println("Tersimpan: $andi")
println("\n=== Ambil user id=1 ===")
println(service.getUser(1))
println("\n=== Ambil user id=999 (error) ===")
try {
service.getUser(999)
} catch (ex: Exception) {
println("Exception: ${ex.message}")
}
println("\n=== List semua user ===")
service.listUsers().forEach { println(" $it") }
}
// Helper untuk expose beans di demo
fun SpringApplicationContext.beansInContext(): Set<String> = synchronized(this) {
beans.keys.toSet()
}Output yang Dihasilkan:
=== Spring Boot (mock) berjalan ===
Beans terdaftar: [UserRepository, UserService]
=== Register user baru ===
Tersimpan: User(id=3, nama=Andi, [email protected])
=== Ambil user id=1 ===
User(id=1, nama=Budi, [email protected])
=== Ambil user id=999 (error) ===
Exception: User 999 tidak ditemukan
=== List semua user ===
User(id=1, nama=Budi, [email protected])
User(id=2, nama=Siti, [email protected])
User(id=3, nama=Andi, [email protected])Mari kita bedah bagian penting kode di atas:
annotation class SpringBootApplication: mock anotasi yang hanya berfungsi sebagai marker. Pada produksi, anotasi ini memicu Spring memulai container IoC dan auto-konfigurasi.interface UserRepository: kontrak repository yang dapat dimock di test. Pada produksi, Spring Data JPA menghasilkan implementasi otomatis — kita hanya menulis interface.@Repository class InMemoryUserRepository: anotasi@Repositoryadalah specialization dari@Component. Saat classpath discan, Spring membuat instance singleton dan mendaftar sebagai bean bertipeUserRepository.class UserService(private val repo: UserRepository): konstruktor injection. Spring mencari beanUserRepositorydan menyuntikkan saat membuatUserService. Pada Kotlin, satu konstruktor berarti@Autowiredopsional.ctx.register<UserRepository>(repo): registrasi manual di demo. Pada produksi,@ComponentScanmelakukan ini otomatis — kita tidak perlu memanggilregistereksplisit.
Starter Dependencies #
Spring Boot memakai starter dependencies — paket Maven/Gradle yang membawa kumpulan library yang kompatibel. spring-boot-starter-web membahas Spring MVC + Tomcat. spring-boot-starter-data-jpa membahas Spring Data JPA + Hibernate. Dengan starter, kita tidak perlu menentukan versi setiap library — Spring Boot mengelola kompatibilitas versi lewat dependency management bawaan.
Referensi Lanjutan #
- Spring Boot reference
— rujukan resmi Spring Boot untuk konfigurasi aplikasi Kotlin dengan
kotlin-springplugin - Spring guides — kumpulan panduan Spring untuk memulai proyek Kotlin + Spring Boot
Selanjutnya #
Setelah memahami dasar Spring Boot, langkah berikutnya adalah memahami Dependency Injection lebih dalam. Chapter Spring Dependency Injection akan membahas @Component, @Service, @Repository, @Autowired, dan cara container menyuntikkan dependensi otomatis.