From 3596bcc99635f1c4cab9a30e1ebaaa2a6c4e2b0b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 24 Mar 2025 14:53:17 +0100 Subject: [PATCH] Refactor `RepositoryFactoryBeanSupport` fragments setters. Introduce RepositoryFragmentsFunction and a collection of functions to provide a well-formed contract. Use RepositoryMetadata from initialized RepositoryFactoryBean. See #3265 --- .../aot/generate/AotCodeContributor.java | 25 -- .../AotQueryMethodGenerationContext.java | 320 ++++++++++++++++++ .../aot/generate/AotRepositoryBuilder.java | 168 +++++---- .../AotRepositoryConstructorBuilder.java | 43 ++- ...ava => AotRepositoryFragmentMetadata.java} | 11 +- .../generate/AotRepositoryMethodBuilder.java | 163 +++++---- .../AotRepositoryMethodGenerationContext.java | 207 ----------- ...epositoryMethodImplementationMetadata.java | 32 +- .../repository/aot/generate/CodeBlocks.java | 22 +- .../aot/generate/MethodContributor.java | 98 ++++++ .../aot/generate/RepositoryContributor.java | 63 +++- .../repository/aot/generate/package-info.java | 5 + ...toryBeanDefinitionPropertiesDecorator.java | 91 +++++ .../config/DefaultAotRepositoryContext.java | 2 +- ...RepositoryRegistrationAotContribution.java | 163 +++++---- .../core/RepositoryInformation.java | 5 +- .../core/RepositoryInformationSupport.java | 73 +++- .../support/DefaultRepositoryInformation.java | 22 +- .../core/support/MethodLookups.java | 21 +- .../QueryExecutorMethodInterceptor.java | 14 +- .../core/support/RepositoryComposition.java | 25 +- .../support/RepositoryFactoryBeanSupport.java | 157 +++++++-- .../support/RepositoryFactorySupport.java | 20 +- .../core/support/RepositoryFragment.java | 115 ++++++- .../data/util/ReactiveWrappers.java | 18 +- .../data/util/TypeDiscoverer.java | 5 + .../data/util/TypeInformation.java | 9 + ...istrationAotProcessorIntegrationTests.java | 18 +- .../RepositoryContributorUnitTests.java | 32 +- .../generate/StubRepositoryInformation.java | 4 +- .../support/DummyRepositoryInformation.java | 6 +- ...eryExecutorMethodInterceptorUnitTests.java | 6 +- 32 files changed, 1310 insertions(+), 653 deletions(-) delete mode 100644 src/main/java/org/springframework/data/repository/aot/generate/AotCodeContributor.java create mode 100644 src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java rename src/main/java/org/springframework/data/repository/aot/generate/{AotRepositoryImplementationMetadata.java => AotRepositoryFragmentMetadata.java} (90%) delete mode 100644 src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodGenerationContext.java create mode 100644 src/main/java/org/springframework/data/repository/aot/generate/MethodContributor.java create mode 100644 src/main/java/org/springframework/data/repository/aot/generate/package-info.java create mode 100644 src/main/java/org/springframework/data/repository/config/AotRepositoryBeanDefinitionPropertiesDecorator.java diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotCodeContributor.java b/src/main/java/org/springframework/data/repository/aot/generate/AotCodeContributor.java deleted file mode 100644 index 350afa768..000000000 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotCodeContributor.java +++ /dev/null @@ -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); -} diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java b/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java new file mode 100644 index 000000000..08ab69c3f --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java @@ -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 MergedAnnotation getAnnotation(Class 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 getBindableParameterNames() { + + List 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 getAllParameterNames() { + + List 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> 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()); + } + +} diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java index 7cee91e2e..18e5fb03b 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java @@ -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 constructorBuilderCustomizer; - private Function methodContextFunction; - private RepositoryCustomizer customizer; + private Consumer constructorCustomizer; + private BiFunction> 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 AotRepositoryBuilder forRepository(RepositoryInformation repositoryInformation, + ProjectionFactory projectionFactory) { + return new AotRepositoryBuilder(repositoryInformation, projectionFactory); + } - YearMonth creationDate = YearMonth.now(ZoneId.of("UTC")); + public AotRepositoryBuilder withConstructorCustomizer( + Consumer constructorCustomizer) { + + this.constructorCustomizer = constructorCustomizer; + return this; + } + + public AotRepositoryBuilder withQueryMethodContributor( + BiFunction> 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. 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 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 constuctorBuilder) { - - this.constructorBuilderCustomizer = constuctorBuilder; - return this; - } - - AotRepositoryBuilder withDerivedMethodFunction( - Function 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 getAutowireFields() { + public Map 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); } } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java index c1d592150..0a62a6a5c 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java @@ -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 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 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); } + } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryImplementationMetadata.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryFragmentMetadata.java similarity index 90% rename from src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryImplementationMetadata.java rename to src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryFragmentMetadata.java index c9b004c08..cf66c0a05 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryImplementationMetadata.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryFragmentMetadata.java @@ -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 fields = new HashMap<>(3); + private final ClassName className; + private final Map fields = new HashMap<>(3); private final Map constructorArguments = new LinkedHashMap<>(3); - public AotRepositoryImplementationMetadata(ClassName className) { + public AotRepositoryFragmentMetadata(ClassName className) { this.className = className; } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java index f4ca54264..071735753 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java @@ -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[] tvs = context.getMethod().getTypeParameters(); + + for (TypeVariable 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); + } } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodGenerationContext.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodGenerationContext.java deleted file mode 100644 index fe3540171..000000000 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodGenerationContext.java +++ /dev/null @@ -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> 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 annotationValue(Class 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; - } -} diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodImplementationMetadata.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodImplementationMetadata.java index 791c217fb..ee58afef6 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodImplementationMetadata.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodImplementationMetadata.java @@ -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 methodArguments; - @Nullable private TypeName actualReturnType; - @Nullable private TypeName returnType; + private final Map 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 getMethodArguments() { return methodArguments; } - - void setActualReturnType(@Nullable TypeName actualReturnType) { - this.actualReturnType = actualReturnType; - } - - void setReturnType(@Nullable TypeName returnType) { - this.returnType = returnType; - } } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/CodeBlocks.java b/src/main/java/org/springframework/data/repository/aot/generate/CodeBlocks.java index 742984c6a..b81797c4b 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/CodeBlocks.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/CodeBlocks.java @@ -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); - } - } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/MethodContributor.java b/src/main/java/org/springframework/data/repository/aot/generate/MethodContributor.java new file mode 100644 index 000000000..639ece69d --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/MethodContributor.java @@ -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 query method type. + * @author Mark Paluch + * @since 4.0 + */ +public abstract class MethodContributor { + + 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 query method type. + */ + public static QueryMethodContributorBuilder 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 query method type. + */ + public interface QueryMethodContributorBuilder { + + /** + * 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 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 using(Consumer builderConsumer); + + } + +} diff --git a/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java index cf626b8d2..96cf50a6d 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java @@ -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 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 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 contributeQueryMethod(Method method, + RepositoryInformation repositoryInformation) { return null; } + } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/package-info.java b/src/main/java/org/springframework/data/repository/aot/generate/package-info.java new file mode 100644 index 000000000..263ae530c --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/package-info.java @@ -0,0 +1,5 @@ +/** + * Ahead-of-Time (AOT) generation for Spring Data repositories. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.data.repository.aot.generate; diff --git a/src/main/java/org/springframework/data/repository/config/AotRepositoryBeanDefinitionPropertiesDecorator.java b/src/main/java/org/springframework/data/repository/config/AotRepositoryBeanDefinitionPropertiesDecorator.java new file mode 100644 index 000000000..1326ac437 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/config/AotRepositoryBeanDefinitionPropertiesDecorator.java @@ -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 inheritedProperties; + private final RepositoryContributor repositoryContributor; + + public AotRepositoryBeanDefinitionPropertiesDecorator(Supplier 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 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(); + } + +} diff --git a/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java b/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java index 3c6b8cccb..ec2bfa73c 100644 --- a/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java +++ b/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java @@ -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); } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java index 75ffb140e..0ba5e9143 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java @@ -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 moduleContribution; + private @Nullable BiFunction moduleContribution; private final RepositoryRegistrationAotProcessor repositoryRegistrationAotProcessor; @@ -119,8 +118,8 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo return getRepositoryRegistrationAotProcessor().getBeanFactory(); } - protected Optional> getModuleContribution() { - return Optional.ofNullable(this.moduleContribution); + protected @Nullable BiFunction 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> resolveIdentifyingAnnotations() { - - Set> 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 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 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 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> 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> resolveIdentifyingAnnotations() { + + Set> 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 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()); + } } diff --git a/src/main/java/org/springframework/data/repository/core/RepositoryInformation.java b/src/main/java/org/springframework/data/repository/core/RepositoryInformation.java index e3293b33b..2c0edc8cf 100644 --- a/src/main/java/org/springframework/data/repository/core/RepositoryInformation.java +++ b/src/main/java/org/springframework/data/repository/core/RepositoryInformation.java @@ -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 getQueryMethods(); + List getQueryMethods(); /** * Returns the base class to be used to create the proxy backing instance. diff --git a/src/main/java/org/springframework/data/repository/core/RepositoryInformationSupport.java b/src/main/java/org/springframework/data/repository/core/RepositoryInformationSupport.java index 298ec6141..269563dc1 100644 --- a/src/main/java/org/springframework/data/repository/core/RepositoryInformationSupport.java +++ b/src/main/java/org/springframework/data/repository/core/RepositoryInformationSupport.java @@ -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 getQueryMethods() { + public List 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 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 methods; + private final List methods; + private final Set methodSet; private final boolean hasCustomMethod, hasQueryMethod; - DefaultQueryMethods(Streamable methods, boolean hasCustomMethod) { + DefaultQueryMethods(List 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); + } } } diff --git a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java index 623947c98..feebc870a 100644 --- a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java +++ b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java @@ -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 diff --git a/src/main/java/org/springframework/data/repository/core/support/MethodLookups.java b/src/main/java/org/springframework/data/repository/core/support/MethodLookups.java index 66ee77311..75942a71c 100644 --- a/src/main/java/org/springframework/data/repository/core/support/MethodLookups.java +++ b/src/main/java/org/springframework/data/repository/core/support/MethodLookups.java @@ -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 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); } diff --git a/src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java b/src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java index e9ebaf992..ba4681992 100644 --- a/src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java +++ b/src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java @@ -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, NamedQueries namedQueries, + ProjectionFactory projectionFactory, @Nullable QueryLookupStrategy queryLookupStrategy, NamedQueries namedQueries, List> queryPostProcessors, List 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 mapMethodsToQuery(RepositoryInformation repositoryInformation, QueryLookupStrategy lookupStrategy, ProjectionFactory projectionFactory) { - List queryMethods = repositoryInformation.getQueryMethods().toList(); + List queryMethods = repositoryInformation.getQueryMethods(); Map result = new HashMap<>(queryMethods.size(), 1.0f); for (Method method : queryMethods) { diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java index 329cc14be..3c0faa020 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java @@ -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 methodCache = new ConcurrentHashMap<>(); private final RepositoryFragments fragments; private final MethodLookup methodLookup; + private final List lookups; private final BiFunction 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> fragments; private RepositoryFragments(List> 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> methodStreamSupplier) { + private static @Nullable Method findMethod(InvokedMethod invokedMethod, List lookups, + RepositoryFragments fragments) { - for (MethodLookup.MethodPredicate methodPredicate : lookup.getLookups()) { - - Optional 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; + } + } } } diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java index 891f8d370..f82750d34 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java @@ -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, private @Nullable Key queryLookupStrategyKey; private @Nullable Class repositoryBaseClass; private @Nullable Object customImplementation; - private RepositoryFragments repositoryFragments = RepositoryFragments.empty(); + private final List 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 repositoryFactoryCustomizers = new ArrayList<>(); - private @Nullable Function aotImplementationFunction; + private RepositoryFragments cachedFragments = RepositoryFragments.empty(); private @Nullable Lazy repository; private @Nullable RepositoryMetadata repositoryMetadata; @@ -146,12 +148,24 @@ public abstract class RepositoryFactoryBeanSupport, } /** - * 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, this.publisher = publisher; } - public void setAotImplementationFunction(@Nullable Function aotImplementationFunction) { - this.aotImplementationFunction = aotImplementationFunction; - } - - @Nullable - protected Function getAotImplementationFunction() { - return aotImplementationFunction; - } - @Override @SuppressWarnings("unchecked") public EntityInformation getEntityInformation() { @@ -260,11 +265,7 @@ public abstract class RepositoryFactoryBeanSupport, @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, @Override public void afterPropertiesSet() { - this.factory = createRepositoryFactory(); this.factory.setExposeMetadata(exposeMetadata); this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey); @@ -331,23 +331,18 @@ public abstract class RepositoryFactoryBeanSupport, 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, */ protected abstract RepositoryFactorySupport createRepositoryFactory(); + private RepositoryFragments getRepositoryFragments(RepositoryMetadata repositoryMetadata) { + + RepositoryFactorySupport factory = getRequiredFactory(); + ValueExpressionDelegate valueExpressionDelegate = factory.getValueExpressionDelegate(); + List 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}. + *

