From 580060047069d6b4631c23c1c6c85476cea03a86 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 7 May 2025 12:21:15 +0200 Subject: [PATCH] Refine AOT composition detection. Associate Repository Bean Definition with RepositoryConfiguration and RepositoryConfigurationExtension attributes to capture configuration details such as the module name or the configuration source. Introduce RepositoryFragmentsContributor to provide an abstraction for structural fragment implementation allowing to describe the implementation type instead of requiring the implementation object. Obtain repository fragments from a RepositoryFragmentsContributor (either the configured one or one from a RepositoryFactoryBean). Closes: #3279 Original Pull Request: #3282 --- .../aot/generate/AotRepositoryBuilder.java | 103 +++++++--- .../aot/generate/RepositoryContributor.java | 31 ++- ...notationRepositoryConfigurationSource.java | 11 + .../config/AotRepositoryContext.java | 14 +- .../config/AotRepositoryInformation.java | 54 ++--- .../config/DefaultAotRepositoryContext.java | 58 +++--- .../DefaultRepositoryConfiguration.java | 9 +- .../RepositoryBeanDefinitionBuilder.java | 9 + .../RepositoryBeanDefinitionReader.java | 194 ++++++++++++------ .../config/RepositoryConfiguration.java | 12 +- .../RepositoryConfigurationAdapter.java | 5 + .../RepositoryConfigurationExtension.java | 13 +- .../config/RepositoryConfigurationSource.java | 9 + ...RepositoryRegistrationAotContribution.java | 161 ++++++++------- .../RepositoryRegistrationAotProcessor.java | 12 +- .../XmlRepositoryConfigurationSource.java | 5 + .../core/RepositoryInformation.java | 5 - .../support/DefaultRepositoryInformation.java | 7 +- .../support/RepositoryFactoryBeanSupport.java | 17 +- .../support/RepositoryFactoryInformation.java | 9 + .../core/support/RepositoryFragment.java | 59 ++++-- .../RepositoryFragmentsContributor.java | 56 +++++ .../data/repository/support/Repositories.java | 6 + .../ConfigWithCustomRepositoryBaseClass.java | 4 +- .../ConfigWithSimpleCrudRepository.java | 4 +- ...istrationAotProcessorIntegrationTests.java | 4 +- .../AotRepositoryBuilderUnitTests.java | 68 ++++-- .../DummyModuleAotRepositoryContext.java | 5 + .../RepositoryContributorUnitTests.java | 28 +-- ...epositoryConfigurationSourceUnitTests.java | 38 +++- .../config/DummyRegistrarWithContributor.java | 40 ++++ .../EnableRepositoriesWithContributor.java | 61 ++++++ .../RepositoryBeanDefinitionReaderTests.java | 103 +++++++--- .../SampleRepositoryFragmentsContributor.java | 33 +++ .../support/DummyRepositoryFactoryBean.java | 14 ++ .../support/RepositoriesUnitTests.java | 6 + 36 files changed, 912 insertions(+), 355 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/core/support/RepositoryFragmentsContributor.java create mode 100644 src/test/java/org/springframework/data/repository/config/DummyRegistrarWithContributor.java create mode 100644 src/test/java/org/springframework/data/repository/config/EnableRepositoriesWithContributor.java create mode 100644 src/test/java/org/springframework/data/repository/config/SampleRepositoryFragmentsContributor.java diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java index 199ca89f6..7ca6b536e 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java @@ -41,6 +41,7 @@ import org.springframework.data.repository.query.QueryMethod; import org.springframework.javapoet.ClassName; import org.springframework.javapoet.FieldSpec; import org.springframework.javapoet.JavaFile; +import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeSpec; @@ -53,6 +54,7 @@ import org.springframework.javapoet.TypeSpec; class AotRepositoryBuilder { private final RepositoryInformation repositoryInformation; + private final String moduleName; private final ProjectionFactory projectionFactory; private final AotRepositoryFragmentMetadata generationMetadata; @@ -60,9 +62,11 @@ class AotRepositoryBuilder { private @Nullable BiFunction> methodContributorFunction; private ClassCustomizer customizer; - private AotRepositoryBuilder(RepositoryInformation repositoryInformation, ProjectionFactory projectionFactory) { + private AotRepositoryBuilder(RepositoryInformation repositoryInformation, String moduleName, + ProjectionFactory projectionFactory) { this.repositoryInformation = repositoryInformation; + this.moduleName = moduleName; this.projectionFactory = projectionFactory; this.generationMetadata = new AotRepositoryFragmentMetadata(className()); @@ -74,11 +78,37 @@ class AotRepositoryBuilder { this.customizer = (info, metadata, builder) -> {}; } - public static AotRepositoryBuilder forRepository(RepositoryInformation repositoryInformation, + /** + * Create a new {@code AotRepositoryBuilder} for the given {@link RepositoryInformation}. + * + * @param information must not be {@literal null}. + * @param moduleName must not be {@literal null}. + * @param projectionFactory must not be {@literal null}. + * @return + */ + public static AotRepositoryBuilder forRepository(RepositoryInformation information, String moduleName, ProjectionFactory projectionFactory) { - return new AotRepositoryBuilder(repositoryInformation, projectionFactory); + return new AotRepositoryBuilder(information, moduleName, projectionFactory); } + /** + * Configure a {@link ClassCustomizer} customizer. + * + * @param classCustomizer must not be {@literal null}. + * @return {@code this}. + */ + public AotRepositoryBuilder withClassCustomizer(ClassCustomizer classCustomizer) { + + this.customizer = classCustomizer; + return this; + } + + /** + * Configure a {@link AotRepositoryConstructorBuilder} customizer. + * + * @param constructorCustomizer must not be {@literal null}. + * @return {@code this}. + */ public AotRepositoryBuilder withConstructorCustomizer( Consumer constructorCustomizer) { @@ -86,42 +116,33 @@ class AotRepositoryBuilder { return this; } + /** + * Configure a {@link MethodContributor}. + * + * @param methodContributorFunction must not be {@literal null}. + * @return {@code this}. + */ public AotRepositoryBuilder withQueryMethodContributor( BiFunction> methodContributorFunction) { + this.methodContributorFunction = methodContributorFunction; return this; } - public AotRepositoryBuilder withClassCustomizer(ClassCustomizer classCustomizer) { - - this.customizer = classCustomizer; - return this; - } - public AotBundle build() { + List methodMetadata = new ArrayList<>(); + RepositoryComposition repositoryComposition = repositoryInformation.getRepositoryComposition(); + // start creating the type TypeSpec.Builder builder = TypeSpec.classBuilder(this.generationMetadata.getTargetTypeName()) // .addModifiers(Modifier.PUBLIC) // .addAnnotation(Generated.class) // - .addJavadoc("AOT generated repository implementation for {@link $T}.\n", + .addJavadoc("AOT generated $L repository implementation for {@link $T}.\n", moduleName, repositoryInformation.getRepositoryInterface()); // create the constructor - AotRepositoryConstructorBuilder constructorBuilder = new AotRepositoryConstructorBuilder(repositoryInformation, - generationMetadata); - if (constructorCustomizer != null) { - constructorCustomizer.accept(constructorBuilder); - } - - builder.addMethod(constructorBuilder.buildConstructor()); - - List methodMetadata = new ArrayList<>(); - AotRepositoryMetadata.RepositoryType repositoryType = repositoryInformation.isReactiveRepository() - ? AotRepositoryMetadata.RepositoryType.REACTIVE - : AotRepositoryMetadata.RepositoryType.IMPERATIVE; - - RepositoryComposition repositoryComposition = repositoryInformation.getRepositoryComposition(); + builder.addMethod(buildConstructor()); Arrays.stream(repositoryInformation.getRepositoryInterface().getMethods()) .sorted(Comparator. comparing(it -> { @@ -136,12 +157,35 @@ class AotRepositoryBuilder { // finally customize the file itself this.customizer.customize(repositoryInformation, generationMetadata, builder); + JavaFile javaFile = JavaFile.builder(packageName(), builder.build()).build(); + AotRepositoryMetadata metadata = getAotRepositoryMetadata(methodMetadata); - AotRepositoryMetadata metadata = new AotRepositoryMetadata(repositoryInformation.getRepositoryInterface().getName(), - repositoryInformation.moduleName() != null ? repositoryInformation.moduleName() : "", repositoryType, methodMetadata); + return new AotBundle(javaFile, metadata); + } - return new AotBundle(javaFile, metadata.toJson()); + private MethodSpec buildConstructor() { + + AotRepositoryConstructorBuilder constructorBuilder = new AotRepositoryConstructorBuilder(repositoryInformation, + generationMetadata); + + if (constructorCustomizer != null) { + constructorCustomizer.accept(constructorBuilder); + } + + return constructorBuilder.buildConstructor(); + } + + private AotRepositoryMetadata getAotRepositoryMetadata(List methodMetadata) { + + AotRepositoryMetadata.RepositoryType repositoryType = repositoryInformation.isReactiveRepository() + ? AotRepositoryMetadata.RepositoryType.REACTIVE + : AotRepositoryMetadata.RepositoryType.IMPERATIVE; + + String jsonModuleName = moduleName.replaceAll("Reactive", "").trim(); + + return new AotRepositoryMetadata(repositoryInformation.getRepositoryInterface().getName(), jsonModuleName, + repositoryType, methodMetadata); } private void contributeMethod(Method method, RepositoryComposition repositoryComposition, @@ -185,8 +229,7 @@ class AotRepositoryBuilder { private AotRepositoryMethod getFragmentMetadata(Method method, RepositoryFragment fragment) { String signature = fragment.getSignatureContributor().getName(); - String implementation = fragment.getImplementation().map(it -> it.getClass().getName()).orElse(null); - + String implementation = fragment.getImplementationClass().map(Class::getName).orElse(null); AotFragmentTarget fragmentTarget = new AotFragmentTarget(signature, implementation); return new AotRepositoryMethod(method.getName(), method.toGenericString(), null, fragmentTarget); @@ -240,7 +283,7 @@ class AotRepositoryBuilder { } - record AotBundle(JavaFile javaFile, JSONObject metadata) { + record AotBundle(JavaFile javaFile, AotRepositoryMetadata metadata) { } } 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 9fffacb9c..bcfc9d7a1 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 @@ -39,6 +39,7 @@ import org.springframework.util.StringUtils; * * @author Christoph Strobl * @author Mark Paluch + * @since 4.0 */ public class RepositoryContributor { @@ -46,19 +47,34 @@ public class RepositoryContributor { private final AotRepositoryBuilder builder; + /** + * Create a new {@code RepositoryContributor} for the given {@link AotRepositoryContext}. + * + * @param repositoryContext + */ public RepositoryContributor(AotRepositoryContext repositoryContext) { this.builder = AotRepositoryBuilder.forRepository(repositoryContext.getRepositoryInformation(), - createProjectionFactory()); + repositoryContext.getModuleName(), createProjectionFactory()); } + /** + * @return a new {@link ProjectionFactory} to be used with the AOT repository builder. The actual instance should be + * accessed through {@link #getProjectionFactory()}. + */ protected ProjectionFactory createProjectionFactory() { return new SpelAwareProxyProjectionFactory(); } + /** + * @return the used {@link ProjectionFactory}. + */ protected ProjectionFactory getProjectionFactory() { return builder.getProjectionFactory(); } + /** + * @return the used {@link RepositoryInformation}. + */ protected RepositoryInformation getRepositoryInformation() { return builder.getRepositoryInformation(); } @@ -73,13 +89,10 @@ public class RepositoryContributor { public void contribute(GenerationContext generationContext) { - // TODO: do we need - generationContext.withName("spring-data"); - - builder.withClassCustomizer(this::customizeClass); - builder.withConstructorCustomizer(this::customizeConstructor); - builder.withQueryMethodContributor(this::contributeQueryMethod); - - AotRepositoryBuilder.AotBundle aotBundle = builder.build(); + AotRepositoryBuilder.AotBundle aotBundle = builder.withClassCustomizer(this::customizeClass) // + .withConstructorCustomizer(this::customizeConstructor) // + .withQueryMethodContributor(this::contributeQueryMethod) // + .build(); Class repositoryInterface = getRepositoryInformation().getRepositoryInterface(); String repositoryJsonFileName = getRepositoryJsonFileName(repositoryInterface); @@ -89,7 +102,7 @@ public class RepositoryContributor { String repositoryJson; try { - repositoryJson = aotBundle.metadata().toString(2); + repositoryJson = aotBundle.metadata().toJson().toString(2); } catch (JSONException e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSource.java b/src/main/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSource.java index f143cbb2a..48f2d42f9 100644 --- a/src/main/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSource.java +++ b/src/main/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSource.java @@ -65,6 +65,7 @@ public class AnnotationRepositoryConfigurationSource extends RepositoryConfigura private static final String QUERY_LOOKUP_STRATEGY = "queryLookupStrategy"; private static final String REPOSITORY_FACTORY_BEAN_CLASS = "repositoryFactoryBeanClass"; private static final String REPOSITORY_BASE_CLASS = "repositoryBaseClass"; + private static final String REPOSITORY_FRAGMENTS_CONTRIBUTOR_CLASS = "fragmentsContributor"; private static final String CONSIDER_NESTED_REPOSITORIES = "considerNestedRepositories"; private static final String BOOTSTRAP_MODE = "bootstrapMode"; private static final String BEAN_NAME_GENERATOR = "nameGenerator"; @@ -187,6 +188,16 @@ public class AnnotationRepositoryConfigurationSource extends RepositoryConfigura : Optional.of(repositoryBaseClass.getName()); } + @Override + public Optional getRepositoryFragmentsContributorClassName() { + + if (!attributes.containsKey(REPOSITORY_FRAGMENTS_CONTRIBUTOR_CLASS)) { + return Optional.empty(); + } + + return Optional.of(attributes.getClass(REPOSITORY_FRAGMENTS_CONTRIBUTOR_CLASS).getName()); + } + /** * Returns the {@link AnnotationAttributes} of the annotation configured. * 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 995aa0408..231e7bba1 100644 --- a/src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java +++ b/src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java @@ -16,9 +16,9 @@ package org.springframework.data.repository.config; import java.lang.annotation.Annotation; +import java.util.Collection; import java.util.Set; -import org.springframework.core.SpringProperties; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.data.aot.AotContext; import org.springframework.data.repository.core.RepositoryInformation; @@ -28,8 +28,9 @@ import org.springframework.data.repository.core.RepositoryInformation; * * @author Christoph Strobl * @author John Blum - * @see AotContext + * @author Mark Paluch * @since 3.0 + * @see AotContext */ public interface AotRepositoryContext extends AotContext { @@ -38,6 +39,12 @@ public interface AotRepositoryContext extends AotContext { */ String getBeanName(); + /** + * @return the Spring Data module name, see {@link RepositoryConfigurationExtension#getModuleName()}. + * @since 4.0 + */ + String getModuleName(); + /** * @return a {@link Set} of {@link String base packages} to search for repositories. */ @@ -46,7 +53,7 @@ public interface AotRepositoryContext extends AotContext { /** * @return the {@link Annotation} types used to identify domain types. */ - Set> getIdentifyingAnnotations(); + Collection> getIdentifyingAnnotations(); /** * @return {@link RepositoryInformation metadata} about the repository itself. @@ -64,4 +71,5 @@ public interface AotRepositoryContext extends AotContext { * @return all {@link Class types} reachable from the repository. */ Set> getResolvedTypes(); + } diff --git a/src/main/java/org/springframework/data/repository/config/AotRepositoryInformation.java b/src/main/java/org/springframework/data/repository/config/AotRepositoryInformation.java index 1ddcbde9a..0237d5136 100644 --- a/src/main/java/org/springframework/data/repository/config/AotRepositoryInformation.java +++ b/src/main/java/org/springframework/data/repository/config/AotRepositoryInformation.java @@ -17,52 +17,40 @@ package org.springframework.data.repository.config; import java.lang.reflect.Method; import java.util.Collection; -import java.util.LinkedHashSet; import java.util.Set; -import java.util.function.Supplier; -import org.jspecify.annotations.Nullable; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryInformationSupport; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; import org.springframework.data.repository.core.support.RepositoryFragment; -import org.springframework.data.util.Lazy; /** * {@link RepositoryInformation} based on {@link RepositoryMetadata} collected at build time. * * @author Christoph Strobl + * @author Mark Paluch * @since 3.0 */ -class AotRepositoryInformation extends RepositoryInformationSupport implements RepositoryInformation { +public class AotRepositoryInformation extends RepositoryInformationSupport implements RepositoryInformation { - private final @Nullable String moduleName; - private final Supplier>> fragments; + private final RepositoryComposition fragmentsComposition; + private final RepositoryComposition baseComposition; + private final RepositoryComposition composition; - private final Lazy repositoryComposition; - private final Lazy baseComposition; + public AotRepositoryInformation(RepositoryMetadata repositoryMetadata, Class repositoryBaseClass, + Collection> fragments) { - AotRepositoryInformation(@Nullable String moduleName, Supplier repositoryMetadata, - Supplier> repositoryBaseClass, Supplier>> fragments) { + super(() -> repositoryMetadata, () -> repositoryBaseClass); - super(repositoryMetadata, repositoryBaseClass); + this.fragmentsComposition = RepositoryComposition.fromMetadata(getMetadata()) + .append(RepositoryFragments.from(fragments)); + this.baseComposition = RepositoryComposition.of(RepositoryFragment.structural(getRepositoryBaseClass())) // + .withArgumentConverter(this.fragmentsComposition.getArgumentConverter()) // + .withMethodLookup(this.fragmentsComposition.getMethodLookup()); - this.moduleName = moduleName; - this.fragments = fragments; - - this.repositoryComposition = Lazy - .of(() -> RepositoryComposition.fromMetadata(getMetadata()).append(RepositoryFragments.from(getFragments()))); - - this.baseComposition = Lazy.of(() -> { - - RepositoryComposition targetRepoComposition = repositoryComposition.get(); - - return RepositoryComposition.of(RepositoryFragment.structural(getRepositoryBaseClass())) // - .withArgumentConverter(targetRepoComposition.getArgumentConverter()) // - .withMethodLookup(targetRepoComposition.getMethodLookup()); - }); + this.composition = this.fragmentsComposition.append(this.baseComposition.getFragments()); } /** @@ -71,31 +59,27 @@ class AotRepositoryInformation extends RepositoryInformationSupport implements R */ @Override public Set> getFragments() { - return new LinkedHashSet<>(fragments.get()); + return fragmentsComposition.getFragments().toSet(); } @Override public boolean isCustomMethod(Method method) { - return repositoryComposition.get().findMethod(method).isPresent(); + return fragmentsComposition.findMethod(method).isPresent(); } @Override public boolean isBaseClassMethod(Method method) { - return baseComposition.get().findMethod(method).isPresent(); + return baseComposition.findMethod(method).isPresent(); } @Override public Method getTargetClassMethod(Method method) { - return baseComposition.get().findMethod(method).orElse(method); + return baseComposition.findMethod(method).orElse(method); } @Override public RepositoryComposition getRepositoryComposition() { - return repositoryComposition.get(); + return composition; } - @Override - public @Nullable String moduleName() { - return moduleName; - } } 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 f40985b27..5f695f627 100644 --- a/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java +++ b/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2022. the original author or authors. + * Copyright 2022-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. @@ -16,14 +16,14 @@ package org.springframework.data.repository.config; import java.lang.annotation.Annotation; +import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; import java.util.stream.Collectors; -import org.jspecify.annotations.Nullable; - 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; @@ -37,29 +37,42 @@ import org.springframework.data.util.TypeUtils; * * @author Christoph Strobl * @author John Blum + * @author Mark Paluch * @see AotRepositoryContext * @since 3.0 */ @SuppressWarnings("NullAway") // TODO class DefaultAotRepositoryContext implements AotRepositoryContext { + private final RegisteredBean bean; + private final String moduleName; private final AotContext aotContext; + private final RepositoryInformation repositoryInformation; private final Lazy>> resolvedAnnotations = Lazy.of(this::discoverAnnotations); private final Lazy>> managedTypes = Lazy.of(this::discoverTypes); - private @Nullable RepositoryInformation repositoryInformation; - private @Nullable Set basePackages; - private @Nullable Set> identifyingAnnotations; - private @Nullable String beanName; + private Set basePackages = Collections.emptySet(); + private Collection> identifyingAnnotations = Collections.emptySet(); + private String beanName; - public DefaultAotRepositoryContext(AotContext aotContext) { + public DefaultAotRepositoryContext(RegisteredBean bean, RepositoryInformation repositoryInformation, + String moduleName, AotContext aotContext) { + this.bean = bean; + this.repositoryInformation = repositoryInformation; + this.moduleName = moduleName; this.aotContext = aotContext; + this.beanName = bean.getBeanName(); } public AotContext getAotContext() { return aotContext; } + @Override + public String getModuleName() { + return moduleName; + } + @Override public ConfigurableListableBeanFactory getBeanFactory() { return getAotContext().getBeanFactory(); @@ -72,7 +85,7 @@ class DefaultAotRepositoryContext implements AotRepositoryContext { @Override public Set getBasePackages() { - return basePackages == null ? Collections.emptySet() : basePackages; + return basePackages; } public void setBasePackages(Set basePackages) { @@ -89,11 +102,11 @@ class DefaultAotRepositoryContext implements AotRepositoryContext { } @Override - public Set> getIdentifyingAnnotations() { - return identifyingAnnotations == null ? Collections.emptySet() : identifyingAnnotations; + public Collection> getIdentifyingAnnotations() { + return identifyingAnnotations; } - public void setIdentifyingAnnotations(Set> identifyingAnnotations) { + public void setIdentifyingAnnotations(Collection> identifyingAnnotations) { this.identifyingAnnotations = identifyingAnnotations; } @@ -102,10 +115,6 @@ class DefaultAotRepositoryContext implements AotRepositoryContext { return repositoryInformation; } - public void setRepositoryInformation(RepositoryInformation repositoryInformation) { - this.repositoryInformation = repositoryInformation; - } - @Override public Set> getResolvedAnnotations() { return resolvedAnnotations.get(); @@ -132,24 +141,18 @@ class DefaultAotRepositoryContext implements AotRepositoryContext { .flatMap(type -> TypeUtils.resolveUsedAnnotations(type).stream()) .collect(Collectors.toCollection(LinkedHashSet::new)); - if (repositoryInformation != null) { - annotations.addAll(TypeUtils.resolveUsedAnnotations(repositoryInformation.getRepositoryInterface())); - } + annotations.addAll(TypeUtils.resolveUsedAnnotations(repositoryInformation.getRepositoryInterface())); return annotations; } protected Set> discoverTypes() { - Set> types = new LinkedHashSet<>(); + Set> types = new LinkedHashSet<>(TypeCollector.inspect(repositoryInformation.getDomainType()).list()); - if (repositoryInformation != null) { - types.addAll(TypeCollector.inspect(repositoryInformation.getDomainType()).list()); - - repositoryInformation.getQueryMethods().stream() - .flatMap(it -> TypeUtils.resolveTypesInSignature(repositoryInformation.getRepositoryInterface(), it).stream()) - .flatMap(it -> TypeCollector.inspect(it).list().stream()).forEach(types::add); - } + repositoryInformation.getQueryMethods().stream() + .flatMap(it -> TypeUtils.resolveTypesInSignature(repositoryInformation.getRepositoryInterface(), it).stream()) + .flatMap(it -> TypeCollector.inspect(it).list().stream()).forEach(types::add); if (!getIdentifyingAnnotations().isEmpty()) { @@ -160,4 +163,5 @@ class DefaultAotRepositoryContext implements AotRepositoryContext { return types; } + } diff --git a/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java b/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java index 1ebdb1c90..eefcdf404 100644 --- a/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java +++ b/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java @@ -114,12 +114,17 @@ public class DefaultRepositoryConfiguration getRepositoryBaseClassName() { - return configurationSource.getRepositoryBaseClassName(); + return configurationSource.getRepositoryBaseClassName() + .or(() -> Optional.ofNullable(extension.getRepositoryBaseClassName())); + } + + @Override + public Optional getRepositoryFragmentsContributorClassName() { + return configurationSource.getRepositoryFragmentsContributorClassName(); } @Override public String getRepositoryFactoryBeanClassName() { - return configurationSource.getRepositoryFactoryBeanClassName() .orElseGet(extension::getRepositoryFactoryBeanClassName); } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java index ada478eb9..994ae1e0c 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java @@ -116,6 +116,11 @@ class RepositoryBeanDefinitionBuilder { .rootBeanDefinition(configuration.getRepositoryFactoryBeanClassName()); builder.getRawBeanDefinition().setSource(configuration.getSource()); + + // AOT Repository hints + builder.getRawBeanDefinition().setAttribute(RepositoryConfiguration.class.getName(), configuration); + builder.getRawBeanDefinition().setAttribute(RepositoryConfigurationExtension.class.getName(), extension); + builder.addConstructorArgValue(configuration.getRepositoryInterface()); builder.addPropertyValue("queryLookupStrategyKey", configuration.getQueryLookupStrategyKey()); builder.addPropertyValue("lazyInit", configuration.isLazyInit()); @@ -125,6 +130,10 @@ class RepositoryBeanDefinitionBuilder { configuration.getRepositoryBaseClassName()// .ifPresent(it -> builder.addPropertyValue("repositoryBaseClass", it)); + configuration.getRepositoryFragmentsContributorClassName()// + .ifPresent(it -> builder.addPropertyValue("repositoryFragmentsContributor", + BeanDefinitionBuilder.genericBeanDefinition(it).getRawBeanDefinition())); + NamedQueriesBeanDefinitionBuilder definitionBuilder = new NamedQueriesBeanDefinitionBuilder( extension.getDefaultNamedQueryLocation()); configuration.getNamedQueriesLocation().ifPresent(definitionBuilder::setLocations); diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionReader.java b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionReader.java index 1209903d3..6d064a294 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionReader.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionReader.java @@ -15,139 +15,197 @@ */ package org.springframework.data.repository.config; +import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.RegisteredBean; -import org.springframework.core.ResolvableType; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFragment; -import org.springframework.data.repository.core.support.RepositoryFragment.ImplementedRepositoryFragment; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; /** * Reader used to extract {@link RepositoryInformation} from {@link RepositoryConfiguration}. * * @author Christoph Strobl * @author John Blum + * @author Mark Paluch * @since 3.0 */ class RepositoryBeanDefinitionReader { - /** - * @return - */ - static RepositoryInformation repositoryInformation(RepositoryConfiguration repoConfig, RegisteredBean repoBean) { - return repositoryInformation(repoConfig, repoBean.getMergedBeanDefinition(), repoBean.getBeanFactory()); + private final RootBeanDefinition beanDefinition; + private final ConfigurableListableBeanFactory beanFactory; + private final ClassLoader beanClassLoader; + private final @Nullable RepositoryConfiguration configuration; + private final @Nullable RepositoryConfigurationExtensionSupport extension; + + public RepositoryBeanDefinitionReader(RegisteredBean bean) { + + this.beanDefinition = bean.getMergedBeanDefinition(); + this.beanFactory = bean.getBeanFactory(); + this.beanClassLoader = bean.getBeanClass().getClassLoader(); + this.configuration = (RepositoryConfiguration) beanDefinition + .getAttribute(RepositoryConfiguration.class.getName()); + this.extension = (RepositoryConfigurationExtensionSupport) beanDefinition + .getAttribute(RepositoryConfigurationExtension.class.getName()); + } + + public @Nullable RepositoryConfiguration getConfiguration() { + return this.configuration; + } + + public @Nullable RepositoryConfigurationExtensionSupport getConfigurationExtension() { + return this.extension; } /** - * @param source the RepositoryFactoryBeanSupport bean definition. - * @param beanFactory - * @return + * @return the {@link RepositoryInformation} derived from the repository bean. */ - @SuppressWarnings("NullAway") - static RepositoryInformation repositoryInformation(RepositoryConfiguration repoConfig, BeanDefinition source, - ConfigurableListableBeanFactory beanFactory) { + public RepositoryInformation getRepositoryInformation() { RepositoryMetadata metadata = AbstractRepositoryMetadata - .getMetadata(forName(repoConfig.getRepositoryInterface(), beanFactory)); - Class repositoryBaseClass = readRepositoryBaseClass(source, beanFactory); - List> fragmentList = readRepositoryFragments(source, beanFactory); - if (source.getPropertyValues().contains("customImplementation")) { + .getMetadata(forName(configuration.getRepositoryInterface())); + Class repositoryBaseClass = getRepositoryBaseClass(); - Object o = source.getPropertyValues().get("customImplementation"); - if (o instanceof RuntimeBeanReference rbr) { - BeanDefinition customImplBeanDefintion = beanFactory.getBeanDefinition(rbr.getBeanName()); - Class beanType = forName(customImplBeanDefintion.getBeanClassName(), beanFactory); - ResolvableType[] interfaces = ResolvableType.forClass(beanType).getInterfaces(); - if (interfaces.length == 1) { - fragmentList.add(new ImplementedRepositoryFragment(interfaces[0].toClass(), beanType)); - } else { - boolean found = false; - for (ResolvableType i : interfaces) { - if (beanType.getSimpleName().contains(i.resolve().getSimpleName())) { - fragmentList.add(new ImplementedRepositoryFragment(interfaces[0].toClass(), beanType)); - found = true; - break; - } - } - if (!found) { - fragmentList.add(RepositoryFragment.implemented(beanType)); - } - } + List> fragments = new ArrayList<>(); + fragments.addAll(readRepositoryFragments()); + fragments.addAll(readContributedRepositoryFragments(metadata)); + + RepositoryFragment customImplementation = getCustomImplementation(); + if (customImplementation != null) { + fragments.add(0, customImplementation); + } + + return new AotRepositoryInformation(metadata, repositoryBaseClass, fragments); + } + + private @Nullable RepositoryFragment getCustomImplementation() { + + PropertyValues mpv = beanDefinition.getPropertyValues(); + PropertyValue customImplementation = mpv.getPropertyValue("customImplementation"); + + if (customImplementation != null) { + + if (customImplementation.getValue() instanceof RuntimeBeanReference rbr) { + BeanDefinition customImplementationBean = beanFactory.getBeanDefinition(rbr.getBeanName()); + Class beanType = getClass(customImplementationBean); + return RepositoryFragment.structural(beanType); + } else if (customImplementation.getValue() instanceof BeanDefinition bd) { + Class beanType = getClass(bd); + return RepositoryFragment.structural(beanType); } } - String moduleName = (String) source.getPropertyValues().get("moduleName"); - AotRepositoryInformation repositoryInformation = new AotRepositoryInformation(moduleName, () -> metadata, - () -> repositoryBaseClass, () -> fragmentList); - return repositoryInformation; + return null; } @SuppressWarnings("NullAway") - private static Class readRepositoryBaseClass(BeanDefinition source, ConfigurableListableBeanFactory beanFactory) { + private Class getRepositoryBaseClass() { + + Object repoBaseClassName = beanDefinition.getPropertyValues().get("repositoryBaseClass"); - Object repoBaseClassName = source.getPropertyValues().get("repositoryBaseClass"); if (repoBaseClassName != null) { - return forName(repoBaseClassName.toString(), beanFactory); - } - if (source.getPropertyValues().contains("moduleBaseClass")) { - return forName((String) source.getPropertyValues().get("moduleBaseClass"), beanFactory); + return forName(repoBaseClassName.toString()); } + return Dummy.class; } @SuppressWarnings("NullAway") - private static List> readRepositoryFragments(BeanDefinition source, - ConfigurableListableBeanFactory beanFactory) { + private List> readRepositoryFragments() { - RuntimeBeanReference beanReference = (RuntimeBeanReference) source.getPropertyValues().get("repositoryFragments"); + RuntimeBeanReference beanReference = (RuntimeBeanReference) beanDefinition.getPropertyValues() + .get("repositoryFragments"); BeanDefinition fragments = beanFactory.getBeanDefinition(beanReference.getBeanName()); ValueHolder fragmentBeanNameList = fragments.getConstructorArgumentValues().getArgumentValue(0, List.class); List fragmentBeanNames = (List) fragmentBeanNameList.getValue(); List> fragmentList = new ArrayList<>(); + for (String beanName : fragmentBeanNames) { BeanDefinition fragmentBeanDefinition = beanFactory.getBeanDefinition(beanName); - ValueHolder argumentValue = fragmentBeanDefinition.getConstructorArgumentValues().getArgumentValue(0, - String.class); - ValueHolder argumentValue1 = fragmentBeanDefinition.getConstructorArgumentValues().getArgumentValue(1, null, null, - null); - Object fragmentClassName = argumentValue.getValue(); + ConstructorArgumentValues cv = fragmentBeanDefinition.getConstructorArgumentValues(); + ValueHolder interfaceClassVh = cv.getArgumentValue(0, String.class); + ValueHolder implementationVh = cv.getArgumentValue(1, null, null, null); - try { - Class type = ClassUtils.forName(fragmentClassName.toString(), beanFactory.getBeanClassLoader()); + Object fragmentClassName = interfaceClassVh.getValue(); + Class interfaceClass = forName(fragmentClassName.toString()); - if (argumentValue1 != null && argumentValue1.getValue() instanceof RuntimeBeanReference rbf) { - BeanDefinition implBeanDef = beanFactory.getBeanDefinition(rbf.getBeanName()); - Class implClass = ClassUtils.forName(implBeanDef.getBeanClassName(), beanFactory.getBeanClassLoader()); - fragmentList.add(new RepositoryFragment.ImplementedRepositoryFragment(type, implClass)); - } else { - fragmentList.add(RepositoryFragment.structural(type)); - } - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); + if (implementationVh != null && implementationVh.getValue() instanceof RuntimeBeanReference rbf) { + BeanDefinition implBeanDef = beanFactory.getBeanDefinition(rbf.getBeanName()); + Class implClass = getClass(implBeanDef); + fragmentList.add(RepositoryFragment.structural(interfaceClass, implClass)); + } else { + fragmentList.add(RepositoryFragment.structural(interfaceClass)); } } + return fragmentList; } + private List> readContributedRepositoryFragments(RepositoryMetadata metadata) { + + RepositoryFragmentsContributor contributor = getFragmentsContributor(metadata.getRepositoryInterface()); + return contributor.describe(metadata).stream().toList(); + } + + private RepositoryFragmentsContributor getFragmentsContributor(Class repositoryInterface) { + + Object repositoryFragmentsContributor = beanDefinition.getPropertyValues().get("repositoryFragmentsContributor"); + + if (repositoryFragmentsContributor instanceof BeanDefinition bd) { + return (RepositoryFragmentsContributor) BeanUtils.instantiateClass(getClass(bd)); + } + + Class repositoryFactoryBean = forName(beanDefinition.getBeanClassName()); + Constructor constructor = ClassUtils.getConstructorIfAvailable(repositoryFactoryBean, Class.class); + + if (constructor == null) { + throw new IllegalStateException("No constructor accepting Class in " + repositoryFactoryBean.getName()); + } + RepositoryFactoryBeanSupport factoryBean = (RepositoryFactoryBeanSupport) BeanUtils + .instantiateClass(constructor, repositoryInterface); + + return factoryBean.getRepositoryFragmentsContributor(); + } + + private Class getClass(BeanDefinition definition) { + + String beanClassName = definition.getBeanClassName(); + + if (ObjectUtils.isEmpty(beanClassName)) { + throw new IllegalStateException("No bean class name specified for %s".formatted(definition)); + } + + return forName(beanClassName); + } + static abstract class Dummy implements CrudRepository, PagingAndSortingRepository {} - static Class forName(String name, ConfigurableListableBeanFactory beanFactory) { + private Class forName(String name) { try { - return ClassUtils.forName(name, beanFactory.getBeanClassLoader()); + return ClassUtils.forName(name, beanClassLoader); } catch (ClassNotFoundException cause) { throw new TypeNotPresentException(name, cause); } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java index 0a42dcd10..4ee3ceb4a 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java @@ -78,6 +78,15 @@ public interface RepositoryConfiguration getRepositoryBaseClassName(); + /** + * Returns the name of the repository fragments contributor class to be used or {@link Optional#empty()} if the store + * specific defaults shall be applied. + * + * @return + * @since 4.0 + */ + Optional getRepositoryFragmentsContributorClassName(); + /** * Returns the name of the repository factory bean class to be used. * @@ -157,11 +166,12 @@ public interface RepositoryConfiguration return repositoryConfiguration.getRepositoryBaseClassName(); } + @Override + public Optional getRepositoryFragmentsContributorClassName() { + return repositoryConfiguration.getRepositoryFragmentsContributorClassName(); + } + @Override public String getRepositoryFactoryBeanClassName() { return repositoryConfiguration.getRepositoryFactoryBeanClassName(); diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java index 1b9531da3..1c5a5530f 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java @@ -18,7 +18,7 @@ package org.springframework.data.repository.config; import java.util.Collection; import java.util.Locale; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; import org.springframework.beans.factory.config.BeanDefinition; @@ -63,7 +63,6 @@ public interface RepositoryConfigurationExtension { * @see org.springframework.beans.factory.aot.BeanRegistrationAotProcessor * @since 3.0 */ - @NonNull default Class getRepositoryAotProcessor() { return RepositoryRegistrationAotProcessor.class; } @@ -90,6 +89,16 @@ public interface RepositoryConfigurationExtension { */ String getDefaultNamedQueryLocation(); + /** + * Returns the {@link String name} of the repository base class to be used. + * + * @return can be {@literal null} if the base class cannot be provided. + * @since 4.0 + */ + default @Nullable String getRepositoryBaseClassName() { + return null; + } + /** * Returns the {@link String name} of the repository factory class to be used. * diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSource.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSource.java index af1dec7a6..7d750f6cf 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSource.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSource.java @@ -81,6 +81,15 @@ public interface RepositoryConfigurationSource { */ Optional getRepositoryBaseClassName(); + /** + * Returns the name of the repository fragments contributor class to be used or {@link Optional#empty()} if the store + * specific defaults shall be applied. + * + * @return + * @since 4.0 + */ + Optional getRepositoryFragmentsContributorClassName(); + /** * Returns the name of the repository factory bean class or {@link Optional#empty()} if not defined in the source. * 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 40b2cc43a..feddf13e5 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java @@ -16,18 +16,18 @@ package org.springframework.data.repository.config; import java.io.Serializable; -import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Predicate; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; + import org.springframework.aop.SpringProxy; import org.springframework.aop.framework.Advised; import org.springframework.aot.generate.GenerationContext; @@ -37,11 +37,11 @@ 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.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.DecoratingProxy; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.Environment; import org.springframework.data.aot.AotContext; import org.springframework.data.projection.EntityProjectionIntrospector; import org.springframework.data.projection.TargetAware; @@ -66,33 +66,38 @@ import org.springframework.util.ClassUtils; * @author Mark Paluch * @since 3.0 */ -// TODO: Consider moving to data.repository.aot public class RepositoryRegistrationAotContribution implements BeanRegistrationAotContribution { + 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 @Nullable AotRepositoryContext repositoryContext; - private @Nullable BiFunction moduleContribution; - private final RepositoryRegistrationAotProcessor aotProcessor; - /** * 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) { + 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; } /** @@ -101,16 +106,57 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo * * @param processor reference back to the {@link RepositoryRegistrationAotProcessor} from which this contribution was * created. - * @return a new instance of {@link RepositoryRegistrationAotContribution}. - * @throws IllegalArgumentException if the {@link RepositoryRegistrationAotProcessor} is {@literal null}. + * @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 RepositoryRegistrationAotContribution fromProcessor(RepositoryRegistrationAotProcessor processor) { - return new RepositoryRegistrationAotContribution(processor); + 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, + repositoryMetadata); + + if (repositoryContext == null) { + return null; + } + + return new RepositoryRegistrationAotContribution(processor, repositoryContext); } - protected ConfigurableListableBeanFactory getBeanFactory() { - return getRepositoryRegistrationAotProcessor().getBeanFactory(); + /** + * 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) { + + RepositoryConfiguration repositoryMetadata = getRepositoryRegistrationAotProcessor() + .getRepositoryMetadata(repositoryBean); + + if (repositoryMetadata == null) { + return null; + } + + AotRepositoryContext repositoryContext = buildAotRepositoryContext(aotProcessor.getEnvironment(), repositoryBean, + repositoryMetadata); + + if (repositoryContext == null) { + return null; + } + + return new RepositoryRegistrationAotContribution(getRepositoryRegistrationAotProcessor(), repositoryContext); } protected @Nullable BiFunction getModuleContribution() { @@ -118,10 +164,6 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo } protected AotRepositoryContext getRepositoryContext() { - - Assert.state(this.repositoryContext != null, - "The AOT RepositoryContext was not properly initialized; did you call the forBean(:RegisteredBean) method"); - return this.repositoryContext; } @@ -137,28 +179,27 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo getRepositoryRegistrationAotProcessor().logTrace(message, arguments); } - /** - * 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}. - * @see org.springframework.beans.factory.support.RegisteredBean - */ - public RepositoryRegistrationAotContribution forBean(RegisteredBean repositoryBean) { + private static @Nullable AotRepositoryContext buildAotRepositoryContext(Environment environment, RegisteredBean bean, + RepositoryConfiguration repositoryConfiguration) { - Assert.notNull(repositoryBean, "The RegisteredBean for the repository must not be null"); + RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(bean); + RepositoryConfiguration configuration = reader.getConfiguration(); + RepositoryConfigurationExtensionSupport extension = reader.getConfigurationExtension(); - RepositoryConfiguration repositoryMetadata = getRepositoryRegistrationAotProcessor() - .getRepositoryMetadata(repositoryBean); + 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)); - Assert.state(repositoryMetadata != null, "The RepositoryConfiguration for the repository must not be null"); + repositoryContext.setBasePackages(repositoryConfiguration.getBasePackages().toSet()); + repositoryContext.setIdentifyingAnnotations(extension.getIdentifyingAnnotations()); - this.repositoryContext = buildAotRepositoryContext(repositoryBean, repositoryMetadata); - - return this; + return repositoryContext; } /** @@ -176,9 +217,6 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo @Override public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { - Assert.state(this.repositoryContext != null, - "RepositoryContext cannot be null. Make sure to initialize this class with forBean(…)."); - contributeRepositoryInfo(this.repositoryContext, generationContext); var moduleContribution = getModuleContribution(); @@ -219,6 +257,10 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo }; } + public Predicate> typeFilter() { // like only document ones. // TODO: As in MongoDB? + return Predicates.isTrue(); + } + private void contributeRepositoryInfo(AotRepositoryContext repositoryContext, GenerationContext contribution) { RepositoryInformation repositoryInformation = getRepositoryInformation(); @@ -239,7 +281,7 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo for (RepositoryFragment fragment : getRepositoryInformation().getFragments()) { Class repositoryFragmentType = fragment.getSignatureContributor(); - Optional implementation = fragment.getImplementation(); + Optional> implementation = fragment.getImplementationClass(); contribution.getRuntimeHints().reflection().registerType(repositoryFragmentType, hint -> { @@ -250,13 +292,12 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo } }); - implementation.ifPresent(impl -> { - Class typeToRegister = impl instanceof Class c ? c : impl.getClass(); + implementation.ifPresent(typeToRegister -> { contribution.getRuntimeHints().reflection().registerType(typeToRegister, hint -> { hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); - if (!impl.getClass().isInterface()) { + if (!typeToRegister.isInterface()) { hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); } }); @@ -289,12 +330,6 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo } // }); - // Reactive Repositories - if (repositoryInformation.isReactiveRepository()) { - // TODO: do we still need this and how to configure it? - // registry.initialization().add(NativeInitializationEntry.ofBuildTimeType(configuration.getRepositoryInterface())); - } - // Kotlin if (isKotlinCoroutineRepository(repositoryContext, repositoryInformation)) { contribution.getRuntimeHints().reflection().registerTypes(kotlinRepositoryReflectionTypeReferences(), @@ -356,29 +391,5 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo || ClassUtils.isPrimitiveArray(type); // } - public Predicate> typeFilter() { // like only document ones. // TODO: As in MongoDB? - return Predicates.isTrue(); - } - - @SuppressWarnings("rawtypes") - private DefaultAotRepositoryContext buildAotRepositoryContext(RegisteredBean bean, - RepositoryConfiguration repositoryConfiguration) { - - DefaultAotRepositoryContext repositoryContext = new DefaultAotRepositoryContext( - AotContext.from(getBeanFactory(), getRepositoryRegistrationAotProcessor().getEnvironment())); - - repositoryContext.setBeanName(bean.getBeanName()); - repositoryContext.setBasePackages(repositoryConfiguration.getBasePackages().toSet()); - repositoryContext.setIdentifyingAnnotations(resolveIdentifyingAnnotations()); - repositoryContext - .setRepositoryInformation(RepositoryBeanDefinitionReader.repositoryInformation(repositoryConfiguration, bean)); - - return repositoryContext; - } - - // TODO: Capture Repository Config - private Set> resolveIdentifyingAnnotations() { - return Collections.emptySet(); - } } 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 7bed43d30..4fbb08610 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java @@ -69,6 +69,7 @@ import org.springframework.util.Assert; * * @author Christoph Strobl * @author John Blum + * @author Mark Paluch * @since 3.0 */ public class RepositoryRegistrationAotProcessor @@ -123,11 +124,16 @@ public class RepositoryRegistrationAotProcessor return getConfigMap().containsKey(bean.getBeanName()); } - protected RepositoryRegistrationAotContribution newRepositoryRegistrationAotContribution( + protected @Nullable RepositoryRegistrationAotContribution newRepositoryRegistrationAotContribution( RegisteredBean repositoryBean) { - RepositoryRegistrationAotContribution contribution = RepositoryRegistrationAotContribution.fromProcessor(this) - .forBean(repositoryBean); + RepositoryRegistrationAotContribution contribution = RepositoryRegistrationAotContribution.load(this, + repositoryBean); + + // cannot contribute a repository bean. + if (contribution == null) { + return null; + } //TODO: add the hook for customizing bean initialization code here! diff --git a/src/main/java/org/springframework/data/repository/config/XmlRepositoryConfigurationSource.java b/src/main/java/org/springframework/data/repository/config/XmlRepositoryConfigurationSource.java index 5573613c7..61dd29a4a 100644 --- a/src/main/java/org/springframework/data/repository/config/XmlRepositoryConfigurationSource.java +++ b/src/main/java/org/springframework/data/repository/config/XmlRepositoryConfigurationSource.java @@ -144,6 +144,11 @@ public class XmlRepositoryConfigurationSource extends RepositoryConfigurationSou return getNullDefaultedAttribute(element, REPOSITORY_BASE_CLASS_NAME); } + @Override + public Optional getRepositoryFragmentsContributorClassName() { + return Optional.empty(); + } + @Override public Optional getRepositoryFactoryBeanClassName() { return getNullDefaultedAttribute(element, REPOSITORY_FACTORY_BEAN_CLASS_NAME); diff --git a/src/main/java/org/springframework/data/repository/core/RepositoryInformation.java b/src/main/java/org/springframework/data/repository/core/RepositoryInformation.java index 3ebee41f2..e3f77cc33 100644 --- a/src/main/java/org/springframework/data/repository/core/RepositoryInformation.java +++ b/src/main/java/org/springframework/data/repository/core/RepositoryInformation.java @@ -18,7 +18,6 @@ package org.springframework.data.repository.core; import java.lang.reflect.Method; import java.util.List; -import org.jspecify.annotations.Nullable; import org.springframework.data.repository.core.support.RepositoryComposition; /** @@ -106,8 +105,4 @@ public interface RepositoryInformation extends RepositoryMetadata { */ RepositoryComposition getRepositoryComposition(); - default @Nullable String moduleName() { - return null; - } - } diff --git a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java index 79e2078b4..42074d3ea 100644 --- a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java +++ b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java @@ -25,6 +25,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryInformationSupport; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.util.Lazy; import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -44,6 +45,7 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen private final RepositoryComposition composition; private final RepositoryComposition baseComposition; + private final Lazy fullComposition; /** * Creates a new {@link DefaultRepositoryMetadata} for the given repository interface and repository base class. @@ -62,6 +64,8 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen this.baseComposition = RepositoryComposition.of(RepositoryFragment.structural(repositoryBaseClass)) // .withArgumentConverter(composition.getArgumentConverter()) // .withMethodLookup(composition.getMethodLookup()); + + this.fullComposition = Lazy.of(() -> composition.append(baseComposition.getFragments())); } @Override @@ -106,7 +110,6 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen @Override protected boolean isQueryMethodCandidate(Method method) { - // FIXME - that should be simplified boolean queryMethodCandidate = super.isQueryMethodCandidate(method); if(!isQueryAnnotationPresentOn(method)) { return queryMethodCandidate; @@ -133,7 +136,7 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen @Override public RepositoryComposition getRepositoryComposition() { - return composition.append(baseComposition.getFragments()); + return fullComposition.get(); } } diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java index a0f19c5fc..4ccaba6c5 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java @@ -95,10 +95,6 @@ public abstract class RepositoryFactoryBeanSupport, private @Nullable Lazy repository; private @Nullable RepositoryMetadata repositoryMetadata; - // AOT bean factory hint? - private @Nullable String moduleBaseClass; - private @Nullable String moduleName; - /** * Creates a new {@link RepositoryFactoryBeanSupport} for the given repository interface. * @@ -261,14 +257,6 @@ public abstract class RepositoryFactoryBeanSupport, this.publisher = publisher; } - public void setModuleBaseClass(String moduleBaseClass) { - this.moduleBaseClass = moduleBaseClass; - } - - public void setModuleName(String moduleName) { - this.moduleName = moduleName; - } - @Override @SuppressWarnings("unchecked") public EntityInformation getEntityInformation() { @@ -281,6 +269,11 @@ public abstract class RepositoryFactoryBeanSupport, return getRequiredFactory().getRepositoryInformation(getRequiredRepositoryMetadata(), cachedFragments); } + @Override + public RepositoryFragmentsContributor getRepositoryFragmentsContributor() { + return RepositoryFragmentsContributor.empty(); + } + @Override public PersistentEntity getPersistentEntity() { diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryInformation.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryInformation.java index 6ee3adbbf..75ee8ac65 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryInformation.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryInformation.java @@ -46,6 +46,15 @@ public interface RepositoryFactoryInformation { */ RepositoryInformation getRepositoryInformation(); + /** + * Returns the {@link RepositoryFragmentsContributor} that is used to contribute additional fragments based on the + * repository declaration. + * + * @return + * @since 4.0 + */ + RepositoryFragmentsContributor getRepositoryFragmentsContributor(); + /** * Returns the {@link PersistentEntity} managed by the underlying repository. Can be {@literal null} in case the * underlying persistence mechanism does not expose a {@link MappingContext}. diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java index 7b34326a8..98a6b6073 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java @@ -82,6 +82,18 @@ public interface RepositoryFragment { return new StructuralRepositoryFragment<>(interfaceOrImplementation); } + /** + * Create a structural {@link RepositoryFragment} given {@code interfaceClass} and {@code implementationClass}. + * + * @param interfaceClass must not be {@literal null}. + * @param implementationClass must not be {@literal null}. + * @return + * @since 4.0 + */ + static RepositoryFragment structural(Class interfaceClass, Class implementationClass) { + return new StructuralRepositoryFragment<>(interfaceClass, implementationClass); + } + /** * Attempt to find the {@link Method} by name and exact parameters. Returns {@literal true} if the method was found or * {@literal false} otherwise. @@ -103,6 +115,15 @@ public interface RepositoryFragment { return Optional.empty(); } + /** + * @return the optional implementation class. Only available for fragments that ship an implementation descriptor. + * Structural (interface-only) fragments return always {@link Optional#empty()}. + * @since 4.0 + */ + default Optional> getImplementationClass() { + return getImplementation().map(it -> it.getClass()); + } + /** * @return a {@link Stream} of methods exposed by this {@link RepositoryFragment}. */ @@ -186,17 +207,30 @@ public interface RepositoryFragment { class StructuralRepositoryFragment implements RepositoryFragment { - private final Class interfaceOrImplementation; + private final Class interfaceClass; + private final Class implementationClass; private final Method[] methods; public StructuralRepositoryFragment(Class interfaceOrImplementation) { - this.interfaceOrImplementation = interfaceOrImplementation; - this.methods = getSignatureContributor().getMethods(); + this.interfaceClass = interfaceOrImplementation; + this.implementationClass = interfaceOrImplementation; + this.methods = interfaceOrImplementation.getMethods(); + } + + public StructuralRepositoryFragment(Class interfaceClass, Class implementationClass) { + this.interfaceClass = interfaceClass; + this.implementationClass = implementationClass; + this.methods = interfaceClass.getMethods(); } @Override public Class getSignatureContributor() { - return interfaceOrImplementation; + return interfaceClass; + } + + @Override + public Optional> getImplementationClass() { + return Optional.of(implementationClass); } @Override @@ -221,31 +255,30 @@ public interface RepositoryFragment { @Override public RepositoryFragment withImplementation(T implementation) { - return new ImplementedRepositoryFragment<>(interfaceOrImplementation, implementation); + return new ImplementedRepositoryFragment<>(interfaceClass, implementation); } @Override public String toString() { - return String.format("StructuralRepositoryFragment %s", ClassUtils.getShortName(interfaceOrImplementation)); + return String.format("StructuralRepositoryFragment %s", ClassUtils.getShortName(interfaceClass)); } @Override public boolean equals(Object o) { - - if (this == o) { - return true; - } - if (!(o instanceof StructuralRepositoryFragment that)) { return false; } - return ObjectUtils.nullSafeEquals(interfaceOrImplementation, that.interfaceOrImplementation); + if (!ObjectUtils.nullSafeEquals(interfaceClass, that.interfaceClass)) { + return false; + } + + return ObjectUtils.nullSafeEquals(implementationClass, that.implementationClass); } @Override public int hashCode() { - return ObjectUtils.nullSafeHashCode(interfaceOrImplementation); + return ObjectUtils.nullSafeHash(interfaceClass, implementationClass); } } diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFragmentsContributor.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragmentsContributor.java new file mode 100644 index 000000000..782b8356c --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragmentsContributor.java @@ -0,0 +1,56 @@ +/* + * 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.core.support; + +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; + +/** + * Strategy interface support allowing to contribute a {@link RepositoryFragments} based on {@link RepositoryMetadata}. + *

