How do Spring repositories instantiate results from query methods?

Assume the following (jdbc) repository:

interface Repo : CrudRepository<MyType, Long> {
    @Query("select * from blabla")
    fun abc(): List<MyType>
}

Both for the method abc() and the methods included from the parent interface the auto generated runtime implementation of the Repo interface knows how to serialise result sets into instances of MyType subject to certain restrictions.

How does Spring have access to the type information from the generic parameter at runtime? How does it manage to create lists of the correct runtime types based solely on the type information provided in the interface?

My understanding is that I would not have sufficient information from a signature like mapV1() below to instantiate my result, and therefore I would need to introduce a second parameter with type Class<T> as shown in mapV2():

class Mapper {
   fun <T> mapV1(source: Any): T {
       /* 
       try to instantiate a result 
       of type T reflectively
       */
   }
   fun <T> mapV2(source: Any, klass: Class<T>): T {
       /* 
       try to instantiate a result 
       of type T reflectively
       */
   }
}

Somehow Spring avoids this problem.

Answer

Not all type information are erased. Things like the return types and parameter types of methods, the superclass and super interfaces, the types of fields etc are stored in the class file as metadata. The Java reflection API allows you to get them:

// note that the "as ParameterizedType" cast only succeeds if the type is generic
println((Repo::class.java.genericInterfaces[0] as ParameterizedType)
    .actualTypeArguments.toList()) // [class MyType, class java.lang.Long]
println((Repo::class.java.getDeclaredMethod("abc").genericReturnType as ParameterizedType)
    .actualTypeArguments.toList()) // [class MyType]

You can also do this with the Kotlin reflection API:

println(Repo::class.supertypes[0].arguments) // [MyType, kotlin.Long]
println(Repo::class.declaredMembers.find { it.name == "abc" }?.returnType?.arguments) // [MyType]

In the case of mapV1 however, the only metadata you can get is just “T“, which isn’t very useful. You can’t get the arguments that the caller passed to it.