Search code examples
javaspring-bootkotlindependency-injectionnullpointerexception

Spring Boot and Kotlin DSL Configuration


Lately, I have been assigned to a project that disables some of the auto-config and configures the spring boot app mostly manually using KotlinDSL.

@SpringBootApplication(exclude = {
        DataSourceAutoConfiguration.class,
        DataSourceTransactionManagerAutoConfiguration.class,
        JpaRepositoriesAutoConfiguration.class,
        HibernateJpaAutoConfiguration.class,
        CassandraDataAutoConfiguration.class,
        CassandraAutoConfiguration.class
})

I am facing I believe Kotlin lang issue with spring integration.

Let me show you the setup.

  1. An abstract sign-in strategy with a @Transactional method in it.
  2. A concrete child of the above abstract class. IndividualSignIn with supporting two different implementations. Those are Google and Apple individual sign-ins. The difference maker is the service (Google sign-in service, Apple sign-in service) which is injected into the above concrete class beans. I'll show the setup below.

So, the Kotlin dsl is like the below;

bean(name = "googleUserSignIn") {
            IndividualUserSignIn(
                ref("googleUserSignInService"),
                ref("userHibernateDAO"),
                ref("socialAccountHibernateDAO"),
                ref("userService"),
                ...
            )
        }

bean(name = "appleUserSignIn") {
            IndividualUserSignIn(
                ref("appleUserSignInService"),
                ref("userHibernateDAO"),
                ref("socialAccountHibernateDAO"),
                ref("userService"),
                ...
            )
        }

And finally, the strategy which proxies the request is like below;

bean<UserSignInFactory>()

The implementation of these classes are look like below; first the AbstractStrategy

abstract class AbstractUserSignIn(
    private val userSignInService: UserSignInService,
    private val userDAO: UserDAO,
    private val socialAccountDAO: SocialAccountDAO,
    private val userService: UserService,
    ....
) {

    @Transactional
    open fun signIn(userSignInRequest: SignInRequest): SignInResult {...}
    
    fun getSignInStrategy(): UserSignInStrategy{ // **(A)**
       return userSignInService.getSignInStrategy()
    }
}

Then the classes inherited from this class;

open class IndividualUserSignIn constructor(
    userSignInService: UserSignInService,
    userDAO: UserDAO,
    socialAccountDAO: SocialAccountDAO,
    userService: UserService,
    ...
) : AbstractUserSignIn(
    userSignInService,
    userDAO,
    socialAccountDAO,
    userService,
    ...
) {

    @PostConstruct
    private fun init()
    {
        println("strategy :" + getSignInStrategy()) // **(B)**
    }
...
}

and the Factory class.

@Component
open class UserSignInFactory @Autowired constructor(private val userSignInServices: Set<IndividualUserSignIn>) {

    @PostConstruct
    private fun createStrategies() {
        userSignInServices.forEach { strategy ->
            strategyMap[strategy.getSignInStrategy()] = strategy // **(C)**
        }
    }
    ....
    companion object {
        private val strategyMap: EnumMap<UserSignInStrategy, AbstractUserSignIn> = EnumMap(UserSignInStrategy::class.java)
    }
}

Point (A) is where the problem arises. The abstract class uses the injected service to let callers know about its supporting implementation.

Well, here the problem is.

  1. At point (B); While the concrete strategies are being instantiated, the @PostConstruct works as expected and prints the supported strategy. Debugging says this is the instance itself of strategy.
  2. At point (C); While traversing the Set, I am receiving an NPE because the injected service that is used in point (A) looks null. Here the elements in the set, are instances of spring-generated proxies pointing to instances from step #1 above.

Solution

  • Consider defining the concrete strategies as lazy beans.

    This will ensure that the strategies are instantiated with the correct dependencies injected before they are accessed in the factory.

    Updated as lazy beans:

    bean(name = "googleUserSignIn") {
      lazy {
        IndividualUserSignIn(
          ref("googleUserSignInService"),
          ref("userHibernateDAO"),
          ref("socialAccountHibernateDAO"),
          ref("userService"),
          ...
        )
      }
    }
    
    bean(name = "appleUserSignIn") {
      lazy {
        IndividualUserSignIn(
          ref("appleUserSignInService"),
          ref("userHibernateDAO"),
          ref("socialAccountHibernateDAO"),
          ref("userService"),
          ...
        )
      }
    }
    

    Then update UserSignInFactory class to accommodate lazy initialisation:

    @Component
    open class UserSignInFactory @Autowired constructor(
      private val userSignInServices: Lazy<Set<IndividualUserSignIn>>) {
    
      @PostConstruct
      private fun createStrategies() {
        userSignInServices.forEach { strategy ->
          strategyMap[strategy.getSignInStrategy()] = strategy // **(C)**
        }
      }
      ....
      companion object {
        private val strategyMap: 
        EnumMap<UserSignInStrategy, AbstractUserSignIn> = EnumMap(UserSignInStrategy::class.java)
      }
    }