Skip to content

Commit 95ff169

Browse files
committed
#1982 - Rearrangement of AOT reflection configuration creation.
The reflection configuration of core Spring HATEOAS types is now done via HateoasRuntimeHints (previously RepresentationModelRuntimeHints). This allows the configuration to be contributed, even without @EnableHypermediaSupport in play, especially helpful in Web.fn scenarios.
1 parent a695354 commit 95ff169

File tree

6 files changed

+126
-82
lines changed

6 files changed

+126
-82
lines changed

src/main/java/org/springframework/hateoas/aot/AotUtils.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,30 @@
1515
*/
1616
package org.springframework.hateoas.aot;
1717

18+
import java.io.IOException;
19+
import java.util.Arrays;
1820
import java.util.HashSet;
1921
import java.util.List;
2022
import java.util.Optional;
2123
import java.util.Set;
24+
import java.util.stream.Stream;
2225

2326
import org.slf4j.Logger;
2427
import org.slf4j.LoggerFactory;
2528
import org.springframework.aot.hint.MemberCategory;
2629
import org.springframework.aot.hint.ReflectionHints;
30+
import org.springframework.aot.hint.TypeReference;
31+
import org.springframework.beans.factory.config.BeanDefinition;
32+
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
2733
import org.springframework.core.ResolvableType;
34+
import org.springframework.core.type.classreading.MetadataReader;
35+
import org.springframework.core.type.classreading.MetadataReaderFactory;
36+
import org.springframework.core.type.filter.AssignableTypeFilter;
37+
import org.springframework.core.type.filter.TypeFilter;
2838
import org.springframework.hateoas.CollectionModel;
2939
import org.springframework.hateoas.EntityModel;
3040
import org.springframework.http.HttpEntity;
41+
import org.springframework.util.ClassUtils;
3142

3243
/**
3344
* Some helper classes to register types for reflection.
@@ -115,4 +126,52 @@ private static Optional<Class<?>> extractGenerics(Class<?> modelType, Resolvable
115126
.flatMap(it -> extractGenerics(it, unresolved).stream())
116127
.findFirst();
117128
}
129+
130+
public static FullTypeScanner getScanner(String packageName, TypeFilter... includeFilters) {
131+
132+
var provider = new ClassPathScanningCandidateComponentProvider(false);
133+
134+
if (includeFilters.length == 0) {
135+
provider.addIncludeFilter(new AssignableTypeFilter(Object.class));
136+
} else {
137+
Arrays.stream(includeFilters).forEach(provider::addIncludeFilter);
138+
}
139+
140+
provider.addExcludeFilter(new EnforcedPackageFilter(packageName));
141+
142+
return () -> provider.findCandidateComponents(packageName).stream()
143+
.map(BeanDefinition::getBeanClassName)
144+
.map(TypeReference::of);
145+
}
146+
147+
/**
148+
* A {@link TypeFilter} to only match types <em>outside</em> the configured package. Usually used as exclude filter to
149+
* limit scans to not find nested packages.
150+
*
151+
* @author Oliver Drotbohm
152+
*/
153+
private static class EnforcedPackageFilter implements TypeFilter {
154+
155+
private final String referencePackage;
156+
157+
public EnforcedPackageFilter(String referencePackage) {
158+
this.referencePackage = referencePackage;
159+
}
160+
161+
/*
162+
* (non-Javadoc)
163+
* @see org.springframework.core.type.filter.TypeFilter#match(org.springframework.core.type.classreading.MetadataReader, org.springframework.core.type.classreading.MetadataReaderFactory)
164+
*/
165+
@Override
166+
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
167+
throws IOException {
168+
return !referencePackage
169+
.equals(ClassUtils.getPackageName(metadataReader.getClassMetadata().getClassName()));
170+
}
171+
}
172+
173+
static interface FullTypeScanner {
174+
175+
abstract Stream<TypeReference> findClasses();
176+
}
118177
}

src/main/java/org/springframework/hateoas/aot/RepresentationModelRuntimeHints.java renamed to src/main/java/org/springframework/hateoas/aot/HateoasTypesRuntimeHints.java

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,17 @@
1515
*/
1616
package org.springframework.hateoas.aot;
1717

18-
import java.util.Arrays;
19-
import java.util.List;
20-
import java.util.stream.Stream;
21-
2218
import org.springframework.aot.hint.MemberCategory;
2319
import org.springframework.aot.hint.RuntimeHints;
2420
import org.springframework.aot.hint.RuntimeHintsRegistrar;
25-
import org.springframework.hateoas.CollectionModel;
26-
import org.springframework.hateoas.EntityModel;
27-
import org.springframework.hateoas.PagedModel;
2821
import org.springframework.hateoas.RepresentationModel;
2922

