From 303e03a5b44e0b55023aedd8cedaa0477f4b0028 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Thu, 3 Nov 2022 23:04:20 +0100 Subject: [PATCH] #1866, #1867 - More AOT hints from controller methods and RepresentationModelAssemblers. We now inspect controller method return types and the generics of RepresentationModelAssembler for EntityModel and CollectionModel types and unwrap the type they box to register that for constructor and method invocation reflection. --- .../springframework/hateoas/aot/AotUtils.java | 118 ++++++++++++++++++ ...ontrollerMethodReturnTypeAotProcessor.java | 14 ++- ...resentationModelAssemblerAotProcessor.java | 58 +++++++++ .../resources/META-INF/spring/aot.factories | 3 +- 4 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/springframework/hateoas/aot/AotUtils.java create mode 100644 src/main/java/org/springframework/hateoas/aot/RepresentationModelAssemblerAotProcessor.java diff --git a/src/main/java/org/springframework/hateoas/aot/AotUtils.java b/src/main/java/org/springframework/hateoas/aot/AotUtils.java new file mode 100644 index 00000000..5bd48f1e --- /dev/null +++ b/src/main/java/org/springframework/hateoas/aot/AotUtils.java @@ -0,0 +1,118 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.hateoas.aot; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.core.ResolvableType; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.HttpEntity; + +/** + * Some helper classes to register types for reflection. + * + * @author Oliver Drotbohm + * @since 2.0 + */ +class AotUtils { + + private static final Logger LOGGER = LoggerFactory.getLogger(AotUtils.class); + private static final List> MODEL_TYPES = List.of(EntityModel.class, CollectionModel.class); + private static final Set> SEEN_TYPES = new HashSet<>(); + + /** + * Registers domain types held in {@link EntityModel} and {@link CollectionModel}s for reflection. + * + * @param type must not be {@literal null}. + * @param reflection must not be {@literal null}. + * @param context must not be {@literal null}. + */ + public static void registerModelDomainTypesForReflection(ResolvableType type, ReflectionHints reflection, + Class context) { + + if (HttpEntity.class.isAssignableFrom(type.resolve(Object.class))) { + registerModelDomainTypesForReflection(type.as(HttpEntity.class).getGeneric(0), reflection, context); + } + + MODEL_TYPES.stream() + .flatMap(it -> extractGenerics(it, type).stream()) + .forEach(it -> registerTypeForReflection(it, reflection, context)); + } + + /** + * Registers the given type for constructor and method invocation reflection. + * + * @param type must not be {@literal null}. + * @param reflection must not be {@literal null}. + * @param context must not be {@literal null}. + */ + public static void registerTypeForReflection(Class type, ReflectionHints reflection, Class context) { + + if (SEEN_TYPES.contains(type)) { + return; + } + + LOGGER.info("Registering {} for reflection (for {})", type.getName(), context.getName()); + + reflection.registerType(type, + MemberCategory.INVOKE_DECLARED_METHODS, + MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS); + + SEEN_TYPES.add(type); + } + + /** + * Extracts the generics from the given model type if the given {@link ResolvableType} is assignable. + * + * @param modelType must not be {@literal null}. + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ + private static Optional> extractGenerics(Class modelType, ResolvableType type) { + + if (!modelType.isAssignableFrom(type.resolve(Object.class))) { + return Optional.empty(); + } + + var unresolved = type.as(modelType).getGeneric(0); + var resolved = unresolved.resolve(); + + if (resolved == null) { + return Optional.empty(); + } + + var nested = MODEL_TYPES.stream() + .filter(it -> it.isAssignableFrom(resolved)) + .toList(); + + // No nested matches -> return original + if (nested.isEmpty()) { + return Optional.of(resolved); + } + + return nested.stream() + .flatMap(it -> extractGenerics(it, unresolved).stream()) + .findFirst(); + } +} diff --git a/src/main/java/org/springframework/hateoas/aot/ControllerMethodReturnTypeAotProcessor.java b/src/main/java/org/springframework/hateoas/aot/ControllerMethodReturnTypeAotProcessor.java index e577284f..c4c0a794 100644 --- a/src/main/java/org/springframework/hateoas/aot/ControllerMethodReturnTypeAotProcessor.java +++ b/src/main/java/org/springframework/hateoas/aot/ControllerMethodReturnTypeAotProcessor.java @@ -15,6 +15,8 @@ */ package org.springframework.hateoas.aot; +import static org.springframework.hateoas.aot.AotUtils.*; + import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; @@ -32,6 +34,7 @@ import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; import org.springframework.beans.factory.aot.BeanRegistrationCode; import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.hateoas.server.core.DummyInvocationUtils; import org.springframework.hateoas.server.core.LastInvocationAware; @@ -54,8 +57,7 @@ public class ControllerMethodReturnTypeAotProcessor implements BeanRegistrationA private final Class controllerAnnotationType; /** - * Creates a new {@link ControllerMethodReturnTypeAotProcessor} looking for classes annotated with - * {@link Controller}. + * Creates a new {@link ControllerMethodReturnTypeAotProcessor} looking for classes annotated with {@link Controller}. */ public ControllerMethodReturnTypeAotProcessor() { this(Controller.class); @@ -134,8 +136,13 @@ public class ControllerMethodReturnTypeAotProcessor implements BeanRegistrationA return; } + var runtimeHints = generationContext.getRuntimeHints(); + var methodReturnType = ResolvableType.forMethodReturnType(method); + + registerModelDomainTypesForReflection(methodReturnType, runtimeHints.reflection(), beanClass); + if (returnType.isInterface()) { - generationContext.getRuntimeHints().proxies().registerJdkProxy(returnType); + runtimeHints.proxies().registerJdkProxy(returnType); return; } @@ -198,5 +205,4 @@ public class ControllerMethodReturnTypeAotProcessor implements BeanRegistrationA return null; } } - } diff --git a/src/main/java/org/springframework/hateoas/aot/RepresentationModelAssemblerAotProcessor.java b/src/main/java/org/springframework/hateoas/aot/RepresentationModelAssemblerAotProcessor.java new file mode 100644 index 00000000..01b33d99 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/aot/RepresentationModelAssemblerAotProcessor.java @@ -0,0 +1,58 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.hateoas.aot; + +import static org.springframework.hateoas.aot.AotUtils.*; + +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.server.RepresentationModelAssembler; + +/** + * A {@link BeanRegistrationAotProcessor} that inspects {@link RepresentationModelAssembler}'s generics for domain types + * wrapped in {@link EntityModel} and {@link CollectionModel}. + * + * @author Oliver Drotbohm + * @since 2.0 + */ +class RepresentationModelAssemblerAotProcessor implements BeanRegistrationAotProcessor { + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.aot.BeanRegistrationAotProcessor#processAheadOfTime(org.springframework.beans.factory.support.RegisteredBean) + */ + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + + var beanClass = registeredBean.getBeanClass(); + + if (!RepresentationModelAssembler.class.isAssignableFrom(beanClass)) { + return null; + } + + var modelType = registeredBean.getBeanType().as(RepresentationModelAssembler.class).getGeneric(1); + + return (context, code) -> { + + var reflection = context.getRuntimeHints().reflection(); + + registerModelDomainTypesForReflection(modelType, reflection, beanClass); + }; + } +} diff --git a/src/main/resources/META-INF/spring/aot.factories b/src/main/resources/META-INF/spring/aot.factories index 89100538..821b0b39 100644 --- a/src/main/resources/META-INF/spring/aot.factories +++ b/src/main/resources/META-INF/spring/aot.factories @@ -1,6 +1,7 @@ org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ org.springframework.hateoas.aot.ControllerMethodReturnTypeAotProcessor,\ - org.springframework.hateoas.aot.HypermediaTypeAotProcessor + org.springframework.hateoas.aot.HypermediaTypeAotProcessor,\ + org.springframework.hateoas.aot.RepresentationModelAssemblerAotProcessor org.springframework.aot.hint.RuntimeHintsRegistrar=\ org.springframework.hateoas.aot.RepresentationModelRuntimeHints