Skip to content

Commit

Permalink
Avoid loading all MessageBodyWriter classes at startup
Browse files Browse the repository at this point in the history
The classes themselves aren't really needed
for the lookup process, but loading them all
does incur a non-zero cost.

In order to make this possible the commit
also includes moving some of the work to
build time
  • Loading branch information
geoand committed Jan 3, 2025
1 parent abcadde commit 30d2886
Show file tree
Hide file tree
Showing 12 changed files with 195 additions and 119 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public <T> BeanFactory<T> factory(String targetClass, BeanContainer beanContaine

public void registerWriter(Serialisers serialisers, String entityClassName,
ResourceWriter writer) {
serialisers.addWriter(loadClass(entityClassName), writer);
serialisers.addWriter(entityClassName, writer);
}

public void registerReader(Serialisers serialisers, String entityClassName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.PUBLISHER;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.RESOURCE_INFO;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_MULTI;
import static org.jboss.resteasy.reactive.common.util.types.Types.getEffectiveReturnType;
import static org.jboss.resteasy.reactive.common.util.types.Types.getRawType;
import static org.jboss.resteasy.reactive.common.util.types.Types.isNotVoid;
import static org.jboss.resteasy.reactive.common.util.types.Types.primitiveWrapper;

import java.io.File;
import java.io.IOException;
Expand Down Expand Up @@ -49,6 +53,7 @@
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyReader;
import jakarta.ws.rs.ext.MessageBodyWriter;
import jakarta.ws.rs.ext.Providers;
Expand Down Expand Up @@ -91,6 +96,8 @@
import org.jboss.resteasy.reactive.common.processor.scanning.ResourceScanningResult;
import org.jboss.resteasy.reactive.common.types.AllWriteableMarker;
import org.jboss.resteasy.reactive.common.util.Encode;
import org.jboss.resteasy.reactive.common.util.MediaTypeHelper;
import org.jboss.resteasy.reactive.common.util.types.TypeSignatureParser;
import org.jboss.resteasy.reactive.common.util.types.Types;
import org.jboss.resteasy.reactive.server.core.Deployment;
import org.jboss.resteasy.reactive.server.core.DeploymentInfo;
Expand Down Expand Up @@ -1390,10 +1397,29 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem,
servletPresent = true;
}

//check to make sure that the return type is build time selectable
//this fails when there are eligible writers for a sub type of the entity type
//e.g. if the entity type is Object and there are mappers for String then we
//can't determine the type at build time
Set<String> returnTypeHasAssignableButNotEqualWriter = new HashSet<>();
for (ResourceClass resourceClass : resourceClasses) {
List<ResourceMethod> methods = resourceClass.getMethods();
for (ResourceMethod method : methods) {
java.lang.reflect.Type effectiveReturnType = getEffectiveReturnType(
TypeSignatureParser.parse(method.getReturnType()));
Class<?> rawEffectiveReturnType = getRawType(effectiveReturnType);
Class<?> typeToCheck = primitiveWrapper(rawEffectiveReturnType);
if ((method.getProduces() != null && method.getProduces().length == 1) && isNotVoid(typeToCheck)) {
returnTypeHasAssignableButNotEqualWriter.add(typeToCheck.getName());
}
}
}

RuntimeValue<Deployment> deployment = recorder.createDeployment(deploymentPath, deploymentInfo,
beanContainerBuildItem.getValue(), shutdownContext, vertxConfig,
requestContextFactoryBuildItem.map(RequestContextFactoryBuildItem::getFactory).orElse(null),
initClassFactory, launchModeBuildItem.getLaunchMode(), servletPresent);
initClassFactory, launchModeBuildItem.getLaunchMode(), servletPresent,
returnTypeHasAssignableButNotEqualWriter);

quarkusRestDeploymentBuildItemBuildProducer
.produce(new ResteasyReactiveDeploymentBuildItem(deployment, deploymentPath));
Expand Down Expand Up @@ -1442,6 +1468,28 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem,
}
}

private boolean hasAssignableButNotEqualWriter(Class<?> entityType, MultivaluedMap<Class<?>, ResourceWriter> map,
List<MediaType> produces) {
for (Map.Entry<Class<?>, List<ResourceWriter>> entry : map.entrySet()) {
if (entityType.isAssignableFrom(entry.getKey()) && !entry.getKey().equals(entityType)) {
//this is a writer registered under a sub type
//check to see if the media type is relevant
if (produces == null || produces.isEmpty()) {
return true;
} else {
List<ResourceWriter> list = entry.getValue();
for (int i = 0; i < list.size(); i++) {
MediaType match = MediaTypeHelper.getFirstMatch(produces, list.get(i).mediaTypes());
if (match != null) {
return true;
}
}
}
}
}
return false;
}

private ServerRestHandler determinePreExceptionMapperHandler(
List<PreExceptionMapperHandlerBuildItem> preExceptionMapperHandlerBuildItems) {
if ((preExceptionMapperHandlerBuildItems == null) || preExceptionMapperHandlerBuildItems.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
Expand Down Expand Up @@ -113,9 +114,10 @@ public RuntimeValue<Deployment> createDeployment(String applicationPath, Deploym
RequestContextFactory contextFactory,
BeanFactory<ResteasyReactiveInitialiser> initClassFactory,
LaunchMode launchMode,
boolean servletPresent) {
boolean servletPresent,
Set<String> returnTypeHasAssignableButNotEqualWriter) {

info.setServletPresent(servletPresent);
info.setServletPresent(servletPresent).setHasAssignableButNotEqualWriterCache(returnTypeHasAssignableButNotEqualWriter);

CurrentRequestManager
.setCurrentRequestInstance(new QuarkusCurrentRequest(beanContainer.beanInstance(CurrentVertxRequest.class)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ public void registerBuiltins(RuntimeType constraint) {
resourceWriter.setMediaTypeStrings(Collections.singletonList(builtinWriter.mediaType));
// FIXME: we could still support beans
resourceWriter.setFactory(new UnmanagedBeanFactory<MessageBodyWriter<?>>(writer));
addWriter(builtinWriter.entityClass, resourceWriter);
addWriter(builtinWriter.entityClass.getName(), resourceWriter);
}
}
for (BuiltinReader builtinReader : getBuiltinReaders()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import jakarta.ws.rs.RuntimeType;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.MessageBodyReader;
import jakarta.ws.rs.ext.MessageBodyWriter;
import jakarta.ws.rs.ext.ReaderInterceptor;
Expand All @@ -24,17 +23,15 @@
import org.jboss.resteasy.reactive.common.util.MediaTypeHelper;
import org.jboss.resteasy.reactive.common.util.QuarkusMultivaluedHashMap;
import org.jboss.resteasy.reactive.common.util.QuarkusMultivaluedMap;
import org.jboss.resteasy.reactive.common.util.types.Types;

@SuppressWarnings("ForLoopReplaceableByForEach")
public abstract class Serialisers {
public static final Annotation[] NO_ANNOTATION = new Annotation[0];
public static final ReaderInterceptor[] NO_READER_INTERCEPTOR = new ReaderInterceptor[0];
public static final WriterInterceptor[] NO_WRITER_INTERCEPTOR = new WriterInterceptor[0];
// FIXME: spec says we should use generic type, but not sure how to pass that type from Jandex to reflection
protected final QuarkusMultivaluedMap<Class<?>, ResourceWriter> writers = new QuarkusMultivaluedHashMap<>();
// for readers the key is a string in order to avoid loading all the reader classes at startup
// for writers we can't really do the same trick because we need to go through (potentially) all the writers
// at startup in order to determine the writer used for each resource method
// the key is a string in order to avoid loading all the reader classes at startup
protected final QuarkusMultivaluedMap<String, ResourceWriter> writers = new QuarkusMultivaluedHashMap<>();
protected final QuarkusMultivaluedMap<String, ResourceReader> readers = new QuarkusMultivaluedHashMap<>();

public List<MessageBodyReader<?>> findReaders(ConfigurationImpl configuration, Class<?> entityType,
Expand All @@ -47,7 +44,7 @@ public List<MessageBodyReader<?>> findReaders(ConfigurationImpl configuration, C
List<MediaType> desired = MediaTypeHelper.getUngroupedMediaTypes(mediaType);
List<MessageBodyReader<?>> ret = new ArrayList<>();
Deque<Class<?>> toProcess = new LinkedList<>();
Class<?> klass = lookupPrimitiveWrapper(entityType);
Class<?> klass = Types.primitiveWrapper(entityType);
QuarkusMultivaluedMap<String, ResourceReader> readers;
if (configuration != null && !configuration.getResourceReaders().isEmpty()) {
readers = new QuarkusMultivaluedHashMap<>();
Expand Down Expand Up @@ -110,50 +107,15 @@ private void readerLookup(MediaType mediaType, RuntimeType runtimeType, List<Med
}
}

public <T> void addWriter(Class<T> entityClass, ResourceWriter writer) {
writers.add(entityClass, writer);
public <T> void addWriter(String entityClassName, ResourceWriter writer) {
writers.add(entityClassName, writer);
}

public <T> void addReader(String entityClassName, ResourceReader reader) {
readers.add(entityClassName, reader);
}

public List<MessageBodyWriter<?>> findBuildTimeWriters(Class<?> entityType, RuntimeType runtimeType,
List<MediaType> produces) {
if (Response.class.isAssignableFrom(entityType)) {
return Collections.emptyList();
}
Class<?> klass = lookupPrimitiveWrapper(entityType);
//first we check to make sure that the return type is build time selectable
//this fails when there are eligible writers for a sub type of the entity type
//e.g. if the entity type is Object and there are mappers for String then we
//can't determine the type at build time
for (Map.Entry<Class<?>, List<ResourceWriter>> entry : writers.entrySet()) {
if (klass.isAssignableFrom(entry.getKey()) && !entry.getKey().equals(klass)) {
//this is a writer registered under a sub type
//check to see if the media type is relevant
if (produces == null || produces.isEmpty()) {
return null;
} else {
List<ResourceWriter> writers = entry.getValue();
for (int i = 0; i < writers.size(); i++) {
MediaType match = MediaTypeHelper.getFirstMatch(produces, writers.get(i).mediaTypes());
if (match != null) {
return null;
}
}
}
}

}

var resourceWriters = findResourceWriters(writers, klass, produces, runtimeType);
// we must NOT sort here because the spec mentions that the writers closer to the requested java type are tried first
// and the list has already been built up in this way
return toMessageBodyWriters(resourceWriters);
}

protected List<ResourceWriter> findResourceWriters(QuarkusMultivaluedMap<Class<?>, ResourceWriter> writers, Class<?> klass,
protected List<ResourceWriter> findResourceWriters(QuarkusMultivaluedMap<String, ResourceWriter> writers, Class<?> klass,
List<MediaType> produces, RuntimeType runtimeType) {
Class<?> currentClass = klass;
List<MediaType> desired = MediaTypeHelper.getUngroupedMediaTypes(produces);
Expand All @@ -166,7 +128,7 @@ protected List<ResourceWriter> findResourceWriters(QuarkusMultivaluedMap<Class<?
Set<Class<?>> seen = new HashSet<>(toProcess);
while (!toProcess.isEmpty()) {
Class<?> iface = toProcess.poll();
List<ResourceWriter> goodTypeWriters = writers.get(iface);
List<ResourceWriter> goodTypeWriters = writers.get(iface.getName());
writerLookup(runtimeType, produces, desired, ret, goodTypeWriters);
for (Class<?> i : iface.getInterfaces()) {
if (!seen.contains(i)) {
Expand All @@ -176,7 +138,7 @@ protected List<ResourceWriter> findResourceWriters(QuarkusMultivaluedMap<Class<?
}
}
}
List<ResourceWriter> goodTypeWriters = writers.get(currentClass);
List<ResourceWriter> goodTypeWriters = writers.get(currentClass.getName());
writerLookup(runtimeType, produces, desired, ret, goodTypeWriters);
var prevClass = currentClass;
// if we're an interface, pretend our superclass is Object to get us through the same logic as a class
Expand Down Expand Up @@ -238,42 +200,17 @@ public List<MessageBodyWriter<?>> findWriters(ConfigurationImpl configuration, C
return findWriters(configuration, entityType, resolvedMediaType, null);
}

protected final Class<?> lookupPrimitiveWrapper(final Class<?> entityType) {
if (!entityType.isPrimitive()) {
return entityType;
}
if (entityType == boolean.class) {
return Boolean.class;
} else if (entityType == char.class) {
return Character.class;
} else if (entityType == byte.class) {
return Byte.class;
} else if (entityType == short.class) {
return Short.class;
} else if (entityType == int.class) {
return Integer.class;
} else if (entityType == long.class) {
return Long.class;
} else if (entityType == float.class) {
return Float.class;
} else if (entityType == double.class) {
return Double.class;
}
// this shouldn't really happen, but better be safe than sorry
return entityType;
}

public List<MessageBodyWriter<?>> findWriters(ConfigurationImpl configuration, Class<?> entityType,
MediaType resolvedMediaType, RuntimeType runtimeType) {
// FIXME: invocation is very different between client and server, where the server doesn't treat GenericEntity specially
// it's probably missing from there, while the client handles it upstack
List<MediaType> mt = Collections.singletonList(resolvedMediaType);
Class<?> klass = lookupPrimitiveWrapper(entityType);
QuarkusMultivaluedMap<Class<?>, ResourceWriter> writers;
Class<?> klass = Types.primitiveWrapper(entityType);
QuarkusMultivaluedMap<String, ResourceWriter> writers;
if (configuration != null && !configuration.getResourceWriters().isEmpty()) {
writers = new QuarkusMultivaluedHashMap<>();
writers.addAll(configuration.getResourceWriters());
for (Map.Entry<Class<?>, List<ResourceWriter>> writersEntry : this.writers.entrySet()) {
for (Map.Entry<String, List<ResourceWriter>> writersEntry : this.writers.entrySet()) {
writers.addAll(writersEntry.getKey(), writersEntry.getValue());
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public class ConfigurationImpl implements Configuration {
private final MultivaluedMap<Integer, ClientResponseFilter> responseFilters;
private final MultivaluedMap<Integer, WriterInterceptor> writerInterceptors;
private final MultivaluedMap<Integer, ReaderInterceptor> readerInterceptors;
private final MultivaluedMap<Class<?>, ResourceWriter> resourceWriters;
private final MultivaluedMap<String, ResourceWriter> resourceWriters;
private final MultivaluedMap<String, ResourceReader> resourceReaders;
private final MultivaluedMap<Class<?>, RxInvokerProvider<?>> rxInvokerProviders;
private final Map<Class<?>, MultivaluedMap<Integer, ContextResolver<?>>> contextResolvers;
Expand Down Expand Up @@ -305,8 +305,8 @@ private void register(Object component, Integer priority) {
resourceWriter.setPriority(priority);
}
Type[] args = Types.findParameterizedTypes(componentClass, MessageBodyWriter.class);
resourceWriters.add(args != null && args.length == 1 ? Types.getRawType(args[0]) : Object.class,
resourceWriter);
Class<?> clazz = args != null && args.length == 1 ? Types.getRawType(args[0]) : Object.class;
resourceWriters.add(clazz.getName(), resourceWriter);
}
}
if (component instanceof RxInvokerProvider) {
Expand Down Expand Up @@ -420,8 +420,8 @@ public void register(Object component, Map<Class<?>, Integer> componentContracts
.setMediaTypeStrings(
produces != null ? Arrays.asList(produces.value()) : WILDCARD_STRING_LIST);
Type[] args = Types.findParameterizedTypes(componentClass, MessageBodyWriter.class);
resourceWriters.add(args != null && args.length == 1 ? Types.getRawType(args[0]) : Object.class,
resourceWriter);
Class<?> clazz = args != null && args.length == 1 ? Types.getRawType(args[0]) : Object.class;
resourceWriters.add(clazz.getName(), resourceWriter);
}
}
if (component instanceof ContextResolver) {
Expand Down Expand Up @@ -470,7 +470,7 @@ public void registerMessageBodyWriter(MessageBodyWriter<?> messageBodyWriter, Cl
resourceWriter.setBuiltin(builtin);
resourceWriter.setPriority(priority);
resourceWriter.setConstraint(runtimeType);
resourceWriters.add(handledType, resourceWriter);
resourceWriters.add(handledType.getName(), resourceWriter);
allInstances.put(messageBodyWriter.getClass(), messageBodyWriter);
}

Expand Down Expand Up @@ -582,7 +582,7 @@ public MultivaluedMap<String, ResourceReader> getResourceReaders() {
return resourceReaders;
}

public MultivaluedMap<Class<?>, ResourceWriter> getResourceWriters() {
public MultivaluedMap<String, ResourceWriter> getResourceWriters() {
return resourceWriters;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@

import java.util.Map;

import org.jboss.resteasy.reactive.common.core.Serialisers;
import org.jboss.resteasy.reactive.common.model.ResourceWriter;

public abstract class DeploymentUtils {
private static final Map<String, Class<?>> primitiveTypes = Map.of(
byte.class.getName(), byte.class,
Expand All @@ -16,11 +13,6 @@ public abstract class DeploymentUtils {
double.class.getName(), double.class,
long.class.getName(), long.class);

public static void registerWriter(Serialisers serialisers, String entityClassName,
ResourceWriter writer) {
serialisers.addWriter(loadClass(entityClassName), writer);
}

public static <T> Class<T> loadClass(String name) {
if (primitiveTypes.containsKey(name)) {
return (Class<T>) primitiveTypes.get(name);
Expand Down
Loading

0 comments on commit 30d2886

Please sign in to comment.