Search code examples
spring-bootkotlindependency-injectionkotlin-lateinit

spring boot startup process failed with lateinit var not initialized : DI Issue


I'm buidling a backend that will fetch some data from several externals APIs, to populate DB after some processes, and then expose these data via a rest api. Using JPA and postgresql, in springboot.

I have created the entities, repositories and fetch external apis from a webclient. However, I have a dependency injection issue. I read tons of articles and issues, but can not make it work. When trying to inject the repository, I got the well known error lateinit var not initialized. I also tried constructor injection, but still doesn't work. It seems that the repository is not considered to be a bean that could be autowired.

Any help would be appreciated

FIXED SEE SOLUTION BELOW. PB WAS IN THE SERVICE

Application. Running the startuprocess from the context

@SpringBootApplication(exclude = [JacksonAutoConfiguration::class])
class SpringbootkotlinApplication
   fun main(args: Array<String>) {
      val context = runApplication<SpringbootkotlinApplication>(*args)
      val startupProcess = context.getBean(StartupProcesses::class.java)
      startupProcess.fetchGroup()
   }

startup class as @component :fetches external api and calls a service to save data in db

@Component
class StartupProcesses  {
        fun fetchGroup() {
            val fetch = IronMaidenSourceApi.fetchIronMaidenSource() //<-- fetch ext api OK
            SplitDataSourceToEntities().populateGroup(fetch) //<-- call service to populate db
    }
}

Service that should populates the DB through repository but error in lateinit repo

@Service
class SplitDataSourceToEntities  {
        @Autowired
        lateinit var repo:IronMaidenGroupRepository // <-- error is here

        fun populateGroup(dto: DTOIronMaidenAPi): IronMaidenGroupEntity {
            val dbgroup = IronMaidenGroupEntity(
                groupId = dto.id!!,
                name = dto.name ?: ""
            )
            return repo.save(dbgroup)
        }
}

the repo, extending JPA repository

import com.jerome.springbootkotlin.model.dbentities.ironmaidengroupentity.IronMaidenGroupEntity
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface IronMaidenGroupRepository:JpaRepository<IronMaidenGroupEntity, String> {
}

SOLUTION

Service should be defined like this (the SplitDataSourceToEntitiesclass should also be late init instead of beeing instantiated)

@Component
class StartupProcesses  {
        @Autowired
        lateinit var splitDataSourceToEntities: SplitDataSourceToEntities // <--add this lateinit
        fun fetchGroup() {
            val fetch = IronMaidenSourceApi.fetchIronMaidenSource()
            splitDataSourceToEntities.populateGroup(fetch) //<-- fix is here
    }
}

Solution

  • @Autowired works only when managed by Spring

    It is important to note, that @Autowired does only work when the class has been initialized by Spring. In your code, you have an @Autowired annotation in class SplitDataSourceToEntities, but you manually instantiate SplitDataSourceToEntities in StartupProcesses.fetchGroup. That cannot work, because Spring had no possibility to auto-wire the lateinit var.

    The problem can easily be solved by using an autowired instance of your service:

    @Component
    class StartupProcesses  {
        @Autowired
        lateinit var splitDataSourceToEntities: SplitDataSourceToEntities
        
        fun fetchGroup() {
            val fetch = IronMaidenSourceApi.fetchIronMaidenSource() //<-- fetch ext api OK
            splitDataSourceToEntities.populateGroup(fetch) //<-- call service to populate db
        }
    }
    

    Help Spring finding your JpaRepository

    Additionally, you probably need to add an @EnableJpaRepositories annotation to your application:

    @EnableJpaRepositories(basePackageClasses = [IronMaidenGroupRepository::class])
    @SpringBootApplication(exclude = [JacksonAutoConfiguration::class])
    class SpringbootkotlinApplication
    ...
    

    Instead of basePackageClasses = ... you can just define the package directly by name, but the package name is not included in your example.


    Do you have to use @Autowired at all?

    Considering your question in the comments:

    There is nothing wrong with your design, because it is not not necessary to "play" with @Component and @Autowired. You could as well get rid of the @Autowired annotations and define those variables as, e.g., constructor parameters. That could look like this:

    class SpringbootkotlinApplication
    fun main(args: Array<String>) {
        val context = runApplication<SpringbootkotlinApplication>(*args)
        val repo = context.getBean(IronMaidenGroupRepository::class.java)
        StartupProcesses(SplitDataSourceToEntities(repo))
    }
    
    @Component
    class StartupProcesses(
        val splitDataSourceToEntities: SplitDataSourceToEntities
    ) {
        ...
    }
    
    
    @Service
    class SplitDataSourceToEntities(
        val repo: IronMaidenGroupRepository
    ) {
        ...
    }
    

    Now only your Repository is managed by Spring, but on the flip-side you have to manage everything else by yourself which might get very tedious when your classes and their dependencies grow. It is much more comfortable (and in the end leads to better readable code) to let just Spring manage all the dependencies.