-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Support constructor injection for RESTEasy Reactive multipart bodies #19686
Comments
Is this still an issue? |
This is still an issue on 3.7.1 |
Thanks for checking |
This is more of a note to myself for a possible way to approach this. The problem is that we have ASM code that transforms the |
The example above is not a multipart body, is it? It's a JSON body. There are no |
Regardless, the problem of not being able to use Java Record for multipart and for beanparam remains |
Yes, agreed, perhaps. But this doesn't appear what this issue is asking, though. Does CDI injection work with records? |
Isn't it? OP explicitly mentions Multipart |
Also, why |
So to provide a complete code example: data class ThisWorks(
val foo: String
)
data class ThisDoesNotWork(
@RestPath("foo")
val foo: String,
@RestForm(FileUpload.ALL)
val files: List<FileUpload>
)
@POST
@Consumes(MediaType.APPLICATION_JSON)
fun thisWorks(data: ThisWorks): Uni<RestResponse<...>>
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
fun thisDoesNotWork(data: ThisDoesNotWork): Uni<RestResponse<....>> A workaround for the second data class is to apply the kotlin @NoArgConstructor
data class ThisDoesNotWork(
@field:RestPath("foo")
val foo: String,
@field:RestForm(FileUpload.ALL)
val files: List<FileUpload>
) |
Well, it was missing an example showing multipart usage.
It is reasonable, for sure. But, I'm not entirely sure that it's easier to write: public record DoesNotWork(
@RestPath String foo,
@RestForm(FileUpload.ALL) List<FileUpload> files) {} Than: public class Works {
@RestPath public String foo;
@RestForm(FileUpload.ALL) public List<FileUpload> files;
} So, sure, we should support it. But it's not like the workaround is horrible or harder to write. It's a good thing I wrote a bunch of docs on how this is handled: https://github.com/quarkusio/quarkus/wiki/RESTEasy-Reactive-(Server) |
Hello together, for me, using records or Kotlin data classes with constructor injection is also about clean code by using final fields or val in Kotlin. Similarly, it is not so nice to have public/protected mutable fields in a bean where every user could meddle with its internal workings. Instead, when using constructor injection, all is sealed inside the class and only the interface intended to be used is public. On top, records/data classes provide additional benefits like toString etc. |
I am so glad I documented this 😂 Now, IIRC the reason why we didn't go with constructor creation and usage was because we wanted to be able to inject our stuff into beans that we got via CDI, so that CDI could create the bean (making all There was no API to call CDI injection if we created the bean via our generated constructor. Perhaps this has changed. If we support records, I'm not sure how CDI injection will work. I doubt we can create a record via CDI, for the same reason as us, but perhaps I'm wrong. Now, if we do not care about CDI injection, we can change the pattern of our injected type from:
Into something like:
So, for each annotated field, we declare a local variable instead of assigning the field, until we have them all. Then we call the record constructor and return it. It's not a big variation. We can keep the static converters and mediatypes and most of the code, I think. Records don't have supertypes, so that's also one less thing to worry about. Writing that, I realised that CDI injection probably only works for the first level, unless we don't really do Unless you have a better idea? |
IIRC, the CDI injection thing is only relevant for the Resources themselves, not for the "structs" needed for multipart or beanparam |
Is that true? Oh, perhaps, right, I forgot people use field injection for rest endpoints. Hopefully nobody asks us to support record rest endpoints 😂 |
Doubtful 😃 |
Turns out I wasn't wrong, we do create all our bean param method parameters as beans, to support injection: public class InjectParamExtractor implements ParameterExtractor {
private final BeanFactory<Object> factory;
public InjectParamExtractor(BeanFactory<Object> factory) {
this.factory = factory;
}
@Override
public Object extractParameter(ResteasyReactiveRequestContext context) {
BeanFactory.BeanInstance<Object> instance = factory.createInstance();
context.registerCompletionCallback(new CompletionCallback() {
@Override
public void onComplete(Throwable throwable) {
instance.close();
}
});
((ResteasyReactiveInjectionTarget) instance.getInstance()).__quarkus_rest_inject(context);
return instance.getInstance();
}
} So, that's the first thing that needs to change for records. |
And that's not a lot of effort? |
Not sure. It's certainly more effort than I anticipated, which reminds us both how good I am at estimating tasks ;) |
So, I need to stop registering those as beans, that's one thing. Then I need to generate an instantiator class as a |
It'd be so much easier to transform records to stop being records and be regular classes 😅 I wonder if there's any trick we can use to generate a |
So, this turned out to not be as trivial as expected. But I have records support for Missing support for records containing records (or bean params) and context objects (since there's no CDI injection in them). |
Thanks for taking this up! |
Man, did I underestimate the problem touching this nest of vipers. Turns out we register But records can't be beans, or not in a way that I know how, so injection can't work in them. Fine, that's a limitation. But also, we register Soooo many lose ends… 4 days in, and still no end in sight. And I haven't even had time to refactor the ASM into gizmo :( |
And also, remove the |
Actually, I just discovered this:
And I'm wondering if we should not do the same for bean param records :-/ |
I'm trying to make an annotation transformer that removes the public static class OtherBeanParam {
@RestQuery
String q;
@BeanParam // I WANT TO REMOVE THIS
OtherBeanParamRecord obpr;
@BeanParam
OtherBeanParamClass obpc;
}
public record OtherBeanParamRecord(@RestQuery String q) {
} But apparently, this is not working, perhaps because my annotation transformer is declared before Is this correct @manovotn @mkouba @Ladicek ? If yes, any idea how I can achieve the same and tell CDI to not inject record fields? |
OK, sorry, my bad. Annotation transformers are indeed chained, and I wanted mine to be before auto-inject-field, and my annotation removal code would have worked, if only I'd called User error, as often. Sorry for the ping, thanks anyway :) |
Alright, so one further surprise is that |
Also unify the parameter container detection code paths Make sure we do not turn record parameter containers into beans For records, we store every contructor parameter in a local variable until we have enough to call the constructor via a generated static method. Fixes quarkusio#19686
Also unify the parameter container detection code paths Make sure we do not turn record parameter containers into beans For records, we store every contructor parameter in a local variable until we have enough to call the constructor via a generated static method. Fixes quarkusio#19686
OK, so #42642 contains support for record bean params on the server. But this issue is about kotlin data classes, which is something else entirely. Do they compile to records? Should I open another issue for the record support and leave this one open? |
Sorry I had a sick leave yesterday but it seems you have it all figured out now :) However, I was thinking maybe we should tweak |
Not by default IIRC |
That is an option indeed. |
I have updated https://github.com/quarkusio/quarkus/wiki/RESTEasy-Reactive-(Server) with everything I learned during this rabbit hole, and everything I changed for records. |
I've added #42646 where I collected all the work remaining to be done that I noticed while working on this. |
Actually, I looked into it and recalled that we cannot do that. |
Oh, so in the case that every record component is a CDI bean? |
No, just like not every class is a CDI bean. |
Sure, I mean that for a record to be allowed to be a bean, all of its components must also be beans, otherwise the constructor won't be able to be called. But it only applies when there's a bean defining annotation. |
Yea, that's right 👍 |
Also unify the parameter container detection code paths Make sure we do not turn record parameter containers into beans For records, we store every contructor parameter in a local variable until we have enough to call the constructor via a generated static method. Fixes quarkusio#19686
Also unify the parameter container detection code paths Make sure we do not turn record parameter containers into beans For records, we store every contructor parameter in a local variable until we have enough to call the constructor via a generated static method. Fixes quarkusio#19686
Description
When using Jackson with RestEasy Reactive, normal method bodies can be POJOs that initialize the fields via the constructor. For Kotlin this works with the dependency
com.fasterxml.jackson.module:jackson-module-kotlin
and the body class can look like this:Unfortunately, for the @MultipartBody objects, this does not work. There, you have to have a default constructor.
Please also implement the constructor creation for the @MultipartBody objects.
Implementation ideas
No response
The text was updated successfully, but these errors were encountered: