quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.85k stars 2.7k forks source link

@BeanParam not work for Kotlin #35382

Open red010182 opened 1 year ago

red010182 commented 1 year ago

Describe the bug

In resteasy-reactive + kotlin

data class MyBean(@QueryParam("a") val a: Int)

@Path("/")
class GreetingResource {
    @GET
    fun intTest(@BeanParam query: MyBean): Int {
        Log.info(query)
        return query.a
    }
}

Expected behavior

Extract the variable query as type of MyBean.

Actual behavior

throw error

2023-08-16 23:36:57,118 INFO  [io.qua.dep.dev.RuntimeUpdatesProcessor] (vert.x-worker-thread-1) Restarting quarkus due to changes in MyBean.class, GreetingResource.class.
2023-08-16 23:36:57,325 ERROR [io.qua.dep.dev.IsolatedDevModeMain] (vert.x-worker-thread-1) Failed to start quarkus: java.lang.RuntimeException: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
    [error]: Build step io.quarkus.arc.deployment.ArcProcessor#validate threw an exception: jakarta.enterprise.inject.spi.DeploymentException: jakarta.enterprise.inject.UnsatisfiedResolutionException: Unsatisfied dependency for type int and qualifiers [@Default]
    - java member: org.acme.MyBean():a
    - declared on CLASS bean [types=[org.acme.MyBean, java.lang.Object], qualifiers=[@Default, @Any], target=org.acme.MyBean]
    The following beans match by type, but none have matching qualifiers:
        - Bean [class=java.lang.Integer, qualifiers=[@ConfigProperty, @Any]]
    at io.quarkus.arc.processor.BeanDeployment.processErrors(BeanDeployment.java:1435)
    at io.quarkus.arc.processor.BeanDeployment.init(BeanDeployment.java:310)
    at io.quarkus.arc.processor.BeanProcessor.initialize(BeanProcessor.java:155)
    at io.quarkus.arc.deployment.ArcProcessor.validate(ArcProcessor.java:469)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
    at io.quarkus.deployment.ExtensionLoader$3.execute(ExtensionLoader.java:864)
    at io.quarkus.builder.BuildContext.run(BuildContext.java:282)
    at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18)
    at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
    at java.base/java.lang.Thread.run(Thread.java:1589)
    at org.jboss.threads.JBossThread.run(JBossThread.java:501)
Caused by: jakarta.enterprise.inject.UnsatisfiedResolutionException: Unsatisfied dependency for type int and qualifiers [@Default]
    - java member: org.acme.MyBean():a
    - declared on CLASS bean [types=[org.acme.MyBean, java.lang.Object], qualifiers=[@Default, @Any], target=org.acme.MyBean]
    The following beans match by type, but none have matching qualifiers:
        - Bean [class=java.lang.Integer, qualifiers=[@ConfigProperty, @Any]]
    at io.quarkus.arc.processor.Beans.resolveInjectionPoint(Beans.java:477)
    at io.quarkus.arc.processor.BeanInfo.init(BeanInfo.java:624)
    at io.quarkus.arc.processor.BeanDeployment.init(BeanDeployment.java:298)
    ... 11 more

    at io.quarkus.runner.bootstrap.AugmentActionImpl.runAugment(AugmentActionImpl.java:336)
    at io.quarkus.runner.bootstrap.AugmentActionImpl.reloadExistingApplication(AugmentActionImpl.java:266)
    at io.quarkus.runner.bootstrap.AugmentActionImpl.reloadExistingApplication(AugmentActionImpl.java:60)
    at io.quarkus.deployment.dev.IsolatedDevModeMain.restartApp(IsolatedDevModeMain.java:196)
    at io.quarkus.deployment.dev.IsolatedDevModeMain.restartCallback(IsolatedDevModeMain.java:179)
    at io.quarkus.deployment.dev.RuntimeUpdatesProcessor.doScan(RuntimeUpdatesProcessor.java:541)
    at io.quarkus.deployment.dev.RuntimeUpdatesProcessor.doScan(RuntimeUpdatesProcessor.java:441)
    at io.quarkus.vertx.http.runtime.devmode.VertxHttpHotReplacementSetup$4.handle(VertxHttpHotReplacementSetup.java:152)
    at io.quarkus.vertx.http.runtime.devmode.VertxHttpHotReplacementSetup$4.handle(VertxHttpHotReplacementSetup.java:139)
    at io.vertx.core.impl.ContextBase.lambda$null$0(ContextBase.java:137)
    at io.vertx.core.impl.ContextInternal.dispatch(ContextInternal.java:264)
    at io.vertx.core.impl.ContextBase.lambda$executeBlocking$1(ContextBase.java:135)
    at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18)
    at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
    at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
    at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:1589)
