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:
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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!
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
|
||||
@@ -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}.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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> {}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user