I've encountered a bug in Spring Framework version 6.1.0 related to Kotlin value classes in web endpoints. The issue arises when a Kotlin value class
is used as a path variable in a Spring Boot controller. This results in a failure to correctly handle the value type, causing runtime exceptions.
Environment
Spring Framework Version: 6.1.0 (with spring-boot 3.2.0) Kotlin Version: 1.9.20 JVM Version: 17
Steps to reproduce
- Define a Spring Boot controller with two endpoints, one accepting a Kotlin data class as a path variable and another accepting a Kotlin value class.
- Create tests for these endpoints using SpringBootTest.
- Observe that the endpoint with the Kotlin data class functions correctly, while the one with the kotlin value class fails.
Expected behavior
Both endpoints should accept their respective path variables without any issue
Actual Behavior
The endpoint with the Kotlin value class as a path variable fails at runtime with the error message "object is not an instance of declaring class".
Minimal example
@RestController
@RequestMapping(value = ["/exhibit"])
class KotlinCallByBugController {
@GetMapping("/working-id/{id}")
fun works(
@PathVariable id: WorkingId
) = Unit
@GetMapping("/value-id/{id}")
fun broken(
@PathVariable id: SomeId
) = Unit
}
@JvmInline // "Value classes without @JvmInline annotation are not supported yet"
value class SomeId(val s: String)
// data classes work just fine
data class WorkingId(val s: String)
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = [MyApplication::class])
class KotlinCallByBugControllerTest {
@LocalServerPort
protected val port = 0
@Test
fun `this works - call endpoint with path variable as data class`() {
val client = HttpClients.createDefault()
client.execute(HttpGet("http://localhost:$port/exhibit/working-id/123")) { response ->
assertThat(response.code).isEqualTo(200)
}
}
@Test
fun `this does not work - call endpoint with path variable as value class`() {
// breaks: "object is not an instance of declaring class"
val client = HttpClients.createDefault()
client.execute(HttpGet("http://localhost:$port/exhibit/value-id/123")) { response ->
assertThat(response.code).isEqualTo(200)
}
}
}
Comment From: CharlyRien
Hello,
I wanted to mention that I am experiencing the same issue, even though I am not directly using the value class in the controller path variable. If I invoke any bean methods with a value class in the parameters, the outcome remains the same: I receive the error message object is not an instance of declaring class
from java.base/jdk.internal.reflect.DirectMethodHandleAccessor.checkReceiver(DirectMethodHandleAccessor.java:197)
.
We are currently utilizing Java 21 on our end.
Thanks
Comment From: sdeleuze
It looks like Kotlin reflection does not support kotlin.reflect.KCallable#callBy
invocation with the data class unwrapped type, I have asked Kotlin team feedback.
Comment From: sdeleuze
Confirmed to be a Kotlin bug, you can follow the resolution of https://youtrack.jetbrains.com/issue/KT-64097.
Comment From: junkdog
While not an ideal solution, one possible workaround for the Kotlin callBy
bug could be to manually box arguments that are value class
types. One can determine which KParameters
correspond to value classes by checking the KClass::isValue
property.
A simpler mitigation might be to fallback on the java reflection code path if callBy
encounters failures (alt if any KParameters point to value classes) - since it is only aware of the underlying type.
Edit: Just to clarify. We upgraded from 6.0.13, where it worked fine.
Comment From: sdeleuze
Based on latest Kotlin team feedback, we may have to handle that on Spring side (in a way close to what you suggested @junkdog). See https://github.com/sdeleuze/spring-framework/commit/gh-31698 draft commit.
Comment From: koo-taejin
@sdeleuze
Since spring-boot 3.2.1, we have noticed a significant performance drop in our application.
A colleague of mine (@koisyu) have tried to figure out the cause.
He had analyzed that loading the Java class every time while invoking getClassifier()
from KType
was the reason.
I think that if cache these parts, spring-framework can use this feature with similar performance as before.
Could you please take a look at this?
Thanks 🙇
Comment From: sdeleuze
Could you please share an indication of the impact you see, for example % of drop of the throughput observed for example, with how much parameters are used.
Comment From: koo-taejin
This is a very simple Kotlin-based controller method that makes it easy to test.
@GetMapping("/message")
fun message(message1: String, message2: String, message3: String): String {
return "$message1 $message2 $message3"
}
benchmark
- spring-boot 3.2.0
[~/Workspaces/spring]$ hey -n 100000 -c 1 http://localhost:8080/message\?message1\=hello\&message2\=hi\&message3\=thanks
Summary:
Total: 13.5588 secs
Slowest: 0.0098 secs
Fastest: 0.0001 secs
Average: 0.0001 secs
Requests/sec: 7375.3008
- spring-boot 3.2.1
[~/Workspaces/spring]$ hey -n 100000 -c 1 http://localhost:8080/message\?message1\=hello\&message2\=hi\&message3\=thanks
Summary:
Total: 14.9566 secs
Slowest: 0.1087 secs
Fastest: 0.0001 secs
Average: 0.0001 secs
Requests/sec: 6686.0123
async-profiler
-
spring-boot 3.2.0
-
spring-boot 3.2.1
Let me know if you need more information. 😄
Comment From: sdeleuze
@koo-taejin I confirm a significant slowdown with my own tests, could you please create a new dedicated related issue, reusing the informations you shared above?