Caused by: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
    [error]: Build step io.quarkus.arc.deployment.ArcProcessor#validate threw an exception: jakarta.enterprise.inject.spi.DeploymentException: jakarta.enterprise.inject.UnsatisfiedResolutionException: Unsatisfied dependency for type int and qualifiers [@Default]
    - java member: org.acme.MyBean():a
    - declared on CLASS bean [types=[org.acme.MyBean, java.lang.Object], qualifiers=[@Default, @Any], target=org.acme.MyBean]
    The following beans match by type, but none have matching qualifiers:
        - Bean [class=java.lang.Integer, qualifiers=[@ConfigProperty, @Any]]
    at io.quarkus.arc.processor.BeanDeployment.processErrors(BeanDeployment.java:1435)
    at io.quarkus.arc.processor.BeanDeployment.init(BeanDeployment.java:310)
    at io.quarkus.arc.processor.BeanProcessor.initialize(BeanProcessor.java:155)
    at io.quarkus.arc.deployment.ArcProcessor.validate(ArcProcessor.java:469)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
    at io.quarkus.deployment.ExtensionLoader$3.execute(ExtensionLoader.java:864)
    at io.quarkus.builder.BuildContext.run(BuildContext.java:282)
    at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18)
    at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
    at java.base/java.lang.Thread.run(Thread.java:1589)
    at org.jboss.threads.JBossThread.run(JBossThread.java:501)
Caused by: jakarta.enterprise.inject.UnsatisfiedResolutionException: Unsatisfied dependency for type int and qualifiers [@Default]
    - java member: org.acme.MyBean():a
    - declared on CLASS bean [types=[org.acme.MyBean, java.lang.Object], qualifiers=[@Default, @Any], target=org.acme.MyBean]
    The following beans match by type, but none have matching qualifiers:
        - Bean [class=java.lang.Integer, qualifiers=[@ConfigProperty, @Any]]
    at io.quarkus.arc.processor.Beans.resolveInjectionPoint(Beans.java:477)
    at io.quarkus.arc.processor.BeanInfo.init(BeanInfo.java:624)
    at io.quarkus.arc.processor.BeanDeployment.init(BeanDeployment.java:298)
    ... 11 more

    at io.quarkus.builder.Execution.run(Execution.java:123)
    at io.quarkus.builder.BuildExecutionBuilder.execute(BuildExecutionBuilder.java:79)
    at io.quarkus.deployment.QuarkusAugmentor.run(QuarkusAugmentor.java:160)
    at io.quarkus.runner.bootstrap.AugmentActionImpl.runAugment(AugmentActionImpl.java:332)
    ... 18 more
Caused by: jakarta.enterprise.inject.spi.DeploymentException: jakarta.enterprise.inject.UnsatisfiedResolutionException: Unsatisfied dependency for type int and qualifiers [@Default]
    - java member: org.acme.MyBean():a
    - declared on CLASS bean [types=[org.acme.MyBean, java.lang.Object], qualifiers=[@Default, @Any], target=org.acme.MyBean]
    The following beans match by type, but none have matching qualifiers:
        - Bean [class=java.lang.Integer, qualifiers=[@ConfigProperty, @Any]]
    at io.quarkus.arc.processor.BeanDeployment.processErrors(BeanDeployment.java:1435)
    at io.quarkus.arc.processor.BeanDeployment.init(BeanDeployment.java:310)
    at io.quarkus.arc.processor.BeanProcessor.initialize(BeanProcessor.java:155)
    at io.quarkus.arc.deployment.ArcProcessor.validate(ArcProcessor.java:469)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
    at io.quarkus.deployment.ExtensionLoader$3.execute(ExtensionLoader.java:864)
    at io.quarkus.builder.BuildContext.run(BuildContext.java:282)
    at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18)
    at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
    at java.base/java.lang.Thread.run(Thread.java:1589)
    at org.jboss.threads.JBossThread.run(JBossThread.java:501)
Caused by: jakarta.enterprise.inject.UnsatisfiedResolutionException: Unsatisfied dependency for type int and qualifiers [@Default]
    - java member: org.acme.MyBean():a
    - declared on CLASS bean [types=[org.acme.MyBean, java.lang.Object], qualifiers=[@Default, @Any], target=org.acme.MyBean]
    The following beans match by type, but none have matching qualifiers:
        - Bean [class=java.lang.Integer, qualifiers=[@ConfigProperty, @Any]]
    at io.quarkus.arc.processor.Beans.resolveInjectionPoint(Beans.java:477)
    at io.quarkus.arc.processor.BeanInfo.init(BeanInfo.java:624)
    at io.quarkus.arc.processor.BeanDeployment.init(BeanDeployment.java:298)
    ... 11 more

