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 8 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;
import org.springframework.javapoet.ClassName; import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.FieldSpec; import org.springframework.javapoet.FieldSpec;
import org.springframework.javapoet.JavaFile; import org.springframework.javapoet.JavaFile;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeName;
import org.springframework.javapoet.TypeSpec; import org.springframework.javapoet.TypeSpec;
@ -53,6 +54,7 @@ import org.springframework.javapoet.TypeSpec;
class AotRepositoryBuilder { class AotRepositoryBuilder {
private final RepositoryInformation repositoryInformation; private final RepositoryInformation repositoryInformation;
private final String moduleName;
private final ProjectionFactory projectionFactory; private final ProjectionFactory projectionFactory;
private final AotRepositoryFragmentMetadata generationMetadata; private final AotRepositoryFragmentMetadata generationMetadata;
@ -60,9 +62,11 @@ class AotRepositoryBuilder {
private @Nullable BiFunction<Method, RepositoryInformation, @Nullable MethodContributor<? extends QueryMethod>> methodContributorFunction; private @Nullable BiFunction<Method, RepositoryInformation, @Nullable MethodContributor<? extends QueryMethod>> methodContributorFunction;
private ClassCustomizer customizer; private ClassCustomizer customizer;
private AotRepositoryBuilder(RepositoryInformation repositoryInformation, ProjectionFactory projectionFactory) { private AotRepositoryBuilder(RepositoryInformation repositoryInformation, String moduleName,
ProjectionFactory projectionFactory) {
this.repositoryInformation = repositoryInformation; this.repositoryInformation = repositoryInformation;
this.moduleName = moduleName;
this.projectionFactory = projectionFactory; this.projectionFactory = projectionFactory;
this.generationMetadata = new AotRepositoryFragmentMetadata(className()); this.generationMetadata = new AotRepositoryFragmentMetadata(className());
@ -74,11 +78,37 @@ class AotRepositoryBuilder {
this.customizer = (info, metadata, builder) -> {}; 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) { 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( public AotRepositoryBuilder withConstructorCustomizer(
Consumer<AotRepositoryConstructorBuilder> constructorCustomizer) { Consumer<AotRepositoryConstructorBuilder> constructorCustomizer) {
@ -86,42 +116,33 @@ class AotRepositoryBuilder {
return this; return this;
} }
/**
* Configure a {@link MethodContributor}.
*
* @param methodContributorFunction must not be {@literal null}.
* @return {@code this}.
*/
public AotRepositoryBuilder withQueryMethodContributor( public AotRepositoryBuilder withQueryMethodContributor(
BiFunction<Method, RepositoryInformation, @Nullable MethodContributor<? extends QueryMethod>> methodContributorFunction) { BiFunction<Method, RepositoryInformation, @Nullable MethodContributor<? extends QueryMethod>> methodContributorFunction) {
this.methodContributorFunction = methodContributorFunction;
return this;
}
public AotRepositoryBuilder withClassCustomizer(ClassCustomizer classCustomizer) { this.methodContributorFunction = methodContributorFunction;
this.customizer = classCustomizer;
return this; return this;
} }
public AotBundle build() { public AotBundle build() {
List<AotRepositoryMethod> methodMetadata = new ArrayList<>();
RepositoryComposition repositoryComposition = repositoryInformation.getRepositoryComposition();
// start creating the type // start creating the type
TypeSpec.Builder builder = TypeSpec.classBuilder(this.generationMetadata.getTargetTypeName()) // TypeSpec.Builder builder = TypeSpec.classBuilder(this.generationMetadata.getTargetTypeName()) //
.addModifiers(Modifier.PUBLIC) // .addModifiers(Modifier.PUBLIC) //
.addAnnotation(Generated.class) // .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()); repositoryInformation.getRepositoryInterface());
// create the constructor // create the constructor
AotRepositoryConstructorBuilder constructorBuilder = new AotRepositoryConstructorBuilder(repositoryInformation, builder.addMethod(buildConstructor());
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();
Arrays.stream(repositoryInformation.getRepositoryInterface().getMethods()) Arrays.stream(repositoryInformation.getRepositoryInterface().getMethods())
.sorted(Comparator.<Method, String> comparing(it -> { .sorted(Comparator.<Method, String> comparing(it -> {
@ -136,12 +157,35 @@ class AotRepositoryBuilder {
// finally customize the file itself // finally customize the file itself
this.customizer.customize(repositoryInformation, generationMetadata, builder); this.customizer.customize(repositoryInformation, generationMetadata, builder);
JavaFile javaFile = JavaFile.builder(packageName(), builder.build()).build(); 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(), private AotRepositoryMetadata getAotRepositoryMetadata(List<AotRepositoryMethod> methodMetadata) {
repositoryInformation.moduleName() != null ? repositoryInformation.moduleName() : "", repositoryType, 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, private void contributeMethod(Method method, RepositoryComposition repositoryComposition,
@ -185,8 +229,7 @@ class AotRepositoryBuilder {
private AotRepositoryMethod getFragmentMetadata(Method method, RepositoryFragment<?> fragment) { private AotRepositoryMethod getFragmentMetadata(Method method, RepositoryFragment<?> fragment) {
String signature = fragment.getSignatureContributor().getName(); 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); AotFragmentTarget fragmentTarget = new AotFragmentTarget(signature, implementation);
return new AotRepositoryMethod(method.getName(), method.toGenericString(), null, fragmentTarget); return new AotRepositoryMethod(method.getName(), method.toGenericString(), null, fragmentTarget);
@ -240,7 +283,7 @@ class AotRepositoryBuilder {
} }
record AotBundle(JavaFile javaFile, JSONObject metadata) { record AotBundle(JavaFile javaFile, AotRepositoryMetadata metadata) {
} }
} }

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

@ -39,6 +39,7 @@ import org.springframework.util.StringUtils;
* *
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch * @author Mark Paluch
* @since 4.0
*/ */
public class RepositoryContributor { public class RepositoryContributor {
@ -46,19 +47,34 @@ public class RepositoryContributor {
private final AotRepositoryBuilder builder; private final AotRepositoryBuilder builder;
/**
* Create a new {@code RepositoryContributor} for the given {@link AotRepositoryContext}.
*
* @param repositoryContext
*/
public RepositoryContributor(AotRepositoryContext repositoryContext) { public RepositoryContributor(AotRepositoryContext repositoryContext) {
this.builder = AotRepositoryBuilder.forRepository(repositoryContext.getRepositoryInformation(), 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() { protected ProjectionFactory createProjectionFactory() {
return new SpelAwareProxyProjectionFactory(); return new SpelAwareProxyProjectionFactory();
} }
/**
* @return the used {@link ProjectionFactory}.
*/
protected ProjectionFactory getProjectionFactory() { protected ProjectionFactory getProjectionFactory() {
return builder.getProjectionFactory(); return builder.getProjectionFactory();
} }
/**
* @return the used {@link RepositoryInformation}.
*/
protected RepositoryInformation getRepositoryInformation() { protected RepositoryInformation getRepositoryInformation() {
return builder.getRepositoryInformation(); return builder.getRepositoryInformation();
} }
@ -73,13 +89,10 @@ public class RepositoryContributor {
public void contribute(GenerationContext generationContext) { public void contribute(GenerationContext generationContext) {
// TODO: do we need - generationContext.withName("spring-data"); AotRepositoryBuilder.AotBundle aotBundle = builder.withClassCustomizer(this::customizeClass) //
.withConstructorCustomizer(this::customizeConstructor) //
builder.withClassCustomizer(this::customizeClass); .withQueryMethodContributor(this::contributeQueryMethod) //
builder.withConstructorCustomizer(this::customizeConstructor); .build();
builder.withQueryMethodContributor(this::contributeQueryMethod);
AotRepositoryBuilder.AotBundle aotBundle = builder.build();
Class<?> repositoryInterface = getRepositoryInformation().getRepositoryInterface(); Class<?> repositoryInterface = getRepositoryInformation().getRepositoryInterface();
String repositoryJsonFileName = getRepositoryJsonFileName(repositoryInterface); String repositoryJsonFileName = getRepositoryJsonFileName(repositoryInterface);
@ -89,7 +102,7 @@ public class RepositoryContributor {
String repositoryJson; String repositoryJson;
try { try {
repositoryJson = aotBundle.metadata().toString(2); repositoryJson = aotBundle.metadata().toJson().toString(2);
} catch (JSONException e) { } catch (JSONException e) {
throw new RuntimeException(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
private static final String QUERY_LOOKUP_STRATEGY = "queryLookupStrategy"; private static final String QUERY_LOOKUP_STRATEGY = "queryLookupStrategy";
private static final String REPOSITORY_FACTORY_BEAN_CLASS = "repositoryFactoryBeanClass"; private static final String REPOSITORY_FACTORY_BEAN_CLASS = "repositoryFactoryBeanClass";
private static final String REPOSITORY_BASE_CLASS = "repositoryBaseClass"; 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 CONSIDER_NESTED_REPOSITORIES = "considerNestedRepositories";
private static final String BOOTSTRAP_MODE = "bootstrapMode"; private static final String BOOTSTRAP_MODE = "bootstrapMode";
private static final String BEAN_NAME_GENERATOR = "nameGenerator"; private static final String BEAN_NAME_GENERATOR = "nameGenerator";
@ -187,6 +188,16 @@ public class AnnotationRepositoryConfigurationSource extends RepositoryConfigura
: Optional.of(repositoryBaseClass.getName()); : 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. * Returns the {@link AnnotationAttributes} of the annotation configured.
* *

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

@ -16,9 +16,9 @@
package org.springframework.data.repository.config; package org.springframework.data.repository.config;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Set; import java.util.Set;
import org.springframework.core.SpringProperties;
import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.data.aot.AotContext; import org.springframework.data.aot.AotContext;
import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryInformation;
@ -28,8 +28,9 @@ import org.springframework.data.repository.core.RepositoryInformation;
* *
* @author Christoph Strobl * @author Christoph Strobl
* @author John Blum * @author John Blum
* @see AotContext * @author Mark Paluch
* @since 3.0 * @since 3.0
* @see AotContext
*/ */
public interface AotRepositoryContext extends AotContext { public interface AotRepositoryContext extends AotContext {
@ -38,6 +39,12 @@ public interface AotRepositoryContext extends AotContext {
*/ */
String getBeanName(); 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. * @return a {@link Set} of {@link String base packages} to search for repositories.
*/ */
@ -46,7 +53,7 @@ public interface AotRepositoryContext extends AotContext {
/** /**
* @return the {@link Annotation} types used to identify domain types. * @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. * @return {@link RepositoryInformation metadata} about the repository itself.
@ -64,4 +71,5 @@ public interface AotRepositoryContext extends AotContext {
* @return all {@link Class types} reachable from the repository. * @return all {@link Class types} reachable from the repository.
*/ */
Set<Class<?>> getResolvedTypes(); Set<Class<?>> getResolvedTypes();
} }

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

@ -17,52 +17,40 @@ package org.springframework.data.repository.config;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set; 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.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryInformationSupport; import org.springframework.data.repository.core.RepositoryInformationSupport;
import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryComposition;
import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.util.Lazy;
/** /**
* {@link RepositoryInformation} based on {@link RepositoryMetadata} collected at build time. * {@link RepositoryInformation} based on {@link RepositoryMetadata} collected at build time.
* *
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch
* @since 3.0 * @since 3.0
*/ */
class AotRepositoryInformation extends RepositoryInformationSupport implements RepositoryInformation { public class AotRepositoryInformation extends RepositoryInformationSupport implements RepositoryInformation {
private final @Nullable String moduleName; private final RepositoryComposition fragmentsComposition;
private final Supplier<Collection<RepositoryFragment<?>>> fragments; private final RepositoryComposition baseComposition;
private final RepositoryComposition composition;
private final Lazy<RepositoryComposition> repositoryComposition; public AotRepositoryInformation(RepositoryMetadata repositoryMetadata, Class<?> repositoryBaseClass,
private final Lazy<RepositoryComposition> baseComposition; Collection<RepositoryFragment<?>> fragments) {
AotRepositoryInformation(@Nullable String moduleName, Supplier<RepositoryMetadata> repositoryMetadata, super(() -> repositoryMetadata, () -> repositoryBaseClass);
Supplier<Class<?>> repositoryBaseClass, Supplier<Collection<RepositoryFragment<?>>> fragments) {
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.composition = this.fragmentsComposition.append(this.baseComposition.getFragments());
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());
});
} }
/** /**
@ -71,31 +59,27 @@ class AotRepositoryInformation extends RepositoryInformationSupport implements R
*/ */
@Override @Override
public Set<RepositoryFragment<?>> getFragments() { public Set<RepositoryFragment<?>> getFragments() {
return new LinkedHashSet<>(fragments.get()); return fragmentsComposition.getFragments().toSet();
} }
@Override @Override
public boolean isCustomMethod(Method method) { public boolean isCustomMethod(Method method) {
return repositoryComposition.get().findMethod(method).isPresent(); return fragmentsComposition.findMethod(method).isPresent();
} }
@Override @Override
public boolean isBaseClassMethod(Method method) { public boolean isBaseClassMethod(Method method) {
return baseComposition.get().findMethod(method).isPresent(); return baseComposition.findMethod(method).isPresent();
} }
@Override @Override
public Method getTargetClassMethod(Method method) { public Method getTargetClassMethod(Method method) {
return baseComposition.get().findMethod(method).orElse(method); return baseComposition.findMethod(method).orElse(method);
} }
@Override @Override
public RepositoryComposition getRepositoryComposition() { 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 @@
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,14 +16,14 @@
package org.springframework.data.repository.config; package org.springframework.data.repository.config;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.data.aot.AotContext; import org.springframework.data.aot.AotContext;
@ -37,29 +37,42 @@ import org.springframework.data.util.TypeUtils;
* *
* @author Christoph Strobl * @author Christoph Strobl
* @author John Blum * @author John Blum
* @author Mark Paluch
* @see AotRepositoryContext * @see AotRepositoryContext
* @since 3.0 * @since 3.0
*/ */
@SuppressWarnings("NullAway") // TODO @SuppressWarnings("NullAway") // TODO
class DefaultAotRepositoryContext implements AotRepositoryContext { class DefaultAotRepositoryContext implements AotRepositoryContext {
private final RegisteredBean bean;
private final String moduleName;
private final AotContext aotContext; private final AotContext aotContext;
private final RepositoryInformation repositoryInformation;
private final Lazy<Set<MergedAnnotation<Annotation>>> resolvedAnnotations = Lazy.of(this::discoverAnnotations); private final Lazy<Set<MergedAnnotation<Annotation>>> resolvedAnnotations = Lazy.of(this::discoverAnnotations);
private final Lazy<Set<Class<?>>> managedTypes = Lazy.of(this::discoverTypes); private final Lazy<Set<Class<?>>> managedTypes = Lazy.of(this::discoverTypes);
private @Nullable RepositoryInformation repositoryInformation; private Set<String> basePackages = Collections.emptySet();
private @Nullable Set<String> basePackages; private Collection<Class<? extends Annotation>> identifyingAnnotations = Collections.emptySet();
private @Nullable Set<Class<? extends Annotation>> identifyingAnnotations; private String beanName;
private @Nullable 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.aotContext = aotContext;
this.beanName = bean.getBeanName();
} }
public AotContext getAotContext() { public AotContext getAotContext() {
return aotContext; return aotContext;
} }
@Override
public String getModuleName() {
return moduleName;
}
@Override @Override
public ConfigurableListableBeanFactory getBeanFactory() { public ConfigurableListableBeanFactory getBeanFactory() {
return getAotContext().getBeanFactory(); return getAotContext().getBeanFactory();
@ -72,7 +85,7 @@ class DefaultAotRepositoryContext implements AotRepositoryContext {
@Override @Override
public Set<String> getBasePackages() { public Set<String> getBasePackages() {
return basePackages == null ? Collections.emptySet() : basePackages; return basePackages;
} }
public void setBasePackages(Set<String> basePackages) { public void setBasePackages(Set<String> basePackages) {
@ -89,11 +102,11 @@ class DefaultAotRepositoryContext implements AotRepositoryContext {
} }
@Override @Override
public Set<Class<? extends Annotation>> getIdentifyingAnnotations() { public Collection<Class<? extends Annotation>> getIdentifyingAnnotations() {
return identifyingAnnotations == null ? Collections.emptySet() : identifyingAnnotations; return identifyingAnnotations;
} }
public void setIdentifyingAnnotations(Set<Class<? extends Annotation>> identifyingAnnotations) { public void setIdentifyingAnnotations(Collection<Class<? extends Annotation>> identifyingAnnotations) {
this.identifyingAnnotations = identifyingAnnotations; this.identifyingAnnotations = identifyingAnnotations;
} }
@ -102,10 +115,6 @@ class DefaultAotRepositoryContext implements AotRepositoryContext {
return repositoryInformation; return repositoryInformation;
} }
public void setRepositoryInformation(RepositoryInformation repositoryInformation) {
this.repositoryInformation = repositoryInformation;
}
@Override @Override
public Set<MergedAnnotation<Annotation>> getResolvedAnnotations() { public Set<MergedAnnotation<Annotation>> getResolvedAnnotations() {
return resolvedAnnotations.get(); return resolvedAnnotations.get();
@ -132,24 +141,18 @@ class DefaultAotRepositoryContext implements AotRepositoryContext {
.flatMap(type -> TypeUtils.resolveUsedAnnotations(type).stream()) .flatMap(type -> TypeUtils.resolveUsedAnnotations(type).stream())
.collect(Collectors.toCollection(LinkedHashSet::new)); .collect(Collectors.toCollection(LinkedHashSet::new));
if (repositoryInformation != null) { annotations.addAll(TypeUtils.resolveUsedAnnotations(repositoryInformation.getRepositoryInterface()));
annotations.addAll(TypeUtils.resolveUsedAnnotations(repositoryInformation.getRepositoryInterface()));
}
return annotations; return annotations;
} }
protected Set<Class<?>> discoverTypes() { protected Set<Class<?>> discoverTypes() {
Set<Class<?>> types = new LinkedHashSet<>(); Set<Class<?>> types = new LinkedHashSet<>(TypeCollector.inspect(repositoryInformation.getDomainType()).list());
if (repositoryInformation != null) { repositoryInformation.getQueryMethods().stream()
types.addAll(TypeCollector.inspect(repositoryInformation.getDomainType()).list()); .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()) { if (!getIdentifyingAnnotations().isEmpty()) {
@ -160,4 +163,5 @@ class DefaultAotRepositoryContext implements AotRepositoryContext {
return types; return types;
} }
} }

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

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

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

@ -116,6 +116,11 @@ class RepositoryBeanDefinitionBuilder {
.rootBeanDefinition(configuration.getRepositoryFactoryBeanClassName()); .rootBeanDefinition(configuration.getRepositoryFactoryBeanClassName());
builder.getRawBeanDefinition().setSource(configuration.getSource()); 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.addConstructorArgValue(configuration.getRepositoryInterface());
builder.addPropertyValue("queryLookupStrategyKey", configuration.getQueryLookupStrategyKey()); builder.addPropertyValue("queryLookupStrategyKey", configuration.getQueryLookupStrategyKey());
builder.addPropertyValue("lazyInit", configuration.isLazyInit()); builder.addPropertyValue("lazyInit", configuration.isLazyInit());
@ -125,6 +130,10 @@ class RepositoryBeanDefinitionBuilder {
configuration.getRepositoryBaseClassName()// configuration.getRepositoryBaseClassName()//
.ifPresent(it -> builder.addPropertyValue("repositoryBaseClass", it)); .ifPresent(it -> builder.addPropertyValue("repositoryBaseClass", it));
configuration.getRepositoryFragmentsContributorClassName()//
.ifPresent(it -> builder.addPropertyValue("repositoryFragmentsContributor",
BeanDefinitionBuilder.genericBeanDefinition(it).getRawBeanDefinition()));
NamedQueriesBeanDefinitionBuilder definitionBuilder = new NamedQueriesBeanDefinitionBuilder( NamedQueriesBeanDefinitionBuilder definitionBuilder = new NamedQueriesBeanDefinitionBuilder(
extension.getDefaultNamedQueryLocation()); extension.getDefaultNamedQueryLocation());
configuration.getNamedQueriesLocation().ifPresent(definitionBuilder::setLocations); configuration.getNamedQueriesLocation().ifPresent(definitionBuilder::setLocations);

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

@ -15,139 +15,197 @@
*/ */
package org.springframework.data.repository.config; package org.springframework.data.repository.config;
import java.lang.reflect.Constructor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; 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.ConstructorArgumentValues.ValueHolder;
import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.RegisteredBean; 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.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; 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;
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.ClassUtils;
import org.springframework.util.ObjectUtils;
/** /**
* Reader used to extract {@link RepositoryInformation} from {@link RepositoryConfiguration}. * Reader used to extract {@link RepositoryInformation} from {@link RepositoryConfiguration}.
* *
* @author Christoph Strobl * @author Christoph Strobl
* @author John Blum * @author John Blum
* @author Mark Paluch
* @since 3.0 * @since 3.0
*/ */
class RepositoryBeanDefinitionReader { class RepositoryBeanDefinitionReader {
/** private final RootBeanDefinition beanDefinition;
* @return private final ConfigurableListableBeanFactory beanFactory;
*/ private final ClassLoader beanClassLoader;
static RepositoryInformation repositoryInformation(RepositoryConfiguration<?> repoConfig, RegisteredBean repoBean) { private final @Nullable RepositoryConfiguration<?> configuration;
return repositoryInformation(repoConfig, repoBean.getMergedBeanDefinition(), repoBean.getBeanFactory()); 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. * @return the {@link RepositoryInformation} derived from the repository bean.
* @param beanFactory
* @return
*/ */
@SuppressWarnings("NullAway") public RepositoryInformation getRepositoryInformation() {
static RepositoryInformation repositoryInformation(RepositoryConfiguration<?> repoConfig, BeanDefinition source,
ConfigurableListableBeanFactory beanFactory) {
RepositoryMetadata metadata = AbstractRepositoryMetadata RepositoryMetadata metadata = AbstractRepositoryMetadata
.getMetadata(forName(repoConfig.getRepositoryInterface(), beanFactory)); .getMetadata(forName(configuration.getRepositoryInterface()));
Class<?> repositoryBaseClass = readRepositoryBaseClass(source, beanFactory); Class<?> repositoryBaseClass = getRepositoryBaseClass();
List<RepositoryFragment<?>> fragmentList = readRepositoryFragments(source, beanFactory);
if (source.getPropertyValues().contains("customImplementation")) { List<RepositoryFragment<?>> fragments = new ArrayList<>();
fragments.addAll(readRepositoryFragments());
Object o = source.getPropertyValues().get("customImplementation"); fragments.addAll(readContributedRepositoryFragments(metadata));
if (o instanceof RuntimeBeanReference rbr) {
BeanDefinition customImplBeanDefintion = beanFactory.getBeanDefinition(rbr.getBeanName()); RepositoryFragment<?> customImplementation = getCustomImplementation();
Class<?> beanType = forName(customImplBeanDefintion.getBeanClassName(), beanFactory); if (customImplementation != null) {
ResolvableType[] interfaces = ResolvableType.forClass(beanType).getInterfaces(); fragments.add(0, customImplementation);
if (interfaces.length == 1) { }
fragmentList.add(new ImplementedRepositoryFragment(interfaces[0].toClass(), beanType));
} else { return new AotRepositoryInformation(metadata, repositoryBaseClass, fragments);
boolean found = false; }
for (ResolvableType i : interfaces) {
if (beanType.getSimpleName().contains(i.resolve().getSimpleName())) { private @Nullable RepositoryFragment<?> getCustomImplementation() {
fragmentList.add(new ImplementedRepositoryFragment(interfaces[0].toClass(), beanType));
found = true; PropertyValues mpv = beanDefinition.getPropertyValues();
break; PropertyValue customImplementation = mpv.getPropertyValue("customImplementation");
}
} if (customImplementation != null) {
if (!found) {
fragmentList.add(RepositoryFragment.implemented(beanType)); 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"); return null;
AotRepositoryInformation repositoryInformation = new AotRepositoryInformation(moduleName, () -> metadata,
() -> repositoryBaseClass, () -> fragmentList);
return repositoryInformation;
} }
@SuppressWarnings("NullAway") @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) { if (repoBaseClassName != null) {
return forName(repoBaseClassName.toString(), beanFactory); return forName(repoBaseClassName.toString());
}
if (source.getPropertyValues().contains("moduleBaseClass")) {
return forName((String) source.getPropertyValues().get("moduleBaseClass"), beanFactory);
} }
return Dummy.class; return Dummy.class;
} }
@SuppressWarnings("NullAway") @SuppressWarnings("NullAway")
private static List<RepositoryFragment<?>> readRepositoryFragments(BeanDefinition source, private List<RepositoryFragment<?>> readRepositoryFragments() {
ConfigurableListableBeanFactory beanFactory) {
RuntimeBeanReference beanReference = (RuntimeBeanReference) source.getPropertyValues().get("repositoryFragments"); RuntimeBeanReference beanReference = (RuntimeBeanReference) beanDefinition.getPropertyValues()
.get("repositoryFragments");
BeanDefinition fragments = beanFactory.getBeanDefinition(beanReference.getBeanName()); BeanDefinition fragments = beanFactory.getBeanDefinition(beanReference.getBeanName());
ValueHolder fragmentBeanNameList = fragments.getConstructorArgumentValues().getArgumentValue(0, List.class); ValueHolder fragmentBeanNameList = fragments.getConstructorArgumentValues().getArgumentValue(0, List.class);
List<String> fragmentBeanNames = (List<String>) fragmentBeanNameList.getValue(); List<String> fragmentBeanNames = (List<String>) fragmentBeanNameList.getValue();
List<RepositoryFragment<?>> fragmentList = new ArrayList<>(); List<RepositoryFragment<?>> fragmentList = new ArrayList<>();
for (String beanName : fragmentBeanNames) { for (String beanName : fragmentBeanNames) {
BeanDefinition fragmentBeanDefinition = beanFactory.getBeanDefinition(beanName); BeanDefinition fragmentBeanDefinition = beanFactory.getBeanDefinition(beanName);
ValueHolder argumentValue = fragmentBeanDefinition.getConstructorArgumentValues().getArgumentValue(0, ConstructorArgumentValues cv = fragmentBeanDefinition.getConstructorArgumentValues();
String.class); ValueHolder interfaceClassVh = cv.getArgumentValue(0, String.class);
ValueHolder argumentValue1 = fragmentBeanDefinition.getConstructorArgumentValues().getArgumentValue(1, null, null, ValueHolder implementationVh = cv.getArgumentValue(1, null, null, null);
null);
Object fragmentClassName = argumentValue.getValue(); Object fragmentClassName = interfaceClassVh.getValue();
Class<?> interfaceClass = forName(fragmentClassName.toString());
try {
Class<?> type = ClassUtils.forName(fragmentClassName.toString(), beanFactory.getBeanClassLoader()); if (implementationVh != null && implementationVh.getValue() instanceof RuntimeBeanReference rbf) {
BeanDefinition implBeanDef = beanFactory.getBeanDefinition(rbf.getBeanName());
if (argumentValue1 != null && argumentValue1.getValue() instanceof RuntimeBeanReference rbf) { Class<?> implClass = getClass(implBeanDef);
BeanDefinition implBeanDef = beanFactory.getBeanDefinition(rbf.getBeanName()); fragmentList.add(RepositoryFragment.structural(interfaceClass, implClass));
Class implClass = ClassUtils.forName(implBeanDef.getBeanClassName(), beanFactory.getBeanClassLoader()); } else {
fragmentList.add(new RepositoryFragment.ImplementedRepositoryFragment(type, implClass)); fragmentList.add(RepositoryFragment.structural(interfaceClass));
} else {
fragmentList.add(RepositoryFragment.structural(type));
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} }
} }
return fragmentList; 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 abstract class Dummy implements CrudRepository<Object, Object>, PagingAndSortingRepository<Object, Object> {}
static Class<?> forName(String name, ConfigurableListableBeanFactory beanFactory) { private Class<?> forName(String name) {
try { try {
return ClassUtils.forName(name, beanFactory.getBeanClassLoader()); return ClassUtils.forName(name, beanClassLoader);
} catch (ClassNotFoundException cause) { } catch (ClassNotFoundException cause) {
throw new TypeNotPresentException(name, 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
*/ */
Optional<String> getRepositoryBaseClassName(); 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. * Returns the name of the repository factory bean class to be used.
* *
@ -157,11 +166,12 @@ public interface RepositoryConfiguration<T extends RepositoryConfigurationSource
ImplementationLookupConfiguration toLookupConfiguration(MetadataReaderFactory factory); 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}. * @return can be {@literal null}.
* @since 2.3 * @since 2.3
*/ */
@Nullable @Nullable
String getResourceDescription(); String getResourceDescription();
} }

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

@ -71,6 +71,11 @@ class RepositoryConfigurationAdapter<T extends RepositoryConfigurationSource>
return repositoryConfiguration.getRepositoryBaseClassName(); return repositoryConfiguration.getRepositoryBaseClassName();
} }
@Override
public Optional<String> getRepositoryFragmentsContributorClassName() {
return repositoryConfiguration.getRepositoryFragmentsContributorClassName();
}
@Override @Override
public String getRepositoryFactoryBeanClassName() { public String getRepositoryFactoryBeanClassName() {
return repositoryConfiguration.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;
import java.util.Collection; import java.util.Collection;
import java.util.Locale; 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.aot.BeanRegistrationAotProcessor;
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinition;
@ -63,7 +63,6 @@ public interface RepositoryConfigurationExtension {
* @see org.springframework.beans.factory.aot.BeanRegistrationAotProcessor * @see org.springframework.beans.factory.aot.BeanRegistrationAotProcessor
* @since 3.0 * @since 3.0
*/ */
@NonNull
default Class<? extends BeanRegistrationAotProcessor> getRepositoryAotProcessor() { default Class<? extends BeanRegistrationAotProcessor> getRepositoryAotProcessor() {
return RepositoryRegistrationAotProcessor.class; return RepositoryRegistrationAotProcessor.class;
} }
@ -90,6 +89,16 @@ public interface RepositoryConfigurationExtension {
*/ */
String getDefaultNamedQueryLocation(); 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. * 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 {
*/ */
Optional<String> getRepositoryBaseClassName(); 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. * 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 @@
package org.springframework.data.repository.config; package org.springframework.data.repository.config;
import java.io.Serializable; import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.aop.SpringProxy; import org.springframework.aop.SpringProxy;
import org.springframework.aop.framework.Advised; import org.springframework.aop.framework.Advised;
import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.GenerationContext;
@ -37,11 +37,11 @@ import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.aot.BeanRegistrationCode; import org.springframework.beans.factory.aot.BeanRegistrationCode;
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments;
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator; 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.RegisteredBean;
import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.DecoratingProxy; import org.springframework.core.DecoratingProxy;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.Environment;
import org.springframework.data.aot.AotContext; import org.springframework.data.aot.AotContext;
import org.springframework.data.projection.EntityProjectionIntrospector; import org.springframework.data.projection.EntityProjectionIntrospector;
import org.springframework.data.projection.TargetAware; import org.springframework.data.projection.TargetAware;
@ -66,18 +66,19 @@ import org.springframework.util.ClassUtils;
* @author Mark Paluch * @author Mark Paluch
* @since 3.0 * @since 3.0
*/ */
// TODO: Consider moving to data.repository.aot
public class RepositoryRegistrationAotContribution implements BeanRegistrationAotContribution { 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 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 * Constructs a new instance of the {@link RepositoryRegistrationAotContribution} initialized with the given, required
@ -85,14 +86,18 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
* *
* @param processor reference back to the {@link RepositoryRegistrationAotProcessor} from which this contribution was * @param processor reference back to the {@link RepositoryRegistrationAotProcessor} from which this contribution was
* created. * created.
* @param context reference back to the {@link AotRepositoryContext} from which this contribution was created.
* @throws IllegalArgumentException if the {@link RepositoryRegistrationAotProcessor} is {@literal null}. * @throws IllegalArgumentException if the {@link RepositoryRegistrationAotProcessor} is {@literal null}.
* @see RepositoryRegistrationAotProcessor * @see RepositoryRegistrationAotProcessor
*/ */
protected RepositoryRegistrationAotContribution(RepositoryRegistrationAotProcessor processor) { protected RepositoryRegistrationAotContribution(RepositoryRegistrationAotProcessor processor,
AotRepositoryContext context) {
Assert.notNull(processor, "RepositoryRegistrationAotProcessor must not be null"); Assert.notNull(processor, "RepositoryRegistrationAotProcessor must not be null");
Assert.notNull(context, "AotRepositoryContext must not be null");
this.aotProcessor = processor; this.aotProcessor = processor;
this.repositoryContext = context;
} }
/** /**
@ -101,16 +106,57 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
* *
* @param processor reference back to the {@link RepositoryRegistrationAotProcessor} from which this contribution was * @param processor reference back to the {@link RepositoryRegistrationAotProcessor} from which this contribution was
* created. * created.
* @return a new instance of {@link RepositoryRegistrationAotContribution}. * @return a new instance of {@link RepositoryRegistrationAotContribution} if a contribution can be made;
* @throws IllegalArgumentException if the {@link RepositoryRegistrationAotProcessor} is {@literal null}. * {@literal null} if no contribution can be made.
* @see RepositoryRegistrationAotProcessor * @see RepositoryRegistrationAotProcessor
*/ */
public static RepositoryRegistrationAotContribution fromProcessor(RepositoryRegistrationAotProcessor processor) { public static @Nullable RepositoryRegistrationAotContribution load(RepositoryRegistrationAotProcessor processor,
return new RepositoryRegistrationAotContribution(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() { protected @Nullable BiFunction<AotRepositoryContext, GenerationContext, @Nullable RepositoryContributor> getModuleContribution() {
@ -118,10 +164,6 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
} }
protected AotRepositoryContext getRepositoryContext() { 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; return this.repositoryContext;
} }
@ -137,28 +179,27 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
getRepositoryRegistrationAotProcessor().logTrace(message, arguments); getRepositoryRegistrationAotProcessor().logTrace(message, arguments);
} }
/** private static @Nullable AotRepositoryContext buildAotRepositoryContext(Environment environment, RegisteredBean bean,
* Builds a {@link RepositoryRegistrationAotContribution} for given, required {@link RegisteredBean} representing the RepositoryConfiguration<?> repositoryConfiguration) {
* {@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");
RepositoryConfiguration<?> repositoryMetadata = getRepositoryRegistrationAotProcessor() RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(bean);
.getRepositoryMetadata(repositoryBean); 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
@Override @Override
public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { 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); contributeRepositoryInfo(this.repositoryContext, generationContext);
var moduleContribution = getModuleContribution(); var moduleContribution = getModuleContribution();
@ -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) { private void contributeRepositoryInfo(AotRepositoryContext repositoryContext, GenerationContext contribution) {
RepositoryInformation repositoryInformation = getRepositoryInformation(); RepositoryInformation repositoryInformation = getRepositoryInformation();
@ -239,7 +281,7 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
for (RepositoryFragment<?> fragment : getRepositoryInformation().getFragments()) { for (RepositoryFragment<?> fragment : getRepositoryInformation().getFragments()) {
Class<?> repositoryFragmentType = fragment.getSignatureContributor(); Class<?> repositoryFragmentType = fragment.getSignatureContributor();
Optional<?> implementation = fragment.getImplementation(); Optional<Class<?>> implementation = fragment.getImplementationClass();
contribution.getRuntimeHints().reflection().registerType(repositoryFragmentType, hint -> { contribution.getRuntimeHints().reflection().registerType(repositoryFragmentType, hint -> {
@ -250,13 +292,12 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
} }
}); });
implementation.ifPresent(impl -> { implementation.ifPresent(typeToRegister -> {
Class<?> typeToRegister = impl instanceof Class c ? c : impl.getClass();
contribution.getRuntimeHints().reflection().registerType(typeToRegister, hint -> { contribution.getRuntimeHints().reflection().registerType(typeToRegister, hint -> {
hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS);
if (!impl.getClass().isInterface()) { if (!typeToRegister.isInterface()) {
hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
} }
}); });
@ -289,12 +330,6 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
} }
// }); // });
// Reactive Repositories
if (repositoryInformation.isReactiveRepository()) {
// TODO: do we still need this and how to configure it?
// registry.initialization().add(NativeInitializationEntry.ofBuildTimeType(configuration.getRepositoryInterface()));
}
// Kotlin // Kotlin
if (isKotlinCoroutineRepository(repositoryContext, repositoryInformation)) { if (isKotlinCoroutineRepository(repositoryContext, repositoryInformation)) {
contribution.getRuntimeHints().reflection().registerTypes(kotlinRepositoryReflectionTypeReferences(), contribution.getRuntimeHints().reflection().registerTypes(kotlinRepositoryReflectionTypeReferences(),
@ -356,29 +391,5 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
|| ClassUtils.isPrimitiveArray(type); // || 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;
* *
* @author Christoph Strobl * @author Christoph Strobl
* @author John Blum * @author John Blum
* @author Mark Paluch
* @since 3.0 * @since 3.0
*/ */
public class RepositoryRegistrationAotProcessor public class RepositoryRegistrationAotProcessor
@ -123,11 +124,16 @@ public class RepositoryRegistrationAotProcessor
return getConfigMap().containsKey(bean.getBeanName()); return getConfigMap().containsKey(bean.getBeanName());
} }
protected RepositoryRegistrationAotContribution newRepositoryRegistrationAotContribution( protected @Nullable RepositoryRegistrationAotContribution newRepositoryRegistrationAotContribution(
RegisteredBean repositoryBean) { RegisteredBean repositoryBean) {
RepositoryRegistrationAotContribution contribution = RepositoryRegistrationAotContribution.fromProcessor(this) RepositoryRegistrationAotContribution contribution = RepositoryRegistrationAotContribution.load(this,
.forBean(repositoryBean); repositoryBean);
// cannot contribute a repository bean.
if (contribution == null) {
return null;
}
//TODO: add the hook for customizing bean initialization code here! //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
return getNullDefaultedAttribute(element, REPOSITORY_BASE_CLASS_NAME); return getNullDefaultedAttribute(element, REPOSITORY_BASE_CLASS_NAME);
} }
@Override
public Optional<String> getRepositoryFragmentsContributorClassName() {
return Optional.empty();
}
@Override @Override
public Optional<String> getRepositoryFactoryBeanClassName() { public Optional<String> getRepositoryFactoryBeanClassName() {
return getNullDefaultedAttribute(element, REPOSITORY_FACTORY_BEAN_CLASS_NAME); 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;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.List; import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryComposition;
/** /**
@ -106,8 +105,4 @@ public interface RepositoryInformation extends RepositoryMetadata {
*/ */
RepositoryComposition getRepositoryComposition(); 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;
import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryInformationSupport; import org.springframework.data.repository.core.RepositoryInformationSupport;
import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.util.Lazy;
import org.springframework.lang.Contract; import org.springframework.lang.Contract;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
@ -44,6 +45,7 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen
private final RepositoryComposition composition; private final RepositoryComposition composition;
private final RepositoryComposition baseComposition; private final RepositoryComposition baseComposition;
private final Lazy<RepositoryComposition> fullComposition;
/** /**
* Creates a new {@link DefaultRepositoryMetadata} for the given repository interface and repository base class. * Creates a new {@link DefaultRepositoryMetadata} for the given repository interface and repository base class.
@ -62,6 +64,8 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen
this.baseComposition = RepositoryComposition.of(RepositoryFragment.structural(repositoryBaseClass)) // this.baseComposition = RepositoryComposition.of(RepositoryFragment.structural(repositoryBaseClass)) //
.withArgumentConverter(composition.getArgumentConverter()) // .withArgumentConverter(composition.getArgumentConverter()) //
.withMethodLookup(composition.getMethodLookup()); .withMethodLookup(composition.getMethodLookup());
this.fullComposition = Lazy.of(() -> composition.append(baseComposition.getFragments()));
} }
@Override @Override
@ -106,7 +110,6 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen
@Override @Override
protected boolean isQueryMethodCandidate(Method method) { protected boolean isQueryMethodCandidate(Method method) {
// FIXME - that should be simplified
boolean queryMethodCandidate = super.isQueryMethodCandidate(method); boolean queryMethodCandidate = super.isQueryMethodCandidate(method);
if(!isQueryAnnotationPresentOn(method)) { if(!isQueryAnnotationPresentOn(method)) {
return queryMethodCandidate; return queryMethodCandidate;
@ -133,7 +136,7 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen
@Override @Override
public RepositoryComposition getRepositoryComposition() { 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>,
private @Nullable Lazy<T> repository; private @Nullable Lazy<T> repository;
private @Nullable RepositoryMetadata repositoryMetadata; 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. * Creates a new {@link RepositoryFactoryBeanSupport} for the given repository interface.
* *
@ -261,14 +257,6 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
this.publisher = publisher; this.publisher = publisher;
} }
public void setModuleBaseClass(String moduleBaseClass) {
this.moduleBaseClass = moduleBaseClass;
}
public void setModuleName(String moduleName) {
this.moduleName = moduleName;
}
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public EntityInformation<S, ID> getEntityInformation() { public EntityInformation<S, ID> getEntityInformation() {
@ -281,6 +269,11 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
return getRequiredFactory().getRepositoryInformation(getRequiredRepositoryMetadata(), cachedFragments); return getRequiredFactory().getRepositoryInformation(getRequiredRepositoryMetadata(), cachedFragments);
} }
@Override
public RepositoryFragmentsContributor getRepositoryFragmentsContributor() {
return RepositoryFragmentsContributor.empty();
}
@Override @Override
public PersistentEntity<?, ?> getPersistentEntity() { public PersistentEntity<?, ?> getPersistentEntity() {

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

@ -46,6 +46,15 @@ public interface RepositoryFactoryInformation<T, ID> {
*/ */
RepositoryInformation getRepositoryInformation(); 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 * 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}. * 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> {
return new StructuralRepositoryFragment<>(interfaceOrImplementation); 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 * Attempt to find the {@link Method} by name and exact parameters. Returns {@literal true} if the method was found or
* {@literal false} otherwise. * {@literal false} otherwise.
@ -103,6 +115,15 @@ public interface RepositoryFragment<T> {
return Optional.empty(); 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}. * @return a {@link Stream} of methods exposed by this {@link RepositoryFragment}.
*/ */
@ -186,17 +207,30 @@ public interface RepositoryFragment<T> {
class StructuralRepositoryFragment<T> implements 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; private final Method[] methods;
public StructuralRepositoryFragment(Class<T> interfaceOrImplementation) { public StructuralRepositoryFragment(Class<T> interfaceOrImplementation) {
this.interfaceOrImplementation = interfaceOrImplementation; this.interfaceClass = interfaceOrImplementation;
this.methods = getSignatureContributor().getMethods(); 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 @Override
public Class<?> getSignatureContributor() { public Class<?> getSignatureContributor() {
return interfaceOrImplementation; return interfaceClass;
}
@Override
public Optional<Class<?>> getImplementationClass() {
return Optional.of(implementationClass);
} }
@Override @Override
@ -221,31 +255,30 @@ public interface RepositoryFragment<T> {
@Override @Override
public RepositoryFragment<T> withImplementation(T implementation) { public RepositoryFragment<T> withImplementation(T implementation) {
return new ImplementedRepositoryFragment<>(interfaceOrImplementation, implementation); return new ImplementedRepositoryFragment<>(interfaceClass, implementation);
} }
@Override @Override
public String toString() { public String toString() {
return String.format("StructuralRepositoryFragment %s", ClassUtils.getShortName(interfaceOrImplementation)); return String.format("StructuralRepositoryFragment %s", ClassUtils.getShortName(interfaceClass));
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (!(o instanceof StructuralRepositoryFragment<?> that)) {
if (this == o) { return false;
return true;
} }
if (!(o instanceof StructuralRepositoryFragment<?> that)) { if (!ObjectUtils.nullSafeEquals(interfaceClass, that.interfaceClass)) {
return false; return false;
} }
return ObjectUtils.nullSafeEquals(interfaceOrImplementation, that.interfaceOrImplementation); return ObjectUtils.nullSafeEquals(implementationClass, that.implementationClass);
} }
@Override @Override
public int hashCode() { 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 @@
/*
* 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;
import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryFactoryInformation; 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.repository.query.QueryMethod;
import org.springframework.data.util.ProxyUtils; import org.springframework.data.util.ProxyUtils;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -365,6 +366,11 @@ public class Repositories implements Iterable<Class<?>> {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public RepositoryFragmentsContributor getRepositoryFragmentsContributor() {
throw new UnsupportedOperationException();
}
@Override @Override
public PersistentEntity<?, ?> getPersistentEntity() { public PersistentEntity<?, ?> getPersistentEntity() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();

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

@ -22,13 +22,13 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.FilterType;
import org.springframework.data.aot.sample.ConfigWithCustomRepositoryBaseClass.RepoBaseClass; import org.springframework.data.aot.sample.ConfigWithCustomRepositoryBaseClass.RepoBaseClass;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.config.EnableRepositories; import org.springframework.data.repository.config.EnableRepositoriesWithContributor;
/** /**
* @author Christoph Strobl * @author Christoph Strobl
*/ */
@Configuration @Configuration
@EnableRepositories(repositoryBaseClass = RepoBaseClass.class, considerNestedRepositories = true, @EnableRepositoriesWithContributor(repositoryBaseClass = RepoBaseClass.class, considerNestedRepositories = true,
includeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*CustomerRepositoryWithCustomBaseRepo$") }) includeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*CustomerRepositoryWithCustomBaseRepo$") })
public class ConfigWithCustomRepositoryBaseClass { public class ConfigWithCustomRepositoryBaseClass {

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

@ -15,6 +15,8 @@
*/ */
package org.springframework.data.aot.sample; package org.springframework.data.aot.sample;
import org.jspecify.annotations.Nullable;
import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.FilterType;
import org.springframework.data.aot.sample.ConfigWithSimpleCrudRepository.MyRepo; import org.springframework.data.aot.sample.ConfigWithSimpleCrudRepository.MyRepo;
@ -34,7 +36,7 @@ public class ConfigWithSimpleCrudRepository {
public static class Person { public static class Person {
@javax.annotation.Nullable @Nullable
Address address; Address address;
} }

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

@ -55,6 +55,7 @@ import org.springframework.data.repository.aot.RepositoryRegistrationAotProcesso
import org.springframework.data.repository.config.EnableRepositories; import org.springframework.data.repository.config.EnableRepositories;
import org.springframework.data.repository.config.RepositoryRegistrationAotContribution; import org.springframework.data.repository.config.RepositoryRegistrationAotContribution;
import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor;
import org.springframework.data.repository.config.SampleRepositoryFragmentsContributor;
import org.springframework.data.repository.reactive.ReactiveSortingRepository; import org.springframework.data.repository.reactive.ReactiveSortingRepository;
import org.springframework.transaction.interceptor.TransactionalProxy; import org.springframework.transaction.interceptor.TransactionalProxy;
@ -237,10 +238,11 @@ public class RepositoryRegistrationAotProcessorIntegrationTests {
assertThatContribution(repositoryBeanContribution) // assertThatContribution(repositoryBeanContribution) //
.targetRepositoryTypeIs(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class) // .targetRepositoryTypeIs(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class) //
.hasNoFragments() // .hasFragments() //
.codeContributionSatisfies(contribution -> { // .codeContributionSatisfies(contribution -> { //
// interface // interface
contribution contribution
.contributesReflectionFor(SampleRepositoryFragmentsContributor.class) // repository structural fragment
.contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class) // repository .contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class) // repository
.contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.RepoBaseClass.class) // base repo class .contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.RepoBaseClass.class) // base repo class
.contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.Person.class); // repository domain type .contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.Person.class); // repository domain type

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

@ -15,33 +15,37 @@
*/ */
package org.springframework.data.repository.aot.generate; package org.springframework.data.repository.aot.generate;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import example.UserRepository;
import example.UserRepository.User; import example.UserRepository.User;
import java.util.List;
import java.util.TimeZone; import java.util.TimeZone;
import javax.lang.model.element.Modifier; import javax.lang.model.element.Modifier;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.data.geo.Metric; import org.springframework.data.geo.Metric;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory; 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.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.repository.query.QueryMethod;
import org.springframework.data.util.TypeInformation;
import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeName;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
/** /**
* Unit tests for {@link AotRepositoryBuilder}.
*
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch
*/ */
class AotRepositoryBuilderUnitTests { class AotRepositoryBuilderUnitTests {
@ -57,7 +61,7 @@ class AotRepositoryBuilderUnitTests {
@Test // GH-3279 @Test // GH-3279
void writesClassSkeleton() { void writesClassSkeleton() {
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons",
new SpelAwareProxyProjectionFactory()); new SpelAwareProxyProjectionFactory());
assertThat(repoBuilder.build().javaFile().toString()) assertThat(repoBuilder.build().javaFile().toString())
.contains("package %s;".formatted(UserRepository.class.getPackageName())) // same package as source repo .contains("package %s;".formatted(UserRepository.class.getPackageName())) // same package as source repo
@ -69,7 +73,7 @@ class AotRepositoryBuilderUnitTests {
@Test // GH-3279 @Test // GH-3279
void appliesCtorArguments() { void appliesCtorArguments() {
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons",
new SpelAwareProxyProjectionFactory()); new SpelAwareProxyProjectionFactory());
repoBuilder.withConstructorCustomizer(ctor -> { repoBuilder.withConstructorCustomizer(ctor -> {
ctor.addParameter("param1", Metric.class); ctor.addParameter("param1", Metric.class);
@ -89,7 +93,7 @@ class AotRepositoryBuilderUnitTests {
@Test // GH-3279 @Test // GH-3279
void appliesCtorCodeBlock() { void appliesCtorCodeBlock() {
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons",
new SpelAwareProxyProjectionFactory()); new SpelAwareProxyProjectionFactory());
repoBuilder.withConstructorCustomizer(ctor -> { repoBuilder.withConstructorCustomizer(ctor -> {
ctor.customize((info, code) -> { ctor.customize((info, code) -> {
@ -103,7 +107,7 @@ class AotRepositoryBuilderUnitTests {
@Test // GH-3279 @Test // GH-3279
void appliesClassCustomizations() { void appliesClassCustomizations() {
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons",
new SpelAwareProxyProjectionFactory()); new SpelAwareProxyProjectionFactory());
repoBuilder.withClassCustomizer((info, metadata, clazz) -> { repoBuilder.withClassCustomizer((info, metadata, clazz) -> {
@ -128,12 +132,11 @@ class AotRepositoryBuilderUnitTests {
@Test // GH-3279 @Test // GH-3279
void appliesQueryMethodContributor() { void appliesQueryMethodContributor() {
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, AotRepositoryInformation repositoryInformation = new AotRepositoryInformation(
new SpelAwareProxyProjectionFactory()); AnnotationRepositoryMetadata.getMetadata(UserRepository.class), CrudRepository.class, List.of());
when(repositoryInformation.isQueryMethod(Mockito.argThat(arg -> arg.getName().equals("findByFirstname")))) AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons",
.thenReturn(true); new SpelAwareProxyProjectionFactory());
doReturn(TypeInformation.of(User.class)).when(repositoryInformation).getReturnType(any());
repoBuilder.withQueryMethodContributor((method, info) -> { repoBuilder.withQueryMethodContributor((method, info) -> {
@ -154,4 +157,35 @@ class AotRepositoryBuilderUnitTests {
assertThat(repoBuilder.build().javaFile().toString()) // assertThat(repoBuilder.build().javaFile().toString()) //
.containsIgnoringWhitespaces("void oops() { }"); .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 {
this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition);
} }
@Override
public String getModuleName() {
return "Commons";
}
@Override @Override
public ConfigurableListableBeanFactory getBeanFactory() { public ConfigurableListableBeanFactory getBeanFactory() {
return null; return null;

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

@ -15,10 +15,9 @@
*/ */
package org.springframework.data.repository.aot.generate; package org.springframework.data.repository.aot.generate;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.*;
import static org.mockito.Mockito.when;
import example.UserRepository; import example.UserRepository;
import example.UserRepositoryExtension; import example.UserRepositoryExtension;
@ -31,7 +30,7 @@ import java.util.Set;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.core.test.tools.TestCompiler; import org.springframework.core.test.tools.TestCompiler;
import org.springframework.data.aot.CodeContributionAssert; import org.springframework.data.aot.CodeContributionAssert;
@ -97,8 +96,8 @@ class RepositoryContributorUnitTests {
@Test // GH-3279 @Test // GH-3279
void callsMethodContributionForQueryMethod() { void callsMethodContributionForQueryMethod() {
AotRepositoryContext repositoryContext = Mockito.mock(AotRepositoryContext.class); AotRepositoryContext repositoryContext = mock(AotRepositoryContext.class);
RepositoryInformation repositoryInformation = Mockito.mock(RepositoryInformation.class); RepositoryInformation repositoryInformation = mock(RepositoryInformation.class);
when(repositoryContext.getRepositoryInformation()).thenReturn(repositoryInformation); when(repositoryContext.getRepositoryInformation()).thenReturn(repositoryInformation);
when(repositoryInformation.getRepositoryInterface()).thenReturn((Class) UserRepository.class); when(repositoryInformation.getRepositoryInterface()).thenReturn((Class) UserRepository.class);
@ -113,8 +112,9 @@ class RepositoryContributorUnitTests {
@Test // GH-3279 @Test // GH-3279
void doesNotContributeBaseClassMethods() { void doesNotContributeBaseClassMethods() {
AotRepositoryContext repositoryContext = Mockito.mock(AotRepositoryContext.class); AotRepositoryContext repositoryContext = mock(AotRepositoryContext.class);
RepositoryInformation repositoryInformation = Mockito.mock(RepositoryInformation.class); when(repositoryContext.getModuleName()).thenReturn("Commons");
RepositoryInformation repositoryInformation = mock(RepositoryInformation.class);
when(repositoryContext.getRepositoryInformation()).thenReturn(repositoryInformation); when(repositoryContext.getRepositoryInformation()).thenReturn(repositoryInformation);
when(repositoryInformation.getRepositoryInterface()).thenReturn((Class) UserRepository.class); when(repositoryInformation.getRepositoryInterface()).thenReturn((Class) UserRepository.class);
@ -133,8 +133,9 @@ class RepositoryContributorUnitTests {
@Test // GH-3279 @Test // GH-3279
void doesNotContributeFragmentMethod() { void doesNotContributeFragmentMethod() {
AotRepositoryContext repositoryContext = Mockito.mock(AotRepositoryContext.class); AotRepositoryContext repositoryContext = mock(AotRepositoryContext.class);
RepositoryInformation repositoryInformation = Mockito.mock(RepositoryInformation.class); when(repositoryContext.getModuleName()).thenReturn("Commons");
RepositoryInformation repositoryInformation = mock(RepositoryInformation.class);
when(repositoryContext.getRepositoryInformation()).thenReturn(repositoryInformation); when(repositoryContext.getRepositoryInformation()).thenReturn(repositoryInformation);
when(repositoryInformation.getRepositoryInterface()).thenReturn((Class) UserRepository.class); when(repositoryInformation.getRepositoryInterface()).thenReturn((Class) UserRepository.class);
@ -157,8 +158,9 @@ class RepositoryContributorUnitTests {
@Test // GH-3279 @Test // GH-3279
void contributesBaseClassMethodIfQueryMethod() { void contributesBaseClassMethodIfQueryMethod() {
AotRepositoryContext repositoryContext = Mockito.mock(AotRepositoryContext.class); AotRepositoryContext repositoryContext = mock(AotRepositoryContext.class);
RepositoryInformation repositoryInformation = Mockito.mock(RepositoryInformation.class); when(repositoryContext.getModuleName()).thenReturn("Commons");
RepositoryInformation repositoryInformation = mock(RepositoryInformation.class);
when(repositoryContext.getRepositoryInformation()).thenReturn(repositoryInformation); when(repositoryContext.getRepositoryInformation()).thenReturn(repositoryInformation);
when(repositoryInformation.getRepositoryInterface()).thenReturn((Class) UserRepository.class); when(repositoryInformation.getRepositoryInterface()).thenReturn((Class) UserRepository.class);

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

@ -15,15 +15,15 @@
*/ */
package org.springframework.data.repository.config; package org.springframework.data.repository.config;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.Mockito.*;
import static org.mockito.Mockito.mock;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.ComponentScan.Filter;
@ -184,6 +184,34 @@ class AnnotationRepositoryConfigurationSourceUnitTests {
assertThat(getConfigSource(DefaultConfiguration.class).generateBeanName(bd)).isEqualTo("personRepository"); 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 @Test // GH-3082
void considerBeanNameGeneratorForReactiveRepos() { void considerBeanNameGeneratorForReactiveRepos() {
@ -219,6 +247,9 @@ class AnnotationRepositoryConfigurationSourceUnitTests {
@EnableRepositories(nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class) @EnableRepositories(nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class)
static class ConfigurationWithBeanNameGenerator {} static class ConfigurationWithBeanNameGenerator {}
@EnableRepositoriesWithContributor()
static class ConfigurationWithFragmentsContributor {}
@EnableReactiveRepositories(nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class) @EnableReactiveRepositories(nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class)
static class ReactiveConfigurationWithBeanNameGenerator {} static class ReactiveConfigurationWithBeanNameGenerator {}
@ -234,4 +265,5 @@ class AnnotationRepositoryConfigurationSourceUnitTests {
static class ConfigWithSampleAnnotation {} static class ConfigWithSampleAnnotation {}
interface ReactivePersonRepository extends ReactiveCrudRepository<Person, String> {} interface ReactivePersonRepository extends ReactiveCrudRepository<Person, String> {}
} }

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

@ -0,0 +1,40 @@
/*
* Copyright 2022-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.repository.config;
import java.lang.annotation.Annotation;
import org.springframework.core.io.DefaultResourceLoader;
/**
* @author Mark Paluch
*/
class DummyRegistrarWithContributor extends RepositoryBeanDefinitionRegistrarSupport {
DummyRegistrarWithContributor() {
setResourceLoader(new DefaultResourceLoader());
}
@Override
protected Class<? 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 @@
/*
* 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 @@
*/ */
package org.springframework.data.repository.config; package org.springframework.data.repository.config;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHints;
import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.aot.sample.ConfigWithCustomImplementation; import org.springframework.data.aot.sample.ConfigWithCustomImplementation;
import org.springframework.data.aot.sample.ConfigWithCustomRepositoryBaseClass; import org.springframework.data.aot.sample.ConfigWithCustomRepositoryBaseClass;
import org.springframework.data.aot.sample.ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo; 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.ConfigWithSimpleCrudRepository;
import org.springframework.data.aot.sample.ReactiveConfig;
import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; 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 Christoph Strobl
* @author Mark Paluch
*/ */
class RepositoryBeanDefinitionReaderTests { class RepositoryBeanDefinitionReaderTests {
@ -42,10 +47,10 @@ class RepositoryBeanDefinitionReaderTests {
RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithSimpleCrudRepository.class); RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithSimpleCrudRepository.class);
RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.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, RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean);
repoFactoryBean.getMergedBeanDefinition(), repoFactoryBean.getBeanFactory()); RepositoryInformation repositoryInformation = reader.getRepositoryInformation();
assertThat(repositoryInformation.getRepositoryInterface()).isEqualTo(ConfigWithSimpleCrudRepository.MyRepo.class); assertThat(repositoryInformation.getRepositoryInterface()).isEqualTo(ConfigWithSimpleCrudRepository.MyRepo.class);
assertThat(repositoryInformation.getDomainType()).isEqualTo(ConfigWithSimpleCrudRepository.Person.class); assertThat(repositoryInformation.getDomainType()).isEqualTo(ConfigWithSimpleCrudRepository.Person.class);
@ -59,49 +64,86 @@ class RepositoryBeanDefinitionReaderTests {
RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.class); RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.class);
Class<?> repositoryInterfaceType = CustomerRepositoryWithCustomBaseRepo.class; Class<?> repositoryInterfaceType = CustomerRepositoryWithCustomBaseRepo.class;
Mockito.when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName()); when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName());
RepositoryInformation repositoryInformation = RepositoryBeanDefinitionReader.repositoryInformation(repoConfig, RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean);
repoFactoryBean.getMergedBeanDefinition(), repoFactoryBean.getBeanFactory()); RepositoryInformation repositoryInformation = reader.getRepositoryInformation();
assertThat(repositoryInformation.getRepositoryBaseClass()) assertThat(repositoryInformation.getRepositoryBaseClass())
.isEqualTo(ConfigWithCustomRepositoryBaseClass.RepoBaseClass.class); .isEqualTo(ConfigWithCustomRepositoryBaseClass.RepoBaseClass.class);
} }
@Test // GH-3279 @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); RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.class);
Class<?> repositoryInterfaceType = ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class; Class<?> repositoryInterfaceType = ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class;
Mockito.when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName()); when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName());
RepositoryInformation repositoryInformation = RepositoryBeanDefinitionReader.repositoryInformation(repoConfig, RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean);
repoFactoryBean.getMergedBeanDefinition(), repoFactoryBean.getBeanFactory()); RepositoryInformation repositoryInformation = reader.getRepositoryInformation();
assertThat(repositoryInformation.getFragments()).satisfiesExactly(fragment -> { assertThat(repositoryInformation.getFragments()).satisfiesExactly(fragment -> {
assertThat(fragment.getSignatureContributor()) assertThat(fragment.getImplementationClass())
.isEqualTo(ConfigWithCustomImplementation.CustomImplInterface.class); .contains(ConfigWithCustomImplementation.RepositoryWithCustomImplementationImpl.class);
}); });
} }
@Test // GH-3279 @Test // GH-3279, GH-3282
void fallsBackToModuleBaseClassIfSetAndNoRepoBaseDefined() { void readsFragmentsFromBeanFactory() {
RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithSimpleCrudRepository.class);
RootBeanDefinition rootBeanDefinition = repoFactoryBean.getMergedBeanDefinition().cloneBeanDefinition();
// need to unset because its defined as non default
rootBeanDefinition.getPropertyValues().removePropertyValue("repositoryBaseClass");
rootBeanDefinition.getPropertyValues().add("moduleBaseClass", ModuleBase.class.getName());
RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithFragments.class);
RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.class); RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.class);
Mockito.when(repoConfig.getRepositoryInterface()).thenReturn(ConfigWithSimpleCrudRepository.MyRepo.class.getName());
RepositoryInformation repositoryInformation = RepositoryBeanDefinitionReader.repositoryInformation(repoConfig, Class<?> repositoryInterfaceType = ConfigWithFragments.RepositoryWithFragments.class;
rootBeanDefinition, repoFactoryBean.getBeanFactory()); 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) { static RegisteredBean repositoryFactory(Class<?> configClass) {
@ -118,5 +160,4 @@ class RepositoryBeanDefinitionReaderTests {
return RegisteredBean.of(applicationContext.getBeanFactory(), beanNamesForType[0]); 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 @@
/*
* 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
extends RepositoryFactoryBeanSupport<T, S, ID> { extends RepositoryFactoryBeanSupport<T, S, ID> {
private final T repository; private final T repository;
private RepositoryFragmentsContributor repositoryFragmentsContributor = RepositoryFragmentsContributor.empty();
public DummyRepositoryFactoryBean(Class<? extends T> repositoryInterface) { public DummyRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
@ -38,6 +39,19 @@ public class DummyRepositoryFactoryBean<T extends Repository<S, ID>, S, ID exten
setMappingContext(new SampleMappingContext()); setMappingContext(new SampleMappingContext());
} }
public T getRepository() {
return repository;
}
@Override
public RepositoryFragmentsContributor getRepositoryFragmentsContributor() {
return repositoryFragmentsContributor;
}
public void setRepositoryFragmentsContributor(RepositoryFragmentsContributor repositoryFragmentsContributor) {
this.repositoryFragmentsContributor = repositoryFragmentsContributor;
}
@Override @Override
protected RepositoryFactorySupport createRepositoryFactory() { protected RepositoryFactorySupport createRepositoryFactory() {
return new DummyRepositoryFactory(repository); 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;
import org.springframework.data.repository.core.support.DummyRepositoryFactoryBean; import org.springframework.data.repository.core.support.DummyRepositoryFactoryBean;
import org.springframework.data.repository.core.support.DummyRepositoryInformation; import org.springframework.data.repository.core.support.DummyRepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryFactoryInformation; 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.repository.query.QueryMethod;
import org.springframework.data.util.TypeInformation; import org.springframework.data.util.TypeInformation;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
@ -290,6 +291,11 @@ class RepositoriesUnitTests {
return new DummyRepositoryInformation(repositoryMetadata); return new DummyRepositoryInformation(repositoryMetadata);
} }
@Override
public RepositoryFragmentsContributor getRepositoryFragmentsContributor() {
return RepositoryFragmentsContributor.empty();
}
@Override @Override
public PersistentEntity<?, ?> getPersistentEntity() { public PersistentEntity<?, ?> getPersistentEntity() {
return mappingContext.getRequiredPersistentEntity(repositoryMetadata.getDomainType()); return mappingContext.getRequiredPersistentEntity(repositoryMetadata.getDomainType());

Loading…
Cancel
Save