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 @Transactionaljudiciously (not on every method)
- β
 Consider @Transactional(readOnly = true)for read operations
- β Group related operations in single transactional methods
- β
 Use @Servicefor business logic,@Componentfor utilities
π Related Documentation
This configuration ensures your Kotlin Spring applications work seamlessly without the common pitfalls of final classes and proxy creation issues.