Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to use kotlin value classes as function arguments to functions mapping web endpoints #31698

Closed
junkdog opened this issue Nov 27, 2023 · 9 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) theme: kotlin An issue related to Kotlin support type: bug A general bug
Milestone

Comments

@junkdog
Copy link

junkdog commented Nov 27, 2023

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

  1. 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.
  2. Create tests for these endpoints using SpringBootTest.
  3. 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)
        }
    }
}
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Nov 27, 2023
@sdeleuze sdeleuze self-assigned this Nov 28, 2023
@sdeleuze sdeleuze added the theme: kotlin An issue related to Kotlin support label Nov 28, 2023
@sdeleuze sdeleuze added this to the 6.1.2 milestone Nov 29, 2023
@sdeleuze sdeleuze added in: web Issues in web modules (web, webmvc, webflux, websocket) type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Nov 29, 2023
@CharlyRien
Copy link

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

@sdeleuze
Copy link
Contributor

sdeleuze commented Dec 5, 2023

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.

@sdeleuze
Copy link
Contributor

sdeleuze commented Dec 5, 2023

Confirmed to be a Kotlin bug, you can follow the resolution of https://youtrack.jetbrains.com/issue/KT-64097.

@sdeleuze sdeleuze closed this as not planned Won't fix, can't repro, duplicate, stale Dec 5, 2023
@sdeleuze sdeleuze removed this from the 6.1.2 milestone Dec 5, 2023
@sdeleuze sdeleuze added for: external-project Needs a fix in external project and removed type: bug A general bug labels Dec 5, 2023
@junkdog
Copy link
Author

junkdog commented Dec 5, 2023

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.

@sdeleuze sdeleuze added type: bug A general bug and removed for: external-project Needs a fix in external project labels Dec 8, 2023
@sdeleuze sdeleuze added this to the 6.1.2 milestone Dec 8, 2023
@sdeleuze
Copy link
Contributor

sdeleuze commented Dec 8, 2023

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 sdeleuze@gh-31698 draft commit.

@koo-taejin
Copy link
Contributor

koo-taejin commented Feb 22, 2024

@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 🙇

@sdeleuze
Copy link
Contributor

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.

@koo-taejin
Copy link
Contributor

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
    image

  • spring-boot 3.2.1
    image

Let me know if you need more information. 😄

@sdeleuze
Copy link
Contributor

@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?

sdeleuze added a commit to sdeleuze/spring-framework that referenced this issue Feb 28, 2024
This commit fixes a performance regression caused by spring-projectsgh-31698, and more
specifically by KClass#isValue invocations which are slow since they
load the whole module to find the class to get the descriptor.

After discussing with the Kotlin team, it has been decided that only
checking for the presence of `@JvmInline` annotation is enough for
Spring use case.

As a consequence, this commit introduces a new
KotlinDetector#isInlineClass method that performs such check, and
BeanUtils, CoroutinesUtils and WebMVC/WebFlux InvocableHandlerMethod
have been refined to leverage it.

Closes spring-projectsgh-32334
sdeleuze added a commit to sdeleuze/spring-framework that referenced this issue Feb 28, 2024
This commit fixes a performance regression caused by spring-projectsgh-31698, and more
specifically by KClass#isValue invocations which are slow since they
load the whole module to find the class to get the descriptor.

After discussing with the Kotlin team, it has been decided that only
checking for the presence of `@JvmInline` annotation is enough for
Spring use case.

As a consequence, this commit introduces a new
KotlinDetector#isInlineClass method that performs such check, and
BeanUtils, CoroutinesUtils and WebMVC/WebFlux InvocableHandlerMethod
have been refined to leverage it.

Closes spring-projectsgh-32334
sdeleuze added a commit to sdeleuze/spring-framework that referenced this issue Feb 28, 2024
This commit fixes a performance regression caused by spring-projectsgh-31698,
and more specifically by KClass#isValue invocations which are slow since
they load the whole module to find the class to get the descriptor.

After discussing with the Kotlin team, it has been decided that only
checking for the presence of `@JvmInline` annotation is enough for
Spring use case.

As a consequence, this commit introduces a new
KotlinDetector#isInlineClass method that performs such check, and
BeanUtils, CoroutinesUtils and WebMVC/WebFlux InvocableHandlerMethod
have been refined to leverage it.

Closes spring-projectsgh-32334
sdeleuze added a commit to sdeleuze/spring-framework that referenced this issue Feb 28, 2024
This commit fixes a performance regression caused by spring-projectsgh-31698,
and more specifically by KClass#isValue invocations which are slow since
they load the whole module to find the class to get the descriptor.

After discussing with the Kotlin team, it has been decided that only
checking for the presence of `@JvmInline` annotation is enough for
Spring use case.

As a consequence, this commit introduces a new
KotlinDetector#isInlineClass method that performs such check, and
BeanUtils, CoroutinesUtils and WebMVC/WebFlux InvocableHandlerMethod
have been refined to leverage it.

Closes spring-projectsgh-32334
sdeleuze added a commit to sdeleuze/spring-framework that referenced this issue Feb 28, 2024
This commit fixes a performance regression caused by spring-projectsgh-31698,
and more specifically by KClass#isValue invocations which are slow since
they load the whole module to find the class to get the descriptor.

After discussing with the Kotlin team, it has been decided that only
checking for the presence of `@JvmInline` annotation is enough for
Spring use case.

As a consequence, this commit introduces a new
KotlinDetector#isInlineClass method that performs such check, and
BeanUtils, CoroutinesUtils and WebMVC/WebFlux InvocableHandlerMethod
have been refined to leverage it.

Closes spring-projectsgh-32334
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) theme: kotlin An issue related to Kotlin support type: bug A general bug
Projects
None yet
Development

No branches or pull requests

5 participants