diff --git a/src/main/java/org/springframework/hateoas/aot/AotUtils.java b/src/main/java/org/springframework/hateoas/aot/AotUtils.java index 952c2da91..e3a0f76d7 100644 --- a/src/main/java/org/springframework/hateoas/aot/AotUtils.java +++ b/src/main/java/org/springframework/hateoas/aot/AotUtils.java @@ -16,7 +16,9 @@ package org.springframework.hateoas.aot; import java.io.IOException; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Optional; @@ -28,6 +30,7 @@ import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.TypeReference; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.core.ResolvableType; @@ -93,6 +96,20 @@ public static void registerTypeForReflection(Class type, ReflectionHints refl SEEN_TYPES.add(type); } + public static void registerTypesForReflection(String packageName, ReflectionHints reflection, TypeFilter... filters) { + + // Register RepresentationModel types for full reflection + var provider = AotUtils.getScanner(packageName, filters); + + LOGGER.info("Registering Spring HATEOAS types in {} for reflection.", packageName); + + provider.findClasses() + .sorted(Comparator.comparing(TypeReference::getName)) + .peek(type -> LOGGER.debug("> {}", type.getName())) + .forEach(reference -> reflection.registerType(reference, // + MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS)); + } + /** * Extracts the generics from the given model type if the given {@link ResolvableType} is assignable. * @@ -129,15 +146,28 @@ private static Optional> extractGenerics(Class modelType, Resolvable public static FullTypeScanner getScanner(String packageName, TypeFilter... includeFilters) { - var provider = new ClassPathScanningCandidateComponentProvider(false); + var provider = new ClassPathScanningCandidateComponentProvider(false) { + + @Override + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + return super.isCandidateComponent(beanDefinition) || beanDefinition.getMetadata().isAbstract(); + } + }; + + var filters = new ArrayList(); + filters.add(new EnforcedPackageFilter(packageName)); + filters.add(new AssignableTypeFilter(Object.class)); if (includeFilters.length == 0) { - provider.addIncludeFilter(new AssignableTypeFilter(Object.class)); - } else { - Arrays.stream(includeFilters).forEach(provider::addIncludeFilter); + provider.addIncludeFilter(all(filters)); } - provider.addExcludeFilter(new EnforcedPackageFilter(packageName)); + for (TypeFilter filter : includeFilters) { + + var includeFilterComponents = new ArrayList<>(filters); + includeFilterComponents.add(filter); + provider.addIncludeFilter(all(includeFilterComponents)); + } return () -> provider.findCandidateComponents(packageName).stream() .map(BeanDefinition::getBeanClassName) @@ -150,7 +180,7 @@ public static FullTypeScanner getScanner(String packageName, TypeFilter... inclu * * @author Oliver Drotbohm */ - private static class EnforcedPackageFilter implements TypeFilter { + static class EnforcedPackageFilter implements TypeFilter { private final String referencePackage; @@ -165,11 +195,30 @@ public EnforcedPackageFilter(String referencePackage) { @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { - return !referencePackage + return referencePackage .equals(ClassUtils.getPackageName(metadataReader.getClassMetadata().getClassName())); } } + private static TypeFilter all(Collection filters) { + + return new TypeFilter() { + + @Override + public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException { + + for (TypeFilter filter : filters) { + if (!filter.match(metadataReader, metadataReaderFactory)) { + return false; + } + } + + return true; + } + }; + } + static interface FullTypeScanner { abstract Stream findClasses(); diff --git a/src/main/java/org/springframework/hateoas/aot/HateoasTypesRuntimeHints.java b/src/main/java/org/springframework/hateoas/aot/HateoasTypesRuntimeHints.java index 43430e854..f0a240f96 100644 --- a/src/main/java/org/springframework/hateoas/aot/HateoasTypesRuntimeHints.java +++ b/src/main/java/org/springframework/hateoas/aot/HateoasTypesRuntimeHints.java @@ -15,7 +15,6 @@ */ package org.springframework.hateoas.aot; -import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.hateoas.RepresentationModel; @@ -34,12 +33,9 @@ class HateoasTypesRuntimeHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + var packageName = RepresentationModel.class.getPackageName(); var reflection = hints.reflection(); - AotUtils.getScanner(RepresentationModel.class.getPackageName()) // - .findClasses() // - .forEach(it -> reflection.registerType(it, // - MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, // - MemberCategory.INVOKE_DECLARED_METHODS)); + AotUtils.registerTypesForReflection(packageName, reflection); } } diff --git a/src/main/java/org/springframework/hateoas/aot/HypermediaTypeAotProcessor.java b/src/main/java/org/springframework/hateoas/aot/HypermediaTypeAotProcessor.java index e572dfe32..7bba1514c 100644 --- a/src/main/java/org/springframework/hateoas/aot/HypermediaTypeAotProcessor.java +++ b/src/main/java/org/springframework/hateoas/aot/HypermediaTypeAotProcessor.java @@ -15,30 +15,18 @@ */ package org.springframework.hateoas.aot; -import java.io.IOException; import java.util.Arrays; -import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.function.Predicate; import java.util.stream.Stream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.aot.generate.GenerationContext; -import org.springframework.aot.hint.MemberCategory; -import org.springframework.aot.hint.TypeReference; 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.annotation.AnnotatedElementUtils; -import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.core.type.classreading.MetadataReader; -import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.core.type.filter.TypeFilter; -import org.springframework.hateoas.aot.AotUtils.FullTypeScanner; import org.springframework.hateoas.config.EnableHypermediaSupport; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; import org.springframework.util.Assert; @@ -80,8 +68,6 @@ public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registe static class MediaTypeReflectionAotContribution implements BeanRegistrationAotContribution { - private static final Logger LOGGER = LoggerFactory.getLogger(MediaTypeReflectionAotContribution.class); - private final List mediaTypePackage; private final Set packagesSeen; @@ -105,8 +91,6 @@ public MediaTypeReflectionAotContribution(List mediaTypePackage) { @Override public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { - var reflection = generationContext.getRuntimeHints().reflection(); - mediaTypePackage.forEach(it -> { if (packagesSeen.contains(it)) { @@ -115,97 +99,9 @@ public void applyTo(GenerationContext generationContext, BeanRegistrationCode be packagesSeen.add(it); - // Register RepresentationModel types for full reflection - FullTypeScanner provider = AotUtils.getScanner(it, // - new JacksonAnnotationPresentFilter(), // - new JacksonSuperTypeFilter()); - - LOGGER.info("Registering Spring HATEOAS types in {} for reflection.", it); - - provider.findClasses() - .sorted(Comparator.comparing(TypeReference::getName)) - .peek(type -> LOGGER.debug("> {}", type.getName())) - .forEach(reference -> reflection.registerType(reference, // - MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS)); + new HypermediaTypesRuntimeHints(it) // + .registerHints(generationContext.getRuntimeHints(), getClass().getClassLoader()); }); } } - - static abstract class TraversingTypeFilter implements TypeFilter { - - /* - * (non-Javadoc) - * @see org.springframework.core.type.filter.TypeFilter#match(org.springframework.core.type.classreading.MetadataReader, org.springframework.core.type.classreading.MetadataReaderFactory) - */ - @Override - public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) - throws IOException { - - if (doMatch(metadataReader, metadataReaderFactory)) { - return true; - } - - var classMetadata = metadataReader.getClassMetadata(); - - String superClassName = classMetadata.getSuperClassName(); - - if (superClassName != null && !superClassName.startsWith("java") - && match(metadataReaderFactory.getMetadataReader(superClassName), metadataReaderFactory)) { - return true; - } - - for (String names : classMetadata.getInterfaceNames()) { - - MetadataReader reader = metadataReaderFactory.getMetadataReader(names); - - if (match(reader, metadataReaderFactory)) { - return true; - } - } - - return false; - } - - protected abstract boolean doMatch(MetadataReader reader, MetadataReaderFactory factory); - } - - static class JacksonAnnotationPresentFilter extends TraversingTypeFilter { - - private static final Predicate IS_JACKSON_ANNOTATION = it -> it.startsWith("com.fasterxml.jackson"); - - /* - * (non-Javadoc) - * @see org.springframework.hateoas.aot.HateoasRuntimeHints.TraversingTypeFilter#doMatch(org.springframework.core.type.classreading.MetadataReader, org.springframework.core.type.classreading.MetadataReaderFactory) - */ - @Override - protected boolean doMatch(MetadataReader reader, MetadataReaderFactory factory) { - - var annotationMetadata = reader.getAnnotationMetadata(); - - // Type annotations - return annotationMetadata - .getAnnotationTypes() - .stream() - .anyMatch(IS_JACKSON_ANNOTATION) - - // Method annotations - || annotationMetadata.getDeclaredMethods().stream() - .flatMap(it -> it.getAnnotations().stream()) - .map(MergedAnnotation::getType) - .map(Class::getName) - .anyMatch(IS_JACKSON_ANNOTATION); - } - } - - static class JacksonSuperTypeFilter extends TraversingTypeFilter { - - /* - * (non-Javadoc) - * @see org.springframework.hateoas.aot.HateoasRuntimeHints.TraversingTypeFilter#doMatch(org.springframework.core.type.classreading.MetadataReader, org.springframework.core.type.classreading.MetadataReaderFactory) - */ - @Override - protected boolean doMatch(MetadataReader reader, MetadataReaderFactory factory) { - return reader.getClassMetadata().getClassName().startsWith("com.fasterxml.jackson"); - } - } } diff --git a/src/main/java/org/springframework/hateoas/aot/HypermediaTypesRuntimeHints.java b/src/main/java/org/springframework/hateoas/aot/HypermediaTypesRuntimeHints.java new file mode 100644 index 000000000..172447f4c --- /dev/null +++ b/src/main/java/org/springframework/hateoas/aot/HypermediaTypesRuntimeHints.java @@ -0,0 +1,120 @@ +/* + * Copyright 2023 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.io.IOException; +import java.util.function.Predicate; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AbstractTypeHierarchyTraversingFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.util.Assert; + +/** + * {@link RuntimeHintsRegistrar} to register Jackson model types for hypermedia types. + * + * @author Oliver Drotbohm + */ +class HypermediaTypesRuntimeHints implements RuntimeHintsRegistrar { + + private static final TypeFilter HAS_JACKSON_SUPER_TYPE_FILTER = new JacksonSuperTypeFilter(); + private static final TypeFilter IS_JACKSON_ANNOTATION_PRESENT_FILTER = new JacksonAnnotationPresentFilter(); + + private final String hypermediaPackage; + + /** + * Creates a new {@link HypermediaTypesRuntimeHints} for the given package. + * + * @param hypermediaPackage the package to scan for types. + */ + HypermediaTypesRuntimeHints(String hypermediaPackage) { + + Assert.hasText(hypermediaPackage, "Package must not be null or empty!"); + + this.hypermediaPackage = hypermediaPackage; + } + + /* + * (non-Javadoc) + * @see org.springframework.aot.hint.RuntimeHintsRegistrar#registerHints(org.springframework.aot.hint.RuntimeHints, java.lang.ClassLoader) + */ + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + + AotUtils.registerTypesForReflection(hypermediaPackage, hints.reflection(), + HAS_JACKSON_SUPER_TYPE_FILTER, IS_JACKSON_ANNOTATION_PRESENT_FILTER); + } + + static class JacksonAnnotationPresentFilter implements TypeFilter { + + private static final Predicate IS_JACKSON_ANNOTATION = it -> it.startsWith("com.fasterxml.jackson"); + + /* + * (non-Javadoc) + * @see org.springframework.core.type.filter.TypeFilter#match(org.springframework.core.type.classreading.MetadataReader, org.springframework.core.type.classreading.MetadataReaderFactory) + */ + @Override + public boolean match(MetadataReader reader, MetadataReaderFactory factory) + throws IOException { + + var annotationMetadata = reader.getAnnotationMetadata(); + + // Type annotations + return annotationMetadata + .getAnnotationTypes() + .stream() + .anyMatch(IS_JACKSON_ANNOTATION) + + // Method annotations + || annotationMetadata.getDeclaredMethods().stream() + .flatMap(it -> it.getAnnotations().stream()) + .map(MergedAnnotation::getType) + .map(Class::getName) + .anyMatch(IS_JACKSON_ANNOTATION); + } + } + + static class JacksonSuperTypeFilter extends AbstractTypeHierarchyTraversingFilter { + + private static final String JACKSON_PACKAGE = "com.fasterxml.jackson"; + + JacksonSuperTypeFilter() { + super(true, true); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.type.filter.AbstractTypeHierarchyTraversingFilter#matchSuperClass(java.lang.String) + */ + @Override + protected Boolean matchSuperClass(String superClassName) { + return superClassName.startsWith(JACKSON_PACKAGE); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.type.filter.AbstractTypeHierarchyTraversingFilter#matchInterface(java.lang.String) + */ + @Override + protected Boolean matchInterface(String interfaceName) { + return interfaceName.startsWith(JACKSON_PACKAGE); + } + } +} diff --git a/src/test/java/org/springframework/hateoas/aot/HypermediaTypesRuntimeHintsUnitTests.java b/src/test/java/org/springframework/hateoas/aot/HypermediaTypesRuntimeHintsUnitTests.java new file mode 100644 index 000000000..7b3023fbb --- /dev/null +++ b/src/test/java/org/springframework/hateoas/aot/HypermediaTypesRuntimeHintsUnitTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 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.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeHint; +import org.springframework.aot.hint.TypeReference; +import org.springframework.hateoas.mediatype.hal.LinkMixin; +import org.springframework.hateoas.mediatype.hal.RepresentationModelMixin; + +/** + * Unit tests for {@link HypermediaTypesRuntimeHints}. + * + * @author Oliver Drotbohm + */ +class HypermediaTypesRuntimeHintsUnitTests { + + @Test // GH-2024 + void registersMixinsForHal() { + + var registrar = new HypermediaTypesRuntimeHints(RepresentationModelMixin.class.getPackageName()); + var hints = new RuntimeHints(); + + registrar.registerHints(hints, getClass().getClassLoader()); + + assertThat(hints.reflection().typeHints()) + .extracting(TypeHint::getType) + .extracting(TypeReference::getSimpleName) + .contains(RepresentationModelMixin.class.getSimpleName()) + .contains(LinkMixin.class.getSimpleName()); + } +}