I am refactoring an existing Java Spring Boot application into Kotlin. In the intermediate stages, the application contains both Java and Kotlin code. However, when I refactor the service and repository classes into Kotlin, they fail to get autowired.
I encounter the following error:
Parameter 0 of constructor in org.base.server.controller.ProjectController required a bean of type 'org.base.server.service.ProjectService' that could not be found.
The application works fine when I revert the classes back to Java. I also tried searching for the Spring beans using applicationContext.getBeanDefinitionNames
, but the kotlin @Service
and @Repository
classes are not visible in the mixed Java-Kotlin codebase.
Here's the more detailed error message:
Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'projectController' defined in class path resource [org/base/server/controller/ProjectController.class]: Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'org.base.server.service.ProjectService' available: expected at least 1 bean which qualifies as an autowire candidate. Dependency annotations: {}
What could be causing this issue? How can I ensure that the refactored Kotlin classes are correctly recognized as Spring beans in a mixed Java-Kotlin codebase?
Additional Info:
Earlier the (Java) Repository:
@Repository
public interface ProjectRepository extends JpaRepository<Project, Long> {
Optional<Project> findByProjectId(String projectId);
Boolean existsByProjectId(String projectId);
}
Now it has been (Kotlin) refactored to:
@Repository
interface ProjectRepository : JpaRepository<Project, Long> {
fun findByProjectId(projectId: String): Project?
fun existsByProjectId(projectId: String): Boolean
}
Similarly the service class earlier is:
@Service
public class ProjectService {
private final transient ProjectRepository projectRepository;
private final transient ProjectConverter projectConverter;
@Autowired
public ProjectService(ProjectRepository projectRepository, ProjectConverter projectConverter) {
this.projectRepository = projectRepository;
this.projectConverter = projectConverter;
}
@Transactional(readOnly = true)
public ProjectDtos getAllProjects() {
return new ProjectDtos()
.setProjects(projectConverter.entitiesToDtos(projectRepository.findAll()));
}
@Transactional(readOnly = true).......
}
Which is now:
@Service
class ProjectService(
@Autowired private val projectRepository: ProjectRepository,
private val projectConverter: ProjectConverter
) {
/**
* Retrieves all projects from the repository.
*
* @return [ProjectDTOs] object containing a list of all projects as DTOs.
*/
@Transactional(readOnly = true)
fun getAllProjects(): ProjectDTOs {
return ProjectDTOs(projectConverter.entitiesToDtos(projectRepository.findAll()))
}
/**
* Retrieves a project by its unique identifier (ID).
*
* @param id the unique ID of the project
* @return the [ProjectDTO] of the project
* @throws NotFoundException if no project with the given ID exists
*/
@Transactional(readOnly = true)............
}
This is the main Application class (Refactored to kotlin):
@SpringBootApplication(scanBasePackages = ["org.base.server"])
class ServerApplication
fun main(args: Array<String>) {
SpringApplication.run(ServerApplication::class.java, *args).also {
it.beanDefinitionNames.forEach(::println)
}
}
The build.gradle file is:
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id 'pmd'
id 'java'
id 'eclipse'
id 'idea'
id 'scala'
id 'checkstyle'
id 'io.gatling.gradle' version '3.9.2.1'
id 'com.github.johnrengelman.shadow' version '8.1.0'
id 'org.springframework.boot' version '3.3.3'
id 'org.openjfx.javafxplugin' version '0.0.13'
id 'io.spring.dependency-management' version '1.1.6'
id 'com.github.ben-manes.versions' version "0.46.0"
id 'org.jetbrains.kotlin.jvm' version '1.9.25'
id "org.jetbrains.kotlin.kapt" version "1.9.25"
id "org.jetbrains.kotlin.plugin.allopen" version "1.9.25"
id "org.jetbrains.kotlin.plugin.spring" version "1.9.25"
id "org.jetbrains.kotlin.plugin.jpa" version "1.9.25"
id "org.jetbrains.kotlin.plugin.noarg" version "2.1.0"
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
kotlin {
jvmToolchain(17)
}
kapt {
keepJavacAnnotationProcessors = true
}
idea {
module {
downloadJavadoc = true
downloadSources = true
}
}
repositories {
mavenCentral()
maven { url = "https://oss.sonatype.org/content/repositories/snapshots" }
}
springBoot {
mainClass.set('org.base.server.ServerApplicationKt')
}
bootJar {
mainClass = 'org.base.server.ServerApplicationKt'
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
jar {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
ext {
springBootVersion = '3.3.3'
springVersion = '6.0.6'
springOauth2Version = "2.5.2.RELEASE"
springOauth2AutoconfigureVersion = "2.6.8"
springDocVersion = '2.2.0'
lombokVersion = '1.18.26'
junit5Version = '5.9.2'
springSecurityVersion = '6.0.5'
hibernateValidatorVersion = '8.0.0.Final'
minioVersion = '8.5.10'
kotlinVersion = '1.9.25'
jacksonKotlinVersion = '2.15.4'
dateTimeVersion = '0.6.1'
}
sourceSets {
main {
kotlin {
srcDirs += 'src/main/java'
}
}
integrationTest {
java {
compileClasspath += main.output + test.output + test.compileClasspath
runtimeClasspath += main.output + test.output + test.runtimeClasspath
srcDir file('src/integrationTest/java')
}
resources.srcDir file('src/integrationTest/resources')
}
}
dependencies {
implementation('org.springframework.boot:spring-boot-starter-data-jpa')
implementation('org.springframework.boot:spring-boot-starter-web')
implementation('org.springframework.boot:spring-boot-starter-quartz')
implementation('org.springframework.boot:spring-boot-starter-security')
implementation('org.springframework.boot:spring-boot-starter-actuator')
implementation('org.springframework.boot:spring-boot-starter-mail')
implementation group: "org.springframework.security", name: "spring-security-config", version: springSecurityVersion
implementation('org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:' + springOauth2AutoconfigureVersion)
implementation('org.springframework.security.oauth:spring-security-oauth2:' + springOauth2Version)
runtimeOnly("org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion")
implementation("io.minio:minio:$minioVersion") {
exclude group: 'org.jetbrains.kotlin'
}
// Open API spec
implementation(group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: springDocVersion)
//runtimeOnly('org.springframework.boot:spring-boot-devtools')
runtimeOnly('org.hsqldb:hsqldb')
runtimeOnly('org.liquibase:liquibase-core:4.20.0')
runtimeOnly(group: 'org.postgresql', name: 'postgresql', version: '42.5.5')
annotationProcessor group: 'org.projectlombok', name: 'lombok', version: lombokVersion
implementation group: 'org.projectlombok', name: 'lombok', version: lombokVersion
annotationProcessor "org.springframework:spring-context-indexer:$springVersion"
runtimeOnly group: 'org.springframework', name: 'spring-aop', version: springVersion
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonKotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-datetime:$dateTimeVersion"
implementation("org.jetbrains.kotlin:kotlin-reflect")
testImplementation group: 'io.gatling.highcharts', name: 'gatling-charts-highcharts', version: '3.9.2'
implementation('org.liquibase.ext:liquibase-hibernate6:4.20.0')
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit', module: 'junit'
}
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: junit5Version
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: junit5Version
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junit5Version
testImplementation group: 'org.junit.platform', name: 'junit-platform-commons', version: '1.8.2'
testImplementation group: 'org.junit.platform', name: 'junit-platform-launcher', version: '1.8.2'
testImplementation group: 'org.junit.platform', name: 'junit-platform-engine', version: '1.8.2'
gatlingImplementation('com.fasterxml.jackson.datatype:jackson-datatype-jsr310')
}
allOpen {
annotation("org.springframework.stereotype.Service")
annotation("org.springframework.stereotype.Repository")
annotation("org.springframework.stereotype.Controller")
annotation("org.springframework.web.bind.annotation.RestController")
}
noArg {
annotation("org.base.server.util.GenerateZeroArgs")
}
wrapper {
gradleVersion '8.5'
}
tasks.withType(KotlinCompile).configureEach {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
apiVersion = KotlinVersion.KOTLIN_1_9
languageVersion = KotlinVersion.KOTLIN_1_9
}
}
tasks.register('integrationTest', Test) {
testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath
useJUnitPlatform() {
excludeEngines 'junit-vintage'
}
shouldRunAfter test
}
...............
Neither the ProjectRepository
nor the ProjectService
has ben listed on bean definitions when fetching from applicationContext.fetchBeanDefintionNames
.
Kotlin Service class fails to get injected here in java Controller:
@CrossOrigin
@RestController
@Slf4j
public class ProjectController {
private transient ProjectService projectService = null;
public ProjectController(
ProjectService projectService
) {
this.projectService = projectService;
}..............
}
This github repository has the minimal setup, which reproduces the error when converting either ProjectRepository
or ProjectService
to kotlin: https://github.com/this-Aditya/SpringErrorReproducing
Update: After some more digging into this after OP wanted to keep KAPT, it turned out that adding the following line fixed the issue.
kapt("org.springframework:spring-context-indexer:$springVersion")
The change causes annotated Kotlin-classes to be added to spring.components
as well.
Original answer
It seems like kapt caused the issue OP describes. By doing the following changes to build.gradle, dependency injection across Java/Kotlin, worked as expected.
// plugins
// REMOVED id "org.jetbrains.kotlin.kapt" version "1.9.25"
// REMOVED kapt {
// keepJavacAnnotationProcessors = true
// }
// dependencies
// REMOVED annotationProcessor "org.springframework:spring-context-indexer:$springVersion"
// REMOVED kapt("org.springframework.boot:spring-boot-configuration-processor")