Refactor RepositoryFactoryBeanSupport fragments setters.

Introduce RepositoryFragmentsFunction and a collection of functions to provide a well-formed contract.

Use RepositoryMetadata from initialized RepositoryFactoryBean.

See #3265
This commit is contained in:
Mark Paluch
2025-03-24 14:53:17 +01:00
parent 281f186411
commit 3596bcc996
32 changed files with 1310 additions and 653 deletions

View File

@@ -1,25 +0,0 @@
/*
* Copyright 2024 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.aot.generate;
import org.springframework.aot.generate.GenerationContext;
/**
* @author Christoph Strobl
*/
public interface AotCodeContributor {
void contribute(GenerationContext generationContext);
}

View File

@@ -0,0 +1,320 @@
/*
* 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
*
* http://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.
*/
/*
* 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
*
* http://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.aot.generate;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import javax.lang.model.element.Modifier;
import org.jspecify.annotations.Nullable;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotationSelectors;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.javapoet.FieldSpec;
import org.springframework.javapoet.ParameterSpec;
import org.springframework.javapoet.TypeName;
import org.springframework.util.ObjectUtils;
/**
* Generational AOT context for repository query method generation.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 4.0
*/
public class AotQueryMethodGenerationContext {
private final Method method;
private final MergedAnnotations annotations;
private final QueryMethod queryMethod;
private final RepositoryInformation repositoryInformation;
private final AotRepositoryFragmentMetadata targetTypeMetadata;
private final AotRepositoryMethodImplementationMetadata targetMethodMetadata;
private final CodeBlocks codeBlocks;
AotQueryMethodGenerationContext(RepositoryInformation repositoryInformation, Method method, QueryMethod queryMethod,
AotRepositoryFragmentMetadata targetTypeMetadata) {
this.method = method;
this.annotations = MergedAnnotations.from(method);
this.queryMethod = queryMethod;
this.repositoryInformation = repositoryInformation;
this.targetTypeMetadata = targetTypeMetadata;
this.targetMethodMetadata = new AotRepositoryMethodImplementationMetadata(repositoryInformation, method);
this.codeBlocks = new CodeBlocks(targetTypeMetadata);
}
AotRepositoryFragmentMetadata getTargetTypeMetadata() {
return targetTypeMetadata;
}
AotRepositoryMethodImplementationMetadata getTargetMethodMetadata() {
return targetMethodMetadata;
}
public RepositoryInformation getRepositoryInformation() {
return repositoryInformation;
}
public Method getMethod() {
return method;
}
public CodeBlocks codeBlocks() {
return codeBlocks;
}
/**
* @return the {@link MergedAnnotations} that are present on the method.
*/
public MergedAnnotations getAnnotations() {
return annotations;
}
/**
* Get the {@linkplain MergedAnnotationSelectors#nearest() nearest} matching annotation or meta-annotation of the
* specified type, or {@link MergedAnnotation#missing()} if none is present.
*
* @param annotationType the annotation type to get
* @return a {@link MergedAnnotation} instance
*/
public <A extends Annotation> MergedAnnotation<A> getAnnotation(Class<A> annotationType) {
return annotations.get(annotationType);
}
/**
* @return the returned type without considering dynamic projections.
*/
public ReturnedType getReturnedType() {
return queryMethod.getResultProcessor().getReturnedType();
}
public ResolvableType getActualReturnType() {
return targetMethodMetadata.getActualReturnType();
}
public ResolvableType getReturnType() {
return targetMethodMetadata.getReturnType();
}
/**
* @return the {@link TypeName} representing the method return type.
*/
public TypeName getReturnTypeName() {
return TypeName.get(getReturnType().getType());
}
/**
* Returns the required parameter name for the {@link Parameter#isBindable() bindable parameter} at the given
* {@code parameterIndex} or throws {@link IllegalArgumentException} if the parameter cannot be determined by its
* index.
*
* @param parameterIndex the zero-based parameter index as used in the query to reference bindable parameters.
* @return the parameter name.
*/
public String getRequiredBindableParameterName(int parameterIndex) {
String name = getBindableParameterName(parameterIndex);
if (ObjectUtils.isEmpty(name)) {
throw new IllegalArgumentException("No bindable parameter with index %d".formatted(parameterIndex));
}
return name;
}
/**
* Returns the parameter name for the {@link Parameter#isBindable() bindable parameter} at the given
* {@code parameterIndex} or {@code null} if the parameter cannot be determined by its index.
*
* @param parameterIndex the zero-based parameter index as used in the query to reference bindable parameters.
* @return the parameter name.
*/
// TODO: Simplify?!
public @Nullable String getBindableParameterName(int parameterIndex) {
int bindable = 0;
int totalIndex = 0;
for (Parameter parameter : queryMethod.getParameters()) {
if (parameter.isBindable()) {
if (bindable == parameterIndex) {
return getParameterName(totalIndex);
}
bindable++;
}
totalIndex++;
}
return null;
}
/**
* Returns the required parameter name for the {@link Parameter#isBindable() bindable parameter} at the given
* {@code parameterName} or throws {@link IllegalArgumentException} if the parameter cannot be determined by its
* index.
*
* @param parameterName the parameter name as used in the query to reference bindable parameters.
* @return the parameter name.
*/
public String getRequiredBindableParameterName(String parameterName) {
String name = getBindableParameterName(parameterName);
if (ObjectUtils.isEmpty(name)) {
throw new IllegalArgumentException("No bindable parameter with name '%s'".formatted(parameterName));
}
return name;
}
/**
* Returns the required parameter name for the {@link Parameter#isBindable() bindable parameter} at the given
* {@code parameterName} or {@code null} if the parameter cannot be determined by its index.
*
* @param parameterName the parameter name as used in the query to reference bindable parameters.
* @return the parameter name.
*/
// TODO: Simplify?!
public @Nullable String getBindableParameterName(String parameterName) {
int totalIndex = 0;
for (Parameter parameter : queryMethod.getParameters()) {
totalIndex++;
if (!parameter.isBindable()) {
continue;
}
if (parameter.getName().filter(it -> it.equals(parameterName)).isPresent()) {
return getParameterName(totalIndex - 1);
}
}
return null;
}
/**
* @return list of bindable parameter names.
*/
public List<String> getBindableParameterNames() {
List<String> result = new ArrayList<>();
for (Parameter parameter : queryMethod.getParameters().getBindableParameters()) {
parameter.getName().map(result::add);
}
return result;
}
/**
* @return list of all parameter names (including non-bindable special parameters).
*/
public List<String> getAllParameterNames() {
List<String> result = new ArrayList<>();
for (Parameter parameter : queryMethod.getParameters()) {
parameter.getName().map(result::add);
}
return result;
}
public boolean hasField(String fieldName) {
return targetTypeMetadata.hasField(fieldName);
}
public void addField(String fieldName, TypeName type, Modifier... modifiers) {
targetTypeMetadata.addField(fieldName, type, modifiers);
}
public void addField(FieldSpec fieldSpec) {
targetTypeMetadata.addField(fieldSpec);
}
public @Nullable String fieldNameOf(Class<?> type) {
return targetTypeMetadata.fieldNameOf(type);
}
@Nullable
public String getParameterNameOf(Class<?> type) {
return targetMethodMetadata.getParameterNameOf(type);
}
public @Nullable String getParameterName(int position) {
if (0 > position) {
return null;
}
List<Entry<String, ParameterSpec>> entries = new ArrayList<>(
targetMethodMetadata.getMethodArguments().entrySet());
if (position < entries.size()) {
return entries.get(position).getKey();
}
return null;
}
public void addParameter(ParameterSpec parameter) {
this.targetMethodMetadata.addParameter(parameter);
}
@Nullable
public String getSortParameterName() {
return getParameterName(queryMethod.getParameters().getSortIndex());
}
@Nullable
public String getPageableParameterName() {
return getParameterName(queryMethod.getParameters().getPageableIndex());
}
@Nullable
public String getLimitParameterName() {
return getParameterName(queryMethod.getParameters().getLimitIndex());
}
}

View File