2023-08-16 23:36:57,334 INFO  [io.qua.dep.dev.RuntimeUpdatesProcessor] (vert.x-worker-thread-1) Live reload total time: 0.802s 

How to Reproduce?

quarkus-@BeanParam-kotlin-fail.zip

Output of uname -a or ver

No response

Output of java -version

java version "19.0.1" 2022-10-18 Java(TM) SE Runtime Environment (build 19.0.1+10-21) Java HotSpot(TM) 64-Bit Server VM (build 19.0.1+10-21, mixed mode, sharing)

GraalVM version (if different from Java)

No response

Quarkus version or git rev

3.2.4.Final

Build tool (ie. output of mvnw --version or gradlew --version)

Apache Maven 3.8.8 (4c87b05d9aedce574290d1acc98575ed5eb6cd39) Maven home: C:\Users\ptrue.m2\wrapper\dists\apache-maven-3.8.8-bin\67c30f74\apache-maven-3.8.8 Java version: 19.0.1, vendor: Oracle Corporation, runtime: C:\Program Files\Java\jdk-19 Default locale: en_US, platform encoding: UTF-8 OS name: "windows 10", version: "10.0", arch: "amd64", family: "windows"

Additional information

No response

quarkus-bot[bot] commented 1 year ago

/cc @evanchooly (kotlin), @geoand (kotlin)

geoand commented 1 year ago

This is similar to https://github.com/quarkusio/quarkus/issues/19686

csh97 commented 1 year ago

I have had problems with @BeanParam with kotlin too, however I have been able to get it working albeit with a few restrictions.

@red010182 the following should hopefully help

To fix the error you are getting you have to make the type nullable and set the default value to null E.g

data class MyBean(@QueryParam("a") val a: Int? = null)

Which means all of your fields in your bean param have to be nullable and therefore if you wanted to have a required type on your API you would have to do validation yourself to check that.


If you try to run the app with the above change you will then get a different error which will have the following in the stacktrace:

Caused by: javax.enterprise.inject.spi.DeploymentException: No annotations found on fields at 'com.example.MyBean'. Annotations like `@QueryParam` should be used in fields, not in methods

This is because in kotlin when using annotations on a field in a constructor you have to specify the target (you can read more about it here: https://kotlinlang.org/docs/annotations.html#annotation-use-site-targets). To fix this you need to change @QueryParam to @field:QueryParam


So your data class will now look something like:

data class MyBean(@field:QueryParam("a") val a: Int? = null)

and the application should start up without error.


However if you try to hit your endpoint you will find that you get a 404 Not Found response. To fix this issue your field in the bean param has to be mutable. Therefore if you change a to var:

data class MyBean(@field:QueryParam("a") var a: Int? = null)

Now when you hit your endpoint it should successfully match your intTest function and you should get the response you expected


So in summary to use @BeanParam with kotlin your fields must be:

Most of which are not ideal and mean you have to deal with more validation in the code. I'm not sure if there could be any changes in Quarkus to improve this?

@geoand any ideas if this can be improved? or is this just a restriction of the implementation of @BeanParam in JAX-RS?

geoand commented 1 year ago

Thanks a lot for the analysis @csh97, that's very helpful.

I totally agree the situation is far from ideal and we'll look into improving it (although I can't make any promised on when that will happen)

or is this just a restriction of the implementation of @BeanParam in JAX-RS?

it's likely a result of how we have implemented it

csh97 commented 1 year ago

Thanks a lot for the analysis @csh97, that's very helpful.

I totally agree the situation is far from ideal and we'll look into improving it (although I can't make any promised on when that will happen)

or is this just a restriction of the implementation of @BeanParam in JAX-RS?

it's likely a result of how we have implemented it

Thats great thanks!

mluiten commented 1 week ago

For all who run into this tread/issue: I've made it work by just using the annotations inside the bean on the field, so like:

@field:FormParam val foo: String

And it seems to work, even without the nullability and default mentioned above :)

csh97 commented 1 week ago

For all who run into this tread/issue: I've made it work by just using the annotations inside the bean on the field, so like:

@field:FormParam val foo: String And it seems to work, even without the nullability and default mentioned above :)

I have tried to replicate, however I am still getting the same behaviour mentioned in my comment above. Could you provide a small example? Also what version of quarkus are you using?

geoand commented 5 days ago

@FroMage remind me please, have made @BeanParam work with Java records?

FroMage commented 5 days ago

Yes: https://quarkus.io/guides/rest#grouping-parameters-in-a-custom-class

geoand commented 5 days ago

Nice!

So it should be possible to make it work for any class that has a single constructor

FroMage commented 5 days ago

Everything is always possible, given work :) But yes.

geoand commented 5 days ago

👌