#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:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user