@@ -15,49 +15,51 @@
*/
package org.springframework.data.repository.aot.generate;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.temporal.ChronoField;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.lang.model.element.Modifier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aot.generate.ClassNameGenerator;
import org.springframework.aot.generate.Generated;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryInformation;
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.TypeName;
import org.springframework.javapoet.TypeSpec;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
/**
* Builder for AOT repository fragments.
*
* @author Christoph Strobl
* @author Mark Paluch
*/
public class AotRepositoryBuilder {
class AotRepositoryBuilder {
private final RepositoryInformation repositoryInformation;
private final AotRepositoryImplementationMetadata generationMetadata;
private final ProjectionFactory projectionFactory;
private final AotRepositoryFragmentMetadata generationMetadata;
private Consumer<AotRepositoryConstructorBuilder> constructorBuilderCustomizer;
private Function<AotRepositoryMethodGenerationContext, AotRepositoryMethodBuilder> methodContextFunction;
private RepositoryCustomizer customizer;
private Consumer<AotRepositoryConstructorBuilder> constructorCustomizer;
private BiFunction<Method, RepositoryInformation, MethodContributor<? extends QueryMethod>> methodContributorFunction;
private ClassCustomizer customizer;
public static AotRepositoryBuilder forRepository(RepositoryInformation repositoryInformation) {
return new AotRepositoryBuilder(repositoryInformation);
}
AotRepositoryBuilder(RepositoryInformation repositoryInformation) {
private AotRepositoryBuilder(RepositoryInformation repositoryInformation, ProjectionFactory projectionFactory) {
this.repositoryInformation = repositoryInformation;
this.generationMetadata = new AotRepositoryImplementationMetadata(className());
this.projectionFactory = projectionFactory;
this.generationMetadata = new AotRepositoryFragmentMetadata(className());
this.generationMetadata.addField(FieldSpec
.builder(TypeName.get(Log.class), "logger", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("$T.getLog($T.class)", TypeName.get(LogFactory.class), this.generationMetadata.getTargetTypeName())
@@ -66,54 +68,80 @@ public class AotRepositoryBuilder {
this.customizer = (info, metadata, builder) -> {};
}
public JavaFile javaFile() {
public static <M extends QueryMethod> AotRepositoryBuilder forRepository(RepositoryInformation repositoryInformation,
ProjectionFactory projectionFactory) {
return new AotRepositoryBuilder(repositoryInformation, projectionFactory);
}
YearMonth creationDate = YearMonth.now(ZoneId.of("UTC"));
public AotRepositoryBuilder withConstructorCustomizer(
Consumer<AotRepositoryConstructorBuilder> constructorCustomizer) {
this.constructorCustomizer = constructorCustomizer;
return this;
}
public AotRepositoryBuilder withQueryMethodContributor(
BiFunction<Method, RepositoryInformation, MethodContributor<? extends QueryMethod>> methodContributorFunction) {
this.methodContributorFunction = methodContributorFunction;
return this;
}
public AotRepositoryBuilder withClassCustomizer(ClassCustomizer classCustomizer) {
this.customizer = classCustomizer;
return this;
}
public JavaFile build() {
// 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",
repositoryInformation.getRepositoryInterface()) //
.addJavadoc("\n") //
.addJavadoc("@since $L/$L\n", creationDate.get(ChronoField.YEAR), creationDate.get(ChronoField.MONTH_OF_YEAR)) //
.addJavadoc("@author $L", "Spring Data"); // TODO: does System.getProperty("user.name") make sense here?
// TODO: we do not need that here
// .addSuperinterface(repositoryInformation.getRepositoryInterface());
repositoryInformation.getRepositoryInterface());
// create the constructor
AotRepositoryConstructorBuilder constructorBuilder = new AotRepositoryConstructorBuilder(repositoryInformation,
generationMetadata);
constructorBuilderCustomizer.accept(constructorBuilder);
constructorCustomizer.accept(constructorBuilder);
builder.addMethod(constructorBuilder.buildConstructor());
// write methods
// start with the derived ones
ReflectionUtils.doWithMethods(repositoryInformation.getRepositoryInterface(), method -> {
Arrays.stream(repositoryInformation.getRepositoryInterface().getMethods())
.sorted(Comparator.<Method, String> comparing(it -> {
return it.getDeclaringClass().getName();
}).thenComparing(Method::getName).thenComparing(Method::getParameterCount).thenComparing(Method::toString))
.forEach(method -> {
AotRepositoryMethodGenerationContext context = new AotRepositoryMethodGenerationContext(method,
repositoryInformation, generationMetadata);
AotRepositoryMethodBuilder methodBuilder = methodContextFunction.apply(context);
if (methodBuilder != null) {
builder.addMethod(methodBuilder.buildMethod());
}
if (repositoryInformation.isCustomMethod(method)) {
// TODO: fragment
return;
}
}, it -> {
if (repositoryInformation.isBaseClassMethod(method)) {
// TODO: base
return;
}
/*
the isBaseClassMethod(it) check seems to have some issues.
need to hard code it here
*/
if (method.isBridge() || method.isDefault() || java.lang.reflect.Modifier.isStatic(method.getModifiers())) {
// TODO: report what we've skipped
return;
}
if (ReflectionUtils.findMethod(CrudRepository.class, it.getName(), it.getParameterTypes()) != null) {
return false;
}
if (repositoryInformation.isQueryMethod(method)) {
return !repositoryInformation.isBaseClassMethod(it) && !repositoryInformation.isCustomMethod(it)
&& !it.isDefault();
});
MethodContributor<? extends QueryMethod> contributor = methodContributorFunction.apply(method,
repositoryInformation);
if (contributor != null) {
AotQueryMethodGenerationContext context = new AotQueryMethodGenerationContext(repositoryInformation,
method, contributor.getQueryMethod(), generationMetadata);
builder.addMethod(contributor.contribute(context));
}
}
});
// write fields at the end so we make sure to capture things added by methods
generationMetadata.getFields().values().forEach(builder::addField);
@@ -123,25 +151,7 @@ public class AotRepositoryBuilder {
return JavaFile.builder(packageName(), builder.build()).build();
}
AotRepositoryBuilder withConstructorCustomizer(Consumer<AotRepositoryConstructorBuilder> constuctorBuilder) {
this.constructorBuilderCustomizer = constuctorBuilder;
return this;
}
AotRepositoryBuilder withDerivedMethodFunction(
Function<AotRepositoryMethodGenerationContext, AotRepositoryMethodBuilder> methodContextFunction) {
this.methodContextFunction = methodContextFunction;
return this;
}
AotRepositoryBuilder withFileCustomizer(RepositoryCustomizer repositoryCustomizer) {
this.customizer = repositoryCustomizer;
return this;
}
AotRepositoryImplementationMetadata getGenerationMetadata() {
public AotRepositoryFragmentMetadata getGenerationMetadata() {
return generationMetadata;
}
@@ -157,13 +167,31 @@ public class AotRepositoryBuilder {
return "%sImpl".formatted(repositoryInformation.getRepositoryInterface().getSimpleName());
}
Map<String, TypeName> getAutowireFields() {
public Map<String, TypeName> getAutowireFields() {
return generationMetadata.getConstructorArguments();
}
public interface RepositoryCustomizer {
public RepositoryInformation getRepositoryInformation() {
return repositoryInformation;
}
void customize(RepositoryInformation repositoryInformation, AotRepositoryImplementationMetadata metadata,
public ProjectionFactory getProjectionFactory() {
return projectionFactory;
}
/**
* Customizer interface to customize the AOT repository fragment class after it has been defined.
*/
public interface ClassCustomizer {
/**
* Apply customization ot the AOT repository fragment class after it has been defined..
*
* @param information
* @param metadata
* @param builder
*/
void customize(RepositoryInformation information, AotRepositoryFragmentMetadata metadata,
TypeSpec.Builder builder);
}
}

View File

@@ -27,22 +27,32 @@ import org.springframework.javapoet.ParameterizedTypeName;
import org.springframework.javapoet.TypeName;
/**
* Builder for AOT Repository Constructors.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 4.0
*/
public class AotRepositoryConstructorBuilder {
private final RepositoryInformation repositoryInformation;
private final AotRepositoryImplementationMetadata metadata;
private final AotRepositoryFragmentMetadata metadata;
private ConstructorCustomizer customizer = (info, builder) -> {};
AotRepositoryConstructorBuilder(RepositoryInformation repositoryInformation,
AotRepositoryImplementationMetadata metadata) {
AotRepositoryFragmentMetadata metadata) {
this.repositoryInformation = repositoryInformation;
this.metadata = metadata;
}
/**
* Add constructor parameter.
*
* @param parameterName
* @param type
*/
public void addParameter(String parameterName, Class<?> type) {
ResolvableType resolvableType = ResolvableType.forClass(type);
@@ -53,12 +63,24 @@ public class AotRepositoryConstructorBuilder {
addParameter(parameterName, ParameterizedTypeName.get(type, resolvableType.resolveGenerics()));
}
/**
* Add constructor parameter.
*
* @param parameterName
* @param type
*/
public void addParameter(String parameterName, TypeName type) {
this.metadata.addConstructorArgument(parameterName, type);
this.metadata.addField(parameterName, type, Modifier.PRIVATE, Modifier.FINAL);
}
/**
* Add constructor customizer. Customizer is invoked after adding constructor arguments and before assigning
* constructor arguments to fields.
*
* @param customizer
*/
public void customize(ConstructorCustomizer customizer) {
this.customizer = customizer;
}
@@ -66,11 +88,18 @@ public class AotRepositoryConstructorBuilder {
MethodSpec buildConstructor() {
MethodSpec.Builder builder = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC);
for (Entry<String, TypeName> parameter : this.metadata.getConstructorArguments().entrySet()) {
builder.addParameter(parameter.getValue(), parameter.getKey()).addStatement("this.$N = $N", parameter.getKey(),
builder.addParameter(parameter.getValue(), parameter.getKey());
}
customizer.customize(repositoryInformation, builder);
for (Entry<String, TypeName> parameter : this.metadata.getConstructorArguments().entrySet()) {
builder.addStatement("this.$N = $N", parameter.getKey(),
parameter.getKey());
}
customizer.customize(repositoryInformation, builder);
return builder.build();
}
@@ -87,8 +116,12 @@ public class AotRepositoryConstructorBuilder {
return TypeName.get(repositoryInformation.getRepositoryBaseClass());
}
/**
* Customizer for the AOT repository constructor.
*/
public interface ConstructorCustomizer {
void customize(RepositoryInformation repositoryInformation, MethodSpec.Builder builder);
void customize(RepositoryInformation information, MethodSpec.Builder builder);
}
}

View File

@@ -22,21 +22,22 @@ import java.util.Map.Entry;
import javax.lang.model.element.Modifier;
import org.jspecify.annotations.Nullable;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.FieldSpec;
import org.springframework.javapoet.TypeName;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
*/
class AotRepositoryImplementationMetadata {
public class AotRepositoryFragmentMetadata {
private ClassName className;
private Map<String, FieldSpec> fields = new HashMap<>(3);
private final ClassName className;
private final Map<String, FieldSpec> fields = new HashMap<>(3);
private final Map<String, TypeName> constructorArguments = new LinkedHashMap<>(3);
public AotRepositoryImplementationMetadata(ClassName className) {
public AotRepositoryFragmentMetadata(ClassName className) {
this.className = className;
}

View File

@@ -17,124 +17,123 @@ package org.springframework.data.repository.aot.generate;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.lang.reflect.TypeVariable;
import java.util.stream.Collectors;
import javax.lang.model.element.Modifier;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.ParameterSpec;
import org.springframework.javapoet.ParameterizedTypeName;
import org.springframework.javapoet.TypeName;
import org.springframework.lang.Nullable;
import org.springframework.javapoet.TypeVariableName;
import org.springframework.util.StringUtils;
/**
* Builder for AOT repository query methods.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 4.0
*/
public class AotRepositoryMethodBuilder {
private final AotRepositoryMethodGenerationContext context;
private final AotQueryMethodGenerationContext context;
private RepositoryMethodContribution contribution = (context) -> CodeBlock.builder().build();
private RepositoryMethodCustomizer customizer = (context, body) -> {};
public AotRepositoryMethodBuilder(AotRepositoryMethodGenerationContext context) {
AotRepositoryMethodBuilder(AotQueryMethodGenerationContext context) {
this.context = context;
initReturnType(context.getMethod(), context.getRepositoryInformation());
initParameters(context.getMethod(), context.getRepositoryInformation());
}
public void addParameter(String parameterName, Class<?> type) {
ResolvableType resolvableType = ResolvableType.forClass(type);
if (!resolvableType.hasGenerics() || !resolvableType.hasResolvableGenerics()) {
addParameter(parameterName, TypeName.get(type));
return;
}
addParameter(parameterName, ParameterizedTypeName.get(type, resolvableType.resolveGenerics()));
}
public void addParameter(String parameterName, TypeName type) {
addParameter(ParameterSpec.builder(type, parameterName).build());
}
public void addParameter(ParameterSpec parameter) {
this.context.addParameter(parameter);
}
public void setReturnType(@Nullable TypeName returnType, @Nullable TypeName actualReturnType) {
this.context.getTargetMethodMetadata().setReturnType(returnType);
this.context.getTargetMethodMetadata().setActualReturnType(actualReturnType);
}
public AotRepositoryMethodBuilder customize(RepositoryMethodCustomizer customizer) {
this.customizer = customizer;
return this;
}
MethodSpec buildMethod() {
MethodSpec.Builder builder = MethodSpec.methodBuilder(context.getMethod().getName()).addModifiers(Modifier.PUBLIC);
if (!context.returnsVoid()) {
builder.returns(context.getReturnType());
}
builder.addJavadoc("AOT generated implementation of {@link $T#$L($L)}.", context.getMethod().getDeclaringClass(),
context.getMethod().getName(),
StringUtils.collectionToCommaDelimitedString(context.getTargetMethodMetadata().getMethodArguments().values().stream()
.map(it -> it.type.toString()).collect(Collectors.toList())));
context.getTargetMethodMetadata().getMethodArguments().forEach((name, spec) -> builder.addParameter(spec));
customizer.customize(context, builder);
return builder.build();
}
private void initParameters(Method method, RepositoryInformation repositoryInformation) {
ResolvableType repositoryInterface = ResolvableType.forClass(repositoryInformation.getRepositoryInterface());
if (method.getParameterCount() > 0) {
int index = 0;
for (Parameter parameter : method.getParameters()) {
ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(new MethodParameter(method, index),
repositoryInterface);
for (Parameter parameter : method.getParameters()) {
TypeName parameterType = TypeName.get(resolvableParameterType.resolve());
if (resolvableParameterType.hasGenerics()) {
parameterType = ParameterizedTypeName.get(resolvableParameterType.resolve(),
resolvableParameterType.resolveGenerics());
}
addParameter(parameter.getName(), parameterType);
index++;
}
MethodParameter methodParameter = MethodParameter.forParameter(parameter);
methodParameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(methodParameter, repositoryInterface);
TypeName parameterType = TypeName.get(resolvableParameterType.getType());
this.context.addParameter(ParameterSpec.builder(parameterType, methodParameter.getParameterName()).build());
}
}
private void initReturnType(Method method, RepositoryInformation repositoryInformation) {
ResolvableType returnType = ResolvableType.forMethodReturnType(method,
repositoryInformation.getRepositoryInterface());
TypeName returnTypeName = TypeName.get(returnType.resolve());
TypeName actualReturnTypeName = null;
if (returnType.hasGenerics()) {
Class<?>[] generics = returnType.resolveGenerics();
returnTypeName = ParameterizedTypeName.get(returnType.resolve(), generics);
if (generics.length == 1) {
actualReturnTypeName = TypeName.get(generics[0]);
}
}
setReturnType(returnTypeName, actualReturnTypeName);
/**
* Register a {@link RepositoryMethodContribution} for the repository interface that can contribute a query method
* implementation block.
*
* @param contribution
* @return
*/
public AotRepositoryMethodBuilder contribute(RepositoryMethodContribution contribution) {
this.contribution = contribution;
return this;
}
/**
* Register a query method customizer that is applied after a successful {@link RepositoryMethodContribution}.
*
* @param customizer
* @return
*/
public AotRepositoryMethodBuilder customize(RepositoryMethodCustomizer customizer) {
this.customizer = customizer;
return this;
}
/**
* Builds an AOT repository method if {@link RepositoryMethodContribution} can contribute a method.
*
* @return the {@link MethodSpec} or {@literal null}, if the method cannot be contributed.
*/
public MethodSpec buildMethod() {
CodeBlock methodBody = contribution.contribute(context);
MethodSpec.Builder builder = MethodSpec.methodBuilder(context.getMethod().getName()).addModifiers(Modifier.PUBLIC);
builder.returns(TypeName.get(context.getReturnType().getType()));
TypeVariable<Method>[] tvs = context.getMethod().getTypeParameters();
for (TypeVariable<Method> tv : tvs) {
builder.addTypeVariable(TypeVariableName.get(tv));
}
builder.addJavadoc("AOT generated implementation of {@link $T#$L($L)}.", context.getMethod().getDeclaringClass(),
context.getMethod().getName(), StringUtils.collectionToCommaDelimitedString(context.getTargetMethodMetadata()
.getMethodArguments().values().stream().map(it -> it.type.toString()).collect(Collectors.toList())));
context.getTargetMethodMetadata().getMethodArguments().forEach((name, spec) -> builder.addParameter(spec));
builder.addCode(methodBody);
customizer.customize(context, builder);
return builder.build();
}
/**
* AOT contribution from a {@link AotRepositoryMethodBuilder} used to contribute a repository query method body.
*/
public interface RepositoryMethodContribution {
CodeBlock contribute(AotQueryMethodGenerationContext context);
}
/**
* Customizer for a contributed AOT repository query method.
*/
public interface RepositoryMethodCustomizer {
void customize(AotRepositoryMethodGenerationContext context, MethodSpec.Builder builder);
void customize(AotQueryMethodGenerationContext context, MethodSpec.Builder builder);
}
}

View File

@@ -1,207 +0,0 @@
/*
* 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
*
* http://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.
*/
/*
* 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
*
* http://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.aot.generate;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map.Entry;
import java.util.Optional;
import javax.lang.model.element.Modifier;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.javapoet.FieldSpec;
import org.springframework.javapoet.ParameterSpec;
import org.springframework.javapoet.TypeName;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
/**
* @author Christoph Strobl
* @since 2025/01
*/
public class AotRepositoryMethodGenerationContext {
private final Method method;
private final RepositoryInformation repositoryInformation;
private final AotRepositoryImplementationMetadata targetTypeMetadata;
private final AotRepositoryMethodImplementationMetadata targetMethodMetadata;
private final CodeBlocks codeBlocks;
@Nullable PartTree partTree;
public AotRepositoryMethodGenerationContext(Method method, RepositoryInformation repositoryInformation,
AotRepositoryImplementationMetadata targetTypeMetadata) {
this.method = method;
this.repositoryInformation = repositoryInformation;
this.targetTypeMetadata = targetTypeMetadata;
this.targetMethodMetadata = new AotRepositoryMethodImplementationMetadata();
this.codeBlocks = new CodeBlocks(targetTypeMetadata);
try {
this.partTree = new PartTree(method.getName(), repositoryInformation.getDomainType());
} catch (Exception e) {
// not a part tree quer
}
}
public boolean hasField(String fieldName) {
return targetTypeMetadata.hasField(fieldName);
}
public void addField(String fieldName, TypeName type, Modifier... modifiers) {
targetTypeMetadata.addField(fieldName, type, modifiers);
}
public void addField(FieldSpec fieldSpec) {
targetTypeMetadata.addField(fieldSpec);
}
public String fieldNameOf(Class<?> type) {
return targetTypeMetadata.fieldNameOf(type);
}
public RepositoryInformation getRepositoryInformation() {
return repositoryInformation;
}
public Method getMethod() {
return method;
}
AotRepositoryImplementationMetadata getTargetTypeMetadata() {
return targetTypeMetadata;
}
@Nullable
public String getParameterNameOf(Class<?> type) {
return targetMethodMetadata.getParameterNameOf(type);
}
public String getParameterNameOfPosition(int position) {
ArrayList<Entry<String, ParameterSpec>> entries = new ArrayList<>(
targetMethodMetadata.getMethodArguments().entrySet());
if (position < entries.size()) {
return entries.get(position).getKey();
}
return null;
}
public void addParameter(ParameterSpec parameter) {
this.targetMethodMetadata.addParameter(parameter);
}
public boolean returnsVoid() {
return getMethod().getReturnType().equals(Void.TYPE);
}
public boolean returnsPage() {
return ClassUtils.isAssignable(Page.class, getMethod().getReturnType());
}
public boolean returnsSlice() {
return ClassUtils.isAssignable(Slice.class, getMethod().getReturnType());
}
public boolean returnsCollection() {
return ClassUtils.isAssignable(Collection.class, getMethod().getReturnType());
}
public boolean returnsSingleValue() {
return !returnsPage() && !returnsSlice() && !returnsCollection();
}
public boolean returnsOptionalValue() {
return ClassUtils.isAssignable(Optional.class, getMethod().getReturnType());
}
public boolean isCountMethod() {
return partTree != null ? partTree.isCountProjection() : method.getName().startsWith("count");
}
public boolean isExistsMethod() {
return partTree != null ? partTree.isExistsProjection() : method.getName().startsWith("exists");
}
public boolean isDeleteMethod() {
return partTree != null ? partTree.isDelete() : method.getName().startsWith("delete");
}
@Nullable
public TypeName getActualReturnType() {
return targetMethodMetadata.getActualReturnType();
}
@Nullable
public String getSortParameterName() {
return getParameterNameOf(Sort.class);
}
@Nullable
public String getPageableParameterName() {
return getParameterNameOf(Pageable.class);
}
@Nullable
public String getLimitParameterName() {
return getParameterNameOf(Limit.class);
}
@Nullable
public <T> T annotationValue(Class<? extends Annotation> annotation, String attribute) {
AnnotationAttributes values = AnnotatedElementUtils.getMergedAnnotationAttributes(getMethod(), annotation);
return values != null ? (T) values.get(attribute) : null;
}
@Nullable
public TypeName getReturnType() {
return targetMethodMetadata.getReturnType();
}
AotRepositoryMethodImplementationMetadata getTargetMethodMetadata() {
return targetMethodMetadata;
}
public CodeBlocks codeBlocks() {
return codeBlocks;
}
}

View File

@@ -15,25 +15,31 @@
*/
package org.springframework.data.repository.aot.generate;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.jspecify.annotations.Nullable;
import org.springframework.core.ResolvableType;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.javapoet.ParameterSpec;
import org.springframework.javapoet.TypeName;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
*/
class AotRepositoryMethodImplementationMetadata {
private final Map<String, ParameterSpec> methodArguments;
@Nullable private TypeName actualReturnType;
@Nullable private TypeName returnType;
private final Map<String, ParameterSpec> methodArguments = new LinkedHashMap<>();
private final ResolvableType actualReturnType;
private final ResolvableType returnType;
public AotRepositoryMethodImplementationMetadata() {
this.methodArguments = new LinkedHashMap<>();
public AotRepositoryMethodImplementationMetadata(RepositoryInformation repositoryInformation, Method method) {
this.returnType = repositoryInformation.getReturnType(method).toResolvableType();
this.actualReturnType = ResolvableType.forType(repositoryInformation.getReturnedDomainClass(method));
}
@Nullable
@@ -46,13 +52,11 @@ class AotRepositoryMethodImplementationMetadata {
return null;
}
@Nullable
public TypeName getReturnType() {
public ResolvableType getReturnType() {
return returnType;
}
@Nullable
public TypeName getActualReturnType() {
public ResolvableType getActualReturnType() {
return actualReturnType;
}
@@ -63,12 +67,4 @@ class AotRepositoryMethodImplementationMetadata {
Map<String, ParameterSpec> getMethodArguments() {
return methodArguments;
}
void setActualReturnType(@Nullable TypeName actualReturnType) {
this.actualReturnType = actualReturnType;
}
void setReturnType(@Nullable TypeName returnType) {
this.returnType = returnType;
}
}

View File

@@ -27,12 +27,21 @@ import org.springframework.util.StringUtils;
*/
public class CodeBlocks {
private final AotRepositoryImplementationMetadata metadata;
private final AotRepositoryFragmentMetadata metadata;
public CodeBlocks(AotRepositoryImplementationMetadata metadata) {
CodeBlocks(AotRepositoryFragmentMetadata metadata) {
this.metadata = metadata;
}
/**
* @param message the logging message.
* @param args optional args to apply to the message.
* @return a {@link CodeBlock} containing a debug level guarded logging statement.
*/
public CodeBlock logDebug(String message, Object... args) {
return log("debug", message, args);
}
/**
* @param level the log level eg. `debug`.
* @param message the message to print/
@@ -53,13 +62,4 @@ public class CodeBlocks {
return builder.build();
}
/**
* @param message the logging message.
* @param args optional args to apply to the message.
* @return a {@link CodeBlock} containing a debug level guarded logging statement.
*/
public CodeBlock logDebug(String message, Object... args) {
return log("debug", message, args);
}
}

View File

@@ -0,0 +1,98 @@
/*
* 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.aot.generate;
import java.util.function.Consumer;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.javapoet.MethodSpec;
/**
* Strategy for contributing AOT repository methods by looking introspecting query methods.
*
* @param <M> query method type.
* @author Mark Paluch
* @since 4.0
*/
public abstract class MethodContributor<M extends QueryMethod> {
private final M queryMethod;
private MethodContributor(M queryMethod) {
this.queryMethod = queryMethod;
}
/**
* Creates a new builder to build a {@code MethodContributor}.
*
* @param queryMethod the query method to be used.
* @return the new builder.
* @param <M> query method type.
*/
public static <M extends QueryMethod> QueryMethodContributorBuilder<M> forQueryMethod(M queryMethod) {
return builderConsumer -> new MethodContributor<>(queryMethod) {
@Override
public MethodSpec contribute(AotQueryMethodGenerationContext context) {
AotRepositoryMethodBuilder builder = new AotRepositoryMethodBuilder(context);
builderConsumer.accept(builder);
return builder.buildMethod();
}
};
}
public M getQueryMethod() {
return queryMethod;
}
/**
* Contribute the actual method specification to be added to the repository fragment.
*
* @param context generation context.
* @return
*/
public abstract MethodSpec contribute(AotQueryMethodGenerationContext context);
/**
* Builder for a query method contributor.
*
* @param <M> query method type.
*/
public interface QueryMethodContributorBuilder<M extends QueryMethod> {
/**
* Terminal method accepting a consumer that uses {@link AotRepositoryMethodBuilder} to build the method.
*
* @param contribution the method contribution that provides the method to be added to the repository.
* @return the method contributor to use.
*/
default MethodContributor<M> contribute(AotRepositoryMethodBuilder.RepositoryMethodContribution contribution) {
return using(builder -> builder.contribute(contribution));
}
/**
* Terminal method accepting a consumer that uses {@link AotRepositoryMethodBuilder} to build the method.
*
* @param builderConsumer consumer method being provided with the {@link AotRepositoryMethodBuilder} that provides
* the method to be added to the repository.
* @return the method contributor to use.
*/
MethodContributor<M> using(Consumer<AotRepositoryMethodBuilder> builderConsumer);
}
}

View File

@@ -15,19 +15,29 @@
*/
package org.springframework.data.repository.aot.generate;
import java.lang.reflect.Method;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.TypeReference;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.config.AotRepositoryContext;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.javapoet.JavaFile;
import org.springframework.javapoet.TypeName;
import org.springframework.javapoet.TypeSpec;
/**
* Contributor for AOT repository fragments.
*
* @author Christoph Strobl
* @author Mark Paluch
*/
public class RepositoryContributor {
@@ -36,18 +46,39 @@ public class RepositoryContributor {
private final AotRepositoryBuilder builder;
public RepositoryContributor(AotRepositoryContext repositoryContext) {
this.builder = AotRepositoryBuilder.forRepository(repositoryContext.getRepositoryInformation());
this.builder = AotRepositoryBuilder.forRepository(repositoryContext.getRepositoryInformation(),
createProjectionFactory());
}
protected ProjectionFactory createProjectionFactory() {
return new SpelAwareProxyProjectionFactory();
}
protected ProjectionFactory getProjectionFactory() {
return builder.getProjectionFactory();
}
protected RepositoryInformation getRepositoryInformation() {
return builder.getRepositoryInformation();
}
public String getContributedTypeName() {
return builder.getGenerationMetadata().getTargetTypeName().toString();
}
public java.util.Map<String, TypeName> requiredArgs() {
return builder.getAutowireFields();
}
public void contribute(GenerationContext generationContext) {
// TODO: do we need - generationContext.withName("spring-data");
builder.withFileCustomizer(this::customizeFile);
builder.withClassCustomizer(this::customizeClass);
builder.withConstructorCustomizer(this::customizeConstructor);
builder.withDerivedMethodFunction(this::contributeRepositoryMethod);
builder.withQueryMethodContributor(this::contributeQueryMethod);
JavaFile file = builder.javaFile();
JavaFile file = builder.build();
String typeName = "%s.%s".formatted(file.packageName, file.typeSpec.name);
if (logger.isTraceEnabled()) {
@@ -66,27 +97,27 @@ public class RepositoryContributor {
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
}
public String getContributedTypeName() {
return builder.getGenerationMetadata().getTargetTypeName().toString();
}
/**
* Customization hook for store implementations to customize class after building the entire class.
*/
protected void customizeClass(RepositoryInformation information, AotRepositoryFragmentMetadata metadata,
TypeSpec.Builder builder) {
public java.util.Map<String, TypeName> requiredArgs() {
return builder.getAutowireFields();
}
/**
* Customization Hook for Store implementations
* Customization hook for store implementations to customize the fragment constructor after building the constructor.
*/
protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) {
}
protected void customizeFile(RepositoryInformation information, AotRepositoryImplementationMetadata metadata,
TypeSpec.Builder builder) {
}
protected AotRepositoryMethodBuilder contributeRepositoryMethod(AotRepositoryMethodGenerationContext context) {
/**
* Customization hook for store implementations to contribute a query method.
*/
protected @Nullable MethodContributor<? extends QueryMethod> contributeQueryMethod(Method method,
RepositoryInformation repositoryInformation) {
return null;
}
}

View File

@@ -0,0 +1,5 @@
/**
* Ahead-of-Time (AOT) generation for Spring Data repositories.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.data.repository.aot.generate;

View File

@@ -0,0 +1,91 @@
/*
* 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 java.util.Map;
import java.util.function.Supplier;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.data.repository.aot.generate.RepositoryContributor;
import org.springframework.data.repository.core.support.RepositoryComposition;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.TypeName;
import org.springframework.util.StringUtils;
/**
* Delegate to decorate AOT {@code BeanDefinition} properties.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 4.0
*/
class AotRepositoryBeanDefinitionPropertiesDecorator {
private final Supplier<CodeBlock> inheritedProperties;
private final RepositoryContributor repositoryContributor;
public AotRepositoryBeanDefinitionPropertiesDecorator(Supplier<CodeBlock> inheritedProperties,
RepositoryContributor repositoryContributor) {
this.inheritedProperties = inheritedProperties;
this.repositoryContributor = repositoryContributor;
}
/**
* Generate a decorated code block for bean properties.
*
* @return the decorated code block.
*/
public CodeBlock decorate() {
CodeBlock.Builder builder = CodeBlock.builder();
// bring in properties as usual
builder.add(inheritedProperties.get());
builder.add("beanDefinition.getPropertyValues().addPropertyValue(\"repositoryFragments\", new $T() {\n",
RepositoryFactoryBeanSupport.RepositoryFragmentsFunction.class);
builder.indent();
builder.add("public $T getRepositoryFragments($T beanFactory, $T context) {\n",
RepositoryComposition.RepositoryFragments.class, BeanFactory.class,
RepositoryFactoryBeanSupport.FragmentCreationContext.class);
builder.indent();
for (Map.Entry<String, TypeName> entry : repositoryContributor.requiredArgs().entrySet()) {
if (entry.getValue().equals(TypeName.get(RepositoryFactoryBeanSupport.FragmentCreationContext.class))) {
if (!entry.getKey().equals("context")) {
builder.addStatement("$T $L = context", entry.getValue(), entry.getKey(), entry.getValue());
}
} else {
builder.addStatement("$T $L = beanFactory.getBean($T.class)", entry.getValue(), entry.getKey(),
entry.getValue());
}
}
builder.addStatement("return RepositoryComposition.RepositoryFragments.just(new $L($L))",
repositoryContributor.getContributedTypeName(),
StringUtils.collectionToDelimitedString(repositoryContributor.requiredArgs().keySet(), ", "));
builder.unindent();
builder.add("}\n");
builder.unindent();
builder.add("});\n");
return builder.build();
}
}

View File

@@ -140,7 +140,7 @@ class DefaultAotRepositoryContext implements AotRepositoryContext {
if (repositoryInformation != null) {
types.addAll(TypeCollector.inspect(repositoryInformation.getDomainType()).list());
repositoryInformation.getQueryMethods()
repositoryInformation.getQueryMethods().stream()
.flatMap(it -> TypeUtils.resolveTypesInSignature(repositoryInformation.getRepositoryInterface(), it).stream())
.flatMap(it -> TypeCollector.inspect(it).list().stream()).forEach(types::add);
}

View File

@@ -22,12 +22,10 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
@@ -37,7 +35,6 @@ import org.springframework.aop.framework.Advised;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.TypeReference;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.aot.BeanRegistrationCode;
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments;
@@ -53,33 +50,35 @@ import org.springframework.data.projection.TargetAware;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.aot.generate.RepositoryContributor;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.util.Predicates;
import org.springframework.data.util.QTypeContributor;
import org.springframework.data.util.TypeContributor;
import org.springframework.data.util.TypeUtils;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
import org.springframework.javapoet.TypeName;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* {@link BeanRegistrationAotContribution} used to contribute repository registrations.
*
* @author John Blum
* @author Christoph Strobl
* @author Mark Paluch
* @since 3.0
*/
// TODO: Consider moving to data.repository.aot
public class RepositoryRegistrationAotContribution implements BeanRegistrationAotContribution {
private static final String KOTLIN_COROUTINE_REPOSITORY_TYPE_NAME = "org.springframework.data.repository.kotlin.CoroutineCrudRepository";
private @Nullable RepositoryContributor repositoryContributor;
private @Nullable AotRepositoryContext repositoryContext;
private @Nullable BiFunction<AotRepositoryContext, GenerationContext, RepositoryContributor> moduleContribution;
private @Nullable BiFunction<AotRepositoryContext, GenerationContext, @Nullable RepositoryContributor> moduleContribution;
private final RepositoryRegistrationAotProcessor repositoryRegistrationAotProcessor;
@@ -119,8 +118,8 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
return getRepositoryRegistrationAotProcessor().getBeanFactory();
}
protected Optional<BiFunction<AotRepositoryContext, GenerationContext, RepositoryContributor>> getModuleContribution() {
return Optional.ofNullable(this.moduleContribution);
protected @Nullable BiFunction<AotRepositoryContext, GenerationContext, @Nullable RepositoryContributor> getModuleContribution() {
return this.moduleContribution;
}
protected AotRepositoryContext getRepositoryContext() {
@@ -167,52 +166,6 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
return this;
}
DefaultAotRepositoryContext buildAotRepositoryContext(RegisteredBean bean,
RepositoryConfiguration<?> repositoryMetadata) {
RepositoryInformation repositoryInformation = resolveRepositoryInformation(repositoryMetadata);
DefaultAotRepositoryContext repositoryContext = new DefaultAotRepositoryContext(
AotContext.from(this.getBeanFactory()));
repositoryContext.setBeanName(bean.getBeanName());
repositoryContext.setBasePackages(repositoryMetadata.getBasePackages().toSet());
repositoryContext.setIdentifyingAnnotations(resolveIdentifyingAnnotations());
repositoryContext.setRepositoryInformation(repositoryInformation);
return repositoryContext;
}
private Set<Class<? extends Annotation>> resolveIdentifyingAnnotations() {
Set<Class<? extends Annotation>> identifyingAnnotations = Collections.emptySet();
try {
// TODO: Getting all beans of type RepositoryConfigurationExtensionSupport will have the effect that
// if the user is currently operating in multi-store mode, then all identifying annotations from
// all stores will be included in the resulting Set.
// When using AOT, is multi-store mode allowed? I don't see why not, but does this work correctly
// with AOT, ATM?
Map<String, RepositoryConfigurationExtensionSupport> repositoryConfigurationExtensionBeans = getBeanFactory()
.getBeansOfType(RepositoryConfigurationExtensionSupport.class);
// repositoryConfigurationExtensionBeans.values().stream()
// .map(RepositoryConfigurationExtensionSupport::getIdentifyingAnnotations)
// .flatMap(Collection::stream)
// .collect(Collectors.toCollection(() -> identifyingAnnotations));
} catch (Throwable ignore) {
// Possible BeansException because no bean exists of type RepositoryConfigurationExtension,
// which included non-Singletons and occurred during eager initialization.
}
return identifyingAnnotations;
}
private RepositoryInformation resolveRepositoryInformation(RepositoryConfiguration<?> repositoryMetadata) {
return RepositoryBeanDefinitionReader.readRepositoryInformation(repositoryMetadata, getBeanFactory());
}
/**
* {@link BiConsumer Callback} for data module specific contributions.
*
@@ -232,8 +185,12 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
"RepositoryContext cannot be null. Make sure to initialize this class with forBean(…).");
contributeRepositoryInfo(this.repositoryContext, generationContext);
if (getModuleContribution().isPresent() && this.repositoryContributor == null) {
this.repositoryContributor = getModuleContribution().get().apply(getRepositoryContext(), generationContext);
var moduleContribution = getModuleContribution();
if (moduleContribution != null && this.repositoryContributor == null) {
this.repositoryContributor = moduleContribution.apply(getRepositoryContext(), generationContext);
if (this.repositoryContributor != null) {
this.repositoryContributor.contribute(generationContext);
}
@@ -251,35 +208,18 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition,
Predicate<String> attributeFilter) {
if (repositoryContributor == null) { // no aot implementation -> go on as as
if (repositoryContributor == null) { // no aot implementation -> go on as
return super.generateSetBeanDefinitionPropertiesCode(generationContext, beanRegistrationCode, beanDefinition,
attributeFilter);
}
Builder builder = CodeBlock.builder();
// bring in properties as usual
builder.add(super.generateSetBeanDefinitionPropertiesCode(generationContext, beanRegistrationCode,
beanDefinition, attributeFilter));
AotRepositoryBeanDefinitionPropertiesDecorator decorator = new AotRepositoryBeanDefinitionPropertiesDecorator(
() -> super.generateSetBeanDefinitionPropertiesCode(generationContext, beanRegistrationCode, beanDefinition,
attributeFilter),
repositoryContributor);
builder.add(
"beanDefinition.getPropertyValues().addPropertyValue(\"aotImplementationFunction\", new $T<$T, $T>() {\n",
Function.class, BeanFactory.class, Object.class);
builder.indent();
builder.add("public $T apply(BeanFactory beanFactory) {\n", Object.class);
builder.indent();
for (Entry<String, TypeName> entry : repositoryContributor.requiredArgs().entrySet()) {
builder.addStatement("$T $L = beanFactory.getBean($T.class)", entry.getValue(), entry.getKey(),
entry.getValue());
}
builder.addStatement("return new $L($L)", repositoryContributor.getContributedTypeName(),
StringUtils.collectionToDelimitedString(repositoryContributor.requiredArgs().keySet(), ", "));
builder.unindent();
builder.add("}\n");
builder.unindent();
builder.add("});\n");
return builder.build();
return decorator.decorate();
}
};
}
@@ -290,7 +230,6 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
logTrace("Contributing repository information for [%s]", repositoryInformation.getRepositoryInterface());
// TODO: is this the way?
contribution.getRuntimeHints().reflection()
.registerType(repositoryInformation.getRepositoryInterface(),
hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS))
@@ -305,6 +244,7 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
for (RepositoryFragment<?> fragment : getRepositoryInformation().getFragments()) {
Class<?> repositoryFragmentType = fragment.getSignatureContributor();
Optional<?> implementation = fragment.getImplementation();
contribution.getRuntimeHints().reflection().registerType(repositoryFragmentType, hint -> {
@@ -314,6 +254,17 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
}
});
implementation.ifPresent(impl -> {
contribution.getRuntimeHints().reflection().registerType(impl.getClass(), hint -> {
hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS);
if (!impl.getClass().isInterface()) {
hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
}
});
});
}
// Repository Proxy
@@ -355,7 +306,7 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
}
// Repository query methods
repositoryInformation.getQueryMethods().map(repositoryInformation::getReturnedDomainClass)
repositoryInformation.getQueryMethods().stream().map(repositoryInformation::getReturnedDomainClass)
.filter(Class::isInterface).forEach(type -> {
if (EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy().test(type,
repositoryInformation.getDomainType())) {
@@ -415,4 +366,52 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
public Predicate<Class<?>> typeFilter() { // like only document ones. // TODO: As in MongoDB?
return Predicates.isTrue();
}
@SuppressWarnings("rawtypes")
private DefaultAotRepositoryContext buildAotRepositoryContext(RegisteredBean bean,
RepositoryConfiguration<?> repositoryMetadata) {
DefaultAotRepositoryContext repositoryContext = new DefaultAotRepositoryContext(
AotContext.from(this.getBeanFactory()));
RepositoryFactoryBeanSupport rfbs = bean.getBeanFactory().getBean("&" + bean.getBeanName(),
RepositoryFactoryBeanSupport.class);
repositoryContext.setBeanName(bean.getBeanName());
repositoryContext.setBasePackages(repositoryMetadata.getBasePackages().toSet());
repositoryContext.setIdentifyingAnnotations(resolveIdentifyingAnnotations());
repositoryContext.setRepositoryInformation(rfbs.getRepositoryInformation());
return repositoryContext;
}
private Set<Class<? extends Annotation>> resolveIdentifyingAnnotations() {
Set<Class<? extends Annotation>> identifyingAnnotations = Collections.emptySet();
try {
// TODO: Getting all beans of type RepositoryConfigurationExtensionSupport will have the effect that
// if the user is currently operating in multi-store mode, then all identifying annotations from
// all stores will be included in the resulting Set.
// When using AOT, is multi-store mode allowed? I don't see why not, but does this work correctly
// with AOT, ATM?
Map<String, RepositoryConfigurationExtensionSupport> repositoryConfigurationExtensionBeans = getBeanFactory()
.getBeansOfType(RepositoryConfigurationExtensionSupport.class);
// repositoryConfigurationExtensionBeans.values().stream()
// .map(RepositoryConfigurationExtensionSupport::getIdentifyingAnnotations)
// .flatMap(Collection::stream)
// .collect(Collectors.toCollection(() -> identifyingAnnotations));
} catch (Throwable ignore) {
// Possible BeansException because no bean exists of type RepositoryConfigurationExtension,
// which included non-Singletons and occurred during eager initialization.
}
return identifyingAnnotations;
}
private RepositoryInformation resolveRepositoryInformation(RepositoryConfiguration<?> repositoryMetadata) {
return RepositoryBeanDefinitionReader.readRepositoryInformation(repositoryMetadata, getBeanFactory());
}
}

View File

@@ -16,8 +16,7 @@
package org.springframework.data.repository.core;
import java.lang.reflect.Method;
import org.springframework.data.util.Streamable;
import java.util.List;
/**
* Additional repository specific information
@@ -56,7 +55,7 @@ public interface RepositoryInformation extends RepositoryMetadata {
*
* @return
*/
Streamable<Method> getQueryMethods();
List<Method> getQueryMethods();
/**
* Returns the base class to be used to create the proxy backing instance.

View File

@@ -15,19 +15,22 @@
*/
package org.springframework.data.repository.core;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.data.annotation.QueryAnnotation;
import org.springframework.data.repository.Repository;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
@@ -57,7 +60,7 @@ public abstract class RepositoryInformationSupport implements RepositoryInformat
}
@Override
public Streamable<Method> getQueryMethods() {
public List<Method> getQueryMethods() {
return queryMethods.get().methods;
}
@@ -113,7 +116,7 @@ public abstract class RepositoryInformationSupport implements RepositoryInformat
@Override
public boolean isQueryMethod(Method method) {
return getQueryMethods().stream().anyMatch(it -> it.equals(method));
return queryMethods.get().isQueryMethod(method);
}
@Override
@@ -144,7 +147,14 @@ public abstract class RepositoryInformationSupport implements RepositoryInformat
* @return
*/
protected boolean isQueryAnnotationPresentOn(Method method) {
return AnnotationUtils.findAnnotation(method, QueryAnnotation.class) != null;
Annotation[] annotations = method.getAnnotations();
if (annotations.length == 0) {
return false;
}
return MergedAnnotations.from(annotations).isPresent(QueryAnnotation.class);
}
/**
@@ -154,9 +164,24 @@ public abstract class RepositoryInformationSupport implements RepositoryInformat
* @return
*/
protected boolean isQueryMethodCandidate(Method method) {
return !method.isBridge() && !method.isDefault() //
&& !Modifier.isStatic(method.getModifiers()) //
&& (isQueryAnnotationPresentOn(method) || !isCustomMethod(method) && !isBaseClassMethod(method));
if (method.isBridge() || method.isDefault() || Modifier.isStatic(method.getModifiers())) {
return false;
}
if (isCustomMethod(method)) {
return false;
}
if (isQueryAnnotationPresentOn(method)) {
return true;
}
if (isBaseClassMethod(method)) {
return false;
}
return true;
}
private RepositoryMetadata getMetadata() {
@@ -166,21 +191,27 @@ public abstract class RepositoryInformationSupport implements RepositoryInformat
private DefaultQueryMethods calculateQueryMethods() {
Class<?> repositoryInterface = getRepositoryInterface();
Method[] methods = repositoryInterface.getMethods();
List<Method> queryMethods = new ArrayList<>(methods.length);
for (Method method : methods) {
return new DefaultQueryMethods(Streamable.of(Arrays.stream(repositoryInterface.getMethods())
.map(it -> ClassUtils.getMostSpecificMethod(it, repositoryInterface)) //
.filter(this::isQueryMethodCandidate) //
.toList()), calculateHasCustomMethod(repositoryInterface));
Method msm = ClassUtils.getMostSpecificMethod(method, repositoryInterface);
if (isQueryMethodCandidate(method)) {
queryMethods.add(msm);
}
}
return new DefaultQueryMethods(queryMethods, calculateHasCustomMethod(repositoryInterface, methods));
}
private boolean calculateHasCustomMethod(Class<?> repositoryInterface) {
private boolean calculateHasCustomMethod(Class<?> repositoryInterface, Method[] methods) {
// No detection required if no typing interface was configured
if (isGenericRepositoryInterface(repositoryInterface)) {
return false;
}
for (Method method : repositoryInterface.getMethods()) {
for (Method method : methods) {
if (isCustomMethod(method) && !isBaseClassMethod(method)) {
return true;
}
@@ -217,14 +248,20 @@ public abstract class RepositoryInformationSupport implements RepositoryInformat
*/
private static class DefaultQueryMethods {
private final Streamable<Method> methods;
private final List<Method> methods;
private final Set<Method> methodSet;
private final boolean hasCustomMethod, hasQueryMethod;
DefaultQueryMethods(Streamable<Method> methods, boolean hasCustomMethod) {
DefaultQueryMethods(List<Method> methods, boolean hasCustomMethod) {
this.methods = methods;
this.methods = Collections.unmodifiableList(methods);
this.methodSet = new HashSet<>(methods);
this.hasCustomMethod = hasCustomMethod;
this.hasQueryMethod = !methods.isEmpty();
}
public boolean isQueryMethod(Method method) {
return methodSet.contains(method);
}
}
}

View File

@@ -16,7 +16,6 @@
package org.springframework.data.repository.core.support;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -28,7 +27,6 @@ import org.springframework.data.repository.core.RepositoryInformationSupport;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
@@ -105,7 +103,7 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen
return baseComposition.getMethod(method) != null;
}
@Override
protected boolean isQueryMethodCandidate(Method method) {
// FIXME - that should be simplified
@@ -114,14 +112,18 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen
return queryMethodCandidate;
}
return queryMethodCandidate && !getFragments().stream().anyMatch(fragment -> {
if(fragment.getImplementation().isPresent()) {
if(ClassUtils.hasMethod(fragment.getImplementation().get().getClass(), method.getName(), method.getParameterTypes())) {
return true;
}
}
if (!queryMethodCandidate) {
return false;
});
}
for (RepositoryFragment<?> fragment : getFragments()) {
if (fragment.getImplementation().isPresent() && fragment.hasMethod(method)) {
return false;
}
}
return true;
}
@Override

View File

@@ -123,19 +123,26 @@ interface MethodLookups {
Assert.notNull(repositoryMetadata, "Repository metadata must not be null");
this.entityType = repositoryMetadata.getDomainTypeInformation().toTypeDescriptor().getResolvableType();
this.idType = repositoryMetadata.getIdTypeInformation().toTypeDescriptor().getResolvableType();
this.entityType = repositoryMetadata.getDomainTypeInformation().toResolvableType();
this.idType = repositoryMetadata.getIdTypeInformation().toResolvableType();
this.repositoryInterface = repositoryMetadata.getRepositoryInterface();
}
@Override
public List<MethodPredicate> getLookups() {
MethodPredicate detailedComparison = (MethodPredicate) (invoked, candidate) -> Optional.of(candidate)
.filter(baseClassMethod -> baseClassMethod.getName().equals(invoked.getName()))// Right name
.filter(baseClassMethod -> baseClassMethod.getParameterCount() == invoked.getParameterCount())
.filter(baseClassMethod -> parametersMatch(invoked.getMethod(), baseClassMethod))// All parameters match
.isPresent();
MethodPredicate detailedComparison = (MethodPredicate) (invoked, candidate) -> {
if (candidate.getParameterCount() != invoked.getParameterCount()) {
return false;
}
if (!candidate.getName().equals(invoked.getName())) {
return false;
}
return parametersMatch(invoked.getMethod(), candidate);
};
return Collections.singletonList(detailedComparison);
}

View File

@@ -20,7 +20,6 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.aopalliance.intercept.MethodInterceptor;
@@ -69,7 +68,7 @@ class QueryExecutorMethodInterceptor implements MethodInterceptor {
* execution of repository interface methods.
*/
public QueryExecutorMethodInterceptor(RepositoryInformation repositoryInformation,
ProjectionFactory projectionFactory, Optional<QueryLookupStrategy> queryLookupStrategy, NamedQueries namedQueries,
ProjectionFactory projectionFactory, @Nullable QueryLookupStrategy queryLookupStrategy, NamedQueries namedQueries,
List<QueryCreationListener<?>> queryPostProcessors,
List<RepositoryMethodInvocationListener> methodInvocationListeners) {
@@ -81,22 +80,21 @@ class QueryExecutorMethodInterceptor implements MethodInterceptor {
this.resultHandler = new QueryExecutionResultHandler(RepositoryFactorySupport.CONVERSION_SERVICE);
if (!queryLookupStrategy.isPresent() && repositoryInformation.hasQueryMethods()) {
if (queryLookupStrategy == null && repositoryInformation.hasQueryMethods()) {
throw new IllegalStateException(
"You have defined query methods in the repository" + " but do not have any query lookup strategy defined."
+ " The infrastructure apparently does not support query methods");
}
this.queries = queryLookupStrategy //
.map(it -> mapMethodsToQuery(repositoryInformation, it, projectionFactory)) //
.orElse(Collections.emptyMap());
this.queries = queryLookupStrategy != null
? mapMethodsToQuery(repositoryInformation, queryLookupStrategy, projectionFactory)
: Collections.emptyMap();
}
private Map<Method, RepositoryQuery> mapMethodsToQuery(RepositoryInformation repositoryInformation,
QueryLookupStrategy lookupStrategy, ProjectionFactory projectionFactory) {
List<Method> queryMethods = repositoryInformation.getQueryMethods().toList();
List<Method> queryMethods = repositoryInformation.getQueryMethods();
Map<Method, RepositoryQuery> result = new HashMap<>(queryMethods.size(), 1.0f);
for (Method method : queryMethods) {

View File

@@ -25,7 +25,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -106,6 +105,7 @@ public class RepositoryComposition {
private final Map<Method, Method> methodCache = new ConcurrentHashMap<>();
private final RepositoryFragments fragments;
private final MethodLookup methodLookup;
private final List<MethodLookup.MethodPredicate> lookups;
private final BiFunction<Method, Object[], Object[]> argumentConverter;
private final @Nullable RepositoryMetadata metadata;
@@ -115,6 +115,7 @@ public class RepositoryComposition {
this.metadata = metadata;
this.fragments = fragments;
this.methodLookup = methodLookup;
this.lookups = methodLookup.getLookups();
this.argumentConverter = argumentConverter;
}
@@ -308,7 +309,7 @@ public class RepositoryComposition {
Method getMethod(Method method) {
return methodCache.computeIfAbsent(method,
key -> RepositoryFragments.findMethod(InvokedMethod.of(key), methodLookup, fragments::methods));
key -> RepositoryFragments.findMethod(InvokedMethod.of(key), lookups, fragments));
}
/**
@@ -371,7 +372,6 @@ public class RepositoryComposition {
private final List<RepositoryFragment<?>> fragments;
private RepositoryFragments(List<RepositoryFragment<?>> fragments) {
this.fragments = fragments;
}
@@ -528,17 +528,16 @@ public class RepositoryComposition {
.orElseThrow(() -> new IllegalArgumentException(String.format("No fragment found for method %s", key)));
}
private static @Nullable Method findMethod(InvokedMethod invokedMethod, MethodLookup lookup,
Supplier<Stream<Method>> methodStreamSupplier) {
private static @Nullable Method findMethod(InvokedMethod invokedMethod, List<MethodLookup.MethodPredicate> lookups,
RepositoryFragments fragments) {
for (MethodLookup.MethodPredicate methodPredicate : lookup.getLookups()) {
Optional<Method> resolvedMethod = methodStreamSupplier.get()
.filter(it -> methodPredicate.test(invokedMethod, it)) //
.findFirst();
if (resolvedMethod.isPresent()) {
return resolvedMethod.get();
for (RepositoryFragment<?> fragment : fragments) {
for (Method candidate : fragment.findMethods(invokedMethod.getName())) {
for (MethodLookup.MethodPredicate methodPredicate : lookups) {
if (methodPredicate.test(invokedMethod, candidate)) {
return candidate;
}
}
}
}

View File

@@ -20,7 +20,6 @@ import java.util.List;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.function.Function;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
@@ -32,9 +31,11 @@ import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.aot.AbstractAotProcessor;
import org.springframework.core.env.Environment;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.NamedQueries;
@@ -45,6 +46,7 @@ import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.util.Lazy;
import org.springframework.util.Assert;
@@ -77,18 +79,18 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
private @Nullable Key queryLookupStrategyKey;
private @Nullable Class<?> repositoryBaseClass;
private @Nullable Object customImplementation;
private RepositoryFragments repositoryFragments = RepositoryFragments.empty();
private final List<RepositoryFragmentsFunction> fragments = new ArrayList<>();
private NamedQueries namedQueries = PropertiesBasedNamedQueries.EMPTY;
private @Nullable MappingContext<?, ?> mappingContext;
private @Nullable ClassLoader classLoader;
private @Nullable ApplicationEventPublisher publisher;
private @Nullable BeanFactory beanFactory;
private @Nullable Environment environment;
private boolean lazyInit = false;
private boolean lazyInit = Boolean.getBoolean(AbstractAotProcessor.AOT_PROCESSING); // use lazy-init in AOT processing
private @Nullable EvaluationContextProvider evaluationContextProvider;
private final List<RepositoryFactoryCustomizer> repositoryFactoryCustomizers = new ArrayList<>();
private @Nullable Function<BeanFactory, Object> aotImplementationFunction;
private RepositoryFragments cachedFragments = RepositoryFragments.empty();
private @Nullable Lazy<T> repository;
private @Nullable RepositoryMetadata repositoryMetadata;
@@ -146,12 +148,24 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
}
/**
* Setter to inject repository fragments.
* Setter to inject repository fragments. This method is additive and will add another {@link RepositoryFragments} to
* the already existing list of {@link RepositoryFragmentsFunction}.
*
* @param repositoryFragments
*/
public void setRepositoryFragments(RepositoryFragments repositoryFragments) {
this.repositoryFragments = repositoryFragments;
setRepositoryFragments(RepositoryFragmentsFunction.just(repositoryFragments));
}
/**
* Setter to inject repository fragments. This method is additive and will add another {@link RepositoryFragments} to
* the already existing list of {@link RepositoryFragmentsFunction}.
*
* @param fragmentsFunction
* @since 4.0
*/
public void setRepositoryFragments(RepositoryFragmentsFunction fragmentsFunction) {
this.fragments.add(fragmentsFunction);
}
/**
@@ -242,15 +256,6 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
this.publisher = publisher;
}
public void setAotImplementationFunction(@Nullable Function<BeanFactory, Object> aotImplementationFunction) {
this.aotImplementationFunction = aotImplementationFunction;
}
@Nullable
protected Function<BeanFactory, Object> getAotImplementationFunction() {
return aotImplementationFunction;
}
@Override
@SuppressWarnings("unchecked")
public EntityInformation<S, ID> getEntityInformation() {
@@ -260,11 +265,7 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
@Override
public RepositoryInformation getRepositoryInformation() {
RepositoryFragments fragments = customImplementation != null ? RepositoryFragments.just(customImplementation)
: RepositoryFragments.empty();
return getRequiredFactory().getRepositoryInformation(getRequiredRepositoryMetadata(), fragments);
return getRequiredFactory().getRepositoryInformation(getRequiredRepositoryMetadata(), cachedFragments);
}
@Override
@@ -310,7 +311,6 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
@Override
public void afterPropertiesSet() {
this.factory = createRepositoryFactory();
this.factory.setExposeMetadata(exposeMetadata);
this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey);
@@ -331,23 +331,18 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
this.factory.setEnvironment(this.environment);
}
if(this.aotImplementationFunction != null) {
this.factory.setAotImplementation(aotImplementationFunction.apply(beanFactory));
}
if (repositoryBaseClass != null) {
this.factory.setRepositoryBaseClass(repositoryBaseClass);
}
this.repositoryFactoryCustomizers.forEach(customizer -> customizer.customize(this.factory));
RepositoryFragments customImplementationFragment = customImplementation != null ? //
RepositoryFragments.just(customImplementation) : RepositoryFragments.empty();
RepositoryMetadata metadata = this.factory.getRepositoryMetadata(repositoryInterface);
RepositoryFragments repositoryFragments = getRepositoryFragments(metadata);
RepositoryFragments repositoryFragmentsToUse = repositoryFragments.append(customImplementationFragment);
this.repositoryMetadata = this.factory.getRepositoryMetadata(repositoryInterface);
this.repository = Lazy.of(() -> getRequiredFactory().getRepository(repositoryInterface, repositoryFragmentsToUse));
this.cachedFragments = repositoryFragments;
this.repositoryMetadata = metadata;
this.repository = Lazy.of(() -> getRequiredFactory().getRepository(repositoryInterface, repositoryFragments));
// Make sure the aggregate root type is present in the MappingContext (e.g. for auditing)
@@ -367,4 +362,104 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
*/
protected abstract RepositoryFactorySupport createRepositoryFactory();
private RepositoryFragments getRepositoryFragments(RepositoryMetadata repositoryMetadata) {
RepositoryFactorySupport factory = getRequiredFactory();
ValueExpressionDelegate valueExpressionDelegate = factory.getValueExpressionDelegate();
List<RepositoryFragmentsFunction> functions = new ArrayList<>(this.fragments);
if (customImplementation != null) {
functions.add(0, RepositoryFragmentsFunction.just(RepositoryFragments.just(customImplementation)));
}
FragmentCreationContext creationContext = new DefaultFragmentCreationContext(repositoryMetadata,
valueExpressionDelegate, factory.getProjectionFactory());
RepositoryFragments fragments = RepositoryFragments.empty();
for (RepositoryFragmentsFunction function : functions) {
fragments = fragments.append(function.getRepositoryFragments(this.beanFactory,
creationContext));
}
return fragments;
}
/**
* Functional interface to obtain {@link RepositoryFragments} for a given {@link BeanFactory} (can be
* {@literal null}), {@link EntityInformation} and {@link ValueExpressionDelegate}.
* <p>
* This interface is used within the Framework and should not be used in application code.
*
* @since 4.0
*/
public interface RepositoryFragmentsFunction {
/**
* Return {@link RepositoryFragments} for a given {@link BeanFactory} (can be {@literal null}),
* {@link EntityInformation} and {@link ValueExpressionDelegate}.
*
* @param beanFactory can be {@literal null}.
* @param context the creation context.
* @return the repository fragments to use.
*/
RepositoryFragments getRepositoryFragments(@Nullable BeanFactory beanFactory,
FragmentCreationContext context);
/**
* Factory method to create {@link RepositoryFragmentsFunction} for a resolved {@link RepositoryFragments} object.
*
* @param fragments the fragments to use.
* @return a supplier {@link RepositoryFragmentsFunction} returning just {@code fragments}.
*/
static RepositoryFragmentsFunction just(RepositoryFragments fragments) {
return (bf, context) -> fragments;
}
}
/**
* Creation context for a Repository Fragment.
*
* @since 4.0
*/
public interface FragmentCreationContext {
/**
* @return the repository metadata in use.
*/
RepositoryMetadata getRepositoryMetadata();
/**
* @return delegate for Value Expression parsing and evaluation.
*/
ValueExpressionDelegate getValueExpressionDelegate();
/**
* @return the projection factory to use.
*/
ProjectionFactory getProjectionFactory();
}
private record DefaultFragmentCreationContext(RepositoryMetadata repositoryMetadata,
ValueExpressionDelegate valueExpressionDelegate,
ProjectionFactory projectionFactory) implements FragmentCreationContext {
@Override
public RepositoryMetadata getRepositoryMetadata() {
return repositoryMetadata;
}
@Override
public ValueExpressionDelegate getValueExpressionDelegate() {
return valueExpressionDelegate();
}
@Override
public ProjectionFactory getProjectionFactory() {
return projectionFactory();
}
}
}

View File

@@ -122,7 +122,6 @@ public abstract class RepositoryFactorySupport
private @Nullable BeanFactory beanFactory;
private @Nullable Environment environment;
private Lazy<ProjectionFactory> projectionFactory;
private @Nullable Object aotImplementation;
private final QueryCollectingQueryCreationListener collectingListener = new QueryCollectingQueryCreationListener();
@@ -268,10 +267,6 @@ public abstract class RepositoryFactorySupport
this.postProcessors.add(processor);
}
public void setAotImplementation(@Nullable Object aotImplementation) {
this.aotImplementation = aotImplementation;
}
/**
* Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add repository-specific extensions.
*
@@ -417,9 +412,9 @@ public abstract class RepositoryFactorySupport
}
Optional<QueryLookupStrategy> queryLookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey,
new ValueExpressionDelegate(
new QueryMethodValueEvaluationContextAccessor(getEnvironment(), evaluationContextProvider), VALUE_PARSER));
result.addAdvice(new QueryExecutorMethodInterceptor(information, getProjectionFactory(), queryLookupStrategy,
getValueExpressionDelegate());
result.addAdvice(new QueryExecutorMethodInterceptor(information, getProjectionFactory(),
queryLookupStrategy.orElse(null),
namedQueries, queryPostProcessors, methodInvocationListeners));
result.addAdvice(
@@ -437,6 +432,11 @@ public abstract class RepositoryFactorySupport
return repository;
}
ValueExpressionDelegate getValueExpressionDelegate() {
return new ValueExpressionDelegate(
new QueryMethodValueEvaluationContextAccessor(getEnvironment(), evaluationContextProvider), VALUE_PARSER);
}
/**
* Returns the {@link ProjectionFactory} to be used with the repository instances created.
*
@@ -504,10 +504,6 @@ public abstract class RepositoryFactorySupport
RepositoryComposition composition = RepositoryComposition.fromMetadata(metadata);
RepositoryFragments repositoryAspects = getRepositoryFragments(metadata);
if(aotImplementation != null) {
repositoryAspects = RepositoryFragments.just(aotImplementation).append(repositoryAspects);
}
composition = composition.append(fragments).append(repositoryAspects);
Class<?> baseClass = repositoryBaseClass != null ? repositoryBaseClass : getRepositoryBaseClass(metadata);

View File

@@ -16,7 +16,9 @@
package org.springframework.data.repository.core.support;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
@@ -108,6 +110,15 @@ public interface RepositoryFragment<T> {
return Arrays.stream(getSignatureContributor().getMethods());
}
/**
* Find methods that match the given name. The method name must be exact.
*
* @param name the method name.
* @return list of candidate methods.
* @since 4.0
*/
List<Method> findMethods(String name);
/**
* @return the class/interface providing signatures for this {@link RepositoryFragment}.
*/
@@ -122,12 +133,65 @@ public interface RepositoryFragment<T> {
*/
RepositoryFragment<T> withImplementation(T implementation);
private static List<Method> findMethods(String name, Method[] candidates) {
Method firstMatch = null;
for (Method method : candidates) {
if (method.getName().equals(name)) {
if (firstMatch != null) {
firstMatch = null;
break;
}
firstMatch = method;
}
}
if (firstMatch != null) {
return List.of(firstMatch);
}
List<Method> results = new ArrayList<>(candidates.length);
for (Method method : candidates) {
if (method.getName().equals(name)) {
results.add(method);
}
}
return results;
}
private static boolean hasMethod(Method method, Method[] candidates) {
for (Method candidate : candidates) {
if (!candidate.getName().equals(method.getName())) {
continue;
}
if (candidate.getParameterCount() != method.getParameterCount()) {
continue;
}
if (Arrays.equals(candidate.getParameterTypes(), method.getParameterTypes())) {
return true;
}
}
return false;
}
class StructuralRepositoryFragment<T> implements RepositoryFragment<T> {
private final Class<T> interfaceOrImplementation;
private final Method[] methods;
public StructuralRepositoryFragment(Class<T> interfaceOrImplementation) {
this.interfaceOrImplementation = interfaceOrImplementation;
this.methods = getSignatureContributor().getMethods();
}
@Override
@@ -135,6 +199,26 @@ public interface RepositoryFragment<T> {
return interfaceOrImplementation;
}
@Override
public Stream<Method> methods() {
return Arrays.stream(methods);
}
@Override
public List<Method> findMethods(String name) {
return RepositoryFragment.findMethods(name, methods);
}
@Override
public boolean hasMethod(Method method) {
if (RepositoryFragment.hasMethod(method, this.methods)) {
return true;
}
return RepositoryFragment.super.hasMethod(method);
}
@Override
public RepositoryFragment<T> withImplementation(T implementation) {
return new ImplementedRepositoryFragment<>(interfaceOrImplementation, implementation);
@@ -169,6 +253,7 @@ public interface RepositoryFragment<T> {
private final @Nullable Class<T> interfaceClass;
private final T implementation;
private final Method[] methods;
/**
* Creates a new {@link ImplementedRepositoryFragment} for the given interface class and implementation.
@@ -182,14 +267,16 @@ public interface RepositoryFragment<T> {
if (interfaceClass != null) {
Assert.isTrue(ClassUtils.isAssignableValue(interfaceClass, implementation),
() -> "Fragment implementation %s does not implement %s".formatted(
ClassUtils.getQualifiedName(implementation.getClass()),
ClassUtils.getQualifiedName(interfaceClass)));
Assert
.isTrue(ClassUtils.isAssignableValue(interfaceClass, implementation),
() -> "Fragment implementation %s does not implement %s".formatted(
ClassUtils.getQualifiedName(implementation.getClass()),
ClassUtils.getQualifiedName(interfaceClass)));
}
this.interfaceClass = interfaceClass;
this.implementation = implementation;
this.methods = getSignatureContributor().getMethods();
}
@Override
@@ -205,6 +292,26 @@ public interface RepositoryFragment<T> {
return implementation.getClass();
}
@Override
public Stream<Method> methods() {
return Arrays.stream(methods);
}
@Override
public List<Method> findMethods(String name) {
return RepositoryFragment.findMethods(name, this.methods);
}
@Override
public boolean hasMethod(Method method) {
if (RepositoryFragment.hasMethod(method, this.methods)) {
return true;
}
return RepositoryFragment.super.hasMethod(method);
}
@Override
public Optional<T> getImplementation() {
return Optional.of(implementation);

View File

@@ -18,6 +18,7 @@ package org.springframework.data.util;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
@@ -139,9 +140,20 @@ public abstract class ReactiveWrappers {
Assert.notNull(type, "Type must not be null");
return Arrays.stream(type.getMethods())//
.flatMap(ReflectionUtils::returnTypeAndParameters)//
.anyMatch(ReactiveWrappers::supports);
for (Method method : type.getMethods()) {
if (ReactiveWrappers.supports(method.getReturnType())) {
return true;
}
for (Class<?> parameterType : method.getParameterTypes()) {
if (ReactiveWrappers.supports(parameterType)) {
return true;
}
}
}
return false;
}
/**

View File

@@ -200,6 +200,11 @@ class TypeDiscoverer<S> implements TypeInformation<S> {
return new TypeDescriptor(resolvableType, getType(), null);
}
@Override
public ResolvableType toResolvableType() {
return resolvableType;
}
@Override
public TypeInformation<?> getRawTypeInformation() {
return new ClassTypeInformation<>(ResolvableType.forRawClass(resolvableType.toClass()));

View File

@@ -382,4 +382,13 @@ public interface TypeInformation<S> {
* @since 2.7
*/
TypeDescriptor toTypeDescriptor();
/**
* Returns the {@link ResolvableType} for this type information.
*
* @return will never be {@literal null}.
* @since 4.0
*/
ResolvableType toResolvableType();
}

View File

@@ -21,6 +21,7 @@ import static org.springframework.data.repository.aot.RepositoryRegistrationAotC
import java.io.Serializable;
import org.junit.jupiter.api.Test;
import org.springframework.aop.SpringProxy;
import org.springframework.aop.framework.Advised;
import org.springframework.aot.hint.RuntimeHints;
@@ -31,9 +32,18 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.core.DecoratingProxy;
import org.springframework.data.aot.sample.*;
import org.springframework.data.aot.sample.ConfigWithCustomImplementation;
import org.springframework.data.aot.sample.ConfigWithCustomRepositoryBaseClass;
import org.springframework.data.aot.sample.ConfigWithFragments;
import org.springframework.data.aot.sample.ConfigWithQueryMethods;
import org.springframework.data.aot.sample.ConfigWithQueryMethods.ProjectionInterface;
import org.springframework.data.aot.sample.ConfigWithQuerydslPredicateExecutor;
import org.springframework.data.aot.sample.ConfigWithQuerydslPredicateExecutor.Person;
import org.springframework.data.aot.sample.ConfigWithSimpleCrudRepository;
import org.springframework.data.aot.sample.ConfigWithTransactionManagerPresent;
import org.springframework.data.aot.sample.ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepository;
import org.springframework.data.aot.sample.QConfigWithQuerydslPredicateExecutor_Person;
import org.springframework.data.aot.sample.ReactiveConfig;
import org.springframework.data.domain.AbstractAggregateRoot;
import org.springframework.data.domain.AfterDomainEventPublication;
import org.springframework.data.domain.DomainEvents;
@@ -185,10 +195,8 @@ public class RepositoryRegistrationAotProcessorIntegrationTests {
.contributesReflectionFor(PagingAndSortingRepository.class) // base repository
.contributesReflectionFor(ConfigWithCustomImplementation.Person.class) // repository domain type
// fragments
.contributesReflectionFor(ConfigWithCustomImplementation.CustomImplInterface.class,
ConfigWithCustomImplementation.RepositoryWithCustomImplementationImpl.class);
// fragments (custom implementation)
.contributesReflectionFor(ConfigWithCustomImplementation.RepositoryWithCustomImplementationImpl.class);
});
}

View File

@@ -15,15 +15,22 @@
*/
package org.springframework.data.repository.aot.generate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.*;
import example.UserRepository;
import java.lang.reflect.Method;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.core.test.tools.ResourceFile;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.data.aot.CodeContributionAssert;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.javapoet.CodeBlock;
import org.springframework.util.ClassUtils;
/**
* @author Christoph Strobl
@@ -35,14 +42,21 @@ class RepositoryContributorUnitTests {
DummyModuleAotRepositoryContext aotContext = new DummyModuleAotRepositoryContext(UserRepository.class, null);
RepositoryContributor repositoryContributor = new RepositoryContributor(aotContext) {
@Override
protected AotRepositoryMethodBuilder contributeRepositoryMethod(AotRepositoryMethodGenerationContext context) {
return new AotRepositoryMethodBuilder(context).customize(((ctx, builder) -> {
if (!ctx.returnsVoid()) {
builder.addStatement("return null");
}
}));
@Override
protected @Nullable MethodContributor<? extends QueryMethod> contributeQueryMethod(Method method,
RepositoryInformation repositoryInformation) {
return MethodContributor.forQueryMethod(new QueryMethod(method, repositoryInformation, getProjectionFactory()))
.contribute(context -> {
CodeBlock.Builder builder = CodeBlock.builder();
if (!ClassUtils.isVoidType(method.getReturnType())) {
builder.addStatement("return null");
}
return builder.build();
});
}
};

View File

@@ -16,6 +16,7 @@
package org.springframework.data.repository.aot.generate;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import org.springframework.data.repository.core.CrudMethods;
@@ -24,7 +25,6 @@ import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryComposition;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
@@ -111,7 +111,7 @@ class StubRepositoryInformation implements RepositoryInformation {
}
@Override
public Streamable<Method> getQueryMethods() {
public List<Method> getQueryMethods() {
return null;
}

View File

@@ -17,12 +17,12 @@ package org.springframework.data.repository.core.support;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.springframework.data.repository.core.CrudMethods;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.TypeInformation;
public final class DummyRepositoryInformation implements RepositoryInformation {
@@ -83,8 +83,8 @@ public final class DummyRepositoryInformation implements RepositoryInformation {
}
@Override
public Streamable<Method> getQueryMethods() {
return Streamable.empty();
public List<Method> getQueryMethods() {
return Collections.emptyList();
}
@Override

View File

@@ -19,12 +19,12 @@ import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.Collections;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.query.QueryLookupStrategy;
@@ -49,13 +49,13 @@ class QueryExecutorMethodInterceptorUnitTests {
assertThatIllegalStateException()
.isThrownBy(() -> new QueryExecutorMethodInterceptor(information, new SpelAwareProxyProjectionFactory(),
Optional.empty(), PropertiesBasedNamedQueries.EMPTY, Collections.emptyList(), Collections.emptyList()));
null, PropertiesBasedNamedQueries.EMPTY, Collections.emptyList(), Collections.emptyList()));
}
@Test // DATACMNS-1508
void skipsQueryLookupsIfQueryLookupStrategyIsNotPresent() {
new QueryExecutorMethodInterceptor(information, new SpelAwareProxyProjectionFactory(), Optional.empty(),
new QueryExecutorMethodInterceptor(information, new SpelAwareProxyProjectionFactory(), null,
PropertiesBasedNamedQueries.EMPTY, Collections.emptyList(), Collections.emptyList());
verify(strategy, times(0)).resolveQuery(any(), any(), any(), any());