Search code examples
springkotlinspring-mongodbkotlin-reflect

When does Kotlin introspection become available in the lifetime of a Spring Boot Application?


I ran into a surprising bug. I am trying to make an application that would access mongodb using the repository pattern. In order to reduce code duplication I wanted to make a common base class for all repositories. The repository of each root aggregate (such as Person in code bellow), would inherit from this RepositoryBase and inherit all the common functionality.

data class Person(val name: String)

open class RepositoryBase<T: Any> (val template: ReactiveMongoTemplate, private val klass: KClass<T>)
{
    fun count(): Mono<Long> = template.count(Query(), klass.java)
}

@Repository
class PersonRepository(template: ReactiveMongoTemplate): RepositoryBase<Person>(template, Person::class)

@RunWith(SpringRunner::class)
@SpringBootTest
class DemoApplicationTests
{
    @Autowired var personRepository: PersonRepository? = null

    @Test
    fun contextLoads()
    {
        println(personRepository?.count()?.block()!!)
    }
}

However, this does not seem to work:

java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.JvmClassMappingKt.getJavaClass, parameter $receiver

at kotlin.jvm.JvmClassMappingKt.getJavaClass(JvmClassMapping.kt) at com.example.demo.RepositoryBase.count(DemoApplicationTests.kt:18) ...

It seems that at the time Person::class is being called, the introspection capabilities are not fully initialized and the subsequent call to KClass.java, which is defined as:

/**
 * Returns a Java [Class] instance corresponding to the given [KClass] instance.
 */
@Suppress("UPPER_BOUND_VIOLATED")
public val <T> KClass<T>.java: Class<T>
    @JvmName("getJavaClass")
    get() = (this as ClassBasedDeclarationContainer).jClass as Class<T>

results in the null-exception.

I would like to know if there are some rules on using introspection in Spring applications or whether this is a bug either in Kotlin or in Spring.


Solution

  • TL;DR;

    It looks like a bug, but it isn't – it's a consequence of how things work.

    Explanation

    What happens here is that private val klass: KClass<T> is null. Looking at the code, this actually can't happen, but it does. What happens behind the scenes is that Spring creates a proxy for PersonRepository:

    this = {com.example.demo.DemoApplicationTests@5173} 
     personRepository = {com.example.demo.PersonRepository$$EnhancerBySpringCGLIB$$42849208@5193} "com.example.demo.PersonRepository@1961d75a"
      CGLIB$BOUND = false
      CGLIB$CALLBACK_0 = {org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor@5208} 
      CGLIB$CALLBACK_1 = {org.springframework.aop.framework.CglibAopProxy$StaticUnadvisedInterceptor@5209} 
      CGLIB$CALLBACK_2 = {org.springframework.aop.framework.CglibAopProxy$SerializableNoOp@5210} 
      CGLIB$CALLBACK_3 = {org.springframework.aop.framework.CglibAopProxy$StaticDispatcher@5211} 
      CGLIB$CALLBACK_4 = {org.springframework.aop.framework.CglibAopProxy$AdvisedDispatcher@5212} 
      CGLIB$CALLBACK_5 = {org.springframework.aop.framework.CglibAopProxy$EqualsInterceptor@5213} 
      CGLIB$CALLBACK_6 = {org.springframework.aop.framework.CglibAopProxy$HashCodeInterceptor@5214} 
      template = null
      klass = null
    

    As you can see, klass is null. This is a significant fact because you're calling RepositoryBase.count(). count() is final, therefore it can't be proxied by CGLIB. Inside of count() you're accessing the klass field (not using a getter here) therefore the call uses the uninitialized field from the proxy instance instead of the actual target. Using a getter method would route the call to the actual target and retrieve the field.

    Solution

    Make your method non-final:

    open class RepositoryBase<T: Any> (val template: ReactiveMongoTemplate, private val klass: KClass<T>)
    {
        open fun count(): …
    }