From 87565aefb65d631afffaacf6e7bffca3fc1fcc3d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 22 Sep 2025 12:39:03 +0200 Subject: [PATCH] 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. Closes: #3267 Original Pull Request: #3367 --- .../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 4c2247a01..29b071a42 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.Set; 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 @@ import org.springframework.util.StringUtils; /** * 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 @@ public interface AotContext extends EnvironmentCapable { * @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 @@ public interface AotContext extends EnvironmentCapable { } /** - * 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 @@ public interface AotContext extends EnvironmentCapable { * * @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 @@ public interface AotContext extends EnvironmentCapable { * @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 @@ public interface AotContext extends EnvironmentCapable { * * @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 @@ public interface AotContext extends EnvironmentCapable { * * @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 @@ public interface AotContext extends EnvironmentCapable { /** * 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 type the type to configure. * @param configurationConsumer configuration consumer function. + * @since 4.0 */ void typeConfiguration(Class 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 @@ public interface AotContext extends EnvironmentCapable { /** * 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 2adebba35..6879011e2 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 java.util.stream.Stream; 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 @@ public interface AotTypeConfiguration { */ 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 1608cf303..e155c27bb 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 @@ package org.springframework.data.aot; 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.function.Supplier; 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 @@ import org.springframework.util.StringUtils; * @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 @@ class DefaultAotContext implements AotContext { } @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 @@ class DefaultAotContext implements AotContext { } } + @SuppressWarnings("removal") class DefaultIntrospectedBeanDefinition implements IntrospectedBeanDefinition { private final String beanName; @@ -227,7 +231,6 @@ class DefaultAotContext implements AotContext { 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 f3a99e17a..a6c836561 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 @@ public class ManagedTypesBeanRegistrationAotProcessor implements BeanRegistratio 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 367162f87..f8abd3665 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 @@ import org.springframework.util.StringUtils; /** * 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 @@ public class AotRepositoryBeanDefinitionPropertiesDecorator { 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 f8c0787c9..4534592ba 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.lang.reflect.Method; 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.Log; 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 @@ class AotRepositoryCreator { 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 9f6c465f6..fe1ca3008 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 @@ class AotRepositoryFragmentMetadata { 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 725e4ac46..7823ba980 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 @@ package org.springframework.data.repository.aot.generate; 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 @@ public class RepositoryContributor { } /** - * 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 c0eac7cf2..f4e96b25f 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 @@ public interface AotRepositoryContext extends AotContext { */ 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 000000000..9bf7eb16a --- /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 4c1d0e538..8d22b31a2 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.Collection; 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 @@ import org.springframework.data.util.TypeUtils; * @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 @@ class DefaultAotRepositoryContext implements AotRepositoryContext { 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 @@ class DefaultAotRepositoryContext implements AotRepositoryContext { 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 @@ class DefaultAotRepositoryContext implements AotRepositoryContext { 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 0d4ebbd05..c2b2db820 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 @@ import org.springframework.util.ClassUtils; * @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 class RepositoryRegistrationAotContribution implements BeanRegistrationAo 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 c94b64852..99c54f865 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 @@ package org.springframework.data.repository.config; 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.annotation.MergedAnnotation; 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 @@ import org.springframework.util.Assert; * 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 @@ import org.springframework.util.Assert; 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 @@ public class RepositoryRegistrationAotProcessor 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 d16477954..88a365c24 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 @@ package org.springframework.data.aot; 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.junit.jupiter.params.provider.CsvSource; 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 @@ class AotContextUnitTests { 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 @@ class AotContextUnitTests { } @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 dadad5311..d82397b30 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 a38f86994..5249edf8f 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 @@ class AotRepositoryConfigurationUnitTests { 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 @@ class AotRepositoryConfigurationUnitTests { // 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 7e4e0747b..1b3a7861c 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 @@ import org.springframework.mock.env.MockEnvironment; * * @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 @@ class DummyModuleAotRepositoryContext implements AotRepositoryContext { @Override public MockEnvironment getEnvironment() { - return environment; + return (MockEnvironment) super.getEnvironment(); } @Override @@ -78,16 +75,6 @@ class DummyModuleAotRepositoryContext implements AotRepositoryContext { 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 @@ class DummyModuleAotRepositoryContext implements AotRepositoryContext { 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())); - } - } }