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