Browse Source

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
pull/3304/head
Mark Paluch 7 months ago
parent
commit
5800600470
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 103
      src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java
  2. 31
      src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java
  3. 11
      src/main/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSource.java
  4. 14
      src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java
  5. 54
      src/main/java/org/springframework/data/repository/config/AotRepositoryInformation.java
  6. 58
      src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java
  7. 9
      src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java
  8. 9
      src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java
  9. 200
      src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionReader.java
  10. 12
      src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java
  11. 5
      src/main/java/org/springframework/data/repository/config/RepositoryConfigurationAdapter.java
  12. 13
      src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java
  13. 9
      src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSource.java
  14. 161
      src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java
  15. 12
      src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java
  16. 5
      src/main/java/org/springframework/data/repository/config/XmlRepositoryConfigurationSource.java
  17. 5
      src/main/java/org/springframework/data/repository/core/RepositoryInformation.java
  18. 7
      src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java
  19. 17
      src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java
  20. 9
      src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryInformation.java
  21. 57
      src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java
  22. 56
      src/main/java/org/springframework/data/repository/core/support/RepositoryFragmentsContributor.java
  23. 6
      src/main/java/org/springframework/data/repository/support/Repositories.java
  24. 4
      src/test/java/org/springframework/data/aot/sample/ConfigWithCustomRepositoryBaseClass.java
  25. 4
      src/test/java/org/springframework/data/aot/sample/ConfigWithSimpleCrudRepository.java
  26. 4
      src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java
  27. 68
      src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java
  28. 5
      src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java
  29. 28
      src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java
  30. 38
      src/test/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSourceUnitTests.java
  31. 40
      src/test/java/org/springframework/data/repository/config/DummyRegistrarWithContributor.java
  32. 61
      src/test/java/org/springframework/data/repository/config/EnableRepositoriesWithContributor.java
  33. 101
      src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionReaderTests.java
  34. 33
      src/test/java/org/springframework/data/repository/config/SampleRepositoryFragmentsContributor.java
  35. 14
      src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactoryBean.java
  36. 6
      src/test/java/org/springframework/data/repository/support/RepositoriesUnitTests.java

103
src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java

