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.
It looks like a bug, but it isn't – it's a consequence of how things work.
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.
Make your method non-final
:
open class RepositoryBase<T: Any> (val template: ReactiveMongoTemplate, private val klass: KClass<T>)
{
open fun count(): …
}