From 2299e90de311f46d9b2ed2c9d3b2d3a70b396ae8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 22 Sep 2025 12:18:59 +0200 Subject: [PATCH 1/5] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 13143c9f6f..670806f619 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 4.0.0-SNAPSHOT + 4.0.0-GH-3267-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. From c5f41209979ea1a42fcb57fcdfc93750d22b4a16 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 22 Sep 2025 12:39:03 +0200 Subject: [PATCH 2/5] Refine AOT Repositories infrastructure. Decouple RepositoryRegistrationAotContribution and RepositoryRegistrationAotProcessor, introduce support class for AotRepositoryContext implementations to better protect AotRepositoryContext implementations from changes to AotContext. Remove contribute method from AotTypeConfiguration and move contribution code to AotContext. Deprecate introspection methods for removal with only a single use in Commons and no usage in other modules. Add more fine-grained customization hooks to RepositoryRegistrationAotProcessor and remove superfluous context objects that aren't necessary in their context. --- .../springframework/data/aot/AotContext.java | 49 ++- .../data/aot/AotTypeConfiguration.java | 11 - .../data/aot/DefaultAotContext.java | 15 +- ...agedTypesBeanRegistrationAotProcessor.java | 5 +- ...toryBeanDefinitionPropertiesDecorator.java | 7 +- .../aot/generate/AotRepositoryCreator.java | 18 +- .../AotRepositoryFragmentMetadata.java | 9 + .../aot/generate/RepositoryContributor.java | 29 +- .../config/AotRepositoryContext.java | 11 +- .../config/AotRepositoryContextSupport.java | 99 ++++++ .../config/DefaultAotRepositoryContext.java | 64 +--- ...RepositoryRegistrationAotContribution.java | 287 +--------------- .../RepositoryRegistrationAotProcessor.java | 324 +++++++++++++----- .../data/aot/AotContextUnitTests.java | 10 +- ...efinitionPropertiesDecoratorUnitTests.java | 3 +- .../AotRepositoryConfigurationUnitTests.java | 5 +- .../DummyModuleAotRepositoryContext.java | 47 +-- 17 files changed, 448 insertions(+), 545 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/config/AotRepositoryContextSupport.java diff --git a/src/main/java/org/springframework/data/aot/AotContext.java b/src/main/java/org/springframework/data/aot/AotContext.java index 4c2247a013..29b071a423 100644 --- a/src/main/java/org/springframework/data/aot/AotContext.java +++ b/src/main/java/org/springframework/data/aot/AotContext.java @@ -24,6 +24,8 @@ import java.util.function.Consumer; import org.jspecify.annotations.Nullable; + +import org.springframework.aot.generate.GenerationContext; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.BeanDefinition; @@ -40,9 +42,7 @@ /** * The context in which the AOT processing happens. Grants access to the {@link ConfigurableListableBeanFactory - * beanFactory} and {@link ClassLoader}. Holds a few convenience methods to check if a type - * {@link TypeIntrospector#isTypePresent() is present} and allows resolution of them through {@link TypeIntrospector} - * and {@link IntrospectedBeanDefinition}. + * beanFactory} and {@link ClassLoader}. *

