#2024 - Fix regression in AOT reflection metadata generation.

Fixed the detection of abstract classes in AOT metadata generation so that Jackson mixin types get detected again.
This commit is contained in:
Oliver Drotbohm
2023-07-19 21:37:18 +02:00
parent e0f73c5536
commit b4908cd28d
5 changed files with 229 additions and 120 deletions

View File

@@ -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.slf4j.LoggerFactory;
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 @@ class AotUtils {
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 @@ class AotUtils {
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<TypeFilter>();
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 @@ class AotUtils {
*
* @author Oliver Drotbohm
*/
private static class EnforcedPackageFilter implements TypeFilter {
static class EnforcedPackageFilter implements TypeFilter {
private final String referencePackage;
@@ -165,11 +195,30 @@ class AotUtils {
@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<TypeFilter> 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<TypeReference> findClasses();

View File

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

View File

@@ -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 @@ class HypermediaTypeAotProcessor implements BeanRegistrationAotProcessor {
static class MediaTypeReflectionAotContribution implements BeanRegistrationAotContribution {
private static final Logger LOGGER = LoggerFactory.getLogger(MediaTypeReflectionAotContribution.class);
private final List<String> mediaTypePackage;
private final Set<String> packagesSeen;
@@ -105,8 +91,6 @@ class HypermediaTypeAotProcessor implements BeanRegistrationAotProcessor {
@Override
public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) {
var reflection = generationContext.getRuntimeHints().reflection();
mediaTypePackage.forEach(it -> {
if (packagesSeen.contains(it)) {
@@ -115,97 +99,9 @@ class HypermediaTypeAotProcessor implements BeanRegistrationAotProcessor {
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<String> 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");
}
}
}

View File

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

View File

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