Spring-Kotlin Integration

This template includes proper configuration for seamless Spring Framework integration with Kotlin, solving common issues developers face when using Kotlin with Spring.

🎯 The Kotlin-Spring Challenge

The Problem

Kotlin classes are final by default, which creates issues with Spring Framework:

  • Spring cannot create CGLIB proxies for dependency injection
  • AOP (Aspect-Oriented Programming) doesn’t work
  • @Transactional annotations fail to create transaction proxies
  • @Configuration classes can’t be extended by Spring

The Solution: AllOpen Plugin

This template uses the Kotlin allopen plugin to automatically make classes open (non-final) when annotated with specific Spring annotations.

πŸ”§ Configuration Details

Plugin Setup

The allopen plugin is configured in scripts/kotlin.gradle:

apply plugin: "org.jetbrains.kotlin.plugin.allopen"

allOpen {
  annotations(
    "org.springframework.stereotype.Component",
    "org.springframework.stereotype.Service", 
    "org.springframework.stereotype.Repository",
    "org.springframework.stereotype.Controller",
    "org.springframework.web.bind.annotation.RestController",
    "org.springframework.boot.autoconfigure.SpringBootApplication",
    "org.springframework.context.annotation.Configuration",
    "org.springframework.transaction.annotation.Transactional"
  )
}

What This Means

When you annotate a Kotlin class with any of these annotations, the compiler automatically makes it open:

// This class is automatically made 'open' by the allopen plugin
@Service
@Transactional
class ExampleService {
    // Spring can create transaction proxies for this class
    fun someBusinessMethod() { }
}

βœ… Working Examples

Service Layer

@Service
@Transactional
class UserService(
    private val userRepository: UserRepository
) {
    // βœ… Transaction management works
    // βœ… Dependency injection works  
    // βœ… AOP proxies work
    
    fun createUser(userData: UserData): User {
        // This method is automatically wrapped in a transaction
        return userRepository.save(User(userData))
    }
    
    @Transactional(readOnly = true)
    fun findUser(id: UUID): User? {
        // Read-only transaction
        return userRepository.findById(id)
    }
}

Controller Layer

@RestController
@RequestMapping("/api/users")
class UserController(
    private val userService: UserService  // βœ… DI works automatically
) {
    // βœ… Spring can create proxies for this controller
    // βœ… All Spring MVC features work
    
    @PostMapping
    fun createUser(@RequestBody userData: UserData): ResponseEntity<User> {
        val user = userService.createUser(userData)
        return ResponseEntity.ok(user)
    }
    
    @GetMapping("/{id}")
    fun getUser(@PathVariable id: UUID): ResponseEntity<User> {
        val user = userService.findUser(id)
        return ResponseEntity.of(Optional.ofNullable(user))
    }
}

Configuration Classes

@Configuration
@EnableJpaAuditing
class DatabaseConfig {
    // βœ… Spring can extend this configuration class
    
    @Bean
    fun auditingHandler(): AuditingHandler {
        return AuditingHandler(PersistenceContext())
    }
}

🚨 What You DON’T Need to Do

❌ Manual open Keywords

// DON'T do this - the plugin handles it automatically
open class ExampleService { }

❌ Interface-Based Proxies Only

// You CAN do this, but it's not required
interface UserService {
    fun createUser(userData: UserData): User
}

@Service
class UserServiceImpl : UserService {
    override fun createUser(userData: UserData): User { }
}

❌ Workarounds with Companion Objects

// DON'T need complex workarounds
class ExampleService {
    companion object {
        // Complex static initialization
    }
}

πŸ” Verification

Check That It’s Working

@SpringBootTest
class SpringKotlinIntegrationTest {
    
    @Autowired
    private lateinit var exampleService: ExampleService
    
    @Test
    fun verifyProxyCreation() {
        // This will pass if proxies are created correctly
        assertTrue(AopUtils.isAopProxy(exampleService))
    }
    
    @Test  
    fun verifyTransactionSupport() {
        // This will pass if @Transactional works
        assertTrue(TransactionSynchronizationManager.isActualTransactionActive())
    }
}

Debug Information

// In your application, you can check if proxies are created:
@Component
class ProxyChecker(
    @Autowired private val services: List<Any>
) {
    
    @PostConstruct
    fun checkProxies() {
        services.forEach { service ->
            val isProxy = AopUtils.isAopProxy(service)
            logger.info("${service.javaClass.simpleName} is proxy: $isProxy")
        }
    }
}

πŸ§ͺ Testing Spring-Kotlin Integration

Unit Tests with MockK

class ExampleServiceTest {
    
    @MockK
    private lateinit var dependency: SomeDependency
    
    @InjectMockKs
    private lateinit var service: ExampleService
    
    @BeforeEach
    fun setup() {
        MockKAnnotations.init(this)
    }
    
    @Test
    fun testServiceMethod() {
        // Given
        every { dependency.someMethod() } returns "test"
        
        // When
        val result = service.getWelcomeMessage("test")
        
        // Then
        assertEquals("Hello, test! This is your Kotlin Multimodule Template.", result)
        verify { dependency.someMethod() }
    }
}

Integration Tests

@SpringBootTest
@TestPropertySource(properties = ["spring.jpa.hibernate.ddl-auto=create-drop"])
class ServiceIntegrationTest {
    
    @Autowired
    private lateinit var userService: UserService
    
    @Test
    @Transactional
    fun testTransactionalBehavior() {
        // This test verifies that @Transactional works correctly
        val userData = UserData("test@example.com", "Test User")
        
        val user = userService.createUser(userData)
        
        assertNotNull(user.id)
        assertEquals("test@example.com", user.email)
    }
}

πŸŽ“ Advanced Features

Custom Annotations

You can add your own annotations to the allopen configuration:

allOpen {
  annotations(
    // Spring annotations...
    "com.yourcompany.CustomTransactional",
    "com.yourcompany.ProxyRequired"
  )
}

Conditional Configuration

@Configuration
@ConditionalOnProperty(name = "app.feature.enabled", havingValue = "true")
class FeatureConfig {
    // This configuration is automatically made 'open'
    
    @Bean
    @ConditionalOnMissingBean
    fun featureService(): FeatureService {
        return FeatureServiceImpl()
    }
}

πŸ“Š Performance Considerations

Proxy Creation Impact

  • CGLIB proxies: Slight memory overhead, negligible performance impact
  • JDK proxies: Minimal overhead, used when interfaces are available
  • No proxies: For classes without Spring annotations, no overhead

Best Practices

  • βœ… Use @Transactional judiciously (not on every method)
  • βœ… Consider @Transactional(readOnly = true) for read operations
  • βœ… Group related operations in single transactional methods
  • βœ… Use @Service for business logic, @Component for utilities

This configuration ensures your Kotlin Spring applications work seamlessly without the common pitfalls of final classes and proxy creation issues.


Copyright © 2025 Programmer Newbie IO. Distributed under the MIT License.