* Mainly for internal use within the framework. * @@ -99,7 +99,7 @@ static AotContext from(BeanFactory beanFactory, Environment environment) { * @param moduleName name of the module. Can be {@literal null} or {@literal empty}, in which case it will only check * the general {@link #GENERATED_REPOSITORIES_ENABLED} flag. * @return indicator if repository code generation is enabled. - * @since 5.0 + * @since 4.0 */ default boolean isGeneratedRepositoriesEnabled(@Nullable String moduleName) { @@ -119,13 +119,13 @@ default boolean isGeneratedRepositoriesEnabled(@Nullable String moduleName) { } /** - * Checks if repository metadata file writing is enabled by checking environment variables for general - * enablement ({@link #GENERATED_REPOSITORIES_JSON_ENABLED}) + * Checks if repository metadata file writing is enabled by checking environment variables for general enablement + * ({@link #GENERATED_REPOSITORIES_JSON_ENABLED}) *

* Unset properties are considered being {@literal true}. * * @return indicator if repository metadata should be written - * @since 5.0 + * @since 4.0 */ default boolean isGeneratedRepositoriesMetadataEnabled() { return getEnvironment().getProperty(GENERATED_REPOSITORIES_JSON_ENABLED, Boolean.class, true); @@ -177,8 +177,12 @@ default ClassLoader getRequiredClassLoader() { * * @param typeName {@link String name} of the {@link Class type} to evaluate; must not be {@literal null}. * @return the type introspector for further type-based introspection. + * @deprecated since 4.0 as this isn't widely used and can be easily implemented within user code. */ - TypeIntrospector introspectType(String typeName); + @Deprecated(since = "4.0", forRemoval = true) + default TypeIntrospector introspectType(String typeName) { + throw new UnsupportedOperationException(); // preparation for implementation removal. + } /** * Returns a new {@link TypeScanner} used to scan for {@link Class types} that will be contributed to the AOT @@ -201,7 +205,9 @@ default TypeScanner getTypeScanner() { * @param packageNames {@link Collection} of {@link String package names} to scan. * @return a {@link Set} of {@link Class types} found during the scan. * @see #getTypeScanner() + * @deprecated since 4.0, use {@link #getTypeScanner()} directly */ + @Deprecated(since = "4.0", forRemoval = true) default Set> scanPackageForTypes(Collection> identifyingAnnotations, Collection packageNames) { @@ -214,7 +220,9 @@ default Set> scanPackageForTypes(Collection * * @param reference {@link BeanReference} to the managed bean. * @return the introspected bean definition. + * @deprecated since 4.0, use {@link #getBeanFactory()} and interact with the bean factory directly. */ + @Deprecated(since = "4.0", forRemoval = true) default IntrospectedBeanDefinition introspectBeanDefinition(BeanReference reference) { return introspectBeanDefinition(reference.getBeanName()); } @@ -225,15 +233,20 @@ default IntrospectedBeanDefinition introspectBeanDefinition(BeanReference refere * * @param beanName {@link String} containing the {@literal name} of the bean to evaluate; must not be {@literal null}. * @return the introspected bean definition. + * @deprecated since 4.0, use {@link #getBeanFactory()} and interact with the bean factory directly. */ - IntrospectedBeanDefinition introspectBeanDefinition(String beanName); + @Deprecated(since = "4.0", forRemoval = true) + default IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { + throw new UnsupportedOperationException(); // preparation for implementation removal. + } /** * Obtain a {@link AotTypeConfiguration} for the given {@link ResolvableType} to customize the AOT processing for the - * given type. + * given type. Repeated calls to the same type will result in merging the configuration. * * @param resolvableType the resolvable type to configure. * @param configurationConsumer configuration consumer function. + * @since 4.0 */ default void typeConfiguration(ResolvableType resolvableType, Consumer configurationConsumer) { typeConfiguration(resolvableType.toClass(), configurationConsumer); @@ -241,24 +254,29 @@ default void typeConfiguration(ResolvableType resolvableType, Consumer type, Consumer configurationConsumer); /** - * Return all type configurations registered with this {@link AotContext}. + * Contribute type configurations to the given {@link GenerationContext}. This method is called once per + * {@link GenerationContext} after all type configurations have been registered. * - * @return all type configurations registered with this {@link AotContext}. + * @param generationContext the context to contribute the type configurations to. */ - Collection typeConfigurations(); + void contributeTypeConfigurations(GenerationContext generationContext); /** * Type-based introspector to resolve {@link Class} from a type name and to introspect the bean factory for presence * of beans. + * + * @deprecated since 4.0 as this isn't widely used and can be easily implemented within user code. */ + @Deprecated(since = "4.0", forRemoval = true) interface TypeIntrospector { /** @@ -318,7 +336,10 @@ default void ifTypePresent(Consumer> action) { /** * Interface defining introspection methods for bean definitions. + * + * @deprecated since 4.0 as this isn't widely used and can be easily implemented within user code. */ + @Deprecated(since = "4.0", forRemoval = true) interface IntrospectedBeanDefinition { /** diff --git a/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java b/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java index 2adebba35c..6879011e2c 100644 --- a/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java +++ b/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java @@ -22,19 +22,15 @@ import org.springframework.aop.SpringProxy; import org.springframework.aop.framework.Advised; -import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.TypeReference; import org.springframework.core.DecoratingProxy; -import org.springframework.core.env.Environment; import org.springframework.data.projection.TargetAware; /** * Configuration object that captures various AOT configuration aspects of types within the data context by offering * predefined methods to register native configuration necessary for data binding, projection proxy definitions, AOT * cglib bytecode generation and other common tasks. - *

- * On {@link #contribute(Environment, GenerationContext)} the configuration is added to the {@link GenerationContext}. * * @author Christoph Strobl * @since 4.0 @@ -134,11 +130,4 @@ default AotTypeConfiguration proxyInterface(Class... proxyInterfaces) { */ AotTypeConfiguration forQuerydsl(); - /** - * Write the configuration to the given {@link GenerationContext}. - * - * @param environment must not be {@literal null}. - * @param generationContext must not be {@literal null}. - */ - void contribute(Environment environment, GenerationContext generationContext); } diff --git a/src/main/java/org/springframework/data/aot/DefaultAotContext.java b/src/main/java/org/springframework/data/aot/DefaultAotContext.java index 1608cf3039..e155c27bbb 100644 --- a/src/main/java/org/springframework/data/aot/DefaultAotContext.java +++ b/src/main/java/org/springframework/data/aot/DefaultAotContext.java @@ -17,7 +17,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -30,6 +29,7 @@ import java.util.stream.Stream; import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.TypeReference; @@ -55,13 +55,13 @@ * @author Christoph Strobl * @since 3.0 */ +@SuppressWarnings("removal") class DefaultAotContext implements AotContext { private final AotMappingContext mappingContext; private final ConfigurableListableBeanFactory factory; - // TODO: should we reuse the config or potentially have multiple ones with different settings for the same type - private final Map, AotTypeConfiguration> typeConfigurations = new HashMap<>(); + private final Map, ContextualTypeConfiguration> typeConfigurations = new HashMap<>(); private final Environment environment; public DefaultAotContext(BeanFactory beanFactory, Environment environment) { @@ -101,10 +101,13 @@ public void typeConfiguration(Class type, Consumer conf } @Override - public Collection typeConfigurations() { - return typeConfigurations.values(); + public void contributeTypeConfigurations(GenerationContext generationContext) { + typeConfigurations.forEach((type, configuration) -> { + configuration.contribute(this.environment, generationContext); + }); } + @SuppressWarnings("removal") class DefaultTypeIntrospector implements TypeIntrospector { private final String typeName; @@ -144,6 +147,7 @@ public List getBeanNames() { } } + @SuppressWarnings("removal") class DefaultIntrospectedBeanDefinition implements IntrospectedBeanDefinition { private final String beanName; @@ -227,7 +231,6 @@ public AotTypeConfiguration forQuerydsl() { return this; } - @Override public void contribute(Environment environment, GenerationContext generationContext) { try { diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java index f3a99e17a7..a6c8365613 100644 --- a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java @@ -155,10 +155,7 @@ protected void contributeType(ResolvableType type, GenerationContext generationC Set annotationNamespaces = Collections.singleton(TypeContributor.DATA_NAMESPACE); configureTypeContribution(type.toClass(), aotContext); - - aotContext.typeConfiguration(type, config -> { - config.contribute(environment.get(), generationContext); - }); + aotContext.contributeTypeConfigurations(generationContext); TypeUtils.resolveUsedAnnotations(type.toClass()).forEach( annotation -> TypeContributor.contribute(annotation.getType(), annotationNamespaces, generationContext)); diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBeanDefinitionPropertiesDecorator.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBeanDefinitionPropertiesDecorator.java index 367162f879..f8abd36650 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBeanDefinitionPropertiesDecorator.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBeanDefinitionPropertiesDecorator.java @@ -41,8 +41,8 @@ /** * Delegate to decorate AOT {@code BeanDefinition} properties during AOT processing. Adds a {@link CodeBlock} for the - * fragment function that resolves {@link RepositoryContributor#requiredArgs()} from the {@link BeanFactory} and - * provides them to the generated repository fragment. + * fragment function that resolves {@link RepositoryContributor#getAotFragmentMetadata()} from the {@link BeanFactory} + * and provides them to the generated repository fragment. * * @author Mark Paluch * @author Christoph Strobl @@ -128,7 +128,8 @@ private CodeBlock buildCallbackBody() { CodeBlock.Builder callback = CodeBlock.builder(); List arguments = new ArrayList<>(); - for (Entry entry : repositoryContributor.getConstructorArguments().entrySet()) { + for (Entry entry : repositoryContributor.getAotFragmentMetadata() + .getConstructorArguments().entrySet()) { ConstructorArgument argument = entry.getValue(); AotRepositoryConstructorBuilder.ParameterOrigin parameterOrigin = argument.parameterOrigin(); diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java index f8c0787c97..4534592ba8 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java @@ -19,9 +19,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.function.Consumer; import javax.lang.model.element.Modifier; @@ -30,9 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; -import org.springframework.core.ResolvableType; import org.springframework.data.projection.ProjectionFactory; -import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata.ConstructorArgument; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryFragment; @@ -100,18 +96,8 @@ String packageName() { return repositoryInformation.getRepositoryInterface().getPackageName(); } - Map getAutowireFields() { - - Map autowireFields = new LinkedHashMap<>( - generationMetadata.getConstructorArguments().size()); - for (Map.Entry entry : generationMetadata.getConstructorArguments().entrySet()) { - autowireFields.put(entry.getKey(), entry.getValue().parameterType()); - } - return autowireFields; - } - - Map getConstructorArguments() { - return generationMetadata.getConstructorArguments(); + AotRepositoryFragmentMetadata getRepositoryMetadata() { + return generationMetadata; } RepositoryInformation getRepositoryInformation() { diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryFragmentMetadata.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryFragmentMetadata.java index 9f6c465f65..fe1ca30080 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryFragmentMetadata.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryFragmentMetadata.java @@ -129,6 +129,15 @@ public Map getConstructorArguments() { return constructorArguments; } + Map getAutowireFields() { + + Map autowireFields = new LinkedHashMap<>(getConstructorArguments().size()); + for (Map.Entry entry : getConstructorArguments().entrySet()) { + autowireFields.put(entry.getKey(), entry.getValue().parameterType()); + } + return autowireFields; + } + public Map getMethods() { return methods; } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java index 725e4ac460..7823ba9806 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java @@ -18,19 +18,18 @@ import java.io.ByteArrayInputStream; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; -import java.util.Collections; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GeneratedClass; import org.springframework.aot.generate.GeneratedFiles.Kind; import org.springframework.aot.generate.GeneratedTypeReference; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.TypeReference; -import org.springframework.core.ResolvableType; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.aot.generate.AotRepositoryCreator.AotBundle; @@ -66,7 +65,7 @@ public class RepositoryContributor { public RepositoryContributor(AotRepositoryContext repositoryContext) { this.repositoryContext = repositoryContext; - creator = AotRepositoryCreator.forRepository(repositoryContext.getRepositoryInformation(), + this.creator = AotRepositoryCreator.forRepository(repositoryContext.getRepositoryInformation(), repositoryContext.getModuleName(), createProjectionFactory()); } @@ -102,28 +101,10 @@ TypeReference getContributedTypeName() { } /** - * Get the required constructor arguments for the to be generated repository implementation. Types will be obtained by - * type from {@link org.springframework.beans.factory.BeanFactory} upon initialization of the generated fragment - * during application startup. - *

- * Can be overridden if required. Needs to match arguments of generated repository implementation. - * - * @return key/value pairs of required argument required to instantiate the generated fragment. - */ - // TODO: should we switch from ResolvableType to some custom value object to cover qualifiers? - java.util.Map requiredArgs() { - return Collections.unmodifiableMap(creator.getAutowireFields()); - } - - /** - * Get the required constructor arguments for the to be generated repository implementation. - *

- * Can be overridden if required. Needs to match arguments of generated repository implementation. - * - * @return key/value pairs of required argument required to instantiate the generated fragment. + * @return the associated {@link AotRepositoryFragmentMetadata}. */ - java.util.Map getConstructorArguments() { - return Collections.unmodifiableMap(creator.getConstructorArguments()); + AotRepositoryFragmentMetadata getAotFragmentMetadata() { + return creator.getRepositoryMetadata(); } /** diff --git a/src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java b/src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java index c0eac7cf29..f4e96b25fc 100644 --- a/src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java +++ b/src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java @@ -36,8 +36,12 @@ public interface AotRepositoryContext extends AotContext { /** * @return the {@link String bean name} of the repository / factory bean. + * @deprecated since 4.0, this doesn't really belong in here. */ - String getBeanName(); + @Deprecated(since = "4.0", forRemoval = true) + default String getBeanName() { + throw new UnsupportedOperationException(); // prepare for removal + } /** * @return the Spring Data module name, see {@link RepositoryConfigurationExtension#getModuleName()}. @@ -52,7 +56,10 @@ public interface AotRepositoryContext extends AotContext { /** * @return a {@link Set} of {@link String base packages} to search for repositories. + * @deprecated since 4.0, use {@link #getConfigurationSource()} and call + * {@link RepositoryConfigurationSource#getBasePackages()} */ + @Deprecated(since = "4.0", forRemoval = true) default Set getBasePackages() { return getConfigurationSource().getBasePackages().toSet(); } @@ -79,6 +86,4 @@ default Set getBasePackages() { */ Set> getResolvedTypes(); - Set> getUserDomainTypes(); - } diff --git a/src/main/java/org/springframework/data/repository/config/AotRepositoryContextSupport.java b/src/main/java/org/springframework/data/repository/config/AotRepositoryContextSupport.java new file mode 100644 index 0000000000..9bf7eb16a0 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/config/AotRepositoryContextSupport.java @@ -0,0 +1,99 @@ +/* + * Copyright 2025 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.repository.config; + +import java.util.function.Consumer; + +import org.jspecify.annotations.Nullable; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.ResolvableType; +import org.springframework.core.env.Environment; +import org.springframework.data.aot.AotContext; +import org.springframework.data.aot.AotTypeConfiguration; +import org.springframework.data.util.TypeScanner; + +/** + * Support class for {@link AotRepositoryContext} implementations delegating to an underlying {@link AotContext}. + * + * @author Mark Paluch + * @since 4.0 + */ +public abstract class AotRepositoryContextSupport implements AotRepositoryContext { + + private final AotContext aotContext; + + /** + * Create a new {@code AotRepositoryContextSupport} given the {@link AotContext}. + * + * @param aotContext + */ + public AotRepositoryContextSupport(AotContext aotContext) { + this.aotContext = aotContext; + } + + @Override + public boolean isGeneratedRepositoriesEnabled(@Nullable String moduleName) { + return aotContext.isGeneratedRepositoriesEnabled(moduleName); + } + + @Override + public boolean isGeneratedRepositoriesMetadataEnabled() { + return aotContext.isGeneratedRepositoriesMetadataEnabled(); + } + + @Override + public ConfigurableListableBeanFactory getBeanFactory() { + return aotContext.getBeanFactory(); + } + + @Override + public Environment getEnvironment() { + return aotContext.getEnvironment(); + } + + @Override + public @Nullable ClassLoader getClassLoader() { + return aotContext.getClassLoader(); + } + + @Override + public ClassLoader getRequiredClassLoader() { + return aotContext.getRequiredClassLoader(); + } + + @Override + public TypeScanner getTypeScanner() { + return aotContext.getTypeScanner(); + } + + @Override + public void typeConfiguration(ResolvableType resolvableType, Consumer configurationConsumer) { + aotContext.typeConfiguration(resolvableType, configurationConsumer); + } + + @Override + public void typeConfiguration(Class type, Consumer configurationConsumer) { + aotContext.typeConfiguration(type, configurationConsumer); + } + + @Override + public void contributeTypeConfigurations(GenerationContext generationContext) { + aotContext.contributeTypeConfigurations(generationContext); + } + +} diff --git a/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java b/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java index 4c1d0e5384..8d22b31a26 100644 --- a/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java +++ b/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java @@ -20,19 +20,14 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; -import java.util.function.Consumer; import java.util.stream.Collectors; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.core.env.Environment; import org.springframework.data.aot.AotContext; -import org.springframework.data.aot.AotTypeConfiguration; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeCollector; -import org.springframework.data.util.TypeContributor; import org.springframework.data.util.TypeUtils; /** @@ -44,10 +39,8 @@ * @see AotRepositoryContext * @since 3.0 */ -@SuppressWarnings("NullAway") // TODO -class DefaultAotRepositoryContext implements AotRepositoryContext { +class DefaultAotRepositoryContext extends AotRepositoryContextSupport { - private final RegisteredBean bean; private final String moduleName; private final RepositoryConfigurationSource configurationSource; private final AotContext aotContext; @@ -55,23 +48,19 @@ class DefaultAotRepositoryContext implements AotRepositoryContext { private final Lazy>> resolvedAnnotations = Lazy.of(this::discoverAnnotations); private final Lazy>> managedTypes = Lazy.of(this::discoverTypes); - private Set basePackages = Collections.emptySet(); private Collection> identifyingAnnotations = Collections.emptySet(); private String beanName; public DefaultAotRepositoryContext(RegisteredBean bean, RepositoryInformation repositoryInformation, String moduleName, AotContext aotContext, RepositoryConfigurationSource configurationSource) { - this.bean = bean; + + super(aotContext); + this.repositoryInformation = repositoryInformation; this.moduleName = moduleName; this.configurationSource = configurationSource; this.aotContext = aotContext; this.beanName = bean.getBeanName(); - this.basePackages = configurationSource.getBasePackages().toSet(); - } - - public AotContext getAotContext() { - return aotContext; } @Override @@ -84,25 +73,6 @@ public RepositoryConfigurationSource getConfigurationSource() { return configurationSource; } - @Override - public ConfigurableListableBeanFactory getBeanFactory() { - return getAotContext().getBeanFactory(); - } - - @Override - public Environment getEnvironment() { - return getAotContext().getEnvironment(); - } - - @Override - public Set getBasePackages() { - return basePackages; - } - - public void setBasePackages(Set basePackages) { - this.basePackages = basePackages; - } - @Override public String getBeanName() { return beanName; @@ -136,29 +106,6 @@ public Set> getResolvedTypes() { return managedTypes.get(); } - @Override - public Set> getUserDomainTypes() { - - return getResolvedTypes().stream() - .filter(it -> TypeContributor.isPartOf(it, Set.of(repositoryInformation.getDomainType().getPackageName()))) - .collect(Collectors.toSet()); - } - - @Override - public AotContext.TypeIntrospector introspectType(String typeName) { - return aotContext.introspectType(typeName); - } - - @Override - public void typeConfiguration(Class type, Consumer configurationConsumer) { - aotContext.typeConfiguration(type, configurationConsumer); - } - - @Override - public Collection typeConfigurations() { - return aotContext.typeConfigurations(); - } - @Override public AotContext.IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { return aotContext.introspectBeanDefinition(beanName); @@ -185,7 +132,8 @@ protected Set> discoverTypes() { if (!getIdentifyingAnnotations().isEmpty()) { - Set> classes = aotContext.getTypeScanner().scanPackages(getBasePackages()) + Set> classes = aotContext.getTypeScanner() + .scanPackages(getConfigurationSource().getBasePackages().toSet()) .forTypesAnnotatedWith(getIdentifyingAnnotations()).collectAsSet(); types.addAll(TypeCollector.inspect(classes).list()); } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java index 0d4ebbd053..c2b2db8209 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java @@ -15,43 +15,21 @@ */ package org.springframework.data.repository.config; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; import java.util.function.Predicate; import java.util.function.Supplier; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; + 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.BeanRegistrationCode; import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator; -import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.context.EnvironmentAware; -import org.springframework.core.env.Environment; -import org.springframework.core.env.StandardEnvironment; -import org.springframework.data.aot.AotContext; -import org.springframework.data.aot.AotTypeConfiguration; -import org.springframework.data.projection.EntityProjectionIntrospector; -import org.springframework.data.repository.Repository; import org.springframework.data.repository.aot.generate.AotRepositoryBeanDefinitionPropertiesDecorator; import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.core.RepositoryInformation; -import org.springframework.data.repository.core.support.RepositoryFragment; -import org.springframework.data.util.Lazy; -import org.springframework.data.util.TypeUtils; import org.springframework.javapoet.CodeBlock; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * {@link BeanRegistrationAotContribution} used to contribute repository registrations. @@ -61,174 +39,32 @@ * @author Mark Paluch * @since 3.0 */ -public class RepositoryRegistrationAotContribution implements BeanRegistrationAotContribution, EnvironmentAware { - - private static final Log logger = LogFactory.getLog(RepositoryRegistrationAotContribution.class); - - private static final String KOTLIN_COROUTINE_REPOSITORY_TYPE_NAME = "org.springframework.data.repository.kotlin.CoroutineCrudRepository"; - - private final RepositoryRegistrationAotProcessor aotProcessor; - - private final AotRepositoryContext repositoryContext; - - private @Nullable RepositoryContributor repositoryContributor; - private Lazy environment = Lazy.of(StandardEnvironment::new); - - private @Nullable BiFunction moduleContribution; - - /** - * Constructs a new instance of the {@link RepositoryRegistrationAotContribution} initialized with the given, required - * {@link RepositoryRegistrationAotProcessor} from which this contribution was created. - * - * @param processor reference back to the {@link RepositoryRegistrationAotProcessor} from which this contribution was - * created. - * @param context reference back to the {@link AotRepositoryContext} from which this contribution was created. - * @throws IllegalArgumentException if the {@link RepositoryRegistrationAotProcessor} is {@literal null}. - * @see RepositoryRegistrationAotProcessor - */ - protected RepositoryRegistrationAotContribution(RepositoryRegistrationAotProcessor processor, - AotRepositoryContext context) { - - Assert.notNull(processor, "RepositoryRegistrationAotProcessor must not be null"); - Assert.notNull(context, "AotRepositoryContext must not be null"); - - this.aotProcessor = processor; - this.repositoryContext = context; - } - - /** - * Factory method used to construct a new instance of {@link RepositoryRegistrationAotContribution} initialized with - * the given, required {@link RepositoryRegistrationAotProcessor} from which this contribution was created. - * - * @param processor reference back to the {@link RepositoryRegistrationAotProcessor} from which this contribution was - * created. - * @return a new instance of {@link RepositoryRegistrationAotContribution} if a contribution can be made; - * {@literal null} if no contribution can be made. - * @see RepositoryRegistrationAotProcessor - */ - public static @Nullable RepositoryRegistrationAotContribution load(RepositoryRegistrationAotProcessor processor, - RegisteredBean repositoryBean) { - - RepositoryConfiguration repositoryMetadata = processor.getRepositoryMetadata(repositoryBean); - - if (repositoryMetadata == null) { - return null; - } - - AotRepositoryContext repositoryContext = buildAotRepositoryContext(processor.getEnvironment(), repositoryBean); - - if (repositoryContext == null) { - return null; - } - - return new RepositoryRegistrationAotContribution(processor, repositoryContext); - } +public class RepositoryRegistrationAotContribution implements BeanRegistrationAotContribution { - /** - * Builds a {@link RepositoryRegistrationAotContribution} for given, required {@link RegisteredBean} representing the - * {@link Repository} registered in the bean registry. - * - * @param repositoryBean {@link RegisteredBean} for the {@link Repository}; must not be {@literal null}. - * @return a {@link RepositoryRegistrationAotContribution} to contribute AOT metadata and code for the - * {@link Repository} {@link RegisteredBean}. - * @throws IllegalArgumentException if the {@link RegisteredBean} is {@literal null}. - * @deprecated since 4.0. - */ - @Deprecated(since = "4.0", forRemoval = true) - public @Nullable RepositoryRegistrationAotContribution forBean(RegisteredBean repositoryBean) { + private final AotRepositoryContext context; + private final BeanRegistrationAotContribution aotContribution; + private final @Nullable RepositoryContributor repositoryContribution; - RepositoryConfiguration repositoryMetadata = getRepositoryRegistrationAotProcessor() - .getRepositoryMetadata(repositoryBean); + RepositoryRegistrationAotContribution(AotRepositoryContext context, BeanRegistrationAotContribution aotContribution, + @Nullable RepositoryContributor repositoryContribution) { - if (repositoryMetadata == null) { - return null; - } - - AotRepositoryContext repositoryContext = buildAotRepositoryContext(aotProcessor.getEnvironment(), repositoryBean); - - if (repositoryContext == null) { - return null; - } - - return new RepositoryRegistrationAotContribution(getRepositoryRegistrationAotProcessor(), repositoryContext); - } - - protected @Nullable BiFunction getModuleContribution() { - return this.moduleContribution; - } - - protected AotRepositoryContext getRepositoryContext() { - return this.repositoryContext; - } - - protected RepositoryRegistrationAotProcessor getRepositoryRegistrationAotProcessor() { - return this.aotProcessor; + this.context = context; + this.aotContribution = aotContribution; + this.repositoryContribution = repositoryContribution; } public RepositoryInformation getRepositoryInformation() { - return getRepositoryContext().getRepositoryInformation(); - } - - private void logTrace(String message, Object... arguments) { - getRepositoryRegistrationAotProcessor().logTrace(message, arguments); - } - - private static @Nullable AotRepositoryContext buildAotRepositoryContext(Environment environment, - RegisteredBean bean) { - - RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(bean); - RepositoryConfiguration configuration = reader.getConfiguration(); - RepositoryConfigurationExtensionSupport extension = reader.getConfigurationExtension(); - - if (configuration == null || extension == null) { - logger.warn( - "Cannot create AotRepositoryContext for bean [%s]. No RepositoryConfiguration/RepositoryConfigurationExtension. Please make sure to register the repository bean through @Enable…Repositories." - .formatted(bean.getBeanName())); - return null; - } - RepositoryInformation repositoryInformation = reader.getRepositoryInformation(); - DefaultAotRepositoryContext repositoryContext = new DefaultAotRepositoryContext(bean, repositoryInformation, - extension.getModuleName(), AotContext.from(bean.getBeanFactory(), environment), - configuration.getConfigurationSource()); - - repositoryContext.setIdentifyingAnnotations(extension.getIdentifyingAnnotations()); - - return repositoryContext; - } - - /** - * {@link BiConsumer Callback} for data module specific contributions. - * - * @param moduleContribution {@link BiConsumer} used by data modules to submit contributions; can be {@literal null}. - * @return this. - */ - public RepositoryRegistrationAotContribution withModuleContribution( - @Nullable BiFunction moduleContribution) { - this.moduleContribution = moduleContribution; - return this; - } - - @Override - public void setEnvironment(Environment environment) { - this.environment = Lazy.of(environment); + return context.getRepositoryInformation(); } @Override public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { - contributeRepositoryInfo(this.repositoryContext, generationContext); + aotContribution.applyTo(generationContext, beanRegistrationCode); - var moduleContribution = getModuleContribution(); - if (moduleContribution != null && this.repositoryContributor == null) { - - this.repositoryContributor = moduleContribution.apply(getRepositoryContext(), generationContext); - - if (this.repositoryContributor != null) { - this.repositoryContributor.contribute(generationContext); - } + if (this.repositoryContribution != null) { + this.repositoryContribution.contribute(generationContext); } - getRepositoryContext().typeConfigurations() - .forEach(typeConfiguration -> typeConfiguration.contribute(environment.get(), generationContext)); } @Override @@ -245,107 +81,16 @@ public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext gener Supplier inheritedProperties = () -> super.generateSetBeanDefinitionPropertiesCode(generationContext, beanRegistrationCode, beanDefinition, attributeFilter); - if (repositoryContributor == null) { // no aot implementation -> go on as + if (repositoryContribution == null) { // no aot implementation -> go on as return inheritedProperties.get(); } AotRepositoryBeanDefinitionPropertiesDecorator decorator = new AotRepositoryBeanDefinitionPropertiesDecorator( - inheritedProperties, repositoryContributor); + inheritedProperties, repositoryContribution); return decorator.decorate(); } }; } - private void contributeRepositoryInfo(AotRepositoryContext repositoryContext, GenerationContext contribution) { - - RepositoryInformation repositoryInformation = getRepositoryInformation(); - - logTrace("Contributing repository information for [%s]", repositoryInformation.getRepositoryInterface()); - - repositoryContext.typeConfiguration(repositoryInformation.getRepositoryInterface(), - config -> config.forReflectiveAccess(MemberCategory.INVOKE_PUBLIC_METHODS).repositoryProxy()); - - repositoryContext.typeConfiguration(repositoryInformation.getRepositoryBaseClass(), config -> config - .forReflectiveAccess(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS)); - - repositoryContext.typeConfiguration(repositoryInformation.getDomainType(), - config -> config.forDataBinding().forQuerydsl()); - - // TODO: purposeful api for uses cases to have some internal logic - repositoryContext.getUserDomainTypes() // - .forEach(it -> repositoryContext.typeConfiguration(it, AotTypeConfiguration::contributeAccessors)); - - // Repository Fragments - contributeFragments(contribution); - - // Kotlin - if (isKotlinCoroutineRepository(repositoryContext, repositoryInformation)) { - contribution.getRuntimeHints().reflection().registerTypes(kotlinRepositoryReflectionTypeReferences(), hint -> {}); - } - - // Repository query methods - repositoryInformation.getQueryMethods().stream().map(repositoryInformation::getReturnedDomainClass) - .filter(Class::isInterface).forEach(type -> { - if (EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy().test(type, - repositoryInformation.getDomainType())) { - repositoryContext.typeConfiguration(type, AotTypeConfiguration::usedAsProjectionInterface); - } - }); - } - - private void contributeFragments(GenerationContext contribution) { - for (RepositoryFragment fragment : getRepositoryInformation().getFragments()) { - - Class repositoryFragmentType = fragment.getSignatureContributor(); - Optional> implementation = fragment.getImplementationClass(); - - contribution.getRuntimeHints().reflection().registerType(repositoryFragmentType, hint -> { - - hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); - - if (!repositoryFragmentType.isInterface()) { - hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); - } - }); - - implementation.ifPresent(typeToRegister -> { - contribution.getRuntimeHints().reflection().registerType(typeToRegister, hint -> { - - hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); - - if (!typeToRegister.isInterface()) { - hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); - } - }); - }); - } - } - - private boolean isKotlinCoroutineRepository(AotRepositoryContext repositoryContext, - RepositoryInformation repositoryInformation) { - - return repositoryContext.introspectType(KOTLIN_COROUTINE_REPOSITORY_TYPE_NAME).resolveType() - .filter(it -> ClassUtils.isAssignable(it, repositoryInformation.getRepositoryInterface())).isPresent(); - } - - private List kotlinRepositoryReflectionTypeReferences() { - - return new ArrayList<>( - Arrays.asList(TypeReference.of("org.springframework.data.repository.kotlin.CoroutineCrudRepository"), - TypeReference.of(Repository.class), // - TypeReference.of(Iterable.class), // - TypeReference.of("kotlinx.coroutines.flow.Flow"), // - TypeReference.of("kotlin.collections.Iterable"), // - TypeReference.of("kotlin.Unit"), // - TypeReference.of("kotlin.Long"), // - TypeReference.of("kotlin.Boolean"))); - } - - static boolean isJavaOrPrimitiveType(Class type) { - return TypeUtils.type(type).isPartOf("java") // - || ClassUtils.isPrimitiveOrWrapper(type) // - || ClassUtils.isPrimitiveArray(type); // - } - } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java index c94b648522..99c54f865e 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java @@ -17,17 +17,20 @@ import java.lang.annotation.Annotation; import java.util.Collections; +import java.util.List; import java.util.Map; -import java.util.function.BiConsumer; -import java.util.function.Predicate; +import java.util.Optional; +import java.util.Set; import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; -import org.springframework.aot.hint.annotation.Reflective; +import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; @@ -43,12 +46,18 @@ import org.springframework.core.env.Environment; import org.springframework.core.env.EnvironmentCapable; import org.springframework.core.env.StandardEnvironment; +import org.springframework.data.aot.AotContext; import org.springframework.data.aot.AotTypeConfiguration; +import org.springframework.data.projection.EntityProjectionIntrospector; +import org.springframework.data.repository.Repository; import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.util.TypeContributor; +import org.springframework.data.util.TypeUtils; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * {@link BeanRegistrationAotProcessor} responsible processing and providing AOT configuration for repositories. @@ -57,13 +66,13 @@ * AOT tooling to allow deriving target type from the {@link RootBeanDefinition bean definition}. If generic types do * not match due to customization of the factory bean by the user, at least the target repository type is provided via * the {@link FactoryBean#OBJECT_TYPE_ATTRIBUTE}. - *

*

- * With {@link RepositoryRegistrationAotProcessor#contribute(AotRepositoryContext, GenerationContext)}, stores can - * provide custom logic for contributing additional (eg. reflection) configuration. By default, reflection configuration - * will be added for types reachable from the repository declaration and query methods as well as all used - * {@link Annotation annotations} from the {@literal org.springframework.data} namespace. - *

+ * With {@link RepositoryRegistrationAotProcessor#contributeRepositoryHints(AotRepositoryContext, GenerationContext)} + * and {@link RepositoryRegistrationAotProcessor#contributeAotRepository(AotRepositoryContext)}, stores can provide + * custom logic for contributing additional (e.g. reflection) configuration. By default, reflection configuration will + * be added for types reachable from the repository declaration and query methods as well as all used {@link Annotation + * annotations} from the {@literal org.springframework.data} namespace. + *

* The processor is typically configured via {@link RepositoryConfigurationExtension#getRepositoryAotProcessor()} and * gets added by the {@link org.springframework.data.repository.config.RepositoryConfigurationDelegate}. * @@ -75,6 +84,18 @@ public class RepositoryRegistrationAotProcessor implements BeanRegistrationAotProcessor, BeanFactoryAware, EnvironmentAware, EnvironmentCapable { + private static final String KOTLIN_COROUTINE_REPOSITORY_TYPE_NAME = "org.springframework.data.repository.kotlin.CoroutineCrudRepository"; + + private static final List KOTLIN_REFLECTION_TYPE_REFERENCES = List.of( + TypeReference.of("org.springframework.data.repository.kotlin.CoroutineCrudRepository"), + TypeReference.of(Repository.class), // + TypeReference.of(Iterable.class), // + TypeReference.of("kotlinx.coroutines.flow.Flow"), // + TypeReference.of("kotlin.collections.Iterable"), // + TypeReference.of("kotlin.Unit"), // + TypeReference.of("kotlin.Long"), // + TypeReference.of("kotlin.Boolean")); + private final Log logger = LogFactory.getLog(getClass()); private @Nullable ConfigurableListableBeanFactory beanFactory; @@ -83,140 +104,265 @@ public class RepositoryRegistrationAotProcessor private Map> configMap = Collections.emptyMap(); + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + + Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory, + () -> "AutowiredAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory: " + beanFactory); + + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + @Override + public Environment getEnvironment() { + return this.environment; + } + + /** + * Setter for the config map. See {@code RepositoryConfigurationDelegate#registerAotComponents}. + * + * @param configMap + */ + @SuppressWarnings("unused") + public void setConfigMap(Map> configMap) { + this.configMap = configMap; + } + + public Map> getConfigMap() { + return this.configMap; + } + + protected ConfigurableListableBeanFactory getBeanFactory() { + + if (this.beanFactory == null) { + throw new IllegalStateException( + "No BeanFactory available. Make sure to set the BeanFactory before using this processor."); + } + + return this.beanFactory; + } + @Override public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean bean) { - return isRepositoryBean(bean) ? newRepositoryRegistrationAotContribution(bean) : null; + + if (!isRepositoryBean(bean)) { + return null; + } + + RepositoryConfiguration repositoryMetadata = getRepositoryMetadata(bean); + AotRepositoryContext repositoryContext = potentiallyCreateContext(environment, bean); + + if (repositoryMetadata == null || repositoryContext == null) { + return null; + } + + BeanRegistrationAotContribution contribution = (generationContext, beanRegistrationCode) -> { + + contributeRepositoryHints(repositoryContext, generationContext); + contributeTypes(repositoryContext, generationContext); + + repositoryContext.contributeTypeConfigurations(generationContext); + }; + + return new RepositoryRegistrationAotContribution(repositoryContext, contribution, + contributeAotRepository(repositoryContext)); } - @Nullable - protected RepositoryContributor contribute(AotRepositoryContext repositoryContext, + /** + * Contribute repository-specific hints, e.g. for repository proxy, base implementation, fragments. Customization hook + * for subclasses that wish to customize repository hint contribution. + * + * @param repositoryContext the repository context. + * @param generationContext the generation context. + * @since 4.0 + */ + protected void contributeRepositoryHints(AotRepositoryContext repositoryContext, GenerationContext generationContext) { - repositoryContext.getResolvedTypes().stream() - .filter(it -> !RepositoryRegistrationAotContribution.isJavaOrPrimitiveType(it)) - .forEach(it -> contributeType(it, generationContext)); + RepositoryInformation repositoryInformation = repositoryContext.getRepositoryInformation(); + + if (logger.isTraceEnabled()) { + logger.trace( + "Contributing repository information for [%s]".formatted(repositoryInformation.getRepositoryInterface())); + } + + // Native hints for repository proxy + repositoryContext.typeConfiguration(repositoryInformation.getRepositoryInterface(), + config -> config.forReflectiveAccess(MemberCategory.INVOKE_PUBLIC_METHODS).repositoryProxy()); + + // Native hints for reflective base implementation access + repositoryContext.typeConfiguration(repositoryInformation.getRepositoryBaseClass(), config -> config + .forReflectiveAccess(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS)); + + // Repository Fragments + contributeFragments(repositoryInformation.getFragments(), generationContext); + + // Kotlin + if (isKotlinCoroutineRepository(repositoryInformation)) { + generationContext.getRuntimeHints().reflection().registerTypes(KOTLIN_REFLECTION_TYPE_REFERENCES, hint -> {}); + } + } + + /** + * Contribute types for reflection, proxies, etc. Customization hook for subclasses that wish to customize type + * contribution hints. + * + * @param repositoryContext the repository context. + * @param generationContext the generation context. + * @since 4.0 + */ + protected void contributeTypes(AotRepositoryContext repositoryContext, GenerationContext generationContext) { + + contributeDomainTypes(repositoryContext, generationContext); + contributeResolvedTypes(repositoryContext, generationContext); + + RepositoryInformation information = repositoryContext.getRepositoryInformation(); + + // Repository query methods + information.getQueryMethods().stream().map(information::getReturnedDomainClass).filter(Class::isInterface) + .forEach(type -> { + if (EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy().test(type, + information.getDomainType())) { + repositoryContext.typeConfiguration(type, AotTypeConfiguration::usedAsProjectionInterface); + } + }); repositoryContext.getResolvedAnnotations().stream() .filter(RepositoryRegistrationAotProcessor::isSpringDataManagedAnnotation).map(MergedAnnotation::getType) .forEach(it -> contributeType(it, generationContext)); - - return null; } /** - * Processes the repository's domain and alternative domain types to consider {@link Reflective} annotations used on - * it. + * Customization hook for subclasses that wish to customize domain type contribution hints. * - * @param repositoryContext must not be {@literal null}. - * @param generationContext must not be {@literal null}. + * @param repositoryContext the repository context. + * @param generationContext the generation context. + * @since 4.0 */ - // TODO: Can we merge #contribute, #registerReflectiveForAggregateRoot into RepositoryRegistrationAotContribution? - // hints and types are contributed from everywhere. - private void registerReflectiveForAggregateRoot(AotRepositoryContext repositoryContext, - GenerationContext generationContext) { + protected void contributeDomainTypes(AotRepositoryContext repositoryContext, GenerationContext generationContext) { RepositoryInformation information = repositoryContext.getRepositoryInformation(); - ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar(); + RuntimeHints hints = generationContext.getRuntimeHints(); + // Domain types, related types, projections + repositoryContext.typeConfiguration(information.getDomainType(), config -> config.forDataBinding().forQuerydsl()); + + ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar(); Stream.concat(Stream.of(information.getDomainType()), information.getAlternativeDomainTypes().stream()) .forEach(it -> { - // arent we already registering the types in RepositoryRegistrationAotContribution#contributeRepositoryInfo? + // TODO cross check with #contributeResolvedTypes registrar.registerRuntimeHints(hints, it); - repositoryContext.typeConfiguration(it, AotTypeConfiguration::contributeAccessors); }); } - private boolean isRepositoryBean(RegisteredBean bean) { - return getConfigMap().containsKey(bean.getBeanName()); - } + private void contributeResolvedTypes(AotRepositoryContext repositoryContext, GenerationContext generationContext) { - protected @Nullable RepositoryRegistrationAotContribution newRepositoryRegistrationAotContribution( - RegisteredBean repositoryBean) { + RepositoryInformation information = repositoryContext.getRepositoryInformation(); - RepositoryRegistrationAotContribution contribution = RepositoryRegistrationAotContribution.load(this, - repositoryBean); + // TODO: These are twice. + repositoryContext.getResolvedTypes().stream() + .filter(it -> TypeContributor.isPartOf(it, Set.of(information.getDomainType().getPackageName()))) + .forEach(it -> repositoryContext.typeConfiguration(it, AotTypeConfiguration::contributeAccessors)); - // cannot contribute a repository bean. - if (contribution == null) { - return null; - } + repositoryContext.getResolvedTypes().stream().filter(it -> !isJavaOrPrimitiveType(it)) + .forEach(it -> contributeType(it, generationContext)); + } - // TODO: add the hook for customizing bean initialization code here! + /** + * This method allows for the creation to be overridden by subclasses. + * + * @param repositoryContext the context for the repository being processed. + * @return a {@link RepositoryContributor} to contribute store-specific AOT artifacts or {@literal null} to skip + * store-specific AOT contributions. + * @since 4.0 + */ + @Nullable + protected RepositoryContributor contributeAotRepository(AotRepositoryContext repositoryContext) { + return null; + } - return contribution.withModuleContribution((repositoryContext, generationContext) -> { - registerReflectiveForAggregateRoot(repositoryContext, generationContext); - return contribute(repositoryContext, generationContext); - }); + private boolean isRepositoryBean(RegisteredBean bean) { + return getConfigMap().containsKey(bean.getBeanName()); } - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + private RepositoryConfiguration getRepositoryMetadata(RegisteredBean bean) { - Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory, - () -> "AutowiredAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory: " + beanFactory); + RepositoryConfiguration configuration = getConfigMap().get(bean.getBeanName()); - this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; - } + if (configuration == null) { + throw new IllegalArgumentException("No configuration for bean [%s]".formatted(bean.getBeanName())); + } - @Override - public void setEnvironment(Environment environment) { - this.environment = environment; + return configuration; } - @Override - public Environment getEnvironment() { - return this.environment; + private void contributeType(Class type, GenerationContext context) { + TypeContributor.contribute(type, it -> true, context); } - public void setConfigMap(Map> configMap) { - this.configMap = configMap; + private void contributeFragments(Iterable> fragments, GenerationContext contribution) { + fragments.forEach(it -> contributeFragment(it, contribution)); } - public Map> getConfigMap() { - return this.configMap; - } + private static void contributeFragment(RepositoryFragment fragment, GenerationContext context) { - protected ConfigurableListableBeanFactory getBeanFactory() { + Class repositoryFragmentType = fragment.getSignatureContributor(); + Optional> implementation = fragment.getImplementationClass(); - if (this.beanFactory == null) { - throw new IllegalStateException( - "No BeanFactory available. Make sure to set the BeanFactory before using this processor."); - } + registerReflectiveHints(repositoryFragmentType, context); - return this.beanFactory; + implementation.ifPresent(typeToRegister -> registerReflectiveHints(typeToRegister, context)); } - protected @Nullable RepositoryConfiguration getRepositoryMetadata(RegisteredBean bean) { - return getConfigMap().get(bean.getBeanName()); - } + private static void registerReflectiveHints(Class typeToRegister, GenerationContext context) { - protected void contributeType(Class type, GenerationContext generationContext) { - TypeContributor.contribute(type, it -> true, generationContext); - } + context.getRuntimeHints().reflection().registerType(typeToRegister, hint -> { - protected Log getLogger() { - return this.logger; - } + hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); - protected void logDebug(String message, Object... arguments) { - logAt(Log::isDebugEnabled, Log::debug, message, arguments); + if (!typeToRegister.isInterface()) { + hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + } + }); } - protected void logTrace(String message, Object... arguments) { - logAt(Log::isTraceEnabled, Log::trace, message, arguments); + private @Nullable AotRepositoryContext potentiallyCreateContext(Environment environment, RegisteredBean bean) { + + RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(bean); + RepositoryConfiguration configuration = reader.getConfiguration(); + RepositoryConfigurationExtensionSupport extension = reader.getConfigurationExtension(); + + if (configuration == null || extension == null) { + logger.warn( + "Cannot create AotRepositoryContext for bean [%s]. No RepositoryConfiguration/RepositoryConfigurationExtension. Please make sure to register the repository bean through @Enable…Repositories." + .formatted(bean.getBeanName())); + return null; + } + RepositoryInformation repositoryInformation = reader.getRepositoryInformation(); + DefaultAotRepositoryContext repositoryContext = new DefaultAotRepositoryContext(bean, repositoryInformation, + extension.getModuleName(), AotContext.from(bean.getBeanFactory(), environment), + configuration.getConfigurationSource()); + + repositoryContext.setIdentifyingAnnotations(extension.getIdentifyingAnnotations()); + + return repositoryContext; } - private void logAt(Predicate logLevelPredicate, BiConsumer logOperation, String message, - Object... arguments) { + private static boolean isKotlinCoroutineRepository(RepositoryInformation repositoryInformation) { - Log logger = getLogger(); + Class coroutineRepository = org.springframework.data.util.ClassUtils.loadIfPresent( + KOTLIN_COROUTINE_REPOSITORY_TYPE_NAME, repositoryInformation.getRepositoryInterface().getClassLoader()); - if (logLevelPredicate.test(logger)) { - logOperation.accept(logger, String.format(message, arguments)); - } + return coroutineRepository != null + && ClassUtils.isAssignable(coroutineRepository, repositoryInformation.getRepositoryInterface()); } private static boolean isSpringDataManagedAnnotation(MergedAnnotation annotation) { @@ -229,4 +375,10 @@ private static boolean isSpringDataType(Class type) { return type.getPackageName().startsWith(TypeContributor.DATA_NAMESPACE); } + private static boolean isJavaOrPrimitiveType(Class type) { + return ClassUtils.isPrimitiveOrWrapper(type) // + || ClassUtils.isPrimitiveArray(type) // + || TypeUtils.type(type).isPartOf("java"); + } + } diff --git a/src/test/java/org/springframework/data/aot/AotContextUnitTests.java b/src/test/java/org/springframework/data/aot/AotContextUnitTests.java index d164779542..88a365c246 100644 --- a/src/test/java/org/springframework/data/aot/AotContextUnitTests.java +++ b/src/test/java/org/springframework/data/aot/AotContextUnitTests.java @@ -17,8 +17,6 @@ import static org.mockito.Mockito.*; -import java.util.Collection; -import java.util.List; import java.util.function.Consumer; import org.assertj.core.api.Assertions; @@ -28,6 +26,8 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoSettings; + +import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -102,7 +102,7 @@ private void contributeAccessor(Class... classes) { context.typeConfiguration(aClass, AotTypeConfiguration::contributeAccessors); } - context.typeConfigurations().forEach(it -> it.contribute(mockEnvironment, new TestGenerationContext())); + context.contributeTypeConfigurations(new TestGenerationContext()); } @ParameterizedTest // GH-3322 @@ -163,8 +163,8 @@ public void typeConfiguration(Class type, Consumer conf } @Override - public Collection typeConfigurations() { - return List.of(); + public void contributeTypeConfigurations(GenerationContext generationContext) { + } @Override diff --git a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBeanDefinitionPropertiesDecoratorUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBeanDefinitionPropertiesDecoratorUnitTests.java index dadad5311b..d82397b304 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBeanDefinitionPropertiesDecoratorUnitTests.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBeanDefinitionPropertiesDecoratorUnitTests.java @@ -56,10 +56,9 @@ class AotRepositoryBeanDefinitionPropertiesDecoratorUnitTests { void beforeEach() { when(contributor.getContributedTypeName()).thenReturn(GeneratedTypeReference.of(ClassName.bestGuess(TYPE_NAME))); + when(contributor.getAotFragmentMetadata()).thenReturn(metadata); inheritedSource = CodeBlock.builder(); decorator = new AotRepositoryBeanDefinitionPropertiesDecorator(() -> inheritedSource.build(), contributor); - - when(contributor.getConstructorArguments()).thenReturn(metadata.getConstructorArguments()); } @Test // GH-3344 diff --git a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryConfigurationUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryConfigurationUnitTests.java index a38f869949..5249edf8f5 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryConfigurationUnitTests.java @@ -69,7 +69,8 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder builder) { assertThat(contributedTypeName).isNotNull(); // required constructor arguments need to be present at this point - Map requiredArgs = new LinkedHashMap<>(contributor.requiredArgs()); + Map requiredArgs = new LinkedHashMap<>( + contributor.getAotFragmentMetadata().getAutowireFields()); assertThat(requiredArgs).hasSize(1); // decorator kicks in and enhanced the BeanDefinition. No files written so far. @@ -86,7 +87,7 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder builder) { // make sure write operation for generated content did not change constructor nor type name assertThat(contributor.getContributedTypeName()).isEqualTo(contributedTypeName); - assertThat(contributor.requiredArgs()).containsExactlyEntriesOf(requiredArgs); + assertThat(contributor.getAotFragmentMetadata().getAutowireFields()).containsExactlyEntriesOf(requiredArgs); // file is actually present now assertThat(generationContext.getGeneratedFiles().getGeneratedFiles(Kind.SOURCE)) diff --git a/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java b/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java index 7e4e0747b8..1b3a7861c9 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java @@ -15,20 +15,17 @@ */ package org.springframework.data.repository.aot.generate; -import java.io.IOException; import java.lang.annotation.Annotation; -import java.util.Collection; -import java.util.List; import java.util.Set; -import java.util.function.Consumer; import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.test.tools.ClassFile; -import org.springframework.data.aot.AotTypeConfiguration; +import org.springframework.data.aot.AotContext; import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.config.AotRepositoryContextSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryComposition; @@ -39,12 +36,12 @@ * * @author Christoph Strobl */ -class DummyModuleAotRepositoryContext implements AotRepositoryContext { +class DummyModuleAotRepositoryContext extends AotRepositoryContextSupport { private final StubRepositoryInformation repositoryInformation; - private final MockEnvironment environment = new MockEnvironment(); public DummyModuleAotRepositoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition) { + super(AotContext.from(new DefaultListableBeanFactory(), new MockEnvironment())); this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); } @@ -65,7 +62,7 @@ public ConfigurableListableBeanFactory getBeanFactory() { @Override public MockEnvironment getEnvironment() { - return environment; + return (MockEnvironment) super.getEnvironment(); } @Override @@ -78,16 +75,6 @@ public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { return null; } - @Override - public void typeConfiguration(Class type, Consumer configurationConsumer) { - - } - - @Override - public Collection typeConfigurations() { - return List.of(); - } - @Override public String getBeanName() { return "dummyRepository"; @@ -118,24 +105,4 @@ public Set> getResolvedTypes() { return Set.of(); } - @Override - public Set> getUserDomainTypes() { - return Set.of(); - } - - public List getRequiredContextFiles() { - return List.of(classFileForType(repositoryInformation.getRepositoryBaseClass())); - } - - static ClassFile classFileForType(Class type) { - - String name = type.getName(); - ClassPathResource cpr = new ClassPathResource(name.replaceAll("\\.", "/") + ".class"); - - try { - return ClassFile.of(name, cpr.getContentAsByteArray()); - } catch (IOException e) { - throw new IllegalArgumentException("Cannot open [%s].".formatted(cpr.getPath())); - } - } } From 40801c03e9f39446371cc77e1fa99278f40031e6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 23 Sep 2025 10:08:04 +0200 Subject: [PATCH 3/5] Polishing. Add additional conversions for Number that do not require the use of a conversion service. --- .../data/javapoet/LordOfTheStrings.java | 29 +++++++++++- .../data/javapoet/JavaPoetUnitTests.java | 44 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/springframework/data/javapoet/LordOfTheStrings.java b/src/main/java/org/springframework/data/javapoet/LordOfTheStrings.java index 6b924d26e5..83cffb8aeb 100644 --- a/src/main/java/org/springframework/data/javapoet/LordOfTheStrings.java +++ b/src/main/java/org/springframework/data/javapoet/LordOfTheStrings.java @@ -858,10 +858,37 @@ public static class TypedReturnBuilder extends ReturnBuilderSupport { */ @Contract("_ -> this") public TypedReturnBuilder number(String resultToReturn) { + return whenBoxedLong("$1L != null ? $1L.longValue() : null", resultToReturn) .whenLong("$1L != null ? $1L.longValue() : 0L", resultToReturn) .whenBoxedInteger("$1L != null ? $1L.intValue() : null", resultToReturn) - .whenInt("$1L != null ? $1L.intValue() : 0", resultToReturn); + .whenInt("$1L != null ? $1L.intValue() : 0", resultToReturn) + .whenBoxed(Byte.class, "$1L != null ? $1L.byteValue() : null", resultToReturn) + .when(byte.class, "$1L != null ? $1L.byteValue() : 0", resultToReturn) + .whenBoxed(Short.class, "$1L != null ? $1L.shortValue() : null", resultToReturn) + .when(short.class, "$1L != null ? $1L.shortValue() : 0", resultToReturn) + .whenBoxed(Double.class, "$1L != null ? $1L.doubleValue() : null", resultToReturn) + .when(double.class, "$1L != null ? $1L.doubleValue() : 0", resultToReturn) + .whenBoxed(Float.class, "$1L != null ? $1L.floatValue() : null", resultToReturn) + .when(float.class, "$1L != null ? $1L.floatValue() : 0f", resultToReturn); + } + + /** + * Add return statements for numeric types if the given {@code resultToReturn} points to a non-nullable + * {@link Number}. Considers all primitive numeric types assuming that {@code resultToReturn} is never + * {@literal null}. + * + * @param resultToReturn the argument or variable name holding the result. + * @return {@code this} builder. + */ + @Contract("_ -> this") + public TypedReturnBuilder nonNullableNumber(String resultToReturn) { + return whenPrimitiveOrBoxed(long.class, "$1L.longValue()", resultToReturn) + .whenPrimitiveOrBoxed(int.class, "$1L.intValue()", resultToReturn) + .whenPrimitiveOrBoxed(short.class, "$1L.shortValue()", resultToReturn) + .whenPrimitiveOrBoxed(byte.class, "$1L.byteValue()", resultToReturn) + .whenPrimitiveOrBoxed(float.class, "$1L.floatValue()", resultToReturn) + .whenPrimitiveOrBoxed(double.class, "$1L.doubleValue()", resultToReturn); } /** diff --git a/src/test/java/org/springframework/data/javapoet/JavaPoetUnitTests.java b/src/test/java/org/springframework/data/javapoet/JavaPoetUnitTests.java index a39d09fb64..d4346db76b 100644 --- a/src/test/java/org/springframework/data/javapoet/JavaPoetUnitTests.java +++ b/src/test/java/org/springframework/data/javapoet/JavaPoetUnitTests.java @@ -188,6 +188,50 @@ void shouldRenderConditionalNumericReturn() { block = LordOfTheStrings.returning(Integer.class).number("someNumericVariable").otherwise(":-[").build(); assertThat(block).hasToString("return someNumericVariable != null ? someNumericVariable.intValue() : null"); + + block = LordOfTheStrings.returning(Short.class).number("someNumericVariable").otherwise(":-[").build(); + assertThat(block).hasToString("return someNumericVariable != null ? someNumericVariable.shortValue() : null"); + + block = LordOfTheStrings.returning(short.class).number("someNumericVariable").otherwise(":-[").build(); + assertThat(block).hasToString("return someNumericVariable != null ? someNumericVariable.shortValue() : 0"); + + block = LordOfTheStrings.returning(Byte.class).number("someNumericVariable").otherwise(":-[").build(); + assertThat(block).hasToString("return someNumericVariable != null ? someNumericVariable.byteValue() : null"); + + block = LordOfTheStrings.returning(Float.class).number("someNumericVariable").otherwise(":-[").build(); + assertThat(block).hasToString("return someNumericVariable != null ? someNumericVariable.floatValue() : null"); + + block = LordOfTheStrings.returning(float.class).number("someNumericVariable").otherwise(":-[").build(); + assertThat(block).hasToString("return someNumericVariable != null ? someNumericVariable.floatValue() : 0f"); + } + + @Test // GH-3357 + void shouldRenderConditionalSafeNumericReturn() { + + CodeBlock block = LordOfTheStrings.returning(boolean.class).nonNullableNumber("someNumericVariable") + .otherwise(":-[").build(); + assertThat(block).hasToString("return :-["); + + block = LordOfTheStrings.returning(long.class).nonNullableNumber("someNumericVariable").otherwise(":-[").build(); + assertThat(block).hasToString("return someNumericVariable.longValue()"); + + block = LordOfTheStrings.returning(Long.class).nonNullableNumber("someNumericVariable").otherwise(":-[").build(); + assertThat(block).hasToString("return someNumericVariable.longValue()"); + + block = LordOfTheStrings.returning(Integer.class).nonNullableNumber("someNumericVariable").otherwise(":-[").build(); + assertThat(block).hasToString("return someNumericVariable.intValue()"); + + block = LordOfTheStrings.returning(short.class).nonNullableNumber("someNumericVariable").otherwise(":-[").build(); + assertThat(block).hasToString("return someNumericVariable.shortValue()"); + + block = LordOfTheStrings.returning(byte.class).nonNullableNumber("someNumericVariable").otherwise(":-[").build(); + assertThat(block).hasToString("return someNumericVariable.byteValue()"); + + block = LordOfTheStrings.returning(Double.class).nonNullableNumber("someNumericVariable").otherwise(":-[").build(); + assertThat(block).hasToString("return someNumericVariable.doubleValue()"); + + block = LordOfTheStrings.returning(Float.class).nonNullableNumber("someNumericVariable").otherwise(":-[").build(); + assertThat(block).hasToString("return someNumericVariable.floatValue()"); } @Test // GH-3357 From 43428f019627597964d07d28a3252ddb3bfd86c8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 23 Sep 2025 15:59:11 +0200 Subject: [PATCH 4/5] Polishing. Refine naming. --- ...agedTypesBeanRegistrationAotProcessor.java | 12 ++-- ...nagedTypesRegistrationAotContribution.java | 6 +- .../RepositoryRegistrationAotProcessor.java | 69 ++++++++++--------- 3 files changed, 47 insertions(+), 40 deletions(-) diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java index a6c8365613..c5139d44dc 100644 --- a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java @@ -119,7 +119,7 @@ private ManagedTypes resolveManagedTypes(RegisteredBean registeredBean) { /** * Hook to provide a customized flavor of {@link BeanRegistrationAotContribution}. By overriding this method calls to - * {@link #contributeType(ResolvableType, GenerationContext, AotContext)} might no longer be issued. + * {@link #registerTypeHints(ResolvableType, AotContext, GenerationContext)} might no longer be issued. * * @param aotContext never {@literal null}. * @param managedTypes never {@literal null}. @@ -128,7 +128,7 @@ private ManagedTypes resolveManagedTypes(RegisteredBean registeredBean) { protected BeanRegistrationAotContribution contribute(AotContext aotContext, ManagedTypes managedTypes, RegisteredBean registeredBean) { return new ManagedTypesRegistrationAotContribution(aotContext, managedTypes, registeredBean, - typeCollectorCustomizer(), this::contributeType); + typeCollectorCustomizer(), this::registerTypeHints); } /** @@ -140,13 +140,14 @@ protected BeanRegistrationAotContribution contribute(AotContext aotContext, Mana protected Consumer typeCollectorCustomizer() { return typeCollector -> {}; } + /** * Hook to contribute configuration for a given {@literal type}. * * @param type never {@literal null}. * @param generationContext never {@literal null}. */ - protected void contributeType(ResolvableType type, GenerationContext generationContext, AotContext aotContext) { + protected void registerTypeHints(ResolvableType type, AotContext aotContext, GenerationContext generationContext) { if (logger.isDebugEnabled()) { logger.debug(String.format("Contributing type information for [%s]", type.getType())); @@ -154,8 +155,7 @@ protected void contributeType(ResolvableType type, GenerationContext generationC Set annotationNamespaces = Collections.singleton(TypeContributor.DATA_NAMESPACE); - configureTypeContribution(type.toClass(), aotContext); - aotContext.contributeTypeConfigurations(generationContext); + configureTypeHints(type.toClass(), aotContext); TypeUtils.resolveUsedAnnotations(type.toClass()).forEach( annotation -> TypeContributor.contribute(annotation.getType(), annotationNamespaces, generationContext)); @@ -168,7 +168,7 @@ protected void contributeType(ResolvableType type, GenerationContext generationC * @param aotContext AOT context for type configuration. * @since 4.0 */ - protected void configureTypeContribution(Class type, AotContext aotContext) { + protected void configureTypeHints(Class type, AotContext aotContext) { aotContext.typeConfiguration(type, config -> config.forDataBinding().contributeAccessors().forQuerydsl()); } diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java b/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java index 2463576a4d..daeb21b588 100644 --- a/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java +++ b/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java @@ -98,8 +98,10 @@ public void applyTo(GenerationContext generationContext, BeanRegistrationCode be if (!types.isEmpty()) { TypeCollector.inspect(typeCollectorCustomizer, types) - .forEach(type -> contributionAction.register(type, generationContext, aotContext)); + .forEach(type -> contributionAction.register(type, aotContext, generationContext)); } + + aotContext.contributeTypeConfigurations(generationContext); } @Override @@ -117,7 +119,7 @@ public RegisteredBean getSource() { } interface TypeRegistration { - void register(ResolvableType type, GenerationContext generationContext, AotContext aotContext); + void register(ResolvableType type, AotContext aotContext, GenerationContext generationContext); } /** diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java index 99c54f865e..e9f1b0a740 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java @@ -67,11 +67,11 @@ * not match due to customization of the factory bean by the user, at least the target repository type is provided via * the {@link FactoryBean#OBJECT_TYPE_ATTRIBUTE}. *

- * With {@link RepositoryRegistrationAotProcessor#contributeRepositoryHints(AotRepositoryContext, GenerationContext)} - * and {@link RepositoryRegistrationAotProcessor#contributeAotRepository(AotRepositoryContext)}, stores can provide - * custom logic for contributing additional (e.g. reflection) configuration. By default, reflection configuration will - * be added for types reachable from the repository declaration and query methods as well as all used {@link Annotation - * annotations} from the {@literal org.springframework.data} namespace. + * With {@link #registerRepositoryCompositionHints(AotRepositoryContext, GenerationContext)} (specifically + * {@link #configureTypeContribution(Class, AotContext)} and {@link #contributeAotRepository(AotRepositoryContext)}, + * stores can provide custom logic for contributing additional (e.g. reflection) configuration. By default, reflection + * configuration will be added for types reachable from the repository declaration and query methods as well as all used + * {@link Annotation annotations} from the {@literal org.springframework.data} namespace. *

* The processor is typically configured via {@link RepositoryConfigurationExtension#getRepositoryAotProcessor()} and * gets added by the {@link org.springframework.data.repository.config.RepositoryConfigurationDelegate}. @@ -163,8 +163,8 @@ protected ConfigurableListableBeanFactory getBeanFactory() { BeanRegistrationAotContribution contribution = (generationContext, beanRegistrationCode) -> { - contributeRepositoryHints(repositoryContext, generationContext); - contributeTypes(repositoryContext, generationContext); + registerRepositoryCompositionHints(repositoryContext, generationContext); + configureTypeContributions(repositoryContext, generationContext); repositoryContext.contributeTypeConfigurations(generationContext); }; @@ -181,7 +181,7 @@ protected ConfigurableListableBeanFactory getBeanFactory() { * @param generationContext the generation context. * @since 4.0 */ - protected void contributeRepositoryHints(AotRepositoryContext repositoryContext, + protected void registerRepositoryCompositionHints(AotRepositoryContext repositoryContext, GenerationContext generationContext) { RepositoryInformation repositoryInformation = repositoryContext.getRepositoryInformation(); @@ -200,7 +200,7 @@ protected void contributeRepositoryHints(AotRepositoryContext repositoryContext, .forReflectiveAccess(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS)); // Repository Fragments - contributeFragments(repositoryInformation.getFragments(), generationContext); + registerFragmentsHints(repositoryInformation.getFragments(), generationContext); // Kotlin if (isKotlinCoroutineRepository(repositoryInformation)) { @@ -209,20 +209,19 @@ protected void contributeRepositoryHints(AotRepositoryContext repositoryContext, } /** - * Contribute types for reflection, proxies, etc. Customization hook for subclasses that wish to customize type - * contribution hints. + * Register type-specific hints and AOT artifacts for domain types, reachable types, projection interfaces derived + * from query method return types, and annotations from {@literal org.springframework.data} packages. * * @param repositoryContext the repository context. * @param generationContext the generation context. * @since 4.0 */ - protected void contributeTypes(AotRepositoryContext repositoryContext, GenerationContext generationContext) { - - contributeDomainTypes(repositoryContext, generationContext); - contributeResolvedTypes(repositoryContext, generationContext); + private void configureTypeContributions(AotRepositoryContext repositoryContext, GenerationContext generationContext) { RepositoryInformation information = repositoryContext.getRepositoryInformation(); + configureDomainTypeContributions(repositoryContext, generationContext); + // Repository query methods information.getQueryMethods().stream().map(information::getReturnedDomainClass).filter(Class::isInterface) .forEach(type -> { @@ -238,44 +237,50 @@ protected void contributeTypes(AotRepositoryContext repositoryContext, Generatio } /** - * Customization hook for subclasses that wish to customize domain type contribution hints. + * Customization hook for subclasses that wish to customize domain type hint contributions. + *

+ * Type hints are registered for the domain, alternative domain types, and types reachable from there + * ({@link AotRepositoryContext#getResolvedTypes()}) * * @param repositoryContext the repository context. * @param generationContext the generation context. * @since 4.0 */ - protected void contributeDomainTypes(AotRepositoryContext repositoryContext, GenerationContext generationContext) { + protected void configureDomainTypeContributions(AotRepositoryContext repositoryContext, + GenerationContext generationContext) { RepositoryInformation information = repositoryContext.getRepositoryInformation(); - RuntimeHints hints = generationContext.getRuntimeHints(); // Domain types, related types, projections - repositoryContext.typeConfiguration(information.getDomainType(), config -> config.forDataBinding().forQuerydsl()); - ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar(); Stream.concat(Stream.of(information.getDomainType()), information.getAlternativeDomainTypes().stream()) .forEach(it -> { - // TODO cross check with #contributeResolvedTypes registrar.registerRuntimeHints(hints, it); - repositoryContext.typeConfiguration(it, AotTypeConfiguration::contributeAccessors); + configureTypeContribution(it, repositoryContext); }); - } - private void contributeResolvedTypes(AotRepositoryContext repositoryContext, GenerationContext generationContext) { - - RepositoryInformation information = repositoryContext.getRepositoryInformation(); - - // TODO: These are twice. + // TODO: Looks like a duplicate repositoryContext.getResolvedTypes().stream() .filter(it -> TypeContributor.isPartOf(it, Set.of(information.getDomainType().getPackageName()))) - .forEach(it -> repositoryContext.typeConfiguration(it, AotTypeConfiguration::contributeAccessors)); + .forEach(it -> configureTypeContribution(it, repositoryContext)); repositoryContext.getResolvedTypes().stream().filter(it -> !isJavaOrPrimitiveType(it)) .forEach(it -> contributeType(it, generationContext)); } + /** + * Customization hook to configure the {@link TypeContributor} used to register the given {@literal type}. + * + * @param type the class to configure the contribution for. + * @param aotContext AOT context for type configuration. + * @since 4.0 + */ + protected void configureTypeContribution(Class type, AotContext aotContext) { + aotContext.typeConfiguration(type, config -> config.forDataBinding().contributeAccessors().forQuerydsl()); + } + /** * This method allows for the creation to be overridden by subclasses. * @@ -308,11 +313,11 @@ private void contributeType(Class type, GenerationContext context) { TypeContributor.contribute(type, it -> true, context); } - private void contributeFragments(Iterable> fragments, GenerationContext contribution) { - fragments.forEach(it -> contributeFragment(it, contribution)); + private void registerFragmentsHints(Iterable> fragments, GenerationContext contribution) { + fragments.forEach(it -> registerFragmentHints(it, contribution)); } - private static void contributeFragment(RepositoryFragment fragment, GenerationContext context) { + private static void registerFragmentHints(RepositoryFragment fragment, GenerationContext context) { Class repositoryFragmentType = fragment.getSignatureContributor(); Optional> implementation = fragment.getImplementationClass(); From 194f4302a7260a5c11b7352d74cf264f2b9d5b88 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 23 Sep 2025 18:09:16 +0200 Subject: [PATCH 5/5] Move reflection hint handling to repository context. --- .../data/aot/DefaultAotContext.java | 3 +++ .../ManagedTypesBeanRegistrationAotProcessor.java | 4 ++-- .../RepositoryRegistrationAotProcessor.java | 15 ++++----------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/springframework/data/aot/DefaultAotContext.java b/src/main/java/org/springframework/data/aot/DefaultAotContext.java index e155c27bbb..9231471213 100644 --- a/src/main/java/org/springframework/data/aot/DefaultAotContext.java +++ b/src/main/java/org/springframework/data/aot/DefaultAotContext.java @@ -33,6 +33,7 @@ import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.TypeReference; +import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.aot.AotProcessingException; @@ -63,6 +64,7 @@ class DefaultAotContext implements AotContext { private final Map, ContextualTypeConfiguration> typeConfigurations = new HashMap<>(); private final Environment environment; + private final ReflectiveRuntimeHintsRegistrar runtimeHintsRegistrar = new ReflectiveRuntimeHintsRegistrar(); public DefaultAotContext(BeanFactory beanFactory, Environment environment) { this(beanFactory, environment, new AotMappingContext()); @@ -257,6 +259,7 @@ private void doContribute(Environment environment, GenerationContext generationC } if (forDataBinding) { + runtimeHintsRegistrar.registerRuntimeHints(generationContext.getRuntimeHints(), type); TypeContributor.contribute(type, Set.of(TypeContributor.DATA_NAMESPACE), generationContext); } diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java index c5139d44dc..2e978a3c1f 100644 --- a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java @@ -155,7 +155,7 @@ protected void registerTypeHints(ResolvableType type, AotContext aotContext, Gen Set annotationNamespaces = Collections.singleton(TypeContributor.DATA_NAMESPACE); - configureTypeHints(type.toClass(), aotContext); + configureTypeContribution(type.toClass(), aotContext); TypeUtils.resolveUsedAnnotations(type.toClass()).forEach( annotation -> TypeContributor.contribute(annotation.getType(), annotationNamespaces, generationContext)); @@ -168,7 +168,7 @@ protected void registerTypeHints(ResolvableType type, AotContext aotContext, Gen * @param aotContext AOT context for type configuration. * @since 4.0 */ - protected void configureTypeHints(Class type, AotContext aotContext) { + protected void configureTypeContribution(Class type, AotContext aotContext) { aotContext.typeConfiguration(type, config -> config.forDataBinding().contributeAccessors().forQuerydsl()); } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java index e9f1b0a740..c5ae5d09ca 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java @@ -26,12 +26,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; - import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; -import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.TypeReference; -import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -216,7 +213,8 @@ protected void registerRepositoryCompositionHints(AotRepositoryContext repositor * @param generationContext the generation context. * @since 4.0 */ - private void configureTypeContributions(AotRepositoryContext repositoryContext, GenerationContext generationContext) { + protected void configureTypeContributions(AotRepositoryContext repositoryContext, + GenerationContext generationContext) { RepositoryInformation information = repositoryContext.getRepositoryInformation(); @@ -246,22 +244,17 @@ private void configureTypeContributions(AotRepositoryContext repositoryContext, * @param generationContext the generation context. * @since 4.0 */ - protected void configureDomainTypeContributions(AotRepositoryContext repositoryContext, + private void configureDomainTypeContributions(AotRepositoryContext repositoryContext, GenerationContext generationContext) { RepositoryInformation information = repositoryContext.getRepositoryInformation(); - RuntimeHints hints = generationContext.getRuntimeHints(); - // Domain types, related types, projections - ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar(); Stream.concat(Stream.of(information.getDomainType()), information.getAlternativeDomainTypes().stream()) .forEach(it -> { - - registrar.registerRuntimeHints(hints, it); configureTypeContribution(it, repositoryContext); }); - // TODO: Looks like a duplicate + // Domain types my be part of this, but it also contains reachable ones. repositoryContext.getResolvedTypes().stream() .filter(it -> TypeContributor.isPartOf(it, Set.of(information.getDomainType().getPackageName()))) .forEach(it -> configureTypeContribution(it, repositoryContext));