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:
| Anotasi | Peran | Lapisan |
|---|---|---|
@Component | Bean generik | Lapisan utility |
@Service | Logika bisnis | Lapisan service |
@Repository | Akses data | Lapisan data |
@Controller | Handler MVC | Lapisan presentasi |
@RestController | Handler REST | Lapisan API |
@Configuration | Sumber bean | Lapisan 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:
- Constructor injection (direkomendasikan): dependensi di parameter konstruktor.
- Setter injection: dependensi di setter method.
- Field injection: dependensi di field (tidak direkomendasikan).
@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.
// 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:
=== 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: trueMari kita bedah bagian penting kode di atas:
data class BeanDefinition(val name, val type, val constructorDeps): metadata yang disimpan saat container scan.constructorDepsadalah daftar nama bean yang dibutuhkan — Spring memakai ini untuk menentukan urutan pembuatan.fun getBeanByName(name): lookup singleton di cachesingletons. 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.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.@Repository class UserRepository: anotasi marker yang menyatakan class ini adalah bean bertipe repository. Spring juga menerjemahkan exception SQL keDataAccessExceptionuntuk repository.controller === c1: identitas referensi — pada singleton scope, semua pemanggilangetBeanmengembalikan instance yang sama. Verifikasi ini membuktikan container mengelola satu instance.
Bean Scope #
Spring mendukung beberapa scope:
| Scope | Perilaku |
|---|---|
singleton (default) | Satu instance per container |
prototype | Instance baru setiap getBean |
request | Satu instance per HTTP request (web) |
session | Satu instance per HTTP session (web) |
application | Satu instance per ServletContext (web) |
Default singleton cocok untuk service stateless. Untuk objek yang membawa state per request (mis. ShoppingCart), pakai @RequestScope.
Referensi Lanjutan #
- Spring Framework reference
— rujukan resmi Spring IoC container mencakup
@Autowired,@Component, dan@Configuration
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.