Skip to content

RESTEasy Reactive (Server)

Stéphane Épardaud edited this page Aug 20, 2024 · 5 revisions

This page documents some of the internals of RESTEasy Reactive.

@BeanParam classes aka Parameter Containers

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);
	}
}

ParamConverters and multi-part parameters

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.

CDI

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).

Endpoints with constructors

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.

Bean param records

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 a MethodHandle 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.
Clone this wiki locally