javaspringspring-bootkotlin

Spring Boot Autowiring Fails for Kotlin Service and Repository Classes in a Mixed Java-Kotlin Application


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


Solution

  • 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")
    

    Pull request with changes