Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import org.springframework.beans.factory.support.RegisteredBean;
Expand All @@ -36,6 +38,7 @@
* @author Christoph Strobl
* @author John Blum
* @author Mark Paluch
* @author Blaz Snuderl
* @see AotRepositoryContext
* @since 3.0
*/
Expand All @@ -50,6 +53,7 @@ class DefaultAotRepositoryContext extends AotRepositoryContextSupport {

private Collection<Class<? extends Annotation>> identifyingAnnotations = Collections.emptySet();
private String beanName;
private Map<IdentifyingTypesKey, Set<Class<?>>> identifyingTypesCache = new ConcurrentHashMap<>();

public DefaultAotRepositoryContext(RegisteredBean bean, RepositoryInformation repositoryInformation,
String moduleName, AotContext aotContext, RepositoryConfigurationSource configurationSource) {
Expand Down Expand Up @@ -91,6 +95,15 @@ public void setIdentifyingAnnotations(Collection<Class<? extends Annotation>> id
this.identifyingAnnotations = identifyingAnnotations;
}

/**
* Set the cache to reuse types discovered through scanning base packages for
* {@link #getIdentifyingAnnotations() identifying annotations}. Repository contexts sharing a cache scan and inspect a
* set of base packages once instead of once per repository.
*/
public void setIdentifyingTypesCache(Map<IdentifyingTypesKey, Set<Class<?>>> identifyingTypesCache) {
this.identifyingTypesCache = identifyingTypesCache;
}

@Override
public RepositoryInformation getRepositoryInformation() {
return repositoryInformation;
Expand Down Expand Up @@ -132,13 +145,27 @@ protected Set<Class<?>> discoverTypes() {

if (!getIdentifyingAnnotations().isEmpty()) {

Set<Class<?>> classes = aotContext.getTypeScanner()
.scanPackages(getConfigurationSource().getBasePackages().toSet())
.forTypesAnnotatedWith(getIdentifyingAnnotations()).collectAsSet();
types.addAll(TypeCollector.inspect(classes).list());
Set<String> basePackages = getConfigurationSource().getBasePackages().toSet();
IdentifyingTypesKey cacheKey = new IdentifyingTypesKey(basePackages, Set.copyOf(getIdentifyingAnnotations()));

types.addAll(identifyingTypesCache.computeIfAbsent(cacheKey, this::discoverIdentifyingTypes));
}

return types;
}

private Set<Class<?>> discoverIdentifyingTypes(IdentifyingTypesKey key) {

Set<Class<?>> classes = aotContext.getTypeScanner().scanPackages(key.basePackages())
.forTypesAnnotatedWith(key.identifyingAnnotations()).collectAsSet();

return new LinkedHashSet<>(TypeCollector.inspect(classes).list());
}

/**
* Key to cache types discovered by scanning base packages for identifying annotations.
*/
record IdentifyingTypesKey(Set<String> basePackages, Set<Class<? extends Annotation>> identifyingAnnotations) {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.stream.Stream;

Expand Down Expand Up @@ -78,6 +79,7 @@
* @author Christoph Strobl
* @author John Blum
* @author Mark Paluch
* @author Blaz Snuderl
* @since 3.0
*/
public class RepositoryRegistrationAotProcessor
Expand All @@ -103,6 +105,8 @@ public class RepositoryRegistrationAotProcessor

private Map<String, RepositoryConfiguration<?>> configMap = Collections.emptyMap();

private final Map<DefaultAotRepositoryContext.IdentifyingTypesKey, Set<Class<?>>> identifyingTypesCache = new ConcurrentHashMap<>();

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {

Expand Down Expand Up @@ -261,8 +265,9 @@ private void configureDomainTypeContributions(AotRepositoryContext repositoryCon
.filter(it -> TypeContributor.isPartOf(it, Set.of(information.getDomainType().getPackageName())))
.forEach(it -> configureTypeContribution(it, repositoryContext));

repositoryContext.getResolvedTypes().stream().filter(it -> !isJavaOrPrimitiveType(it))
.forEach(it -> contributeType(it, generationContext));
TypeContributor.contribute(
repositoryContext.getResolvedTypes().stream().filter(it -> !isJavaOrPrimitiveType(it)).toList(), it -> true,
generationContext);
}

/**
Expand Down Expand Up @@ -365,6 +370,7 @@ private static void registerReflectiveHints(Class<?> typeToRegister, GenerationC
extension.getModuleName(), aotContext, configuration.getConfigurationSource());

repositoryContext.setIdentifyingAnnotations(extension.getIdentifyingAnnotations());
repositoryContext.setIdentifyingTypesCache(identifyingTypesCache);

return repositoryContext;
}
Expand Down
10 changes: 7 additions & 3 deletions src/main/java/org/springframework/data/util/TypeCollector.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
* @author Sebastien Deleuze
* @author John Blum
* @author Mark Paluch
* @author Blaz Snuderl
* @since 3.0
*/
public class TypeCollector {
Expand Down Expand Up @@ -197,8 +198,8 @@ public Predicate<Class<?>> getTypeFilter() {
return this.typeFilter;
}

private void process(Class<?> root, Consumer<ResolvableType> consumer) {
processType(ResolvableType.forType(root), new InspectionCache(), consumer);
private void process(Class<?> root, InspectionCache cache, Consumer<ResolvableType> consumer) {
processType(ResolvableType.forType(root), cache, consumer);
}

private void processType(ResolvableType type, InspectionCache cache, Consumer<ResolvableType> callback) {
Expand Down Expand Up @@ -318,7 +319,10 @@ public static class ReachableTypes {
* @param action The action to be performed for each element
*/
public void forEach(Consumer<ResolvableType> action) {
roots.forEach(it -> typeCollector.process(it, action));

InspectionCache cache = new InspectionCache();

roots.forEach(it -> typeCollector.process(it, cache, action));
}

/**
Expand Down
78 changes: 78 additions & 0 deletions src/main/java/org/springframework/data/util/TypeContributor.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,26 @@
package org.springframework.data.util;

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;

import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.util.ConcurrentReferenceHashMap;

/**
* @author Christoph Strobl
* @author Blaz Snuderl
* @since 3.0
*/
public class TypeContributor {
Expand All @@ -35,6 +44,14 @@ public class TypeContributor {
public static final BindingReflectionHintsRegistrar DATA_BINDING_REGISTRAR = new BindingReflectionHintsRegistrar();
public static final ReflectiveRuntimeHintsRegistrar REFLECTIVE_REGISTRAR = new ReflectiveRuntimeHintsRegistrar();

/**
* Types already contributed to a {@link RuntimeHints} instance. Contributing a (non-annotation) type twice registers
* the very same hints again while re-walking its entire type graph, which is why contributions are tracked per
* {@link RuntimeHints}. Keys are held weakly to not retain hints beyond AOT processing.
*/
private static final Map<RuntimeHints, Set<Class<?>>> contributedTypes = new ConcurrentReferenceHashMap<>(8,
ConcurrentReferenceHashMap.ReferenceType.WEAK);

/**
* Contribute the type with default reflection configuration, skip annotations.
*
Expand Down Expand Up @@ -68,10 +85,71 @@ public static void contribute(Class<?> type, Predicate<Class<? extends Annotatio
return;
}

if (!isNewContribution(type, contribution)) {
return;
}

DATA_BINDING_REGISTRAR.registerReflectionHints(contribution.getRuntimeHints().reflection(), type);
REFLECTIVE_REGISTRAR.registerRuntimeHints(contribution.getRuntimeHints(), type);
}

/**
* Contribute the given types with default reflection configuration and only include matching annotations.
* <p>
* Data binding hints are registered for all given types in a single pass so that types reachable from more than one of
* the given types are visited once instead of once per given type.
*
* @param types the types to contribute.
* @param filter filter to include annotation types.
* @param contribution the generation context to contribute to.
* @since 4.2
*/
@SuppressWarnings("unchecked")
public static void contribute(Collection<Class<?>> types, Predicate<Class<? extends Annotation>> filter,
GenerationContext contribution) {

List<Class<?>> dataBindingTypes = new ArrayList<>(types.size());

for (Class<?> type : types) {

if (type.isPrimitive()) {
continue;
}

if (type.isAnnotation() && filter.test((Class<? extends Annotation>) type)) {

contribution.getRuntimeHints().reflection().registerType(type, hint -> {});
continue;
}

if (!isNewContribution(type, contribution)) {
continue;
}

dataBindingTypes.add(type);
}

if (dataBindingTypes.isEmpty()) {
return;
}

DATA_BINDING_REGISTRAR.registerReflectionHints(contribution.getRuntimeHints().reflection(),
dataBindingTypes.toArray(Type[]::new));

for (Class<?> type : dataBindingTypes) {
REFLECTIVE_REGISTRAR.registerRuntimeHints(contribution.getRuntimeHints(), type);
}
}

/**
* Register the type as contributed to the given {@link GenerationContext} returning whether the type was contributed
* for the first time.
*/
private static boolean isNewContribution(Class<?> type, GenerationContext contribution) {
return contributedTypes.computeIfAbsent(contribution.getRuntimeHints(), it -> ConcurrentHashMap.newKeySet())
.add(type);
}

/**
* Contribute the type with default reflection configuration and only include annotations from a certain namespace and
* those meta annotated with one of them.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
*
* @author Christoph Strobl
* @author Mark Paluch
* @author Blaz Snuderl
*/
public class TypeCollectorUnitTests {

Expand Down Expand Up @@ -67,6 +68,13 @@ void includesDeclaredClassesInInspection() {
WithDeclaredClass.SomeEnum.class);
}

@Test
void inspectsTypesReachableFromMultipleRootsOnce() {

assertThat(TypeCollector.inspect(CyclicPropertiesA.class, CyclicPropertiesB.class).list())
.containsExactlyInAnyOrder(CyclicPropertiesA.class, CyclicPropertiesB.class);
}

@Test // GH-2744
void skipsCoreFrameworkType() {
assertThat(TypeCollector.inspect(org.springframework.core.AliasRegistry.class).list()).isEmpty();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2025-present 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.data.util;

import static org.assertj.core.api.Assertions.*;

import java.util.List;

import org.junit.jupiter.api.Test;

import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.TypeReference;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.data.aot.types.CyclicPropertiesA;
import org.springframework.data.aot.types.CyclicPropertiesB;

/**
* Unit tests for {@link TypeContributor}.
*
* @author Blaz Snuderl
*/
class TypeContributorUnitTests {

@Test
void contributesTypesOnlyOncePerGenerationContext() {

GenerationContext generationContext = new TestGenerationContext();

TypeContributor.contribute(CyclicPropertiesA.class, it -> true, generationContext);

List<TypeReference> afterFirstContribution = registeredTypes(generationContext);

TypeContributor.contribute(CyclicPropertiesA.class, it -> true, generationContext);

assertThat(registeredTypes(generationContext)).containsExactlyElementsOf(afterFirstContribution);
}

@Test
void contributingCollectionRegistersSameHintsAsIndividualContributions() {

GenerationContext individually = new TestGenerationContext();

TypeContributor.contribute(CyclicPropertiesA.class, it -> true, individually);
TypeContributor.contribute(CyclicPropertiesB.class, it -> true, individually);

GenerationContext batched = new TestGenerationContext();

TypeContributor.contribute(List.of(CyclicPropertiesA.class, CyclicPropertiesB.class), it -> true, batched);

assertThat(registeredTypes(batched)).containsExactlyInAnyOrderElementsOf(registeredTypes(individually));
}

private static List<TypeReference> registeredTypes(GenerationContext generationContext) {
return generationContext.getRuntimeHints().reflection().typeHints().map(it -> it.getType()).sorted().toList();
}
}
Loading