+ * Fragments contributors enhance repository functionality based on a repository declaration and activate additional + * fragments if a repository defines them, such as extending a built-in fragment interface (e.g. + * {@code QuerydslPredicateExecutor}, {@code QueryByExampleExecutor}). + *

+ * This interface is a base-interface serving as a contract for repository fragment introspection. The actual + * implementation and methods to contribute fragments to be used within the repository instance are store-specific and + * require typically access to infrastructure such as a database connection hence those methods must be defined within + * the particular store module. + * + * @author Mark Paluch + * @since 4.0 + */ +public interface RepositoryFragmentsContributor { + + /** + * Empty {@code RepositoryFragmentsContributor} that does not contribute any fragments. + * + * @return empty {@code RepositoryFragmentsContributor} that does not contribute any fragments. + */ + public static RepositoryFragmentsContributor empty() { + return metadata -> RepositoryFragments.empty(); + } + + /** + * Describe fragments that are contributed by {@link RepositoryMetadata}. Fragment description reports typically + * structural fragments that are not suitable for invocation but can be used to introspect the repository structure. + * + * @param metadata the repository metadata describing the repository interface. + * @return fragments to be (structurally) contributed to the repository. + */ + RepositoryFragments describe(RepositoryMetadata metadata); + +} diff --git a/src/main/java/org/springframework/data/repository/support/Repositories.java b/src/main/java/org/springframework/data/repository/support/Repositories.java index 430139305..4b4b4ca38 100644 --- a/src/main/java/org/springframework/data/repository/support/Repositories.java +++ b/src/main/java/org/springframework/data/repository/support/Repositories.java @@ -35,6 +35,7 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryFactoryInformation; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.util.ProxyUtils; import org.springframework.util.Assert; @@ -365,6 +366,11 @@ public class Repositories implements Iterable> { throw new UnsupportedOperationException(); } + @Override + public RepositoryFragmentsContributor getRepositoryFragmentsContributor() { + throw new UnsupportedOperationException(); + } + @Override public PersistentEntity getPersistentEntity() { throw new UnsupportedOperationException(); diff --git a/src/test/java/org/springframework/data/aot/sample/ConfigWithCustomRepositoryBaseClass.java b/src/test/java/org/springframework/data/aot/sample/ConfigWithCustomRepositoryBaseClass.java index 29d747159..790f660b9 100644 --- a/src/test/java/org/springframework/data/aot/sample/ConfigWithCustomRepositoryBaseClass.java +++ b/src/test/java/org/springframework/data/aot/sample/ConfigWithCustomRepositoryBaseClass.java @@ -22,13 +22,13 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.data.aot.sample.ConfigWithCustomRepositoryBaseClass.RepoBaseClass; import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.config.EnableRepositories; +import org.springframework.data.repository.config.EnableRepositoriesWithContributor; /** * @author Christoph Strobl */ @Configuration -@EnableRepositories(repositoryBaseClass = RepoBaseClass.class, considerNestedRepositories = true, +@EnableRepositoriesWithContributor(repositoryBaseClass = RepoBaseClass.class, considerNestedRepositories = true, includeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*CustomerRepositoryWithCustomBaseRepo$") }) public class ConfigWithCustomRepositoryBaseClass { diff --git a/src/test/java/org/springframework/data/aot/sample/ConfigWithSimpleCrudRepository.java b/src/test/java/org/springframework/data/aot/sample/ConfigWithSimpleCrudRepository.java index 09236c441..25e35d924 100644 --- a/src/test/java/org/springframework/data/aot/sample/ConfigWithSimpleCrudRepository.java +++ b/src/test/java/org/springframework/data/aot/sample/ConfigWithSimpleCrudRepository.java @@ -15,6 +15,8 @@ */ package org.springframework.data.aot.sample; +import org.jspecify.annotations.Nullable; + import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.FilterType; import org.springframework.data.aot.sample.ConfigWithSimpleCrudRepository.MyRepo; @@ -34,7 +36,7 @@ public class ConfigWithSimpleCrudRepository { public static class Person { - @javax.annotation.Nullable + @Nullable Address address; } diff --git a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java index 39bc54554..bb7124535 100644 --- a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java +++ b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java @@ -55,6 +55,7 @@ import org.springframework.data.repository.aot.RepositoryRegistrationAotProcesso import org.springframework.data.repository.config.EnableRepositories; import org.springframework.data.repository.config.RepositoryRegistrationAotContribution; import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; +import org.springframework.data.repository.config.SampleRepositoryFragmentsContributor; import org.springframework.data.repository.reactive.ReactiveSortingRepository; import org.springframework.transaction.interceptor.TransactionalProxy; @@ -237,10 +238,11 @@ public class RepositoryRegistrationAotProcessorIntegrationTests { assertThatContribution(repositoryBeanContribution) // .targetRepositoryTypeIs(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class) // - .hasNoFragments() // + .hasFragments() // .codeContributionSatisfies(contribution -> { // // interface contribution + .contributesReflectionFor(SampleRepositoryFragmentsContributor.class) // repository structural fragment .contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class) // repository .contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.RepoBaseClass.class) // base repo class .contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.Person.class); // repository domain type diff --git a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java index f57dc41c1..1ac8d043b 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java @@ -15,33 +15,37 @@ */ package org.springframework.data.repository.aot.generate; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; -import example.UserRepository; import example.UserRepository.User; +import java.util.List; import java.util.TimeZone; import javax.lang.model.element.Modifier; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; + import org.springframework.data.geo.Metric; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.config.AotRepositoryInformation; import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.AnnotationRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.query.QueryMethod; -import org.springframework.data.util.TypeInformation; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.TypeName; import org.springframework.stereotype.Repository; /** + * Unit tests for {@link AotRepositoryBuilder}. + * * @author Christoph Strobl + * @author Mark Paluch */ class AotRepositoryBuilderUnitTests { @@ -57,7 +61,7 @@ class AotRepositoryBuilderUnitTests { @Test // GH-3279 void writesClassSkeleton() { - AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, + AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons", new SpelAwareProxyProjectionFactory()); assertThat(repoBuilder.build().javaFile().toString()) .contains("package %s;".formatted(UserRepository.class.getPackageName())) // same package as source repo @@ -69,7 +73,7 @@ class AotRepositoryBuilderUnitTests { @Test // GH-3279 void appliesCtorArguments() { - AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, + AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons", new SpelAwareProxyProjectionFactory()); repoBuilder.withConstructorCustomizer(ctor -> { ctor.addParameter("param1", Metric.class); @@ -89,7 +93,7 @@ class AotRepositoryBuilderUnitTests { @Test // GH-3279 void appliesCtorCodeBlock() { - AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, + AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons", new SpelAwareProxyProjectionFactory()); repoBuilder.withConstructorCustomizer(ctor -> { ctor.customize((info, code) -> { @@ -103,7 +107,7 @@ class AotRepositoryBuilderUnitTests { @Test // GH-3279 void appliesClassCustomizations() { - AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, + AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons", new SpelAwareProxyProjectionFactory()); repoBuilder.withClassCustomizer((info, metadata, clazz) -> { @@ -128,12 +132,11 @@ class AotRepositoryBuilderUnitTests { @Test // GH-3279 void appliesQueryMethodContributor() { - AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, - new SpelAwareProxyProjectionFactory()); + AotRepositoryInformation repositoryInformation = new AotRepositoryInformation( + AnnotationRepositoryMetadata.getMetadata(UserRepository.class), CrudRepository.class, List.of()); - when(repositoryInformation.isQueryMethod(Mockito.argThat(arg -> arg.getName().equals("findByFirstname")))) - .thenReturn(true); - doReturn(TypeInformation.of(User.class)).when(repositoryInformation).getReturnType(any()); + AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons", + new SpelAwareProxyProjectionFactory()); repoBuilder.withQueryMethodContributor((method, info) -> { @@ -154,4 +157,35 @@ class AotRepositoryBuilderUnitTests { assertThat(repoBuilder.build().javaFile().toString()) // .containsIgnoringWhitespaces("void oops() { }"); } + + @Test // GH-3279 + void shouldContributeFragmentImplementationMetadata() { + + AotRepositoryInformation repositoryInformation = new AotRepositoryInformation( + AnnotationRepositoryMetadata.getMetadata(QuerydslUserRepository.class), CrudRepository.class, + List.of(RepositoryFragment.structural(QuerydslPredicateExecutor.class, DummyQuerydslPredicateExecutor.class))); + + AotRepositoryBuilder builder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons", + new SpelAwareProxyProjectionFactory()); + AotRepositoryBuilder.AotBundle bundle = builder.build(); + + AotRepositoryMethod method = bundle.metadata().methods().stream().filter(it -> it.name().equals("findBy")) + .findFirst().get(); + + assertThat(method.fragment()).isNotNull(); + assertThat(method.fragment().signature()).isEqualTo(QuerydslPredicateExecutor.class.getName()); + assertThat(method.fragment().implementation()).isEqualTo(DummyQuerydslPredicateExecutor.class.getName()); + } + + interface UserRepository extends org.springframework.data.repository.Repository { + + String someMethod(); + } + + interface QuerydslUserRepository + extends org.springframework.data.repository.Repository, QuerydslPredicateExecutor { + + } + + interface DummyQuerydslPredicateExecutor extends QuerydslPredicateExecutor {} } 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 05b058f8e..8c05276a9 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 @@ -43,6 +43,11 @@ class DummyModuleAotRepositoryContext implements AotRepositoryContext { this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); } + @Override + public String getModuleName() { + return "Commons"; + } + @Override public ConfigurableListableBeanFactory getBeanFactory() { return null; diff --git a/src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java index 133281fe0..915670400 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java @@ -15,10 +15,9 @@ */ package org.springframework.data.repository.aot.generate; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; import example.UserRepository; import example.UserRepositoryExtension; @@ -31,7 +30,7 @@ import java.util.Set; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; + import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.core.test.tools.TestCompiler; import org.springframework.data.aot.CodeContributionAssert; @@ -97,8 +96,8 @@ class RepositoryContributorUnitTests { @Test // GH-3279 void callsMethodContributionForQueryMethod() { - AotRepositoryContext repositoryContext = Mockito.mock(AotRepositoryContext.class); - RepositoryInformation repositoryInformation = Mockito.mock(RepositoryInformation.class); + AotRepositoryContext repositoryContext = mock(AotRepositoryContext.class); + RepositoryInformation repositoryInformation = mock(RepositoryInformation.class); when(repositoryContext.getRepositoryInformation()).thenReturn(repositoryInformation); when(repositoryInformation.getRepositoryInterface()).thenReturn((Class) UserRepository.class); @@ -113,8 +112,9 @@ class RepositoryContributorUnitTests { @Test // GH-3279 void doesNotContributeBaseClassMethods() { - AotRepositoryContext repositoryContext = Mockito.mock(AotRepositoryContext.class); - RepositoryInformation repositoryInformation = Mockito.mock(RepositoryInformation.class); + AotRepositoryContext repositoryContext = mock(AotRepositoryContext.class); + when(repositoryContext.getModuleName()).thenReturn("Commons"); + RepositoryInformation repositoryInformation = mock(RepositoryInformation.class); when(repositoryContext.getRepositoryInformation()).thenReturn(repositoryInformation); when(repositoryInformation.getRepositoryInterface()).thenReturn((Class) UserRepository.class); @@ -133,8 +133,9 @@ class RepositoryContributorUnitTests { @Test // GH-3279 void doesNotContributeFragmentMethod() { - AotRepositoryContext repositoryContext = Mockito.mock(AotRepositoryContext.class); - RepositoryInformation repositoryInformation = Mockito.mock(RepositoryInformation.class); + AotRepositoryContext repositoryContext = mock(AotRepositoryContext.class); + when(repositoryContext.getModuleName()).thenReturn("Commons"); + RepositoryInformation repositoryInformation = mock(RepositoryInformation.class); when(repositoryContext.getRepositoryInformation()).thenReturn(repositoryInformation); when(repositoryInformation.getRepositoryInterface()).thenReturn((Class) UserRepository.class); @@ -157,8 +158,9 @@ class RepositoryContributorUnitTests { @Test // GH-3279 void contributesBaseClassMethodIfQueryMethod() { - AotRepositoryContext repositoryContext = Mockito.mock(AotRepositoryContext.class); - RepositoryInformation repositoryInformation = Mockito.mock(RepositoryInformation.class); + AotRepositoryContext repositoryContext = mock(AotRepositoryContext.class); + when(repositoryContext.getModuleName()).thenReturn("Commons"); + RepositoryInformation repositoryInformation = mock(RepositoryInformation.class); when(repositoryContext.getRepositoryInformation()).thenReturn(repositoryInformation); when(repositoryInformation.getRepositoryInterface()).thenReturn((Class) UserRepository.class); diff --git a/src/test/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSourceUnitTests.java b/src/test/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSourceUnitTests.java index 8917668d0..1b1c657cb 100755 --- a/src/test/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSourceUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSourceUnitTests.java @@ -15,15 +15,15 @@ */ package org.springframework.data.repository.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.ComponentScan.Filter; @@ -184,6 +184,34 @@ class AnnotationRepositoryConfigurationSourceUnitTests { assertThat(getConfigSource(DefaultConfiguration.class).generateBeanName(bd)).isEqualTo("personRepository"); } + @Test // GH-3279 + void considersDefaultFragmentsContributor() { + + RootBeanDefinition bd = new RootBeanDefinition(DummyRepositoryFactory.class); + bd.getConstructorArgumentValues().addGenericArgumentValue(PersonRepository.class); + + AnnotationMetadata metadata = new StandardAnnotationMetadata(ConfigurationWithFragmentsContributor.class, true); + AnnotationRepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, + EnableRepositoriesWithContributor.class, resourceLoader, environment, registry, null); + + assertThat(configurationSource.getRepositoryFragmentsContributorClassName()) + .contains(SampleRepositoryFragmentsContributor.class.getName()); + } + + @Test // GH-3279 + void omitsUnspecifiedFragmentsContributor() { + + RootBeanDefinition bd = new RootBeanDefinition(DummyRepositoryFactory.class); + bd.getConstructorArgumentValues().addGenericArgumentValue(PersonRepository.class); + + AnnotationMetadata metadata = new StandardAnnotationMetadata(ReactiveConfigurationWithBeanNameGenerator.class, + true); + AnnotationRepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, + EnableReactiveRepositories.class, resourceLoader, environment, registry, null); + + assertThat(configurationSource.getRepositoryFragmentsContributorClassName()).isEmpty(); + } + @Test // GH-3082 void considerBeanNameGeneratorForReactiveRepos() { @@ -219,6 +247,9 @@ class AnnotationRepositoryConfigurationSourceUnitTests { @EnableRepositories(nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class) static class ConfigurationWithBeanNameGenerator {} + @EnableRepositoriesWithContributor() + static class ConfigurationWithFragmentsContributor {} + @EnableReactiveRepositories(nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class) static class ReactiveConfigurationWithBeanNameGenerator {} @@ -234,4 +265,5 @@ class AnnotationRepositoryConfigurationSourceUnitTests { static class ConfigWithSampleAnnotation {} interface ReactivePersonRepository extends ReactiveCrudRepository {} + } diff --git a/src/test/java/org/springframework/data/repository/config/DummyRegistrarWithContributor.java b/src/test/java/org/springframework/data/repository/config/DummyRegistrarWithContributor.java new file mode 100644 index 000000000..85708eb8f --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/DummyRegistrarWithContributor.java @@ -0,0 +1,40 @@ +/* + * Copyright 2022-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.lang.annotation.Annotation; + +import org.springframework.core.io.DefaultResourceLoader; + +/** + * @author Mark Paluch + */ +class DummyRegistrarWithContributor extends RepositoryBeanDefinitionRegistrarSupport { + + DummyRegistrarWithContributor() { + setResourceLoader(new DefaultResourceLoader()); + } + + @Override + protected Class getAnnotation() { + return EnableRepositoriesWithContributor.class; + } + + @Override + protected RepositoryConfigurationExtension getExtension() { + return new DummyConfigurationExtension(); + } +} diff --git a/src/test/java/org/springframework/data/repository/config/EnableRepositoriesWithContributor.java b/src/test/java/org/springframework/data/repository/config/EnableRepositoriesWithContributor.java new file mode 100644 index 000000000..2c38047e0 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/EnableRepositoriesWithContributor.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-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.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Import; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.core.support.DummyRepositoryFactoryBean; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; + +@Retention(RetentionPolicy.RUNTIME) +@Import(DummyRegistrarWithContributor.class) +@Inherited +public @interface EnableRepositoriesWithContributor { + + String[] value() default {}; + + String[] basePackages() default {}; + + Class[] basePackageClasses() default {}; + + Filter[] includeFilters() default {}; + + Filter[] excludeFilters() default {}; + + Class repositoryFactoryBeanClass() default DummyRepositoryFactoryBean.class; + + Class fragmentsContributor() default SampleRepositoryFragmentsContributor.class; + + Class repositoryBaseClass() default PagingAndSortingRepository.class; + + Class nameGenerator() default BeanNameGenerator.class; + + String namedQueriesLocation() default ""; + + String repositoryImplementationPostfix() default "Impl"; + + boolean considerNestedRepositories() default false; + + boolean limitImplementationBasePackages() default true; + + BootstrapMode bootstrapMode() default BootstrapMode.DEFAULT; +} diff --git a/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionReaderTests.java b/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionReaderTests.java index 54379865c..13482da3f 100644 --- a/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionReaderTests.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionReaderTests.java @@ -15,24 +15,29 @@ */ package org.springframework.data.repository.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; + import org.springframework.aot.hint.RuntimeHints; import org.springframework.beans.factory.support.RegisteredBean; -import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.data.aot.sample.ConfigWithCustomImplementation; import org.springframework.data.aot.sample.ConfigWithCustomRepositoryBaseClass; import org.springframework.data.aot.sample.ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo; +import org.springframework.data.aot.sample.ConfigWithFragments; import org.springframework.data.aot.sample.ConfigWithSimpleCrudRepository; +import org.springframework.data.aot.sample.ReactiveConfig; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.core.support.RepositoryFragment; /** + * Unit tests for {@link RepositoryBeanDefinitionReader}. + * * @author Christoph Strobl + * @author Mark Paluch */ class RepositoryBeanDefinitionReaderTests { @@ -42,10 +47,10 @@ class RepositoryBeanDefinitionReaderTests { RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithSimpleCrudRepository.class); RepositoryConfiguration repoConfig = mock(RepositoryConfiguration.class); - Mockito.when(repoConfig.getRepositoryInterface()).thenReturn(ConfigWithSimpleCrudRepository.MyRepo.class.getName()); + when(repoConfig.getRepositoryInterface()).thenReturn(ConfigWithSimpleCrudRepository.MyRepo.class.getName()); - RepositoryInformation repositoryInformation = RepositoryBeanDefinitionReader.repositoryInformation(repoConfig, - repoFactoryBean.getMergedBeanDefinition(), repoFactoryBean.getBeanFactory()); + RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean); + RepositoryInformation repositoryInformation = reader.getRepositoryInformation(); assertThat(repositoryInformation.getRepositoryInterface()).isEqualTo(ConfigWithSimpleCrudRepository.MyRepo.class); assertThat(repositoryInformation.getDomainType()).isEqualTo(ConfigWithSimpleCrudRepository.Person.class); @@ -59,49 +64,86 @@ class RepositoryBeanDefinitionReaderTests { RepositoryConfiguration repoConfig = mock(RepositoryConfiguration.class); Class repositoryInterfaceType = CustomerRepositoryWithCustomBaseRepo.class; - Mockito.when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName()); + when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName()); - RepositoryInformation repositoryInformation = RepositoryBeanDefinitionReader.repositoryInformation(repoConfig, - repoFactoryBean.getMergedBeanDefinition(), repoFactoryBean.getBeanFactory()); + RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean); + RepositoryInformation repositoryInformation = reader.getRepositoryInformation(); assertThat(repositoryInformation.getRepositoryBaseClass()) .isEqualTo(ConfigWithCustomRepositoryBaseClass.RepoBaseClass.class); } @Test // GH-3279 - void readsFragmentsFromBeanFactory() { + void readsFragmentsContributorFromBeanDefinition() { - RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithCustomImplementation.class); + RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithCustomRepositoryBaseClass.class); RepositoryConfiguration repoConfig = mock(RepositoryConfiguration.class); - Class repositoryInterfaceType = ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class; - Mockito.when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName()); + Class repositoryInterfaceType = CustomerRepositoryWithCustomBaseRepo.class; + when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName()); - RepositoryInformation repositoryInformation = RepositoryBeanDefinitionReader.repositoryInformation(repoConfig, - repoFactoryBean.getMergedBeanDefinition(), repoFactoryBean.getBeanFactory()); + RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean); + RepositoryInformation repositoryInformation = reader.getRepositoryInformation(); - assertThat(repositoryInformation.getFragments()).satisfiesExactly(fragment -> { - assertThat(fragment.getSignatureContributor()) - .isEqualTo(ConfigWithCustomImplementation.CustomImplInterface.class); - }); + assertThat(repositoryInformation.getFragments()) + .contains(RepositoryFragment.structural(SampleRepositoryFragmentsContributor.class)); } @Test // GH-3279 - void fallsBackToModuleBaseClassIfSetAndNoRepoBaseDefined() { + void readsFragmentsContributorFromBeanFactory() { - RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithSimpleCrudRepository.class); - RootBeanDefinition rootBeanDefinition = repoFactoryBean.getMergedBeanDefinition().cloneBeanDefinition(); - // need to unset because its defined as non default - rootBeanDefinition.getPropertyValues().removePropertyValue("repositoryBaseClass"); - rootBeanDefinition.getPropertyValues().add("moduleBaseClass", ModuleBase.class.getName()); + RegisteredBean repoFactoryBean = repositoryFactory(ReactiveConfig.class); RepositoryConfiguration repoConfig = mock(RepositoryConfiguration.class); - Mockito.when(repoConfig.getRepositoryInterface()).thenReturn(ConfigWithSimpleCrudRepository.MyRepo.class.getName()); + Class repositoryInterfaceType = ReactiveConfig.CustomerRepositoryReactive.class; + when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName()); - RepositoryInformation repositoryInformation = RepositoryBeanDefinitionReader.repositoryInformation(repoConfig, - rootBeanDefinition, repoFactoryBean.getBeanFactory()); + RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean); + RepositoryInformation repositoryInformation = reader.getRepositoryInformation(); - assertThat(repositoryInformation.getRepositoryBaseClass()).isEqualTo(ModuleBase.class); + assertThat(repositoryInformation.getFragments()).isEmpty(); + } + + @Test // GH-3279, GH-3282 + void readsCustomImplementationFromBeanFactory() { + + RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithCustomImplementation.class); + RepositoryConfiguration repoConfig = mock(RepositoryConfiguration.class); + + Class repositoryInterfaceType = ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class; + when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName()); + + RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean); + RepositoryInformation repositoryInformation = reader.getRepositoryInformation(); + + assertThat(repositoryInformation.getFragments()).satisfiesExactly(fragment -> { + assertThat(fragment.getImplementationClass()) + .contains(ConfigWithCustomImplementation.RepositoryWithCustomImplementationImpl.class); + }); + } + + @Test // GH-3279, GH-3282 + void readsFragmentsFromBeanFactory() { + + RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithFragments.class); + RepositoryConfiguration repoConfig = mock(RepositoryConfiguration.class); + + Class repositoryInterfaceType = ConfigWithFragments.RepositoryWithFragments.class; + when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName()); + + RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean); + RepositoryInformation repositoryInformation = reader.getRepositoryInformation(); + + assertThat(repositoryInformation.getFragments()).hasSize(2); + + for (RepositoryFragment fragment : repositoryInformation.getFragments()) { + + assertThat(fragment.getSignatureContributor()).isIn(ConfigWithFragments.CustomImplInterface1.class, + ConfigWithFragments.CustomImplInterface2.class); + + assertThat(fragment.getImplementationClass().get()).isIn(ConfigWithFragments.CustomImplInterface1Impl.class, + ConfigWithFragments.CustomImplInterface2Impl.class); + } } static RegisteredBean repositoryFactory(Class configClass) { @@ -118,5 +160,4 @@ class RepositoryBeanDefinitionReaderTests { return RegisteredBean.of(applicationContext.getBeanFactory(), beanNamesForType[0]); } - static class ModuleBase {} } diff --git a/src/test/java/org/springframework/data/repository/config/SampleRepositoryFragmentsContributor.java b/src/test/java/org/springframework/data/repository/config/SampleRepositoryFragmentsContributor.java new file mode 100644 index 000000000..a22db03b6 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/SampleRepositoryFragmentsContributor.java @@ -0,0 +1,33 @@ +/* + * 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 org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; + +/** + * @author Mark Paluch + */ +public class SampleRepositoryFragmentsContributor implements RepositoryFragmentsContributor { + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return RepositoryComposition.RepositoryFragments + .of(RepositoryFragment.structural(SampleRepositoryFragmentsContributor.class)); + } +} diff --git a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactoryBean.java b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactoryBean.java index 8a5b9b6d6..4121d6f0c 100644 --- a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactoryBean.java +++ b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactoryBean.java @@ -29,6 +29,7 @@ public class DummyRepositoryFactoryBean, S, ID exten extends RepositoryFactoryBeanSupport { private final T repository; + private RepositoryFragmentsContributor repositoryFragmentsContributor = RepositoryFragmentsContributor.empty(); public DummyRepositoryFactoryBean(Class repositoryInterface) { @@ -38,6 +39,19 @@ public class DummyRepositoryFactoryBean, S, ID exten setMappingContext(new SampleMappingContext()); } + public T getRepository() { + return repository; + } + + @Override + public RepositoryFragmentsContributor getRepositoryFragmentsContributor() { + return repositoryFragmentsContributor; + } + + public void setRepositoryFragmentsContributor(RepositoryFragmentsContributor repositoryFragmentsContributor) { + this.repositoryFragmentsContributor = repositoryFragmentsContributor; + } + @Override protected RepositoryFactorySupport createRepositoryFactory() { return new DummyRepositoryFactory(repository); diff --git a/src/test/java/org/springframework/data/repository/support/RepositoriesUnitTests.java b/src/test/java/org/springframework/data/repository/support/RepositoriesUnitTests.java index 22c095981..cbb08cd94 100755 --- a/src/test/java/org/springframework/data/repository/support/RepositoriesUnitTests.java +++ b/src/test/java/org/springframework/data/repository/support/RepositoriesUnitTests.java @@ -46,6 +46,7 @@ import org.springframework.data.repository.core.support.DummyEntityInformation; import org.springframework.data.repository.core.support.DummyRepositoryFactoryBean; import org.springframework.data.repository.core.support.DummyRepositoryInformation; import org.springframework.data.repository.core.support.RepositoryFactoryInformation; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.util.TypeInformation; import org.springframework.util.ClassUtils; @@ -290,6 +291,11 @@ class RepositoriesUnitTests { return new DummyRepositoryInformation(repositoryMetadata); } + @Override + public RepositoryFragmentsContributor getRepositoryFragmentsContributor() { + return RepositoryFragmentsContributor.empty(); + } + @Override public PersistentEntity getPersistentEntity() { return mappingContext.getRequiredPersistentEntity(repositoryMetadata.getDomainType());