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:
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Ahead-of-Time (AOT) generation for Spring Data repositories.
|
||||
*/
|
||||
@org.jspecify.annotations.NullMarked
|
||||
package org.springframework.data.repository.aot.generate;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user