@ -41,6 +41,7 @@ import org.springframework.data.repository.query.QueryMethod; @@ -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; @@ -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 { @@ -60,9 +62,11 @@ class AotRepositoryBuilder {
private @Nullable BiFunction<Method, RepositoryInformation, @Nullable MethodContributor<? extends QueryMethod>> 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 { @@ -74,11 +78,37 @@ class AotRepositoryBuilder {
this.customizer = (info, metadata, builder) -> {};
}
public static <M extends QueryMethod> 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<AotRepositoryConstructorBuilder> constructorCustomizer) {
@ -86,42 +116,33 @@ class AotRepositoryBuilder { @@ -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<Method, RepositoryInformation, @Nullable MethodContributor<? extends QueryMethod>> methodContributorFunction) {
this.methodContributorFunction = methodContributorFunction;
return this;
}
public AotRepositoryBuilder withClassCustomizer(ClassCustomizer classCustomizer) {
this.customizer = classCustomizer;
this.methodContributorFunction = methodContributorFunction;
return this;
}
public AotBundle build() {
List<AotRepositoryMethod> 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<AotRepositoryMethod> 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.<Method, String> comparing(it -> {
@ -136,12 +157,35 @@ class AotRepositoryBuilder { @@ -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);
return new AotBundle(javaFile, metadata);
}
private MethodSpec buildConstructor() {
AotRepositoryConstructorBuilder constructorBuilder = new AotRepositoryConstructorBuilder(repositoryInformation,
generationMetadata);
if (constructorCustomizer != null) {
constructorCustomizer.accept(constructorBuilder);
}
return constructorBuilder.buildConstructor();
}
AotRepositoryMetadata metadata = new AotRepositoryMetadata(repositoryInformation.getRepositoryInterface().getName(),
repositoryInformation.moduleName() != null ? repositoryInformation.moduleName() : "", repositoryType, methodMetadata);
private AotRepositoryMetadata getAotRepositoryMetadata(List<AotRepositoryMethod> methodMetadata) {
return new AotBundle(javaFile, metadata.toJson());
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 { @@ -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 { @@ -240,7 +283,7 @@ class AotRepositoryBuilder {
}
record AotBundle(JavaFile javaFile, JSONObject metadata) {
record AotBundle(JavaFile javaFile, AotRepositoryMetadata metadata) {
}
}

31
src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java

@ -39,6 +39,7 @@ import org.springframework.util.StringUtils; @@ -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 { @@ -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 { @@ -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 { @@ -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);
}

11
src/main/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSource.java

@ -65,6 +65,7 @@ public class AnnotationRepositoryConfigurationSource extends RepositoryConfigura @@ -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 @@ -187,6 +188,16 @@ public class AnnotationRepositoryConfigurationSource extends RepositoryConfigura
: Optional.of(repositoryBaseClass.getName());
}
@Override
public Optional<String> 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.
*

14
src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java

@ -16,9 +16,9 @@ @@ -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; @@ -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 { @@ -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 { @@ -46,7 +53,7 @@ public interface AotRepositoryContext extends AotContext {
/**
* @return the {@link Annotation} types used to identify domain types.
*/
Set<Class<? extends Annotation>> getIdentifyingAnnotations();
Collection<Class<? extends Annotation>> getIdentifyingAnnotations();
/**
* @return {@link RepositoryInformation metadata} about the repository itself.
@ -64,4 +71,5 @@ public interface AotRepositoryContext extends AotContext { @@ -64,4 +71,5 @@ public interface AotRepositoryContext extends AotContext {
* @return all {@link Class types} reachable from the repository.
*/
Set<Class<?>> getResolvedTypes();
}

54
src/main/java/org/springframework/data/repository/config/AotRepositoryInformation.java

@ -17,52 +17,40 @@ package org.springframework.data.repository.config; @@ -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<Collection<RepositoryFragment<?>>> fragments;
private final RepositoryComposition fragmentsComposition;
private final RepositoryComposition baseComposition;
private final RepositoryComposition composition;
private final Lazy<RepositoryComposition> repositoryComposition;
private final Lazy<RepositoryComposition> baseComposition;
public AotRepositoryInformation(RepositoryMetadata repositoryMetadata, Class<?> repositoryBaseClass,
Collection<RepositoryFragment<?>> fragments) {
AotRepositoryInformation(@Nullable String moduleName, Supplier<RepositoryMetadata> repositoryMetadata,
Supplier<Class<?>> repositoryBaseClass, Supplier<Collection<RepositoryFragment<?>>> 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 @@ -71,31 +59,27 @@ class AotRepositoryInformation extends RepositoryInformationSupport implements R
*/
@Override
public Set<RepositoryFragment<?>> 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;
}
}

58
src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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; @@ -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<Set<MergedAnnotation<Annotation>>> resolvedAnnotations = Lazy.of(this::discoverAnnotations);
private final Lazy<Set<Class<?>>> managedTypes = Lazy.of(this::discoverTypes);
private @Nullable RepositoryInformation repositoryInformation;
private @Nullable Set<String> basePackages;
private @Nullable Set<Class<? extends Annotation>> identifyingAnnotations;
private @Nullable String beanName;
private Set<String> basePackages = Collections.emptySet();
private Collection<Class<? extends Annotation>> 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 { @@ -72,7 +85,7 @@ class DefaultAotRepositoryContext implements AotRepositoryContext {
@Override
public Set<String> getBasePackages() {
return basePackages == null ? Collections.emptySet() : basePackages;
return basePackages;
}
public void setBasePackages(Set<String> basePackages) {
@ -89,11 +102,11 @@ class DefaultAotRepositoryContext implements AotRepositoryContext { @@ -89,11 +102,11 @@ class DefaultAotRepositoryContext implements AotRepositoryContext {
}
@Override
public Set<Class<? extends Annotation>> getIdentifyingAnnotations() {
return identifyingAnnotations == null ? Collections.emptySet() : identifyingAnnotations;
public Collection<Class<? extends Annotation>> getIdentifyingAnnotations() {
return identifyingAnnotations;
}
public void setIdentifyingAnnotations(Set<Class<? extends Annotation>> identifyingAnnotations) {
public void setIdentifyingAnnotations(Collection<Class<? extends Annotation>> identifyingAnnotations) {
this.identifyingAnnotations = identifyingAnnotations;
}
@ -102,10 +115,6 @@ class DefaultAotRepositoryContext implements AotRepositoryContext { @@ -102,10 +115,6 @@ class DefaultAotRepositoryContext implements AotRepositoryContext {
return repositoryInformation;
}
public void setRepositoryInformation(RepositoryInformation repositoryInformation) {
this.repositoryInformation = repositoryInformation;
}
@Override
public Set<MergedAnnotation<Annotation>> getResolvedAnnotations() {
return resolvedAnnotations.get();
@ -132,24 +141,18 @@ class DefaultAotRepositoryContext implements AotRepositoryContext { @@ -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<Class<?>> discoverTypes() {
Set<Class<?>> types = new LinkedHashSet<>();
Set<Class<?>> 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 { @@ -160,4 +163,5 @@ class DefaultAotRepositoryContext implements AotRepositoryContext {
return types;
}
}

9
src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java

@ -114,12 +114,17 @@ public class DefaultRepositoryConfiguration<T extends RepositoryConfigurationSou @@ -114,12 +114,17 @@ public class DefaultRepositoryConfiguration<T extends RepositoryConfigurationSou
@Override
public Optional<String> getRepositoryBaseClassName() {
return configurationSource.getRepositoryBaseClassName();
return configurationSource.getRepositoryBaseClassName()
.or(() -> Optional.ofNullable(extension.getRepositoryBaseClassName()));
}
@Override
public String getRepositoryFactoryBeanClassName() {
public Optional<String> getRepositoryFragmentsContributorClassName() {
return configurationSource.getRepositoryFragmentsContributorClassName();
}
@Override
public String getRepositoryFactoryBeanClassName() {
return configurationSource.getRepositoryFactoryBeanClassName()
.orElseGet(extension::getRepositoryFactoryBeanClassName);
}

9
src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java

@ -116,6 +116,11 @@ class RepositoryBeanDefinitionBuilder { @@ -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 { @@ -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);

200
src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionReader.java

@ -15,139 +15,197 @@ @@ -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<RepositoryFragment<?>> fragmentList = readRepositoryFragments(source, beanFactory);
if (source.getPropertyValues().contains("customImplementation")) {
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));
}
}
.getMetadata(forName(configuration.getRepositoryInterface()));
Class<?> repositoryBaseClass = getRepositoryBaseClass();
List<RepositoryFragment<?>> 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<RepositoryFragment<?>> readRepositoryFragments(BeanDefinition source,
ConfigurableListableBeanFactory beanFactory) {
private List<RepositoryFragment<?>> 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<String> fragmentBeanNames = (List<String>) fragmentBeanNameList.getValue();
List<RepositoryFragment<?>> 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();
try {
Class<?> type = ClassUtils.forName(fragmentClassName.toString(), beanFactory.getBeanClassLoader());
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);
ConstructorArgumentValues cv = fragmentBeanDefinition.getConstructorArgumentValues();
ValueHolder interfaceClassVh = cv.getArgumentValue(0, String.class);
ValueHolder implementationVh = cv.getArgumentValue(1, null, null, null);
Object fragmentClassName = interfaceClassVh.getValue();
Class<?> interfaceClass = forName(fragmentClassName.toString());
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<RepositoryFragment<?>> 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<Object, Object>, PagingAndSortingRepository<Object, Object> {}
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);
}

12
src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java

@ -78,6 +78,15 @@ public interface RepositoryConfiguration<T extends RepositoryConfigurationSource @@ -78,6 +78,15 @@ public interface RepositoryConfiguration<T extends RepositoryConfigurationSource
*/
Optional<String> 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<String> getRepositoryFragmentsContributorClassName();
/**
* Returns the name of the repository factory bean class to be used.
*
@ -157,11 +166,12 @@ public interface RepositoryConfiguration<T extends RepositoryConfigurationSource @@ -157,11 +166,12 @@ public interface RepositoryConfiguration<T extends RepositoryConfigurationSource
ImplementationLookupConfiguration toLookupConfiguration(MetadataReaderFactory factory);
/**
* Returns a human readable description of the repository interface declaration for error reporting purposes.
* Returns a human-readable description of the repository interface declaration for error reporting purposes.
*
* @return can be {@literal null}.
* @since 2.3
*/
@Nullable
String getResourceDescription();
}

5
src/main/java/org/springframework/data/repository/config/RepositoryConfigurationAdapter.java

@ -71,6 +71,11 @@ class RepositoryConfigurationAdapter<T extends RepositoryConfigurationSource> @@ -71,6 +71,11 @@ class RepositoryConfigurationAdapter<T extends RepositoryConfigurationSource>
return repositoryConfiguration.getRepositoryBaseClassName();
}
@Override
public Optional<String> getRepositoryFragmentsContributorClassName() {
return repositoryConfiguration.getRepositoryFragmentsContributorClassName();
}
@Override
public String getRepositoryFactoryBeanClassName() {
return repositoryConfiguration.getRepositoryFactoryBeanClassName();

13
src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java

@ -18,7 +18,7 @@ package org.springframework.data.repository.config; @@ -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 { @@ -63,7 +63,6 @@ public interface RepositoryConfigurationExtension {
* @see org.springframework.beans.factory.aot.BeanRegistrationAotProcessor
* @since 3.0
*/
@NonNull
default Class<? extends BeanRegistrationAotProcessor> getRepositoryAotProcessor() {
return RepositoryRegistrationAotProcessor.class;
}
@ -90,6 +89,16 @@ public interface RepositoryConfigurationExtension { @@ -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.
*

9
src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSource.java

@ -81,6 +81,15 @@ public interface RepositoryConfigurationSource { @@ -81,6 +81,15 @@ public interface RepositoryConfigurationSource {
*/
Optional<String> 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<String> getRepositoryFragmentsContributorClassName();
/**
* Returns the name of the repository factory bean class or {@link Optional#empty()} if not defined in the source.
*

161
src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java

@ -16,18 +16,18 @@ @@ -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; @@ -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,18 +66,19 @@ import org.springframework.util.ClassUtils; @@ -66,18 +66,19 @@ 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 @Nullable RepositoryContributor repositoryContributor;
private final RepositoryRegistrationAotProcessor aotProcessor;
private @Nullable AotRepositoryContext repositoryContext;
private final AotRepositoryContext repositoryContext;
private @Nullable BiFunction<AotRepositoryContext, GenerationContext, @Nullable RepositoryContributor> moduleContribution;
private @Nullable RepositoryContributor repositoryContributor;
private final RepositoryRegistrationAotProcessor aotProcessor;
private @Nullable BiFunction<AotRepositoryContext, GenerationContext, @Nullable RepositoryContributor> moduleContribution;
/**
* Constructs a new instance of the {@link RepositoryRegistrationAotContribution} initialized with the given, required
@ -85,14 +86,18 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo @@ -85,14 +86,18 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
*
* @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 @@ -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<AotRepositoryContext, GenerationContext, @Nullable RepositoryContributor> getModuleContribution() {
@ -118,10 +164,6 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo @@ -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 @@ -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) {
Assert.notNull(repositoryBean, "The RegisteredBean for the repository must not be null");
private static @Nullable AotRepositoryContext buildAotRepositoryContext(Environment environment, RegisteredBean bean,
RepositoryConfiguration<?> repositoryConfiguration) {
RepositoryConfiguration<?> repositoryMetadata = getRepositoryRegistrationAotProcessor()
.getRepositoryMetadata(repositoryBean);
RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(bean);
RepositoryConfiguration<?> configuration = reader.getConfiguration();
RepositoryConfigurationExtensionSupport extension = reader.getConfigurationExtension();
Assert.state(repositoryMetadata != null, "The RepositoryConfiguration for the repository must not be null");
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));
this.repositoryContext = buildAotRepositoryContext(repositoryBean, repositoryMetadata);
repositoryContext.setBasePackages(repositoryConfiguration.getBasePackages().toSet());
repositoryContext.setIdentifyingAnnotations(extension.getIdentifyingAnnotations());
return this;
return repositoryContext;
}
/**
@ -176,9 +217,6 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo @@ -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 @@ -219,6 +257,10 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
};
}
public Predicate<Class<?>> 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 @@ -239,7 +281,7 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
for (RepositoryFragment<?> fragment : getRepositoryInformation().getFragments()) {
Class<?> repositoryFragmentType = fragment.getSignatureContributor();
Optional<?> implementation = fragment.getImplementation();
Optional<Class<?>> implementation = fragment.getImplementationClass();
contribution.getRuntimeHints().reflection().registerType(repositoryFragmentType, hint -> {
@ -250,13 +292,12 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo @@ -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 @@ -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 @@ -356,29 +391,5 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
|| ClassUtils.isPrimitiveArray(type); //
}
public Predicate<Class<?>> 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<Class<? extends Annotation>> resolveIdentifyingAnnotations() {
return Collections.emptySet();
}
}

12
src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java

@ -69,6 +69,7 @@ import org.springframework.util.Assert; @@ -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 @@ -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!

5
src/main/java/org/springframework/data/repository/config/XmlRepositoryConfigurationSource.java

@ -144,6 +144,11 @@ public class XmlRepositoryConfigurationSource extends RepositoryConfigurationSou @@ -144,6 +144,11 @@ public class XmlRepositoryConfigurationSource extends RepositoryConfigurationSou
return getNullDefaultedAttribute(element, REPOSITORY_BASE_CLASS_NAME);
}
@Override
public Optional<String> getRepositoryFragmentsContributorClassName() {
return Optional.empty();
}
@Override
public Optional<String> getRepositoryFactoryBeanClassName() {
return getNullDefaultedAttribute(element, REPOSITORY_FACTORY_BEAN_CLASS_NAME);

5
src/main/java/org/springframework/data/repository/core/RepositoryInformation.java

@ -18,7 +18,6 @@ package org.springframework.data.repository.core; @@ -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 { @@ -106,8 +105,4 @@ public interface RepositoryInformation extends RepositoryMetadata {
*/
RepositoryComposition getRepositoryComposition();
default @Nullable String moduleName() {
return null;
}
}

7
src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java

@ -25,6 +25,7 @@ import org.jspecify.annotations.Nullable; @@ -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 @@ -44,6 +45,7 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen
private final RepositoryComposition composition;
private final RepositoryComposition baseComposition;
private final Lazy<RepositoryComposition> fullComposition;
/**
* Creates a new {@link DefaultRepositoryMetadata} for the given repository interface and repository base class.
@ -62,6 +64,8 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen @@ -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 @@ -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 @@ -133,7 +136,7 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen
@Override
public RepositoryComposition getRepositoryComposition() {
return composition.append(baseComposition.getFragments());
return fullComposition.get();
}
}

17
src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java

@ -95,10 +95,6 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>, @@ -95,10 +95,6 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
private @Nullable Lazy<T> 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<T extends Repository<S, ID>, @@ -261,14 +257,6 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
this.publisher = publisher;
}
public void setModuleBaseClass(String moduleBaseClass) {
this.moduleBaseClass = moduleBaseClass;
}
public void setModuleName(String moduleName) {
this.moduleName = moduleName;
}
@Override
@SuppressWarnings("unchecked")
public EntityInformation<S, ID> getEntityInformation() {
@ -281,6 +269,11 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>, @@ -281,6 +269,11 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
return getRequiredFactory().getRepositoryInformation(getRequiredRepositoryMetadata(), cachedFragments);
}
@Override
public RepositoryFragmentsContributor getRepositoryFragmentsContributor() {
return RepositoryFragmentsContributor.empty();
}
@Override
public PersistentEntity<?, ?> getPersistentEntity() {

9
src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryInformation.java

@ -46,6 +46,15 @@ public interface RepositoryFactoryInformation<T, ID> { @@ -46,6 +46,15 @@ public interface RepositoryFactoryInformation<T, ID> {
*/
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}.

57
src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java

@ -82,6 +82,18 @@ public interface RepositoryFragment<T> { @@ -82,6 +82,18 @@ public interface RepositoryFragment<T> {
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 <T> RepositoryFragment<T> structural(Class<T> 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<T> { @@ -103,6 +115,15 @@ public interface RepositoryFragment<T> {
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<Class<?>> 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<T> { @@ -186,17 +207,30 @@ public interface RepositoryFragment<T> {
class StructuralRepositoryFragment<T> implements RepositoryFragment<T> {
private final Class<T> interfaceOrImplementation;
private final Class<T> interfaceClass;
private final Class<?> implementationClass;
private final Method[] methods;
public StructuralRepositoryFragment(Class<T> interfaceOrImplementation) {
this.interfaceOrImplementation = interfaceOrImplementation;
this.methods = getSignatureContributor().getMethods();
this.interfaceClass = interfaceOrImplementation;
this.implementationClass = interfaceOrImplementation;
this.methods = interfaceOrImplementation.getMethods();
}
public StructuralRepositoryFragment(Class<T> interfaceClass, Class<?> implementationClass) {
this.interfaceClass = interfaceClass;
this.implementationClass = implementationClass;
this.methods = interfaceClass.getMethods();
}
@Override
public Class<?> getSignatureContributor() {
return interfaceOrImplementation;
return interfaceClass;
}
@Override
public Optional<Class<?>> getImplementationClass() {
return Optional.of(implementationClass);
}
@Override
@ -221,31 +255,30 @@ public interface RepositoryFragment<T> { @@ -221,31 +255,30 @@ public interface RepositoryFragment<T> {
@Override
public RepositoryFragment<T> 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;
}
if (!(o instanceof StructuralRepositoryFragment<?> that)) {
if (!ObjectUtils.nullSafeEquals(interfaceClass, that.interfaceClass)) {
return false;
}
return ObjectUtils.nullSafeEquals(interfaceOrImplementation, that.interfaceOrImplementation);
return ObjectUtils.nullSafeEquals(implementationClass, that.implementationClass);
}
@Override
public int hashCode() {
return ObjectUtils.nullSafeHashCode(interfaceOrImplementation);
return ObjectUtils.nullSafeHash(interfaceClass, implementationClass);
}
}

56
src/main/java/org/springframework/data/repository/core/support/RepositoryFragmentsContributor.java

@ -0,0 +1,56 @@ @@ -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}.
* <p>
* 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}).
* <p>
* 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);
}

6
src/main/java/org/springframework/data/repository/support/Repositories.java

@ -35,6 +35,7 @@ import org.springframework.data.mapping.context.MappingContext; @@ -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<Class<?>> { @@ -365,6 +366,11 @@ public class Repositories implements Iterable<Class<?>> {
throw new UnsupportedOperationException();
}
@Override
public RepositoryFragmentsContributor getRepositoryFragmentsContributor() {
throw new UnsupportedOperationException();
}
@Override
public PersistentEntity<?, ?> getPersistentEntity() {
throw new UnsupportedOperationException();

4
src/test/java/org/springframework/data/aot/sample/ConfigWithCustomRepositoryBaseClass.java

@ -22,13 +22,13 @@ import org.springframework.context.annotation.Configuration; @@ -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 {

4
src/test/java/org/springframework/data/aot/sample/ConfigWithSimpleCrudRepository.java

@ -15,6 +15,8 @@ @@ -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 { @@ -34,7 +36,7 @@ public class ConfigWithSimpleCrudRepository {
public static class Person {
@javax.annotation.Nullable
@Nullable
Address address;
}

4
src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java

@ -55,6 +55,7 @@ import org.springframework.data.repository.aot.RepositoryRegistrationAotProcesso @@ -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 { @@ -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

68
src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java

@ -15,33 +15,37 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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<User, String> {
String someMethod();
}
interface QuerydslUserRepository
extends org.springframework.data.repository.Repository<User, String>, QuerydslPredicateExecutor<User> {
}
interface DummyQuerydslPredicateExecutor<T> extends QuerydslPredicateExecutor<T> {}
}

5
src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java

@ -43,6 +43,11 @@ class DummyModuleAotRepositoryContext implements AotRepositoryContext { @@ -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;

28
src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java

@ -15,10 +15,9 @@ @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);

38
src/test/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSourceUnitTests.java

@ -15,15 +15,15 @@ @@ -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 { @@ -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 { @@ -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 { @@ -234,4 +265,5 @@ class AnnotationRepositoryConfigurationSourceUnitTests {
static class ConfigWithSampleAnnotation {}
interface ReactivePersonRepository extends ReactiveCrudRepository<Person, String> {}
}

40
src/test/java/org/springframework/data/repository/config/DummyRegistrarWithContributor.java

@ -0,0 +1,40 @@ @@ -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<? extends Annotation> getAnnotation() {
return EnableRepositoriesWithContributor.class;
}
@Override
protected RepositoryConfigurationExtension getExtension() {
return new DummyConfigurationExtension();
}
}

61
src/test/java/org/springframework/data/repository/config/EnableRepositoriesWithContributor.java

@ -0,0 +1,61 @@ @@ -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<? extends RepositoryFragmentsContributor> fragmentsContributor() default SampleRepositoryFragmentsContributor.class;
Class<?> repositoryBaseClass() default PagingAndSortingRepository.class;
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
String namedQueriesLocation() default "";
String repositoryImplementationPostfix() default "Impl";
boolean considerNestedRepositories() default false;
boolean limitImplementationBasePackages() default true;
BootstrapMode bootstrapMode() default BootstrapMode.DEFAULT;
}

101
src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionReaderTests.java

@ -15,24 +15,29 @@ @@ -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 { @@ -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 { @@ -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 = CustomerRepositoryWithCustomBaseRepo.class;
when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName());
RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean);
RepositoryInformation repositoryInformation = reader.getRepositoryInformation();
assertThat(repositoryInformation.getFragments())
.contains(RepositoryFragment.structural(SampleRepositoryFragmentsContributor.class));
}
@Test // GH-3279
void readsFragmentsContributorFromBeanFactory() {
RegisteredBean repoFactoryBean = repositoryFactory(ReactiveConfig.class);
RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.class);
Class<?> repositoryInterfaceType = ReactiveConfig.CustomerRepositoryReactive.class;
when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName());
RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean);
RepositoryInformation repositoryInformation = reader.getRepositoryInformation();
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;
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.getFragments()).satisfiesExactly(fragment -> {
assertThat(fragment.getSignatureContributor())
.isEqualTo(ConfigWithCustomImplementation.CustomImplInterface.class);
assertThat(fragment.getImplementationClass())
.contains(ConfigWithCustomImplementation.RepositoryWithCustomImplementationImpl.class);
});
}
@Test // GH-3279
void fallsBackToModuleBaseClassIfSetAndNoRepoBaseDefined() {
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());
@Test // GH-3279, GH-3282
void readsFragmentsFromBeanFactory() {
RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithFragments.class);
RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.class);
Mockito.when(repoConfig.getRepositoryInterface()).thenReturn(ConfigWithSimpleCrudRepository.MyRepo.class.getName());
RepositoryInformation repositoryInformation = RepositoryBeanDefinitionReader.repositoryInformation(repoConfig,
rootBeanDefinition, repoFactoryBean.getBeanFactory());
Class<?> repositoryInterfaceType = ConfigWithFragments.RepositoryWithFragments.class;
when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName());
RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean);
RepositoryInformation repositoryInformation = reader.getRepositoryInformation();
assertThat(repositoryInformation.getRepositoryBaseClass()).isEqualTo(ModuleBase.class);
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 { @@ -118,5 +160,4 @@ class RepositoryBeanDefinitionReaderTests {
return RegisteredBean.of(applicationContext.getBeanFactory(), beanNamesForType[0]);
}
static class ModuleBase {}
}

33
src/test/java/org/springframework/data/repository/config/SampleRepositoryFragmentsContributor.java

@ -0,0 +1,33 @@ @@ -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));
}
}

14
src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactoryBean.java

@ -29,6 +29,7 @@ public class DummyRepositoryFactoryBean<T extends Repository<S, ID>, S, ID exten @@ -29,6 +29,7 @@ public class DummyRepositoryFactoryBean<T extends Repository<S, ID>, S, ID exten
extends RepositoryFactoryBeanSupport<T, S, ID> {
private final T repository;
private RepositoryFragmentsContributor repositoryFragmentsContributor = RepositoryFragmentsContributor.empty();
public DummyRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
@ -38,6 +39,19 @@ public class DummyRepositoryFactoryBean<T extends Repository<S, ID>, S, ID exten @@ -38,6 +39,19 @@ public class DummyRepositoryFactoryBean<T extends Repository<S, ID>, 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);

6
src/test/java/org/springframework/data/repository/support/RepositoriesUnitTests.java

@ -46,6 +46,7 @@ import org.springframework.data.repository.core.support.DummyEntityInformation; @@ -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 { @@ -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());

Loading…
Cancel
Save