+ * 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(); + } + + } + } diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java index 59b21e47a..88b4f11d8 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java @@ -122,7 +122,6 @@ public abstract class RepositoryFactorySupport private @Nullable BeanFactory beanFactory; private @Nullable Environment environment; private Lazy 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 = 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); diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java index 63013371a..f89b80b84 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java @@ -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 { 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 findMethods(String name); + /** * @return the class/interface providing signatures for this {@link RepositoryFragment}. */ @@ -122,12 +133,65 @@ public interface RepositoryFragment { */ RepositoryFragment withImplementation(T implementation); + private static List 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 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 implements RepositoryFragment { private final Class interfaceOrImplementation; + private final Method[] methods; public StructuralRepositoryFragment(Class interfaceOrImplementation) { this.interfaceOrImplementation = interfaceOrImplementation; + this.methods = getSignatureContributor().getMethods(); } @Override @@ -135,6 +199,26 @@ public interface RepositoryFragment { return interfaceOrImplementation; } + @Override + public Stream methods() { + return Arrays.stream(methods); + } + + @Override + public List 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 withImplementation(T implementation) { return new ImplementedRepositoryFragment<>(interfaceOrImplementation, implementation); @@ -169,6 +253,7 @@ public interface RepositoryFragment { private final @Nullable Class 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 { 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 { return implementation.getClass(); } + @Override + public Stream methods() { + return Arrays.stream(methods); + } + + @Override + public List 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 getImplementation() { return Optional.of(implementation); diff --git a/src/main/java/org/springframework/data/util/ReactiveWrappers.java b/src/main/java/org/springframework/data/util/ReactiveWrappers.java index f82d575ef..87a1c75a6 100644 --- a/src/main/java/org/springframework/data/util/ReactiveWrappers.java +++ b/src/main/java/org/springframework/data/util/ReactiveWrappers.java @@ -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; } /** diff --git a/src/main/java/org/springframework/data/util/TypeDiscoverer.java b/src/main/java/org/springframework/data/util/TypeDiscoverer.java index 48c75e0ce..4edf078b6 100644 --- a/src/main/java/org/springframework/data/util/TypeDiscoverer.java +++ b/src/main/java/org/springframework/data/util/TypeDiscoverer.java @@ -200,6 +200,11 @@ class TypeDiscoverer implements TypeInformation { return new TypeDescriptor(resolvableType, getType(), null); } + @Override + public ResolvableType toResolvableType() { + return resolvableType; + } + @Override public TypeInformation getRawTypeInformation() { return new ClassTypeInformation<>(ResolvableType.forRawClass(resolvableType.toClass())); diff --git a/src/main/java/org/springframework/data/util/TypeInformation.java b/src/main/java/org/springframework/data/util/TypeInformation.java index 175ed92ce..ed12ef802 100644 --- a/src/main/java/org/springframework/data/util/TypeInformation.java +++ b/src/main/java/org/springframework/data/util/TypeInformation.java @@ -382,4 +382,13 @@ public interface TypeInformation { * @since 2.7 */ TypeDescriptor toTypeDescriptor(); + + /** + * Returns the {@link ResolvableType} for this type information. + * + * @return will never be {@literal null}. + * @since 4.0 + */ + ResolvableType toResolvableType(); + } diff --git a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java index 3f2439434..39bc54554 100644 --- a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java +++ b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java @@ -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); }); } diff --git a/src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java index ebd9bd48e..3cfa07fe5 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java @@ -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 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(); + }); } }; diff --git a/src/test/java/org/springframework/data/repository/aot/generate/StubRepositoryInformation.java b/src/test/java/org/springframework/data/repository/aot/generate/StubRepositoryInformation.java index e526a2127..f117d9576 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/StubRepositoryInformation.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/StubRepositoryInformation.java @@ -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 getQueryMethods() { + public List getQueryMethods() { return null; } diff --git a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryInformation.java b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryInformation.java index 8eefb2928..5d19910fc 100644 --- a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryInformation.java +++ b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryInformation.java @@ -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 getQueryMethods() { - return Streamable.empty(); + public List getQueryMethods() { + return Collections.emptyList(); } @Override diff --git a/src/test/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptorUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptorUnitTests.java index 082292c59..4d2cc8695 100755 --- a/src/test/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptorUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptorUnitTests.java @@ -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());