Dependency Injection (DI) adalah pola di mana dependensi tidak dibuat oleh class sendiri, melainkan disuntikkan dari luar. Spring mengimplementasi DI via container IoC yang memindai classpath, membuat instance, dan menyuntikkan dependensi otomatis. Memahami DI penting karena pola ini dipakai di seluruh kode Spring — dari service hingga controller. Pada chapter ini kita akan membedah anotasi inti DI dan memodelkan container IoC Spring sebagai class Kotlin biasa.

Stereotype Annotations #

Spring menyediakan beberapa anotasi stereotype — semua turunan dari @Component:

AnotasiPeranLapisan
@ComponentBean generikLapisan utility
@ServiceLogika bisnisLapisan service
@RepositoryAkses dataLapisan data
@ControllerHandler MVCLapisan presentasi
@RestControllerHandler RESTLapisan API
@ConfigurationSumber beanLapisan konfigurasi

Perbedaan anotasi ini bersifat semantik — secara teknis semua menghasilkan bean singleton. Tetapi penggunaan yang tepat memberi sinyal peran class dan memungkinkan tooling (mis. exception translation untuk @Repository).

@Autowired dan Constructor Injection #

@Autowired adalah anotasi yang memerintahkan Spring menyuntikkan dependensi. Ada tiga cara pakai:

  1. Constructor injection (direkomendasikan): dependensi di parameter konstruktor.
  2. Setter injection: dependensi di setter method.
  3. Field injection: dependensi di field (tidak direkomendasikan).
kotlin Constructor injection di Kotlin
@Service
class UserController(
    private val userService: UserService,
    private val greetingService: GreetingService
)

Pada Kotlin, class dengan satu konstruktor tidak perlu @Autowired eksplisit — Spring otomatis memakai konstruktor tersebut. Hal ini menghasilkan kode yang ringkas dan aman: dependensi val (immutable) dan tidak-null.

Bean Lifecycle #

Bean singleton dibuat sekali saat container start. Urutan pembuatan mengikuti dependensi: jika UserController membutuhkan UserService, Spring membuat UserService dulu, baru UserController. Setelah semua bean dibuat, container siap melayani request.

Spring juga mendukung lifecycle callback: @PostConstruct dipanggil setelah bean dibuat dan dependensi disuntikkan, @PreDestroy dipanggil sebelum bean dihapus. Callback ini berguna untuk inisialisasi resource dan cleanup.

Demo Container IoC #

Kita akan memodelkan container IoC Spring sebagai class SpringContainer dengan registry factory dan singleton map. Program ini mendemonstrasikan dependency resolution dan verifikasi singleton.

kotlin
// spring_dependency_injection.kt
// Mendemonstrasikan Dependency Injection Spring: @Component, @Service,
// @Repository, @Autowired, dan constructor injection. Pada kode produksi,
// anotasi diproses oleh Spring IoC container. Di sini kita memodelkannya.

// Mock anotasi Spring
@Target(AnnotationTarget.CLASS) annotation class Component
@Target(AnnotationTarget.CLASS) annotation class Service
@Target(AnnotationTarget.CLASS) annotation class Repository
@Target(AnnotationTarget.CLASS) annotation class Configuration
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) annotation class Bean

// BeanDefinition: metadata yang dibuat saat container scan class
data class BeanDefinition(
    val name: String,
    val type: String,
    val constructorDeps: List<String>
)

// Spring Container: mensimulasikan ApplicationContext dengan factory manual
class SpringContainer {
    private val definitions = mutableMapOf<String, BeanDefinition>()
    private val singletons = mutableMapOf<String, Any>()
    private val factories = mutableMapOf<String, () -> Any>()

    fun registerFactory(name: String, type: String, deps: List<String>, factory: () -> Any) {
        definitions[name] = BeanDefinition(name, type, deps)
        factories[name] = factory
    }

    inline fun <reified T : Any> getBean(): T {
        val name = T::class.simpleName ?: error("Bean tanpa nama")
        return getBeanByName(name) as T
    }

    fun getBeanByName(name: String): Any {
        singletons[name]?.let { return it }
        val factory = factories[name] ?: error("Bean $name tidak terdaftar")
        val instance = factory()
        singletons[name] = instance
        return instance
    }

    fun listBeans(): List<String> = definitions.keys.toList()
}

// Domain model
data class User(val id: Int, val nama: String, val email: String)

