-
Notifications
You must be signed in to change notification settings - Fork 2.7k
RESTEasy Reactive (Server)
This page documents some of the internals of RESTEasy Reactive.
These are classes that contain other request parameters, such as @FormParam
, @HeaderParam
, @PathParam
,
@MatrixParam
, @CookieParam
, @QueryParam
or other parameter containers.
Typically, these parameters are defined as fields on the endpoint, or as method parameters, but for convenience or reusability
we can group them into parameter containers. The endpoint parameter which is a parameter container may be annotated with @BeanParam
but it is not required because we can autodetect them based on the presence of its annotated fields.
@BeanParam
and @MultipartForm
(now deprecated) are equivalent, and now required except for OpenAPI which does not auto-detect parameter containers.
NOTE: we treat endpoint classes as parameter containers if they have parameter fields, so it's exactly the same code.
These are documented for the users at https://quarkus.io/guides/resteasy-reactive#grouping-parameters-in-a-custom-class but this documents how this is implemented.
Generally speaking, we have a transformer (in ClassInjectorTransformer
) that transforms these parameter container classes by making them implement the ResteasyReactiveInjectionTarget
interface, and implementing an __quarkus_rest_inject
method which takes care of populating
the fields by using the request context.
Here is an example of the transformation we apply:
import java.io.File;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.List;
import org.jboss.resteasy.reactive.PartType;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.common.util.DeploymentUtils;
import org.jboss.resteasy.reactive.multipart.FileUpload;
import org.jboss.resteasy.reactive.server.core.Deployment;
import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
import org.jboss.resteasy.reactive.server.core.multipart.MultipartSupport;
import org.jboss.resteasy.reactive.server.core.parameters.converters.CharParamConverter;
import org.jboss.resteasy.reactive.server.core.parameters.converters.ListConverter;
import org.jboss.resteasy.reactive.server.core.parameters.converters.ParameterConverter;
import org.jboss.resteasy.reactive.server.injection.ResteasyReactiveInjectionContext;
import org.jboss.resteasy.reactive.server.injection.ResteasyReactiveInjectionTarget;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
class X {}
class OtherBeanParamClass /* generated if our supertype is not already injectable: */ implements ResteasyReactiveInjectionTarget {
// ...
@Override
public void __quarkus_rest_inject(ResteasyReactiveInjectionContext ctx) {
// ...
}
}
class BeanParamClass /* generated if our supertype is not already injectable: */ implements ResteasyReactiveInjectionTarget {
@DefaultValue("default")
@RestForm
String regular;
@RestForm
char converted;
@RestForm
List<String> list;
@RestForm
List<Character> convertedList;
@RestForm
File multipartSpecial;
@RestForm
@PartType(MediaType.APPLICATION_JSON)
X multipartViaMessageBodyReader;
OtherBeanParamClass otherBeanParamClass;
// the rest of this class is generated
private static ParameterConverter __quarkus_converter__converted;
// this will be called at startup
public static void __quarkus_init_converter__converted(Deployment deployment) {
ParameterConverter converter = deployment.getRuntimeParamConverter(BeanParamClass.class, "converted", true);
// we have predefined ones in some cases
if(converter == null) {
converter = new CharParamConverter();
}
__quarkus_converter__converted = converter;
}
private static ParameterConverter __quarkus_converter__convertedList;
// this will be called at startup
public static void __quarkus_init_converter__convertedList(Deployment deployment) {
ParameterConverter converter = deployment.getRuntimeParamConverter(BeanParamClass.class, "convertedList", true);
// we have predefined ones in some cases
if(converter == null) {
converter = new CharParamConverter();
}
// this is a collection
converter = new ListConverter(converter);
__quarkus_converter__convertedList = converter;
}
private static Class multipartViaMessageBodyReader_type;
private static Type multipartViaMessageBodyReader_genericType;
private static MediaType multipartViaMessageBodyReader_mediaType;
static {
Class var0 = DeploymentUtils.loadClass("com.example.X");
// Note that in the case of collections or arrays, type/genericType represents the element type
multipartViaMessageBodyReader_type = var0;
// or TypeSignatureParser.parse("Ljava/util/Map<Ljava/lang/String;Ljava/lang/String;>;"); for generic types
multipartViaMessageBodyReader_genericType = var0;
multipartViaMessageBodyReader_mediaType = MediaType.valueOf("application/json");
}
@Override
public void __quarkus_rest_inject(ResteasyReactiveInjectionContext ctx) {
// if our supertype is injectable
// super.__quarkus_rest_inject(ctx);
// a regular field with no converter
try {
Object val = ctx.getFormParameter("regular", true, true);
// if we have a default value
if(val == null) {
val = "default";
}
if(val != null) {
regular = (String)val;
}
} catch (WebApplicationException x) {
throw x;
} catch (Throwable x) {
throw new BadRequestException();
}
// a converted field
try {
Object val = ctx.getFormParameter("converted", true, true);
if(val != null) {
converted = (char) __quarkus_converter__converted.convert(val);
}
} catch (Throwable x){ /* omitted */}
// a collection field
try {
Object val = ctx.getFormParameter("list", true, true);
if(val != null && !((Collection)val).isEmpty()) {
list = (List) val;
}
} catch (Throwable x){ /* omitted */}
// a converted collection field
try {
Object val = ctx.getFormParameter("convertedList", true, true);
if(val != null && !((Collection)val).isEmpty()) {
convertedList = (List) __quarkus_converter__convertedList.convert(val);
}
} catch (Throwable x){ /* omitted */}
// a special multipart type
try {
// there are variants for List<X> or X[] or even for name.equals(FileUpload.ALL)
// where X is FileUpload, String, byte[], File, Path, InputStream
FileUpload val = MultipartSupport.getFileUpload("multipartSpecial", (ResteasyReactiveRequestContext) ctx);
if(val != null) {
multipartSpecial = val.uploadedFile().toFile();
}
} catch (Throwable x){ /* omitted */}
// a multipart via MessageBodyReader
try {
// there are variants for List<X> or X[]
Object val = MultipartSupport.getConvertedFormAttribute("multipartViaMessageBodyReader", multipartViaMessageBodyReader_type, multipartViaMessageBodyReader_genericType,
multipartViaMessageBodyReader_mediaType,
(ResteasyReactiveRequestContext) ctx);
if(val != null) {
multipartViaMessageBodyReader = (X) val;
}
} catch (Throwable x){ /* omitted */}
// another bean param class
otherBeanParamClass = new OtherBeanParamClass();
otherBeanParamClass.__quarkus_rest_inject(ctx);
}
}
Parameters that require ParamConverter
(for their type, or because they're collections) get an extra static field storing their converter for efficiency, with an extra static method to initialise them called __quarkus_init_converter__<fieldname>(Deployment deployment)
. This is automatically called at startup.
Parameters that require multi-part deserialisation get three extra static fields Class
, Type
and MediaType
initialised in a static{}
block, for efficiency. Those are used in __quarkus_rest_inject
to invoke MultipartSupport.getConvertedFormAttribute
which performs MessageBodyReader
lookups just like endpoint bodies.
Class parameter containers (endpoints and bean params excluding records) are automatically made into CDI beans by adding AdditionalBeanBuildItem
and UnremovableBeanBuildItem
items for them, and @Typed(MyBean.class)
annotations and declaring @BeanParam
an auto-inject equivalent to @Inject
.
@Context
parameters are also injected via CDI.
Class parameter containers are obtained initially via InjectParamExtractor
for endpoint parameters, using CDI (which handles CDI injection), then calling __quarkus_rest_inject
to handle the non-CDI injection (@*Param
, records).
In the case of class parameter containers inside parameter containers, they are either automatically obtained via CDI (if the current parameter container is a bean), or they are obtained via ResteasyReactiveInjectionContext.getBeanParameter(ctx)
which performs CDI lookup, and then in both cases we call __quarkus_rest_inject(ctx)
.
This is where it's broken. Because they have constructors we can't turn them easily into CDI beans, so we have code in CustomResourceProducersGenerator
that generates CDI producer methods for them.
This is only supported for endpoints when they have constructors, but not bean params (class or record).
Also, this constructor injection uses custom logic that is not the same as the one for endpoint method parameter or bean param, so it appears to call *ParamExtractor.extractParameter
(single only, no separator, not encoded, no default value, no converter, no support for multipart anything).
Sadly, this only works for the most trivial cases.
We generate a class that contains as many CDI producer methods as there are JAX-RS Resources that use JAX-RS params. If for example there was a single such JAX-RS resource looking like:
@Path("/query")
public class QueryParamResource {
private final String queryParamValue;
private final UriInfo uriInfo;
public QueryParamResource(@QueryParam("p1") String headerValue, @Context UriInfo uriInfo) {
this.headerValue = headerValue;
}
@GET
public String get() {
// DO something
}
}
Then the generated producer class would look like this:
@Singleton
public class ResourcesWithParamProducer {
private String getHeaderParam(String name) {
return (String)new HeaderParamExtractor(name, true).extractParameter(getContext());
}
private String getQueryParam(String name) {
return (String)new QueryParamExtractor(name, true, false, null).extractParameter(getContext());
}
private String getPathParam(int index) {
return (String)new PathParamExtractor(index, false, true).extractParameter(getContext());
}
private String getMatrixParam(String name) {
return (String)new MatrixParamExtractor(name, true, false).extractParameter(getContext());
}
private String getCookieParam(String name) {
return (String)new CookieParamExtractor(name, null).extractParameter(getContext());
}
@Produces
@RequestScoped
public QueryParamResource producer_QueryParamResource_somehash(UriInfo uriInfo) {
return new QueryParamResource(getQueryParam("p1"), uriInfo);
}
private ResteasyReactiveRequestContext getContext() {
return CurrentRequestManager.get();
}
}
NOTE: we should fix these, or remove support for it.
For records, this is slighly different, because records do not have zero-param constructors, so instead of instantiating an empty instance and then injecting it by calling __quarkus_rest_inject(ctx)
to set the fields, we generate a static factory method which collects all the fields as local variables before calling the record constructor and returning it:
import java.io.File;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.List;
import org.jboss.resteasy.reactive.PartType;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.common.util.DeploymentUtils;
import org.jboss.resteasy.reactive.multipart.FileUpload;
import org.jboss.resteasy.reactive.server.core.Deployment;
import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
import org.jboss.resteasy.reactive.server.core.multipart.MultipartSupport;
import org.jboss.resteasy.reactive.server.core.parameters.converters.CharParamConverter;
import org.jboss.resteasy.reactive.server.core.parameters.converters.ListConverter;
import org.jboss.resteasy.reactive.server.core.parameters.converters.ParameterConverter;
import org.jboss.resteasy.reactive.server.injection.ResteasyReactiveInjectionContext;
import org.jboss.resteasy.reactive.server.injection.ResteasyReactiveInjectionTarget;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
record OtherBeanParamClass(/*...*/) {
public static OtherBeanParamClass __quarkus_rest_inject(ResteasyReactiveInjectionContext ctx) {
// ...
return new OtherBeanParamClass();
}
}
record BeanParamClass(
@DefaultValue("default")
@RestForm
String regular,
// other types of fields omitted but supported just as previous example (converters, default values, multipart…)
OtherBeanParamClass otherBeanParamClass){}
// the rest of this class is generated
public static BeanParamClass __quarkus_rest_inject(ResteasyReactiveInjectionContext ctx) {
// a regular field with no converter
String regularValue = null;
try {
Object val = ctx.getFormParameter("regular", true, true);
// if we have a default value
if(val == null) {
val = "default";
}
if(val != null) {
regularValue = (String)val;
}
} catch (WebApplicationException x) {
throw x;
} catch (Throwable x) {
throw new BadRequestException();
}
// another bean param class
OtherBeanParamClass otherBeanParamClassValue = OtherBeanParamClass.__quarkus_rest_inject(ctx);
return new BeanParamClass(regularValue, otherBeanParamClassValue);
}
}
So, record parameter containers differ from class parameter containers this way:
- They are not CDI beans (no bean annotation added automatically, no factory method created for them, not obtained via CDI)
- As a result, we have an annotation transformer that removes
@BeanParam
that point to records, or that are located in records. - They are created (for endpoint method parameters) by
RecordBeanParamExtractor
which uses aMethodHandle
to find its static facory method - They do not implement
ResteasyReactiveInjectionTarget
- They get a generated static factory method called
__quarkus_rest_inject(ctx)
- That factory method collects every field into a local variable (instead of setting the field, like we do for class parameter containers, but otherwise parameter is the same except for context fields)
- Context fields are not automatically set by CDI, so they are obtained via
ResteasyReactiveInjectionContext.getContextParameter(Class)
- Class parameter containers are not automatically set by CDI, so they are obtained via
ResteasyReactiveInjectionContext.getBeanParameter(Class)
- Record parameter containers are obtained by calling
RecordClass.__quarkus_rest_inject(ctx)
- At the end of the static factory method, we invoke the record constructor with all the collected fields stored in local variables and return it.