3023
/**
3124
* Registers reflection metadata for {@link RepresentationModel} types.
3225
*
3326
* @author Oliver Drotbohm
3427
*/
35-
class RepresentationModelRuntimeHints implements RuntimeHintsRegistrar {
36-
37-
private static final List<Class<?>> REPRESENTATION_MODELS = List.of(RepresentationModel.class, //
38-
// EntityModel.class, // treated specially below
39-
CollectionModel.class, //
40-
PagedModel.class,
41-
PagedModel.PageMetadata.class);
28+
class HateoasTypesRuntimeHints implements RuntimeHintsRegistrar {
4229

4330
/*
4431
* (non-Javadoc)
@@ -48,11 +35,11 @@ class RepresentationModelRuntimeHints implements RuntimeHintsRegistrar {
4835
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
4936

5037
var reflection = hints.reflection();
51-
var entityModelAndNested = Arrays.stream(EntityModel.class.getNestMembers());
5238

53-
Stream.concat(REPRESENTATION_MODELS.stream(), entityModelAndNested).forEach(it -> { //
54-
reflection.registerType(it, //
55-
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS);
56-
});
39+
AotUtils.getScanner(RepresentationModel.class.getPackageName()) //
40+
.findClasses() //
41+
.forEach(it -> reflection.registerType(it, //
42+
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, //
43+
MemberCategory.INVOKE_DECLARED_METHODS));
5744
}
5845
}

src/main/java/org/springframework/hateoas/aot/HypermediaTypeAotProcessor.java

Lines changed: 9 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.io.IOException;
1919
import java.util.Arrays;
20+
import java.util.Comparator;
2021
import java.util.HashSet;
2122
import java.util.List;
2223
import java.util.Set;
@@ -28,22 +29,19 @@
2829
import org.springframework.aot.generate.GenerationContext;
2930
import org.springframework.aot.hint.MemberCategory;
3031
import org.springframework.aot.hint.TypeReference;
31-
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
3232
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
3333
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
3434
import org.springframework.beans.factory.aot.BeanRegistrationCode;
35-
import org.springframework.beans.factory.config.BeanDefinition;
3635
import org.springframework.beans.factory.support.RegisteredBean;
37-
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
3836
import org.springframework.core.annotation.AnnotatedElementUtils;
3937
import org.springframework.core.annotation.MergedAnnotation;
4038
import org.springframework.core.type.classreading.MetadataReader;
4139
import org.springframework.core.type.classreading.MetadataReaderFactory;
4240
import org.springframework.core.type.filter.TypeFilter;
41+
import org.springframework.hateoas.aot.AotUtils.FullTypeScanner;
4342
import org.springframework.hateoas.config.EnableHypermediaSupport;
4443
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
4544
import org.springframework.util.Assert;
46-
import org.springframework.util.ClassUtils;
4745

4846
/**
4947
* A {@link BeanRegistrationAotProcessor} to register types that will be rendered by Jackson for reflection. The
@@ -75,7 +73,7 @@ public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registe
7573
var mediaTypePackages = Stream.concat(fromConfig, Stream.of("alps", "problem"))
7674
.map("org.springframework.hateoas.mediatype."::concat);
7775

78-
var packagesToScan = Stream.concat(Stream.of("org.springframework.hateoas"), mediaTypePackages).toList();
76+
var packagesToScan = mediaTypePackages.toList();
7977

8078
return packagesToScan.isEmpty() ? null : new MediaTypeReflectionAotContribution(packagesToScan);
8179
}
@@ -118,68 +116,21 @@ public void applyTo(GenerationContext generationContext, BeanRegistrationCode be
118116
packagesSeen.add(it);
119117

120118
// Register RepresentationModel types for full reflection
121-
FullTypeScanner provider = new FullTypeScanner();
122-
provider.addIncludeFilter(new JacksonAnnotationPresentFilter());
123-
provider.addIncludeFilter(new JacksonSuperTypeFilter());
124-
125-
// Add filter to limit scan to sole package, not nested ones
126-
provider.addExcludeFilter(new EnforcedPackageFilter(it));
119+
FullTypeScanner provider = AotUtils.getScanner(it, //
120+
new JacksonAnnotationPresentFilter(), //
121+
new JacksonSuperTypeFilter());
127122

128123
LOGGER.info("Registering Spring HATEOAS types in {} for reflection.", it);
129124

130-
provider.findCandidateComponents(it).stream()
131-
.map(BeanDefinition::getBeanClassName)
132-
.sorted()
133-
.peek(type -> LOGGER.debug("> {}", type))
134-
.map(TypeReference::of)
125+
provider.findClasses()
126+
.sorted(Comparator.comparing(TypeReference::getName))
127+
.peek(type -> LOGGER.debug("> {}", type.getName()))
135128
.forEach(reference -> reflection.registerType(reference, //
136129
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS));
137130
});
138131
}
139132
}
140133

141-
static class FullTypeScanner extends ClassPathScanningCandidateComponentProvider {
142-
143-
public FullTypeScanner() {
144-
super(false);
145-
}
146-
147-
/*
148-
* (non-Javadoc)
149-
* @see org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#isCandidateComponent(org.springframework.beans.factory.annotation.AnnotatedBeanDefinition)
150-
*/
151-
@Override
152-
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
153-
return true;
154-
}
155-
}
156-
157-
/**
158-
* A {@link TypeFilter} to only match types <em>outside</em> the configured package. Usually used as exclude filter
159-
* to limit scans to not find nested packages.
160-
*
161-
* @author Oliver Drotbohm
162-
*/
163-
static class EnforcedPackageFilter implements TypeFilter {
164-
165-
private final String referencePackage;
166-
167-
public EnforcedPackageFilter(String referencePackage) {
168-
this.referencePackage = referencePackage;
169-
}
170-
171-
/*
172-
* (non-Javadoc)
173-
* @see org.springframework.core.type.filter.TypeFilter#match(org.springframework.core.type.classreading.MetadataReader, org.springframework.core.type.classreading.MetadataReaderFactory)
174-
*/
175-
@Override
176-
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
177-
throws IOException {
178-
return !referencePackage
179-
.equals(ClassUtils.getPackageName(metadataReader.getClassMetadata().getClassName()));
180-
}
181-
}
182-
183134
static abstract class TraversingTypeFilter implements TypeFilter {
184135

185136
/*

src/main/resources/META-INF/spring/aot.factories

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
44
org.springframework.hateoas.aot.RepresentationModelAssemblerAotProcessor
55

66
org.springframework.aot.hint.RuntimeHintsRegistrar=\
7-
org.springframework.hateoas.aot.RepresentationModelRuntimeHints
7+
org.springframework.hateoas.aot.HateoasTypesRuntimeHints
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.aot;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import org.junit.jupiter.api.Test;
21+
import org.springframework.aot.hint.TypeReference;
22+
import org.springframework.hateoas.Link;
23+
import org.springframework.hateoas.RepresentationModel;
24+
25+
/**
26+
* Unit tests for {@link AotUtils}.
27+
*
28+
* @author Oliver Drotbohm
29+
*/
30+
class AotUtilsUnitTests {
31+
32+
@Test // GH-1981
33+
void findsTypesInPackage() {
34+
35+
var scanner = AotUtils.getScanner(Link.class.getPackageName());
36+
37+
assertThat(scanner.findClasses())
38+
.extracting(TypeReference::getName)
39+
.contains(Link.class.getName(), //
40+
RepresentationModel.class.getName(),
41+
"org.springframework.hateoas.EntityModel$MapSuppressingUnwrappingSerializer");
42+
}
43+
}

src/test/java/org/springframework/hateoas/aot/RepresentationModelRuntimeHintsUnitTests.java renamed to src/test/java/org/springframework/hateoas/aot/HateoasTypesRuntimeHintsUnitTests.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,29 @@
2121
import org.springframework.aot.hint.RuntimeHints;
2222
import org.springframework.aot.hint.TypeHint;
2323
import org.springframework.aot.hint.TypeReference;
24+
import org.springframework.hateoas.Link;
25+
import org.springframework.hateoas.Links;
2426

2527
/**
2628
* Unit tests for {@link RepresentationModelRuntimeHints}.
2729
*
2830
* @author Oliver Drotbohm
2931
*/
30-
class RepresentationModelRuntimeHintsUnitTests {
32+
class HateoasTypesRuntimeHintsUnitTests {
3133

3234
@Test // GH-1981
33-
void registersHintsForMapSuppressingUnwrappingSerializer() {
35+
void registersHintsForHateoasTypes() {
3436

35-
var registrar = new RepresentationModelRuntimeHints();
37+
var registrar = new HateoasTypesRuntimeHints();
3638
var hints = new RuntimeHints();
3739

3840
registrar.registerHints(hints, getClass().getClassLoader());
3941

4042
assertThat(hints.reflection().typeHints())
4143
.extracting(TypeHint::getType)
4244
.extracting(TypeReference::getSimpleName)
43-
.contains("MapSuppressingUnwrappingSerializer");
45+
.contains("MapSuppressingUnwrappingSerializer", //
46+
Link.class.getSimpleName(), //
47+
Links.class.getSimpleName());
4448
}
4549
}

0 commit comments

Comments
 (0)