@Repository
class UserRepository {
    private val users = mutableListOf(
        User(1, "Budi", "[email protected]"),
        User(2, "Siti", "[email protected]")
    )
    fun findById(id: Int): User? = users.firstOrNull { it.id == id }
    fun findAll(): List<User> = users.toList()
    fun save(user: User): User { users.add(user); return user }
}

@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(repo.findAll().size + 100, nama, email))
}

@Service
class GreetingService {
    fun sapa(nama: String): String = "Halo, $nama!"
}

@Service
class UserController(
    private val userService: UserService,
    private val greetingService: GreetingService
) {
    fun get(id: Int): String = greetingService.sapa(userService.getUser(id).nama)
    fun list(): List<String> = userService.listUsers().map { it.nama }
    fun register(nama: String, email: String): String {
        val user = userService.register(nama, email)
        return "${greetingService.sapa(user.nama)} (id=${user.id})"
    }
}

fun main() {
    val ctx = SpringContainer()
    // Simulasi @ComponentScan: register factory untuk tiap bean
    ctx.registerFactory("UserRepository", "UserRepository", emptyList()) { UserRepository() }
    ctx.registerFactory("UserService", "UserService", listOf("UserRepository")) {
        UserService(ctx.getBeanByName("UserRepository") as UserRepository)
    }
    ctx.registerFactory("GreetingService", "GreetingService", emptyList()) { GreetingService() }
    ctx.registerFactory("UserController", "UserController",
        listOf("UserService", "GreetingService")) {
        UserController(
            ctx.getBeanByName("UserService") as UserService,
            ctx.getBeanByName("GreetingService") as GreetingService
        )
    }

    println("=== Bean terdaftar ===")
    ctx.listBeans().forEach { println("  - $it") }

    println("\n=== Resolve dependencies (constructor injection) ===")
    val controller = ctx.getBean<UserController>()
    println("Controller.list(): ${controller.list()}")

    println("\n=== Panggil controller.get(1) ===")
    println(controller.get(1))

    println("\n=== Panggil controller.register('Andi','[email protected]') ===")
    println(controller.register("Andi", "[email protected]"))

    println("\n=== Singleton: instance sama ===")
    val c1 = ctx.getBean<UserController>()
    val c2 = ctx.getBean<UserController>()
    println("controller === c1: ${controller === c1}, c1 === c2: ${c1 === c2}")
}

Output yang Dihasilkan:

text
=== Bean terdaftar ===
  - UserRepository
  - UserService
  - GreetingService
  - UserController

=== Resolve dependencies (constructor injection) ===
Controller.list(): [Budi, Siti]

=== Panggil controller.get(1) ===
Halo, Budi!

=== Panggil controller.register('Andi','[email protected]') ===
Halo, Andi! (id=102)

=== Singleton: instance sama ===
controller === c1: true, c1 === c2: true

Mari kita bedah bagian penting kode di atas:

  1. data class BeanDefinition(val name, val type, val constructorDeps): metadata yang disimpan saat container scan. constructorDeps adalah daftar nama bean yang dibutuhkan — Spring memakai ini untuk menentukan urutan pembuatan.
  2. fun getBeanByName(name): lookup singleton di cache singletons. Bila belum ada, panggil factory untuk membuat instance baru, simpan ke cache, dan kembalikan. Implementasi ini tidak menangani circular dependency — Spring nyata menangani via early reference.
  3. ctx.registerFactory("UserController", deps=listOf("UserService","GreetingService")) { ... }: factory menerima dependensi yang sudah di-resolve. Pada produksi, Spring membaca konstruktor via refleksi dan menentukan dependensi otomatis — kita tidak perlu menulis factory manual.
  4. @Repository class UserRepository: anotasi marker yang menyatakan class ini adalah bean bertipe repository. Spring juga menerjemahkan exception SQL ke DataAccessException untuk repository.
  5. controller === c1: identitas referensi — pada singleton scope, semua pemanggilan getBean mengembalikan instance yang sama. Verifikasi ini membuktikan container mengelola satu instance.

Bean Scope #

Spring mendukung beberapa scope:

ScopePerilaku
singleton (default)Satu instance per container
prototypeInstance baru setiap getBean
requestSatu instance per HTTP request (web)
sessionSatu instance per HTTP session (web)
applicationSatu instance per ServletContext (web)

Default singleton cocok untuk service stateless. Untuk objek yang membawa state per request (mis. ShoppingCart), pakai @RequestScope.

Referensi Lanjutan #

Selanjutnya #

Setelah memahami DI, langkah berikutnya adalah membangun REST API. Chapter Spring MVC dan REST API akan membahas @RestController, @GetMapping, @PostMapping, dan routing via DispatcherServlet.