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
This commit is contained in:
Mark Paluch
2025-05-07 12:21:15 +02:00
parent 2dd6e74193
commit 5800600470
36 changed files with 912 additions and 355 deletions

View File

@@ -41,6 +41,7 @@ import org.springframework.data.repository.query.QueryMethod;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.FieldSpec;
import org.springframework.javapoet.JavaFile;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.TypeName;
import org.springframework.javapoet.TypeSpec;
@@ -53,6 +54,7 @@ import org.springframework.javapoet.TypeSpec;
class AotRepositoryBuilder {
private final RepositoryInformation repositoryInformation;
private final String moduleName;
private final ProjectionFactory projectionFactory;
private final AotRepositoryFragmentMetadata generationMetadata;
@@ -60,9 +62,11 @@ class AotRepositoryBuilder {
private @Nullable BiFunction<Method, RepositoryInformation, @Nullable MethodContributor<? extends QueryMethod>> methodContributorFunction;
private ClassCustomizer customizer;
private AotRepositoryBuilder(RepositoryInformation repositoryInformation, ProjectionFactory projectionFactory) {
private AotRepositoryBuilder(RepositoryInformation repositoryInformation, String moduleName,
ProjectionFactory projectionFactory) {
this.repositoryInformation = repositoryInformation;
this.moduleName = moduleName;
this.projectionFactory = projectionFactory;
this.generationMetadata = new AotRepositoryFragmentMetadata(className());
@@ -74,11 +78,37 @@ class AotRepositoryBuilder {
this.customizer = (info, metadata, builder) -> {};
}
public static <M extends QueryMethod> AotRepositoryBuilder forRepository(RepositoryInformation repositoryInformation,
/**
* Create a new {@code AotRepositoryBuilder} for the given {@link RepositoryInformation}.
*
* @param information must not be {@literal null}.
* @param moduleName must not be {@literal null}.
* @param projectionFactory must not be {@literal null}.
* @return
*/
public static AotRepositoryBuilder forRepository(RepositoryInformation information, String moduleName,
ProjectionFactory projectionFactory) {
return new AotRepositoryBuilder(repositoryInformation, projectionFactory);
return new AotRepositoryBuilder(information, moduleName, projectionFactory);
}
/**
* Configure a {@link ClassCustomizer} customizer.
*
* @param classCustomizer must not be {@literal null}.
* @return {@code this}.
*/
public AotRepositoryBuilder withClassCustomizer(ClassCustomizer classCustomizer) {
this.customizer = classCustomizer;
return this;
}
/**
* Configure a {@link AotRepositoryConstructorBuilder} customizer.
*
* @param constructorCustomizer must not be {@literal null}.
* @return {@code this}.
*/
public AotRepositoryBuilder withConstructorCustomizer(
Consumer<AotRepositoryConstructorBuilder> constructorCustomizer) {
@@ -86,42 +116,33 @@ class AotRepositoryBuilder {
return this;
}
/**
* Configure a {@link MethodContributor}.
*
* @param methodContributorFunction must not be {@literal null}.
* @return {@code this}.
*/
public AotRepositoryBuilder withQueryMethodContributor(
BiFunction<Method, RepositoryInformation, @Nullable MethodContributor<? extends QueryMethod>> methodContributorFunction) {
this.methodContributorFunction = methodContributorFunction;
return this;
}
public AotRepositoryBuilder withClassCustomizer(ClassCustomizer classCustomizer) {
this.customizer = classCustomizer;
return this;
}
public AotBundle build() {
List<AotRepositoryMethod> methodMetadata = new ArrayList<>();
RepositoryComposition repositoryComposition = repositoryInformation.getRepositoryComposition();
// start creating the type
TypeSpec.Builder builder = TypeSpec.classBuilder(this.generationMetadata.getTargetTypeName()) //
.addModifiers(Modifier.PUBLIC) //
.addAnnotation(Generated.class) //
.addJavadoc("AOT generated repository implementation for {@link $T}.\n",
.addJavadoc("AOT generated $L repository implementation for {@link $T}.\n", moduleName,
repositoryInformation.getRepositoryInterface());
// create the constructor
AotRepositoryConstructorBuilder constructorBuilder = new AotRepositoryConstructorBuilder(repositoryInformation,
generationMetadata);
if (constructorCustomizer != null) {
constructorCustomizer.accept(constructorBuilder);
}
builder.addMethod(constructorBuilder.buildConstructor());
List<AotRepositoryMethod> methodMetadata = new ArrayList<>();
AotRepositoryMetadata.RepositoryType repositoryType = repositoryInformation.isReactiveRepository()
? AotRepositoryMetadata.RepositoryType.REACTIVE
: AotRepositoryMetadata.RepositoryType.IMPERATIVE;
RepositoryComposition repositoryComposition = repositoryInformation.getRepositoryComposition();
builder.addMethod(buildConstructor());
Arrays.stream(repositoryInformation.getRepositoryInterface().getMethods())
.sorted(Comparator.<Method, String> comparing(it -> {
@@ -136,12 +157,35 @@ class AotRepositoryBuilder {
// finally customize the file itself
this.customizer.customize(repositoryInformation, generationMetadata, builder);
JavaFile javaFile = JavaFile.builder(packageName(), builder.build()).build();
AotRepositoryMetadata metadata = getAotRepositoryMetadata(methodMetadata);
AotRepositoryMetadata metadata = new AotRepositoryMetadata(repositoryInformation.getRepositoryInterface().getName(),
repositoryInformation.moduleName() != null ? repositoryInformation.moduleName() : "", repositoryType, methodMetadata);
return new AotBundle(javaFile, metadata);
}
return new AotBundle(javaFile, metadata.toJson());
private MethodSpec buildConstructor() {
AotRepositoryConstructorBuilder constructorBuilder = new AotRepositoryConstructorBuilder(repositoryInformation,
generationMetadata);
if (constructorCustomizer != null) {
constructorCustomizer.accept(constructorBuilder);
}
return constructorBuilder.buildConstructor();
}
private AotRepositoryMetadata getAotRepositoryMetadata(List<AotRepositoryMethod> methodMetadata) {
AotRepositoryMetadata.RepositoryType repositoryType = repositoryInformation.isReactiveRepository()
? AotRepositoryMetadata.RepositoryType.REACTIVE
: AotRepositoryMetadata.RepositoryType.IMPERATIVE;
String jsonModuleName = moduleName.replaceAll("Reactive", "").trim();
return new AotRepositoryMetadata(repositoryInformation.getRepositoryInterface().getName(), jsonModuleName,
repositoryType, methodMetadata);
}
private void contributeMethod(Method method, RepositoryComposition repositoryComposition,
@@ -185,8 +229,7 @@ class AotRepositoryBuilder {
private AotRepositoryMethod getFragmentMetadata(Method method, RepositoryFragment<?> fragment) {
String signature = fragment.getSignatureContributor().getName();
String implementation = fragment.getImplementation().map(it -> it.getClass().getName()).orElse(null);
String implementation = fragment.getImplementationClass().map(Class::getName).orElse(null);
AotFragmentTarget fragmentTarget = new AotFragmentTarget(signature, implementation);
return new AotRepositoryMethod(method.getName(), method.toGenericString(), null, fragmentTarget);
@@ -240,7 +283,7 @@ class AotRepositoryBuilder {
}
record AotBundle(JavaFile javaFile, JSONObject metadata) {
record AotBundle(JavaFile javaFile, AotRepositoryMetadata metadata) {
}
}

View File

@@ -39,6 +39,7 @@ import org.springframework.util.StringUtils;
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 4.0
*/
public class RepositoryContributor {
@@ -46,19 +47,34 @@ public class RepositoryContributor {
private final AotRepositoryBuilder builder;
/**
* Create a new {@code RepositoryContributor} for the given {@link AotRepositoryContext}.
*
* @param repositoryContext
*/
public RepositoryContributor(AotRepositoryContext repositoryContext) {
this.builder = AotRepositoryBuilder.forRepository(repositoryContext.getRepositoryInformation(),
createProjectionFactory());
repositoryContext.getModuleName(), createProjectionFactory());
}
/**
* @return a new {@link ProjectionFactory} to be used with the AOT repository builder. The actual instance should be
* accessed through {@link #getProjectionFactory()}.
*/
protected ProjectionFactory createProjectionFactory() {
return new SpelAwareProxyProjectionFactory();
}
/**
* @return the used {@link ProjectionFactory}.
*/
protected ProjectionFactory getProjectionFactory() {
return builder.getProjectionFactory();
}
/**
* @return the used {@link RepositoryInformation}.
*/
protected RepositoryInformation getRepositoryInformation() {
return builder.getRepositoryInformation();
}
@@ -73,13 +89,10 @@ public class RepositoryContributor {
public void contribute(GenerationContext generationContext) {
// TODO: do we need - generationContext.withName("spring-data");
builder.withClassCustomizer(this::customizeClass);
builder.withConstructorCustomizer(this::customizeConstructor);
builder.withQueryMethodContributor(this::contributeQueryMethod);
AotRepositoryBuilder.AotBundle aotBundle = builder.build();
AotRepositoryBuilder.AotBundle aotBundle = builder.withClassCustomizer(this::customizeClass) //
.withConstructorCustomizer(this::customizeConstructor) //
.withQueryMethodContributor(this::contributeQueryMethod) //
.build();
Class<?> repositoryInterface = getRepositoryInformation().getRepositoryInterface();
String repositoryJsonFileName = getRepositoryJsonFileName(repositoryInterface);
@@ -89,7 +102,7 @@ public class RepositoryContributor {
String repositoryJson;
try {
repositoryJson = aotBundle.metadata().toString(2);
repositoryJson = aotBundle.metadata().toJson().toString(2);
} catch (JSONException e) {
throw new RuntimeException(e);
}

View File

@@ -65,6 +65,7 @@ public class AnnotationRepositoryConfigurationSource extends RepositoryConfigura
private static final String QUERY_LOOKUP_STRATEGY = "queryLookupStrategy";
private static final String REPOSITORY_FACTORY_BEAN_CLASS = "repositoryFactoryBeanClass";
private static final String REPOSITORY_BASE_CLASS = "repositoryBaseClass";
private static final String REPOSITORY_FRAGMENTS_CONTRIBUTOR_CLASS = "fragmentsContributor";
private static final String CONSIDER_NESTED_REPOSITORIES = "considerNestedRepositories";
private static final String BOOTSTRAP_MODE = "bootstrapMode";
private static final String BEAN_NAME_GENERATOR = "nameGenerator";
@@ -187,6 +188,16 @@ public class AnnotationRepositoryConfigurationSource extends RepositoryConfigura
: Optional.of(repositoryBaseClass.getName());
}
@Override
public Optional<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.
*

View File

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

View File

@@ -17,52 +17,40 @@ package org.springframework.data.repository.config;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryInformationSupport;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryComposition;
import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.util.Lazy;
/**
* {@link RepositoryInformation} based on {@link RepositoryMetadata} collected at build time.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 3.0
*/
class AotRepositoryInformation extends RepositoryInformationSupport implements RepositoryInformation {
public class AotRepositoryInformation extends RepositoryInformationSupport implements RepositoryInformation {
private final @Nullable String moduleName;
private final Supplier<Collection<RepositoryFragment<?>>> fragments;
private final RepositoryComposition fragmentsComposition;
private final RepositoryComposition baseComposition;
private final RepositoryComposition composition;
private final Lazy<RepositoryComposition> repositoryComposition;
private final Lazy<RepositoryComposition> baseComposition;
public AotRepositoryInformation(RepositoryMetadata repositoryMetadata, Class<?> repositoryBaseClass,
Collection<RepositoryFragment<?>> fragments) {
AotRepositoryInformation(@Nullable String moduleName, Supplier<RepositoryMetadata> repositoryMetadata,
Supplier<Class<?>> repositoryBaseClass, Supplier<Collection<RepositoryFragment<?>>> fragments) {
super(() -> repositoryMetadata, () -> repositoryBaseClass);
super(repositoryMetadata, repositoryBaseClass);
this.fragmentsComposition = RepositoryComposition.fromMetadata(getMetadata())
.append(RepositoryFragments.from(fragments));
this.baseComposition = RepositoryComposition.of(RepositoryFragment.structural(getRepositoryBaseClass())) //
.withArgumentConverter(this.fragmentsComposition.getArgumentConverter()) //
.withMethodLookup(this.fragmentsComposition.getMethodLookup());
this.moduleName = moduleName;
this.fragments = fragments;
this.repositoryComposition = Lazy
.of(() -> RepositoryComposition.fromMetadata(getMetadata()).append(RepositoryFragments.from(getFragments())));
this.baseComposition = Lazy.of(() -> {
RepositoryComposition targetRepoComposition = repositoryComposition.get();
return RepositoryComposition.of(RepositoryFragment.structural(getRepositoryBaseClass())) //
.withArgumentConverter(targetRepoComposition.getArgumentConverter()) //
.withMethodLookup(targetRepoComposition.getMethodLookup());
});
this.composition = this.fragmentsComposition.append(this.baseComposition.getFragments());
}
/**
@@ -71,31 +59,27 @@ class AotRepositoryInformation extends RepositoryInformationSupport implements R
*/
@Override
public Set<RepositoryFragment<?>> getFragments() {
return new LinkedHashSet<>(fragments.get());
return fragmentsComposition.getFragments().toSet();
}
@Override
public boolean isCustomMethod(Method method) {
return repositoryComposition.get().findMethod(method).isPresent();
return fragmentsComposition.findMethod(method).isPresent();
}
@Override
public boolean isBaseClassMethod(Method method) {
return baseComposition.get().findMethod(method).isPresent();
return baseComposition.findMethod(method).isPresent();
}
@Override
public Method getTargetClassMethod(Method method) {
return baseComposition.get().findMethod(method).orElse(method);
return baseComposition.findMethod(method).orElse(method);
}
@Override
public RepositoryComposition getRepositoryComposition() {
return repositoryComposition.get();
return composition;
}
@Override
public @Nullable String moduleName() {
return moduleName;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022. the original author or authors.
* Copyright 2022-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,14 +16,14 @@
package org.springframework.data.repository.config;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.env.Environment;
import org.springframework.data.aot.AotContext;
@@ -37,29 +37,42 @@ import org.springframework.data.util.TypeUtils;
*
* @author Christoph Strobl
* @author John Blum
* @author Mark Paluch
* @see AotRepositoryContext
* @since 3.0
*/
@SuppressWarnings("NullAway") // TODO
class DefaultAotRepositoryContext implements AotRepositoryContext {
private final RegisteredBean bean;
private final String moduleName;
private final AotContext aotContext;
private final RepositoryInformation repositoryInformation;
private final Lazy<Set<MergedAnnotation<Annotation>>> resolvedAnnotations = Lazy.of(this::discoverAnnotations);
private final Lazy<Set<Class<?>>> managedTypes = Lazy.of(this::discoverTypes);
private @Nullable RepositoryInformation repositoryInformation;
private @Nullable Set<String> basePackages;
private @Nullable Set<Class<? extends Annotation>> identifyingAnnotations;
private @Nullable String beanName;
private Set<String> basePackages = Collections.emptySet();
private Collection<Class<? extends Annotation>> identifyingAnnotations = Collections.emptySet();
private String beanName;
public DefaultAotRepositoryContext(AotContext aotContext) {
public DefaultAotRepositoryContext(RegisteredBean bean, RepositoryInformation repositoryInformation,
String moduleName, AotContext aotContext) {
this.bean = bean;
this.repositoryInformation = repositoryInformation;
this.moduleName = moduleName;
this.aotContext = aotContext;
this.beanName = bean.getBeanName();
}
public AotContext getAotContext() {
return aotContext;
}
@Override
public String getModuleName() {
return moduleName;
}
@Override
public ConfigurableListableBeanFactory getBeanFactory() {
return getAotContext().getBeanFactory();
@@ -72,7 +85,7 @@ class DefaultAotRepositoryContext implements AotRepositoryContext {
@Override
public Set<String> getBasePackages() {
return basePackages == null ? Collections.emptySet() : basePackages;
return basePackages;
}
public void setBasePackages(Set<String> basePackages) {
@@ -89,11 +102,11 @@ class DefaultAotRepositoryContext implements AotRepositoryContext {
}
@Override
public Set<Class<? extends Annotation>> getIdentifyingAnnotations() {
return identifyingAnnotations == null ? Collections.emptySet() : identifyingAnnotations;
public Collection<Class<? extends Annotation>> getIdentifyingAnnotations() {
return identifyingAnnotations;
}
public void setIdentifyingAnnotations(Set<Class<? extends Annotation>> identifyingAnnotations) {
public void setIdentifyingAnnotations(Collection<Class<? extends Annotation>> identifyingAnnotations) {
this.identifyingAnnotations = identifyingAnnotations;
}
@@ -102,10 +115,6 @@ class DefaultAotRepositoryContext implements AotRepositoryContext {
return repositoryInformation;
}
public void setRepositoryInformation(RepositoryInformation repositoryInformation) {
this.repositoryInformation = repositoryInformation;
}
@Override
public Set<MergedAnnotation<Annotation>> getResolvedAnnotations() {
return resolvedAnnotations.get();
@@ -132,24 +141,18 @@ class DefaultAotRepositoryContext implements AotRepositoryContext {
.flatMap(type -> TypeUtils.resolveUsedAnnotations(type).stream())
.collect(Collectors.toCollection(LinkedHashSet::new));
if (repositoryInformation != null) {
annotations.addAll(TypeUtils.resolveUsedAnnotations(repositoryInformation.getRepositoryInterface()));
}
annotations.addAll(TypeUtils.resolveUsedAnnotations(repositoryInformation.getRepositoryInterface()));
return annotations;
}
protected Set<Class<?>> discoverTypes() {
Set<Class<?>> types = new LinkedHashSet<>();
Set<Class<?>> types = new LinkedHashSet<>(TypeCollector.inspect(repositoryInformation.getDomainType()).list());
if (repositoryInformation != null) {
types.addAll(TypeCollector.inspect(repositoryInformation.getDomainType()).list());
repositoryInformation.getQueryMethods().stream()
.flatMap(it -> TypeUtils.resolveTypesInSignature(repositoryInformation.getRepositoryInterface(), it).stream())
.flatMap(it -> TypeCollector.inspect(it).list().stream()).forEach(types::add);
}
repositoryInformation.getQueryMethods().stream()
.flatMap(it -> TypeUtils.resolveTypesInSignature(repositoryInformation.getRepositoryInterface(), it).stream())
.flatMap(it -> TypeCollector.inspect(it).list().stream()).forEach(types::add);
if (!getIdentifyingAnnotations().isEmpty()) {
@@ -160,4 +163,5 @@ class DefaultAotRepositoryContext implements AotRepositoryContext {
return types;
}
}

View File

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

View File

@@ -116,6 +116,11 @@ class RepositoryBeanDefinitionBuilder {
.rootBeanDefinition(configuration.getRepositoryFactoryBeanClassName());
builder.getRawBeanDefinition().setSource(configuration.getSource());
// AOT Repository hints
builder.getRawBeanDefinition().setAttribute(RepositoryConfiguration.class.getName(), configuration);
builder.getRawBeanDefinition().setAttribute(RepositoryConfigurationExtension.class.getName(), extension);
builder.addConstructorArgValue(configuration.getRepositoryInterface());
builder.addPropertyValue("queryLookupStrategyKey", configuration.getQueryLookupStrategyKey());
builder.addPropertyValue("lazyInit", configuration.isLazyInit());
@@ -125,6 +130,10 @@ class RepositoryBeanDefinitionBuilder {
configuration.getRepositoryBaseClassName()//
.ifPresent(it -> builder.addPropertyValue("repositoryBaseClass", it));
configuration.getRepositoryFragmentsContributorClassName()//
.ifPresent(it -> builder.addPropertyValue("repositoryFragmentsContributor",
BeanDefinitionBuilder.genericBeanDefinition(it).getRawBeanDefinition()));
NamedQueriesBeanDefinitionBuilder definitionBuilder = new NamedQueriesBeanDefinitionBuilder(
extension.getDefaultNamedQueryLocation());
configuration.getNamedQueriesLocation().ifPresent(definitionBuilder::setLocations);

View File

@@ -15,139 +15,197 @@
*/
package org.springframework.data.repository.config;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.core.ResolvableType;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.repository.core.support.RepositoryFragment.ImplementedRepositoryFragment;
import org.springframework.data.repository.core.support.RepositoryFragmentsContributor;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* Reader used to extract {@link RepositoryInformation} from {@link RepositoryConfiguration}.
*
* @author Christoph Strobl
* @author John Blum
* @author Mark Paluch
* @since 3.0
*/
class RepositoryBeanDefinitionReader {
/**
* @return
*/
static RepositoryInformation repositoryInformation(RepositoryConfiguration<?> repoConfig, RegisteredBean repoBean) {
return repositoryInformation(repoConfig, repoBean.getMergedBeanDefinition(), repoBean.getBeanFactory());
private final RootBeanDefinition beanDefinition;
private final ConfigurableListableBeanFactory beanFactory;
private final ClassLoader beanClassLoader;
private final @Nullable RepositoryConfiguration<?> configuration;
private final @Nullable RepositoryConfigurationExtensionSupport extension;
public RepositoryBeanDefinitionReader(RegisteredBean bean) {
this.beanDefinition = bean.getMergedBeanDefinition();
this.beanFactory = bean.getBeanFactory();
this.beanClassLoader = bean.getBeanClass().getClassLoader();
this.configuration = (RepositoryConfiguration<?>) beanDefinition
.getAttribute(RepositoryConfiguration.class.getName());
this.extension = (RepositoryConfigurationExtensionSupport) beanDefinition
.getAttribute(RepositoryConfigurationExtension.class.getName());
}
public @Nullable RepositoryConfiguration<?> getConfiguration() {
return this.configuration;
}
public @Nullable RepositoryConfigurationExtensionSupport getConfigurationExtension() {
return this.extension;
}
/**
* @param source the RepositoryFactoryBeanSupport bean definition.
* @param beanFactory
* @return
* @return the {@link RepositoryInformation} derived from the repository bean.
*/
@SuppressWarnings("NullAway")
static RepositoryInformation repositoryInformation(RepositoryConfiguration<?> repoConfig, BeanDefinition source,
ConfigurableListableBeanFactory beanFactory) {
public RepositoryInformation getRepositoryInformation() {
RepositoryMetadata metadata = AbstractRepositoryMetadata
.getMetadata(forName(repoConfig.getRepositoryInterface(), beanFactory));
Class<?> repositoryBaseClass = readRepositoryBaseClass(source, beanFactory);
List<RepositoryFragment<?>> fragmentList = readRepositoryFragments(source, beanFactory);
if (source.getPropertyValues().contains("customImplementation")) {
.getMetadata(forName(configuration.getRepositoryInterface()));
Class<?> repositoryBaseClass = getRepositoryBaseClass();
Object o = source.getPropertyValues().get("customImplementation");
if (o instanceof RuntimeBeanReference rbr) {
BeanDefinition customImplBeanDefintion = beanFactory.getBeanDefinition(rbr.getBeanName());
Class<?> beanType = forName(customImplBeanDefintion.getBeanClassName(), beanFactory);
ResolvableType[] interfaces = ResolvableType.forClass(beanType).getInterfaces();
if (interfaces.length == 1) {
fragmentList.add(new ImplementedRepositoryFragment(interfaces[0].toClass(), beanType));
} else {
boolean found = false;
for (ResolvableType i : interfaces) {
if (beanType.getSimpleName().contains(i.resolve().getSimpleName())) {
fragmentList.add(new ImplementedRepositoryFragment(interfaces[0].toClass(), beanType));
found = true;
break;
}
}
if (!found) {
fragmentList.add(RepositoryFragment.implemented(beanType));
}
}
List<RepositoryFragment<?>> fragments = new ArrayList<>();
fragments.addAll(readRepositoryFragments());
fragments.addAll(readContributedRepositoryFragments(metadata));
RepositoryFragment<?> customImplementation = getCustomImplementation();
if (customImplementation != null) {
fragments.add(0, customImplementation);
}
return new AotRepositoryInformation(metadata, repositoryBaseClass, fragments);
}
private @Nullable RepositoryFragment<?> getCustomImplementation() {
PropertyValues mpv = beanDefinition.getPropertyValues();
PropertyValue customImplementation = mpv.getPropertyValue("customImplementation");
if (customImplementation != null) {
if (customImplementation.getValue() instanceof RuntimeBeanReference rbr) {
BeanDefinition customImplementationBean = beanFactory.getBeanDefinition(rbr.getBeanName());
Class<?> beanType = getClass(customImplementationBean);
return RepositoryFragment.structural(beanType);
} else if (customImplementation.getValue() instanceof BeanDefinition bd) {
Class<?> beanType = getClass(bd);
return RepositoryFragment.structural(beanType);
}
}
String moduleName = (String) source.getPropertyValues().get("moduleName");
AotRepositoryInformation repositoryInformation = new AotRepositoryInformation(moduleName, () -> metadata,
() -> repositoryBaseClass, () -> fragmentList);
return repositoryInformation;
return null;
}
@SuppressWarnings("NullAway")
private static Class<?> readRepositoryBaseClass(BeanDefinition source, ConfigurableListableBeanFactory beanFactory) {
private Class<?> getRepositoryBaseClass() {
Object repoBaseClassName = beanDefinition.getPropertyValues().get("repositoryBaseClass");
Object repoBaseClassName = source.getPropertyValues().get("repositoryBaseClass");
if (repoBaseClassName != null) {
return forName(repoBaseClassName.toString(), beanFactory);
}
if (source.getPropertyValues().contains("moduleBaseClass")) {
return forName((String) source.getPropertyValues().get("moduleBaseClass"), beanFactory);
return forName(repoBaseClassName.toString());
}
return Dummy.class;
}
@SuppressWarnings("NullAway")
private static List<RepositoryFragment<?>> readRepositoryFragments(BeanDefinition source,
ConfigurableListableBeanFactory beanFactory) {
private List<RepositoryFragment<?>> readRepositoryFragments() {
RuntimeBeanReference beanReference = (RuntimeBeanReference) source.getPropertyValues().get("repositoryFragments");
RuntimeBeanReference beanReference = (RuntimeBeanReference) beanDefinition.getPropertyValues()
.get("repositoryFragments");
BeanDefinition fragments = beanFactory.getBeanDefinition(beanReference.getBeanName());
ValueHolder fragmentBeanNameList = fragments.getConstructorArgumentValues().getArgumentValue(0, List.class);
List<String> fragmentBeanNames = (List<String>) fragmentBeanNameList.getValue();
List<RepositoryFragment<?>> fragmentList = new ArrayList<>();
for (String beanName : fragmentBeanNames) {
BeanDefinition fragmentBeanDefinition = beanFactory.getBeanDefinition(beanName);
ValueHolder argumentValue = fragmentBeanDefinition.getConstructorArgumentValues().getArgumentValue(0,
String.class);
ValueHolder argumentValue1 = fragmentBeanDefinition.getConstructorArgumentValues().getArgumentValue(1, null, null,
null);
Object fragmentClassName = argumentValue.getValue();
ConstructorArgumentValues cv = fragmentBeanDefinition.getConstructorArgumentValues();
ValueHolder interfaceClassVh = cv.getArgumentValue(0, String.class);
ValueHolder implementationVh = cv.getArgumentValue(1, null, null, null);
try {
Class<?> type = ClassUtils.forName(fragmentClassName.toString(), beanFactory.getBeanClassLoader());
Object fragmentClassName = interfaceClassVh.getValue();
Class<?> interfaceClass = forName(fragmentClassName.toString());
if (argumentValue1 != null && argumentValue1.getValue() instanceof RuntimeBeanReference rbf) {
BeanDefinition implBeanDef = beanFactory.getBeanDefinition(rbf.getBeanName());
Class implClass = ClassUtils.forName(implBeanDef.getBeanClassName(), beanFactory.getBeanClassLoader());
fragmentList.add(new RepositoryFragment.ImplementedRepositoryFragment(type, implClass));
} else {
fragmentList.add(RepositoryFragment.structural(type));
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
if (implementationVh != null && implementationVh.getValue() instanceof RuntimeBeanReference rbf) {
BeanDefinition implBeanDef = beanFactory.getBeanDefinition(rbf.getBeanName());
Class<?> implClass = getClass(implBeanDef);
fragmentList.add(RepositoryFragment.structural(interfaceClass, implClass));
} else {
fragmentList.add(RepositoryFragment.structural(interfaceClass));
}
}
return fragmentList;
}
private List<RepositoryFragment<?>> readContributedRepositoryFragments(RepositoryMetadata metadata) {
RepositoryFragmentsContributor contributor = getFragmentsContributor(metadata.getRepositoryInterface());
return contributor.describe(metadata).stream().toList();
}
private RepositoryFragmentsContributor getFragmentsContributor(Class<?> repositoryInterface) {
Object repositoryFragmentsContributor = beanDefinition.getPropertyValues().get("repositoryFragmentsContributor");
if (repositoryFragmentsContributor instanceof BeanDefinition bd) {
return (RepositoryFragmentsContributor) BeanUtils.instantiateClass(getClass(bd));
}
Class<?> repositoryFactoryBean = forName(beanDefinition.getBeanClassName());
Constructor<?> constructor = ClassUtils.getConstructorIfAvailable(repositoryFactoryBean, Class.class);
if (constructor == null) {
throw new IllegalStateException("No constructor accepting Class in " + repositoryFactoryBean.getName());
}
RepositoryFactoryBeanSupport<?, ?, ?> factoryBean = (RepositoryFactoryBeanSupport<?, ?, ?>) BeanUtils
.instantiateClass(constructor, repositoryInterface);
return factoryBean.getRepositoryFragmentsContributor();
}
private Class<?> getClass(BeanDefinition definition) {
String beanClassName = definition.getBeanClassName();
if (ObjectUtils.isEmpty(beanClassName)) {
throw new IllegalStateException("No bean class name specified for %s".formatted(definition));
}
return forName(beanClassName);
}
static abstract class Dummy implements CrudRepository<Object, Object>, PagingAndSortingRepository<Object, Object> {}
static Class<?> forName(String name, ConfigurableListableBeanFactory beanFactory) {
private Class<?> forName(String name) {
try {
return ClassUtils.forName(name, beanFactory.getBeanClassLoader());
return ClassUtils.forName(name, beanClassLoader);
} catch (ClassNotFoundException cause) {
throw new TypeNotPresentException(name, cause);
}

View File

@@ -78,6 +78,15 @@ public interface RepositoryConfiguration<T extends RepositoryConfigurationSource
*/
Optional<String> getRepositoryBaseClassName();
/**
* Returns the name of the repository fragments contributor class to be used or {@link Optional#empty()} if the store
* specific defaults shall be applied.
*
* @return
* @since 4.0
*/
Optional<String> getRepositoryFragmentsContributorClassName();
/**
* Returns the name of the repository factory bean class to be used.
*
@@ -157,11 +166,12 @@ public interface RepositoryConfiguration<T extends RepositoryConfigurationSource
ImplementationLookupConfiguration toLookupConfiguration(MetadataReaderFactory factory);
/**
* Returns a human readable description of the repository interface declaration for error reporting purposes.
* Returns a human-readable description of the repository interface declaration for error reporting purposes.
*
* @return can be {@literal null}.
* @since 2.3
*/
@Nullable
String getResourceDescription();
}

View File

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

View File

@@ -18,7 +18,7 @@ package org.springframework.data.repository.config;
import java.util.Collection;
import java.util.Locale;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
import org.springframework.beans.factory.config.BeanDefinition;
@@ -63,7 +63,6 @@ public interface RepositoryConfigurationExtension {
* @see org.springframework.beans.factory.aot.BeanRegistrationAotProcessor
* @since 3.0
*/
@NonNull
default Class<? extends BeanRegistrationAotProcessor> getRepositoryAotProcessor() {
return RepositoryRegistrationAotProcessor.class;
}
@@ -90,6 +89,16 @@ public interface RepositoryConfigurationExtension {
*/
String getDefaultNamedQueryLocation();
/**
* Returns the {@link String name} of the repository base class to be used.
*
* @return can be {@literal null} if the base class cannot be provided.
* @since 4.0
*/
default @Nullable String getRepositoryBaseClassName() {
return null;
}
/**
* Returns the {@link String name} of the repository factory class to be used.
*

View File

@@ -81,6 +81,15 @@ public interface RepositoryConfigurationSource {
*/
Optional<String> getRepositoryBaseClassName();
/**
* Returns the name of the repository fragments contributor class to be used or {@link Optional#empty()} if the store
* specific defaults shall be applied.
*
* @return
* @since 4.0
*/
Optional<String> getRepositoryFragmentsContributorClassName();
/**
* Returns the name of the repository factory bean class or {@link Optional#empty()} if not defined in the source.
*

View File

@@ -16,18 +16,18 @@
package org.springframework.data.repository.config;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.aop.SpringProxy;
import org.springframework.aop.framework.Advised;
import org.springframework.aot.generate.GenerationContext;
@@ -37,11 +37,11 @@ import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.aot.BeanRegistrationCode;
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments;
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.DecoratingProxy;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.Environment;
import org.springframework.data.aot.AotContext;
import org.springframework.data.projection.EntityProjectionIntrospector;
import org.springframework.data.projection.TargetAware;
@@ -66,33 +66,38 @@ import org.springframework.util.ClassUtils;
* @author Mark Paluch
* @since 3.0
*/
// TODO: Consider moving to data.repository.aot
public class RepositoryRegistrationAotContribution implements BeanRegistrationAotContribution {
private static final Log logger = LogFactory.getLog(RepositoryRegistrationAotContribution.class);
private static final String KOTLIN_COROUTINE_REPOSITORY_TYPE_NAME = "org.springframework.data.repository.kotlin.CoroutineCrudRepository";
private final RepositoryRegistrationAotProcessor aotProcessor;
private final AotRepositoryContext repositoryContext;
private @Nullable RepositoryContributor repositoryContributor;
private @Nullable AotRepositoryContext repositoryContext;
private @Nullable BiFunction<AotRepositoryContext, GenerationContext, @Nullable RepositoryContributor> moduleContribution;
private final RepositoryRegistrationAotProcessor aotProcessor;
/**
* Constructs a new instance of the {@link RepositoryRegistrationAotContribution} initialized with the given, required
* {@link RepositoryRegistrationAotProcessor} from which this contribution was created.
*
* @param processor reference back to the {@link RepositoryRegistrationAotProcessor} from which this contribution was
* created.
* @param context reference back to the {@link AotRepositoryContext} from which this contribution was created.
* @throws IllegalArgumentException if the {@link RepositoryRegistrationAotProcessor} is {@literal null}.
* @see RepositoryRegistrationAotProcessor
*/
protected RepositoryRegistrationAotContribution(RepositoryRegistrationAotProcessor processor) {
protected RepositoryRegistrationAotContribution(RepositoryRegistrationAotProcessor processor,
AotRepositoryContext context) {
Assert.notNull(processor, "RepositoryRegistrationAotProcessor must not be null");
Assert.notNull(context, "AotRepositoryContext must not be null");
this.aotProcessor = processor;
this.repositoryContext = context;
}
/**
@@ -101,16 +106,57 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
*
* @param processor reference back to the {@link RepositoryRegistrationAotProcessor} from which this contribution was
* created.
* @return a new instance of {@link RepositoryRegistrationAotContribution}.
* @throws IllegalArgumentException if the {@link RepositoryRegistrationAotProcessor} is {@literal null}.
* @return a new instance of {@link RepositoryRegistrationAotContribution} if a contribution can be made;
* {@literal null} if no contribution can be made.
* @see RepositoryRegistrationAotProcessor
*/
public static RepositoryRegistrationAotContribution fromProcessor(RepositoryRegistrationAotProcessor processor) {
return new RepositoryRegistrationAotContribution(processor);
public static @Nullable RepositoryRegistrationAotContribution load(RepositoryRegistrationAotProcessor processor,
RegisteredBean repositoryBean) {
RepositoryConfiguration<?> repositoryMetadata = processor.getRepositoryMetadata(repositoryBean);
if (repositoryMetadata == null) {
return null;
}
AotRepositoryContext repositoryContext = buildAotRepositoryContext(processor.getEnvironment(), repositoryBean,
repositoryMetadata);
if (repositoryContext == null) {
return null;
}
return new RepositoryRegistrationAotContribution(processor, repositoryContext);
}
protected ConfigurableListableBeanFactory getBeanFactory() {
return getRepositoryRegistrationAotProcessor().getBeanFactory();
/**
* Builds a {@link RepositoryRegistrationAotContribution} for given, required {@link RegisteredBean} representing the
* {@link Repository} registered in the bean registry.
*
* @param repositoryBean {@link RegisteredBean} for the {@link Repository}; must not be {@literal null}.
* @return a {@link RepositoryRegistrationAotContribution} to contribute AOT metadata and code for the
* {@link Repository} {@link RegisteredBean}.
* @throws IllegalArgumentException if the {@link RegisteredBean} is {@literal null}.
* @deprecated since 4.0.
*/
@Deprecated(since = "4.0", forRemoval = true)
public @Nullable RepositoryRegistrationAotContribution forBean(RegisteredBean repositoryBean) {
RepositoryConfiguration<?> repositoryMetadata = getRepositoryRegistrationAotProcessor()
.getRepositoryMetadata(repositoryBean);
if (repositoryMetadata == null) {
return null;
}
AotRepositoryContext repositoryContext = buildAotRepositoryContext(aotProcessor.getEnvironment(), repositoryBean,
repositoryMetadata);
if (repositoryContext == null) {
return null;
}
return new RepositoryRegistrationAotContribution(getRepositoryRegistrationAotProcessor(), repositoryContext);
}
protected @Nullable BiFunction<AotRepositoryContext, GenerationContext, @Nullable RepositoryContributor> getModuleContribution() {
@@ -118,10 +164,6 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
}
protected AotRepositoryContext getRepositoryContext() {
Assert.state(this.repositoryContext != null,
"The AOT RepositoryContext was not properly initialized; did you call the forBean(:RegisteredBean) method");
return this.repositoryContext;
}
@@ -137,28 +179,27 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
getRepositoryRegistrationAotProcessor().logTrace(message, arguments);
}
/**
* Builds a {@link RepositoryRegistrationAotContribution} for given, required {@link RegisteredBean} representing the
* {@link Repository} registered in the bean registry.
*
* @param repositoryBean {@link RegisteredBean} for the {@link Repository}; must not be {@literal null}.
* @return a {@link RepositoryRegistrationAotContribution} to contribute AOT metadata and code for the
* {@link Repository} {@link RegisteredBean}.
* @throws IllegalArgumentException if the {@link RegisteredBean} is {@literal null}.
* @see org.springframework.beans.factory.support.RegisteredBean
*/
public RepositoryRegistrationAotContribution forBean(RegisteredBean repositoryBean) {
private static @Nullable AotRepositoryContext buildAotRepositoryContext(Environment environment, RegisteredBean bean,
RepositoryConfiguration<?> repositoryConfiguration) {
Assert.notNull(repositoryBean, "The RegisteredBean for the repository must not be null");
RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(bean);
RepositoryConfiguration<?> configuration = reader.getConfiguration();
RepositoryConfigurationExtensionSupport extension = reader.getConfigurationExtension();
RepositoryConfiguration<?> repositoryMetadata = getRepositoryRegistrationAotProcessor()
.getRepositoryMetadata(repositoryBean);
if (configuration == null || extension == null) {
logger.warn(
"Cannot create AotRepositoryContext for bean [%s]. No RepositoryConfiguration/RepositoryConfigurationExtension. Please make sure to register the repository bean through @Enable…Repositories."
.formatted(bean.getBeanName()));
return null;
}
RepositoryInformation repositoryInformation = reader.getRepositoryInformation();
DefaultAotRepositoryContext repositoryContext = new DefaultAotRepositoryContext(bean, repositoryInformation,
extension.getModuleName(), AotContext.from(bean.getBeanFactory(), environment));
Assert.state(repositoryMetadata != null, "The RepositoryConfiguration for the repository must not be null");
repositoryContext.setBasePackages(repositoryConfiguration.getBasePackages().toSet());
repositoryContext.setIdentifyingAnnotations(extension.getIdentifyingAnnotations());
this.repositoryContext = buildAotRepositoryContext(repositoryBean, repositoryMetadata);
return this;
return repositoryContext;
}
/**
@@ -176,9 +217,6 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
@Override
public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) {
Assert.state(this.repositoryContext != null,
"RepositoryContext cannot be null. Make sure to initialize this class with forBean(…).");
contributeRepositoryInfo(this.repositoryContext, generationContext);
var moduleContribution = getModuleContribution();
@@ -219,6 +257,10 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
};
}
public Predicate<Class<?>> typeFilter() { // like only document ones. // TODO: As in MongoDB?
return Predicates.isTrue();
}
private void contributeRepositoryInfo(AotRepositoryContext repositoryContext, GenerationContext contribution) {
RepositoryInformation repositoryInformation = getRepositoryInformation();
@@ -239,7 +281,7 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
for (RepositoryFragment<?> fragment : getRepositoryInformation().getFragments()) {
Class<?> repositoryFragmentType = fragment.getSignatureContributor();
Optional<?> implementation = fragment.getImplementation();
Optional<Class<?>> implementation = fragment.getImplementationClass();
contribution.getRuntimeHints().reflection().registerType(repositoryFragmentType, hint -> {
@@ -250,13 +292,12 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
}
});
implementation.ifPresent(impl -> {
Class<?> typeToRegister = impl instanceof Class c ? c : impl.getClass();
implementation.ifPresent(typeToRegister -> {
contribution.getRuntimeHints().reflection().registerType(typeToRegister, hint -> {
hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS);
if (!impl.getClass().isInterface()) {
if (!typeToRegister.isInterface()) {
hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
}
});
@@ -289,12 +330,6 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
}
// });
// Reactive Repositories
if (repositoryInformation.isReactiveRepository()) {
// TODO: do we still need this and how to configure it?
// registry.initialization().add(NativeInitializationEntry.ofBuildTimeType(configuration.getRepositoryInterface()));
}
// Kotlin
if (isKotlinCoroutineRepository(repositoryContext, repositoryInformation)) {
contribution.getRuntimeHints().reflection().registerTypes(kotlinRepositoryReflectionTypeReferences(),
@@ -356,29 +391,5 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
|| ClassUtils.isPrimitiveArray(type); //
}
public Predicate<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();
}
}

View File

@@ -69,6 +69,7 @@ import org.springframework.util.Assert;
*
* @author Christoph Strobl
* @author John Blum
* @author Mark Paluch
* @since 3.0
*/
public class RepositoryRegistrationAotProcessor
@@ -123,11 +124,16 @@ public class RepositoryRegistrationAotProcessor
return getConfigMap().containsKey(bean.getBeanName());
}
protected RepositoryRegistrationAotContribution newRepositoryRegistrationAotContribution(
protected @Nullable RepositoryRegistrationAotContribution newRepositoryRegistrationAotContribution(
RegisteredBean repositoryBean) {
RepositoryRegistrationAotContribution contribution = RepositoryRegistrationAotContribution.fromProcessor(this)
.forBean(repositoryBean);
RepositoryRegistrationAotContribution contribution = RepositoryRegistrationAotContribution.load(this,
repositoryBean);
// cannot contribute a repository bean.
if (contribution == null) {
return null;
}
//TODO: add the hook for customizing bean initialization code here!

View File

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

View File

@@ -18,7 +18,6 @@ package org.springframework.data.repository.core;
import java.lang.reflect.Method;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.data.repository.core.support.RepositoryComposition;
/**
@@ -106,8 +105,4 @@ public interface RepositoryInformation extends RepositoryMetadata {
*/
RepositoryComposition getRepositoryComposition();
default @Nullable String moduleName() {
return null;
}
}

View File

@@ -25,6 +25,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryInformationSupport;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.util.Lazy;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
@@ -44,6 +45,7 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen
private final RepositoryComposition composition;
private final RepositoryComposition baseComposition;
private final Lazy<RepositoryComposition> fullComposition;
/**
* Creates a new {@link DefaultRepositoryMetadata} for the given repository interface and repository base class.
@@ -62,6 +64,8 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen
this.baseComposition = RepositoryComposition.of(RepositoryFragment.structural(repositoryBaseClass)) //
.withArgumentConverter(composition.getArgumentConverter()) //
.withMethodLookup(composition.getMethodLookup());
this.fullComposition = Lazy.of(() -> composition.append(baseComposition.getFragments()));
}
@Override
@@ -106,7 +110,6 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen
@Override
protected boolean isQueryMethodCandidate(Method method) {
// FIXME - that should be simplified
boolean queryMethodCandidate = super.isQueryMethodCandidate(method);
if(!isQueryAnnotationPresentOn(method)) {
return queryMethodCandidate;
@@ -133,7 +136,7 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen
@Override
public RepositoryComposition getRepositoryComposition() {
return composition.append(baseComposition.getFragments());
return fullComposition.get();
}
}

View File

@@ -95,10 +95,6 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
private @Nullable Lazy<T> repository;
private @Nullable RepositoryMetadata repositoryMetadata;
// AOT bean factory hint?
private @Nullable String moduleBaseClass;
private @Nullable String moduleName;
/**
* Creates a new {@link RepositoryFactoryBeanSupport} for the given repository interface.
*
@@ -261,14 +257,6 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
this.publisher = publisher;
}
public void setModuleBaseClass(String moduleBaseClass) {
this.moduleBaseClass = moduleBaseClass;
}
public void setModuleName(String moduleName) {
this.moduleName = moduleName;
}
@Override
@SuppressWarnings("unchecked")
public EntityInformation<S, ID> getEntityInformation() {
@@ -281,6 +269,11 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
return getRequiredFactory().getRepositoryInformation(getRequiredRepositoryMetadata(), cachedFragments);
}
@Override
public RepositoryFragmentsContributor getRepositoryFragmentsContributor() {
return RepositoryFragmentsContributor.empty();
}
@Override
public PersistentEntity<?, ?> getPersistentEntity() {

View File

@@ -46,6 +46,15 @@ public interface RepositoryFactoryInformation<T, ID> {
*/
RepositoryInformation getRepositoryInformation();
/**
* Returns the {@link RepositoryFragmentsContributor} that is used to contribute additional fragments based on the
* repository declaration.
*
* @return
* @since 4.0
*/
RepositoryFragmentsContributor getRepositoryFragmentsContributor();
/**
* Returns the {@link PersistentEntity} managed by the underlying repository. Can be {@literal null} in case the
* underlying persistence mechanism does not expose a {@link MappingContext}.

View File

@@ -82,6 +82,18 @@ public interface RepositoryFragment<T> {
return new StructuralRepositoryFragment<>(interfaceOrImplementation);
}
/**
* Create a structural {@link RepositoryFragment} given {@code interfaceClass} and {@code implementationClass}.
*
* @param interfaceClass must not be {@literal null}.
* @param implementationClass must not be {@literal null}.
* @return
* @since 4.0
*/
static <T> RepositoryFragment<T> structural(Class<T> interfaceClass, Class<?> implementationClass) {
return new StructuralRepositoryFragment<>(interfaceClass, implementationClass);
}
/**
* Attempt to find the {@link Method} by name and exact parameters. Returns {@literal true} if the method was found or
* {@literal false} otherwise.
@@ -103,6 +115,15 @@ public interface RepositoryFragment<T> {
return Optional.empty();
}
/**
* @return the optional implementation class. Only available for fragments that ship an implementation descriptor.
* Structural (interface-only) fragments return always {@link Optional#empty()}.
* @since 4.0
*/
default Optional<Class<?>> getImplementationClass() {
return getImplementation().map(it -> it.getClass());
}
/**
* @return a {@link Stream} of methods exposed by this {@link RepositoryFragment}.
*/
@@ -186,17 +207,30 @@ public interface RepositoryFragment<T> {
class StructuralRepositoryFragment<T> implements RepositoryFragment<T> {
private final Class<T> interfaceOrImplementation;
private final Class<T> interfaceClass;
private final Class<?> implementationClass;
private final Method[] methods;
public StructuralRepositoryFragment(Class<T> interfaceOrImplementation) {
this.interfaceOrImplementation = interfaceOrImplementation;
this.methods = getSignatureContributor().getMethods();
this.interfaceClass = interfaceOrImplementation;
this.implementationClass = interfaceOrImplementation;
this.methods = interfaceOrImplementation.getMethods();
}
public StructuralRepositoryFragment(Class<T> interfaceClass, Class<?> implementationClass) {
this.interfaceClass = interfaceClass;
this.implementationClass = implementationClass;
this.methods = interfaceClass.getMethods();
}
@Override
public Class<?> getSignatureContributor() {
return interfaceOrImplementation;
return interfaceClass;
}
@Override
public Optional<Class<?>> getImplementationClass() {
return Optional.of(implementationClass);
}
@Override
@@ -221,31 +255,30 @@ public interface RepositoryFragment<T> {
@Override
public RepositoryFragment<T> withImplementation(T implementation) {
return new ImplementedRepositoryFragment<>(interfaceOrImplementation, implementation);
return new ImplementedRepositoryFragment<>(interfaceClass, implementation);
}
@Override
public String toString() {
return String.format("StructuralRepositoryFragment %s", ClassUtils.getShortName(interfaceOrImplementation));
return String.format("StructuralRepositoryFragment %s", ClassUtils.getShortName(interfaceClass));
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof StructuralRepositoryFragment<?> that)) {
return false;
}
return ObjectUtils.nullSafeEquals(interfaceOrImplementation, that.interfaceOrImplementation);
if (!ObjectUtils.nullSafeEquals(interfaceClass, that.interfaceClass)) {
return false;
}
return ObjectUtils.nullSafeEquals(implementationClass, that.implementationClass);
}
@Override
public int hashCode() {
return ObjectUtils.nullSafeHashCode(interfaceOrImplementation);
return ObjectUtils.nullSafeHash(interfaceClass, implementationClass);
}
}

View File

@@ -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);
}

View File

@@ -35,6 +35,7 @@ import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryFactoryInformation;
import org.springframework.data.repository.core.support.RepositoryFragmentsContributor;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.util.ProxyUtils;
import org.springframework.util.Assert;
@@ -365,6 +366,11 @@ public class Repositories implements Iterable<Class<?>> {
throw new UnsupportedOperationException();
}
@Override
public RepositoryFragmentsContributor getRepositoryFragmentsContributor() {
throw new UnsupportedOperationException();
}
@Override
public PersistentEntity<?, ?> getPersistentEntity() {
throw new UnsupportedOperationException();

View File

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

View File

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

View File

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

View File

@@ -15,33 +15,37 @@
*/
package org.springframework.data.repository.aot.generate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import example.UserRepository;
import example.UserRepository.User;
import java.util.List;
import java.util.TimeZone;
import javax.lang.model.element.Modifier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.data.geo.Metric;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.config.AotRepositoryInformation;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.AnnotationRepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.util.TypeInformation;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.TypeName;
import org.springframework.stereotype.Repository;
/**
* Unit tests for {@link AotRepositoryBuilder}.
*
* @author Christoph Strobl
* @author Mark Paluch
*/
class AotRepositoryBuilderUnitTests {
@@ -57,7 +61,7 @@ class AotRepositoryBuilderUnitTests {
@Test // GH-3279
void writesClassSkeleton() {
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation,
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons",
new SpelAwareProxyProjectionFactory());
assertThat(repoBuilder.build().javaFile().toString())
.contains("package %s;".formatted(UserRepository.class.getPackageName())) // same package as source repo
@@ -69,7 +73,7 @@ class AotRepositoryBuilderUnitTests {
@Test // GH-3279
void appliesCtorArguments() {
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation,
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons",
new SpelAwareProxyProjectionFactory());
repoBuilder.withConstructorCustomizer(ctor -> {
ctor.addParameter("param1", Metric.class);
@@ -89,7 +93,7 @@ class AotRepositoryBuilderUnitTests {
@Test // GH-3279
void appliesCtorCodeBlock() {
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation,
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons",
new SpelAwareProxyProjectionFactory());
repoBuilder.withConstructorCustomizer(ctor -> {
ctor.customize((info, code) -> {
@@ -103,7 +107,7 @@ class AotRepositoryBuilderUnitTests {
@Test // GH-3279
void appliesClassCustomizations() {
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation,
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons",
new SpelAwareProxyProjectionFactory());
repoBuilder.withClassCustomizer((info, metadata, clazz) -> {
@@ -128,12 +132,11 @@ class AotRepositoryBuilderUnitTests {
@Test // GH-3279
void appliesQueryMethodContributor() {
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation,
new SpelAwareProxyProjectionFactory());
AotRepositoryInformation repositoryInformation = new AotRepositoryInformation(
AnnotationRepositoryMetadata.getMetadata(UserRepository.class), CrudRepository.class, List.of());
when(repositoryInformation.isQueryMethod(Mockito.argThat(arg -> arg.getName().equals("findByFirstname"))))
.thenReturn(true);
doReturn(TypeInformation.of(User.class)).when(repositoryInformation).getReturnType(any());
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons",
new SpelAwareProxyProjectionFactory());
repoBuilder.withQueryMethodContributor((method, info) -> {
@@ -154,4 +157,35 @@ class AotRepositoryBuilderUnitTests {
assertThat(repoBuilder.build().javaFile().toString()) //
.containsIgnoringWhitespaces("void oops() { }");
}
@Test // GH-3279
void shouldContributeFragmentImplementationMetadata() {
AotRepositoryInformation repositoryInformation = new AotRepositoryInformation(
AnnotationRepositoryMetadata.getMetadata(QuerydslUserRepository.class), CrudRepository.class,
List.of(RepositoryFragment.structural(QuerydslPredicateExecutor.class, DummyQuerydslPredicateExecutor.class)));
AotRepositoryBuilder builder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons",
new SpelAwareProxyProjectionFactory());
AotRepositoryBuilder.AotBundle bundle = builder.build();
AotRepositoryMethod method = bundle.metadata().methods().stream().filter(it -> it.name().equals("findBy"))
.findFirst().get();
assertThat(method.fragment()).isNotNull();
assertThat(method.fragment().signature()).isEqualTo(QuerydslPredicateExecutor.class.getName());
assertThat(method.fragment().implementation()).isEqualTo(DummyQuerydslPredicateExecutor.class.getName());
}
interface UserRepository extends org.springframework.data.repository.Repository<User, String> {
String someMethod();
}
interface QuerydslUserRepository
extends org.springframework.data.repository.Repository<User, String>, QuerydslPredicateExecutor<User> {
}
interface DummyQuerydslPredicateExecutor<T> extends QuerydslPredicateExecutor<T> {}
}

View File

@@ -43,6 +43,11 @@ class DummyModuleAotRepositoryContext implements AotRepositoryContext {
this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition);
}
@Override
public String getModuleName() {
return "Commons";
}
@Override
public ConfigurableListableBeanFactory getBeanFactory() {
return null;

View File

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

View File

@@ -15,15 +15,15 @@
*/
package org.springframework.data.repository.config;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ComponentScan.Filter;
@@ -184,6 +184,34 @@ class AnnotationRepositoryConfigurationSourceUnitTests {
assertThat(getConfigSource(DefaultConfiguration.class).generateBeanName(bd)).isEqualTo("personRepository");
}
@Test // GH-3279
void considersDefaultFragmentsContributor() {
RootBeanDefinition bd = new RootBeanDefinition(DummyRepositoryFactory.class);
bd.getConstructorArgumentValues().addGenericArgumentValue(PersonRepository.class);
AnnotationMetadata metadata = new StandardAnnotationMetadata(ConfigurationWithFragmentsContributor.class, true);
AnnotationRepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata,
EnableRepositoriesWithContributor.class, resourceLoader, environment, registry, null);
assertThat(configurationSource.getRepositoryFragmentsContributorClassName())
.contains(SampleRepositoryFragmentsContributor.class.getName());
}
@Test // GH-3279
void omitsUnspecifiedFragmentsContributor() {
RootBeanDefinition bd = new RootBeanDefinition(DummyRepositoryFactory.class);
bd.getConstructorArgumentValues().addGenericArgumentValue(PersonRepository.class);
AnnotationMetadata metadata = new StandardAnnotationMetadata(ReactiveConfigurationWithBeanNameGenerator.class,
true);
AnnotationRepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata,
EnableReactiveRepositories.class, resourceLoader, environment, registry, null);
assertThat(configurationSource.getRepositoryFragmentsContributorClassName()).isEmpty();
}
@Test // GH-3082
void considerBeanNameGeneratorForReactiveRepos() {
@@ -219,6 +247,9 @@ class AnnotationRepositoryConfigurationSourceUnitTests {
@EnableRepositories(nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class)
static class ConfigurationWithBeanNameGenerator {}
@EnableRepositoriesWithContributor()
static class ConfigurationWithFragmentsContributor {}
@EnableReactiveRepositories(nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class)
static class ReactiveConfigurationWithBeanNameGenerator {}
@@ -234,4 +265,5 @@ class AnnotationRepositoryConfigurationSourceUnitTests {
static class ConfigWithSampleAnnotation {}
interface ReactivePersonRepository extends ReactiveCrudRepository<Person, String> {}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -15,24 +15,29 @@
*/
package org.springframework.data.repository.config;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.aot.sample.ConfigWithCustomImplementation;
import org.springframework.data.aot.sample.ConfigWithCustomRepositoryBaseClass;
import org.springframework.data.aot.sample.ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo;
import org.springframework.data.aot.sample.ConfigWithFragments;
import org.springframework.data.aot.sample.ConfigWithSimpleCrudRepository;
import org.springframework.data.aot.sample.ReactiveConfig;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.repository.core.support.RepositoryFragment;
/**
* Unit tests for {@link RepositoryBeanDefinitionReader}.
*
* @author Christoph Strobl
* @author Mark Paluch
*/
class RepositoryBeanDefinitionReaderTests {
@@ -42,10 +47,10 @@ class RepositoryBeanDefinitionReaderTests {
RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithSimpleCrudRepository.class);
RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.class);
Mockito.when(repoConfig.getRepositoryInterface()).thenReturn(ConfigWithSimpleCrudRepository.MyRepo.class.getName());
when(repoConfig.getRepositoryInterface()).thenReturn(ConfigWithSimpleCrudRepository.MyRepo.class.getName());
RepositoryInformation repositoryInformation = RepositoryBeanDefinitionReader.repositoryInformation(repoConfig,
repoFactoryBean.getMergedBeanDefinition(), repoFactoryBean.getBeanFactory());
RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean);
RepositoryInformation repositoryInformation = reader.getRepositoryInformation();
assertThat(repositoryInformation.getRepositoryInterface()).isEqualTo(ConfigWithSimpleCrudRepository.MyRepo.class);
assertThat(repositoryInformation.getDomainType()).isEqualTo(ConfigWithSimpleCrudRepository.Person.class);
@@ -59,49 +64,86 @@ class RepositoryBeanDefinitionReaderTests {
RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.class);
Class<?> repositoryInterfaceType = CustomerRepositoryWithCustomBaseRepo.class;
Mockito.when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName());
when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName());
RepositoryInformation repositoryInformation = RepositoryBeanDefinitionReader.repositoryInformation(repoConfig,
repoFactoryBean.getMergedBeanDefinition(), repoFactoryBean.getBeanFactory());
RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean);
RepositoryInformation repositoryInformation = reader.getRepositoryInformation();
assertThat(repositoryInformation.getRepositoryBaseClass())
.isEqualTo(ConfigWithCustomRepositoryBaseClass.RepoBaseClass.class);
}
@Test // GH-3279
void readsFragmentsFromBeanFactory() {
void readsFragmentsContributorFromBeanDefinition() {
RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithCustomImplementation.class);
RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithCustomRepositoryBaseClass.class);
RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.class);
Class<?> repositoryInterfaceType = ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class;
Mockito.when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName());
Class<?> repositoryInterfaceType = CustomerRepositoryWithCustomBaseRepo.class;
when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName());
RepositoryInformation repositoryInformation = RepositoryBeanDefinitionReader.repositoryInformation(repoConfig,
repoFactoryBean.getMergedBeanDefinition(), repoFactoryBean.getBeanFactory());
RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean);
RepositoryInformation repositoryInformation = reader.getRepositoryInformation();
assertThat(repositoryInformation.getFragments()).satisfiesExactly(fragment -> {
assertThat(fragment.getSignatureContributor())
.isEqualTo(ConfigWithCustomImplementation.CustomImplInterface.class);
});
assertThat(repositoryInformation.getFragments())
.contains(RepositoryFragment.structural(SampleRepositoryFragmentsContributor.class));
}
@Test // GH-3279
void fallsBackToModuleBaseClassIfSetAndNoRepoBaseDefined() {
void readsFragmentsContributorFromBeanFactory() {
RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithSimpleCrudRepository.class);
RootBeanDefinition rootBeanDefinition = repoFactoryBean.getMergedBeanDefinition().cloneBeanDefinition();
// need to unset because its defined as non default
rootBeanDefinition.getPropertyValues().removePropertyValue("repositoryBaseClass");
rootBeanDefinition.getPropertyValues().add("moduleBaseClass", ModuleBase.class.getName());
RegisteredBean repoFactoryBean = repositoryFactory(ReactiveConfig.class);
RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.class);
Mockito.when(repoConfig.getRepositoryInterface()).thenReturn(ConfigWithSimpleCrudRepository.MyRepo.class.getName());
Class<?> repositoryInterfaceType = ReactiveConfig.CustomerRepositoryReactive.class;
when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName());
RepositoryInformation repositoryInformation = RepositoryBeanDefinitionReader.repositoryInformation(repoConfig,
rootBeanDefinition, repoFactoryBean.getBeanFactory());
RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean);
RepositoryInformation repositoryInformation = reader.getRepositoryInformation();
assertThat(repositoryInformation.getRepositoryBaseClass()).isEqualTo(ModuleBase.class);
assertThat(repositoryInformation.getFragments()).isEmpty();
}
@Test // GH-3279, GH-3282
void readsCustomImplementationFromBeanFactory() {
RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithCustomImplementation.class);
RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.class);
Class<?> repositoryInterfaceType = ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class;
when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName());
RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean);
RepositoryInformation repositoryInformation = reader.getRepositoryInformation();
assertThat(repositoryInformation.getFragments()).satisfiesExactly(fragment -> {
assertThat(fragment.getImplementationClass())
.contains(ConfigWithCustomImplementation.RepositoryWithCustomImplementationImpl.class);
});
}
@Test // GH-3279, GH-3282
void readsFragmentsFromBeanFactory() {
RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithFragments.class);
RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.class);
Class<?> repositoryInterfaceType = ConfigWithFragments.RepositoryWithFragments.class;
when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName());
RepositoryBeanDefinitionReader reader = new RepositoryBeanDefinitionReader(repoFactoryBean);
RepositoryInformation repositoryInformation = reader.getRepositoryInformation();
assertThat(repositoryInformation.getFragments()).hasSize(2);
for (RepositoryFragment<?> fragment : repositoryInformation.getFragments()) {
assertThat(fragment.getSignatureContributor()).isIn(ConfigWithFragments.CustomImplInterface1.class,
ConfigWithFragments.CustomImplInterface2.class);
assertThat(fragment.getImplementationClass().get()).isIn(ConfigWithFragments.CustomImplInterface1Impl.class,
ConfigWithFragments.CustomImplInterface2Impl.class);
}
}
static RegisteredBean repositoryFactory(Class<?> configClass) {
@@ -118,5 +160,4 @@ class RepositoryBeanDefinitionReaderTests {
return RegisteredBean.of(applicationContext.getBeanFactory(), beanNamesForType[0]);
}
static class ModuleBase {}
}

View File

@@ -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));
}
}

View File

@@ -29,6 +29,7 @@ public class DummyRepositoryFactoryBean<T extends Repository<S, ID>, S, ID exten
extends RepositoryFactoryBeanSupport<T, S, ID> {
private final T repository;
private RepositoryFragmentsContributor repositoryFragmentsContributor = RepositoryFragmentsContributor.empty();
public DummyRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
@@ -38,6 +39,19 @@ public class DummyRepositoryFactoryBean<T extends Repository<S, ID>, S, ID exten
setMappingContext(new SampleMappingContext());
}
public T getRepository() {
return repository;
}
@Override
public RepositoryFragmentsContributor getRepositoryFragmentsContributor() {
return repositoryFragmentsContributor;
}
public void setRepositoryFragmentsContributor(RepositoryFragmentsContributor repositoryFragmentsContributor) {
this.repositoryFragmentsContributor = repositoryFragmentsContributor;
}
@Override
protected RepositoryFactorySupport createRepositoryFactory() {
return new DummyRepositoryFactory(repository);

View File

@@ -46,6 +46,7 @@ import org.springframework.data.repository.core.support.DummyEntityInformation;
import org.springframework.data.repository.core.support.DummyRepositoryFactoryBean;
import org.springframework.data.repository.core.support.DummyRepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryFactoryInformation;
import org.springframework.data.repository.core.support.RepositoryFragmentsContributor;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.ClassUtils;
@@ -290,6 +291,11 @@ class RepositoriesUnitTests {
return new DummyRepositoryInformation(repositoryMetadata);
}
@Override
public RepositoryFragmentsContributor getRepositoryFragmentsContributor() {
return RepositoryFragmentsContributor.empty();
}
@Override
public PersistentEntity<?, ?> getPersistentEntity() {
return mappingContext.getRequiredPersistentEntity(repositoryMetadata.getDomainType());