From 797bd561872a913f9c10150315fa516f748bbf03 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 1 Oct 2025 14:54:06 +0200 Subject: [PATCH 1/7] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 13143c9f6f..359521e8c6 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 4.0.0-SNAPSHOT + 4.0.x-GH-3374-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. From 7dd638f4df43338f54dfe1c177d49abbeadc24f7 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 1 Oct 2025 15:29:45 +0200 Subject: [PATCH 2/7] Refine TypeName discovery. --- .../data/javapoet/TypeNames.java | 28 ++++++++++++++++- .../AotRepositoryFragmentMetadata.java | 23 +++----------- .../generate/AotRepositoryMethodBuilder.java | 3 +- .../aot/generate/MethodMetadata.java | 7 +++-- .../repository/aot/generate/MethodReturn.java | 4 +-- src/test/java/example/BaseRepository.java | 28 +++++++++++++++++ src/test/java/example/UserRepository.java | 2 +- .../data/javapoet/TypeNamesUnitTests.java | 30 +++++++++++++++++++ .../AotRepositoryMethodBuilderUnitTests.java | 7 ++--- 9 files changed, 100 insertions(+), 32 deletions(-) create mode 100644 src/test/java/example/BaseRepository.java diff --git a/src/main/java/org/springframework/data/javapoet/TypeNames.java b/src/main/java/org/springframework/data/javapoet/TypeNames.java index eb9db1a9b9..f59b1e0ded 100644 --- a/src/main/java/org/springframework/data/javapoet/TypeNames.java +++ b/src/main/java/org/springframework/data/javapoet/TypeNames.java @@ -16,6 +16,7 @@ package org.springframework.data.javapoet; import org.springframework.core.ResolvableType; +import org.springframework.javapoet.ParameterizedTypeName; import org.springframework.javapoet.TypeName; import org.springframework.util.ClassUtils; @@ -28,6 +29,7 @@ * Mainly for internal use within the framework * * @author Mark Paluch + * @author Christoph Strobl * @since 4.0 */ public abstract class TypeNames { @@ -65,6 +67,30 @@ public static TypeName className(ResolvableType resolvableType) { return TypeName.get(resolvableType.toClass()); } + /** + * Obtain a {@link TypeName} for the underlying type of the given {@link ResolvableType}. Can render a class name, a + * type signature with resolved generics or a generic type variable. + * + * @param resolvableType the resolvable type represent. + * @return the corresponding {@link TypeName}. + */ + public static TypeName resolvedTypeName(ResolvableType resolvableType) { + + if (resolvableType.equals(ResolvableType.NONE)) { + return TypeName.get(Object.class); + } + + if (!resolvableType.hasGenerics()) { + return TypeName.get(resolvableType.toClass()); + } + + if (resolvableType.hasResolvableGenerics()) { + return ParameterizedTypeName.get(resolvableType.toClass(), resolvableType.resolveGenerics()); + } + + return TypeName.get(resolvableType.getType()); + } + /** * Obtain a {@link TypeName} for the underlying type of the given {@link ResolvableType}. Can render a class name, a * type signature or a generic type variable. @@ -98,7 +124,7 @@ public static TypeName typeNameOrWrapper(Class type) { public static TypeName typeNameOrWrapper(ResolvableType resolvableType) { return ClassUtils.isPrimitiveOrWrapper(resolvableType.toClass()) ? TypeName.get(ClassUtils.resolvePrimitiveIfNecessary(resolvableType.toClass())) - : typeName(resolvableType); + : resolvedTypeName(resolvableType); } private TypeNames() {} diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryFragmentMetadata.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryFragmentMetadata.java index fe1ca30080..c775c355d5 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryFragmentMetadata.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryFragmentMetadata.java @@ -27,6 +27,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.core.ResolvableType; +import org.springframework.data.javapoet.TypeNames; import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.query.QueryMethod; import org.springframework.javapoet.ParameterizedTypeName; @@ -147,26 +148,10 @@ public Map getDelegateMethods() { } static TypeName typeNameOf(ResolvableType type) { + return TypeNames.resolvedTypeName(type); + } - if (type.equals(ResolvableType.NONE)) { - return TypeName.get(Object.class); - } - - if (!type.hasResolvableGenerics()) { - return TypeName.get(type.getType()); - } - - return ParameterizedTypeName.get(type.toClass(), type.resolveGenerics()); - } - - /** - * Constructor argument metadata. - * - * @param parameterName - * @param parameterType - * @param bindToField - */ - public record ConstructorArgument(String parameterName, ResolvableType parameterType, boolean bindToField, + public record ConstructorArgument(String parameterName, ResolvableType parameterType, boolean bindToField, AotRepositoryConstructorBuilder.ParameterOrigin parameterOrigin) { boolean isBoundToField() { 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 49e6bce7c6..e4ae9c13d8 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 @@ -24,6 +24,7 @@ import javax.lang.model.element.Modifier; +import org.springframework.data.javapoet.TypeNames; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.ParameterSpec; @@ -101,7 +102,7 @@ public MethodSpec buildMethod() { private MethodSpec.Builder initializeMethodBuilder() { MethodSpec.Builder builder = MethodSpec.methodBuilder(context.getMethod().getName()).addModifiers(Modifier.PUBLIC); - builder.returns(TypeName.get(context.getReturnType().getType())); + builder.returns(TypeNames.resolvedTypeName(context.getTargetMethodMetadata().getReturnType())); TypeVariable[] tvs = context.getMethod().getTypeParameters(); for (TypeVariable tv : tvs) { diff --git a/src/main/java/org/springframework/data/repository/aot/generate/MethodMetadata.java b/src/main/java/org/springframework/data/repository/aot/generate/MethodMetadata.java index febbc6d1ac..cebc6013ea 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/MethodMetadata.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/MethodMetadata.java @@ -31,6 +31,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; +import org.springframework.data.javapoet.TypeNames; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.util.TypeInformation; import org.springframework.javapoet.ParameterSpec; @@ -80,11 +81,11 @@ private static void initializeMethodArguments(Method method, ParameterNameDiscov for (Parameter parameter : method.getParameters()) { - MethodParameter methodParameter = MethodParameter.forParameter(parameter); + MethodParameter methodParameter = MethodParameter.forParameter(parameter).withContainingClass(repositoryInterface.resolve()); methodParameter.initParameterNameDiscovery(nameDiscoverer); - ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(methodParameter, repositoryInterface); + ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(methodParameter); - TypeName parameterType = TypeName.get(resolvableParameterType.getType()); + TypeName parameterType = TypeNames.resolvedTypeName(resolvableParameterType); ParameterSpec parameterSpec = ParameterSpec.builder(parameterType, methodParameter.getParameterName()).build(); diff --git a/src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java b/src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java index c8b4c4f296..2e3b3098e3 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java @@ -61,7 +61,7 @@ public MethodReturn(ReturnedType returnedType, ResolvableType returnType) { this.returnedType = returnedType; this.returnType = returnType; - this.typeName = TypeNames.typeName(returnType); + this.typeName = TypeNames.resolvedTypeName(returnType); this.className = TypeNames.className(returnType); Class returnClass = returnType.toClass(); @@ -72,7 +72,7 @@ public MethodReturn(ReturnedType returnedType, ResolvableType returnType) { if (actualType != null) { this.actualType = actualType.toResolvableType(); - this.actualTypeName = TypeNames.typeName(this.actualType); + this.actualTypeName = TypeNames.resolvedTypeName(this.actualType); this.actualClassName = TypeNames.className(this.actualType); this.actualReturnClass = actualType.getType(); } else { diff --git a/src/test/java/example/BaseRepository.java b/src/test/java/example/BaseRepository.java new file mode 100644 index 0000000000..abdd147a35 --- /dev/null +++ b/src/test/java/example/BaseRepository.java @@ -0,0 +1,28 @@ +/* + * Copyright 2025-present 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 example; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.NoRepositoryBean; + +/** + * @author Christoph Strobl + */ +@NoRepositoryBean +public interface BaseRepository extends CrudRepository { + + T findInBaseRepository(ID id); +} diff --git a/src/test/java/example/UserRepository.java b/src/test/java/example/UserRepository.java index d9b35863ef..5370d27a68 100644 --- a/src/test/java/example/UserRepository.java +++ b/src/test/java/example/UserRepository.java @@ -24,7 +24,7 @@ /** * @author Christoph Strobl */ -public interface UserRepository extends CrudRepository, UserRepositoryExtension { +public interface UserRepository extends BaseRepository, UserRepositoryExtension { User findByFirstname(String firstname); diff --git a/src/test/java/org/springframework/data/javapoet/TypeNamesUnitTests.java b/src/test/java/org/springframework/data/javapoet/TypeNamesUnitTests.java index 74a28f90c9..f1112ae80b 100644 --- a/src/test/java/org/springframework/data/javapoet/TypeNamesUnitTests.java +++ b/src/test/java/org/springframework/data/javapoet/TypeNamesUnitTests.java @@ -20,12 +20,16 @@ import java.util.Set; import java.util.stream.Stream; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.javapoet.ParameterizedTypeName; import org.springframework.javapoet.TypeName; +import org.springframework.javapoet.TypeVariableName; +import org.springframework.util.ReflectionUtils; /** * @author Christoph Strobl @@ -59,4 +63,30 @@ void classNames(ResolvableType resolvableType, TypeName expected) { assertThat(TypeNames.className(resolvableType)).isEqualTo(expected); } + @Test + void typeNameQuirksForMethodParameters() { + + ReflectionUtils.doWithMethods(Concrete.class, method -> { + if (!method.getName().contains("baseMethod")) { + return; + } + + MethodParameter methodParameter = new MethodParameter(method, 0).withContainingClass(Concrete.class); + ResolvableType resolvableType = ResolvableType.forMethodParameter(methodParameter); + + assertThat(TypeNames.typeName(resolvableType)).isEqualTo(TypeVariableName.get("T")); + assertThat(TypeNames.resolvedTypeName(resolvableType)).isEqualTo(TypeName.get(MyType.class)); + }); + } + + interface GenericBase { + java.util.List baseMethod(T arg0); + } + + interface Concrete extends GenericBase { + + } + + class MyType {} + } diff --git a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilderUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilderUnitTests.java index d2d2510d39..9c8abafdfc 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilderUnitTests.java @@ -60,7 +60,6 @@ void generatesMethodSkeletonBasedOnGenerationMetadata() throws NoSuchMethodExcep Method method = UserRepository.class.getMethod("findByFirstname", String.class); when(methodGenerationContext.getMethod()).thenReturn(method); - when(methodGenerationContext.getReturnType()).thenReturn(ResolvableType.forClass(User.class)); doReturn(TypeInformation.of(User.class)).when(repositoryInformation).getReturnType(any()); doReturn(TypeInformation.of(User.class)).when(repositoryInformation).getReturnedDomainTypeInformation(any()); MethodMetadata methodMetadata = new MethodMetadata(repositoryInformation, method); @@ -76,11 +75,10 @@ void generatesMethodWithGenerics() throws NoSuchMethodException { Method method = UserRepository.class.getMethod("findByFirstnameIn", List.class); when(methodGenerationContext.getMethod()).thenReturn(method); - when(methodGenerationContext.getReturnType()) - .thenReturn(ResolvableType.forClassWithGenerics(List.class, User.class)); doReturn(TypeInformation.of(User.class)).when(repositoryInformation).getReturnType(any()); doReturn(TypeInformation.of(User.class)).when(repositoryInformation).getReturnedDomainTypeInformation(any()); - MethodMetadata methodMetadata = new MethodMetadata(repositoryInformation, method); + MethodMetadata methodMetadata = spy(new MethodMetadata(repositoryInformation, method)); + when(methodMetadata.getReturnType()).thenReturn(ResolvableType.forClassWithGenerics(List.class, User.class)); when(methodGenerationContext.getTargetMethodMetadata()).thenReturn(methodMetadata); AotRepositoryMethodBuilder builder = new AotRepositoryMethodBuilder(methodGenerationContext); @@ -95,7 +93,6 @@ void generatesExpressionMarkerIfInUse(ExpressionMarker expressionMarker) throws Method method = UserRepository.class.getMethod("findByFirstname", String.class); when(methodGenerationContext.getMethod()).thenReturn(method); - when(methodGenerationContext.getReturnType()).thenReturn(ResolvableType.forClass(User.class)); doReturn(TypeInformation.of(User.class)).when(repositoryInformation).getReturnType(any()); doReturn(TypeInformation.of(User.class)).when(repositoryInformation).getReturnedDomainTypeInformation(any()); MethodMetadata methodMetadata = new MethodMetadata(repositoryInformation, method); From f8014b60e1c53b9cd5067085b8ddb73136ca4e42 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 2 Oct 2025 09:44:26 +0200 Subject: [PATCH 3/7] Additional tests for arrays. --- .../data/javapoet/TypeNames.java | 17 +++- .../data/javapoet/TypeNamesUnitTests.java | 78 +++++++++++++++++-- 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/springframework/data/javapoet/TypeNames.java b/src/main/java/org/springframework/data/javapoet/TypeNames.java index f59b1e0ded..3d38889852 100644 --- a/src/main/java/org/springframework/data/javapoet/TypeNames.java +++ b/src/main/java/org/springframework/data/javapoet/TypeNames.java @@ -16,6 +16,8 @@ package org.springframework.data.javapoet; import org.springframework.core.ResolvableType; +import org.springframework.javapoet.ArrayTypeName; +import org.springframework.javapoet.ClassName; import org.springframework.javapoet.ParameterizedTypeName; import org.springframework.javapoet.TypeName; import org.springframework.util.ClassUtils; @@ -81,14 +83,25 @@ public static TypeName resolvedTypeName(ResolvableType resolvableType) { } if (!resolvableType.hasGenerics()) { - return TypeName.get(resolvableType.toClass()); + Class resolvedType = resolvableType.toClass(); + if (!resolvableType.isArray()) { + return TypeName.get(resolvedType); + } + + if (resolvedType.isArray()) { + return TypeName.get(resolvedType); + } + if (resolvableType.isArray()) { + return ArrayTypeName.of(resolvedType); + } + return TypeName.get(resolvedType); } if (resolvableType.hasResolvableGenerics()) { return ParameterizedTypeName.get(resolvableType.toClass(), resolvableType.resolveGenerics()); } - return TypeName.get(resolvableType.getType()); + return ClassName.get(resolvableType.toClass()); } /** diff --git a/src/test/java/org/springframework/data/javapoet/TypeNamesUnitTests.java b/src/test/java/org/springframework/data/javapoet/TypeNamesUnitTests.java index f1112ae80b..0a26df7c1c 100644 --- a/src/test/java/org/springframework/data/javapoet/TypeNamesUnitTests.java +++ b/src/test/java/org/springframework/data/javapoet/TypeNamesUnitTests.java @@ -17,6 +17,7 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import java.util.List; import java.util.Set; import java.util.stream.Stream; @@ -26,6 +27,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; +import org.springframework.javapoet.ClassName; import org.springframework.javapoet.ParameterizedTypeName; import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeVariableName; @@ -63,30 +65,90 @@ void classNames(ResolvableType resolvableType, TypeName expected) { assertThat(TypeNames.className(resolvableType)).isEqualTo(expected); } - @Test - void typeNameQuirksForMethodParameters() { + @Test // GH-3374 + void resolvedTypeNamesWithoutGenerics() { + + ResolvableType resolvableType = ResolvableType.forClass(List.class); + assertThat(TypeNames.resolvedTypeName(resolvableType)).extracting(TypeName::toString).isEqualTo("java.util.List"); + } + + @Test // GH-3374 + void resolvedTypeNamesForMethodParameters() { ReflectionUtils.doWithMethods(Concrete.class, method -> { if (!method.getName().contains("baseMethod")) { return; } - MethodParameter methodParameter = new MethodParameter(method, 0).withContainingClass(Concrete.class); - ResolvableType resolvableType = ResolvableType.forMethodParameter(methodParameter); + MethodParameter refiedObjectMethodParameter = new MethodParameter(method, 0).withContainingClass(Concrete.class); + ResolvableType resolvedObjectParameterType = ResolvableType.forMethodParameter(refiedObjectMethodParameter); + assertThat(TypeNames.typeName(resolvedObjectParameterType)).isEqualTo(TypeVariableName.get("T")); + assertThat(TypeNames.resolvedTypeName(resolvedObjectParameterType)).isEqualTo(TypeName.get(MyType.class)); + + MethodParameter refiedCollectionMethodParameter = new MethodParameter(method, 1) + .withContainingClass(Concrete.class); + ResolvableType resolvedCollectionParameterType = ResolvableType + .forMethodParameter(refiedCollectionMethodParameter); + assertThat(TypeNames.typeName(resolvedCollectionParameterType)) + .isEqualTo(ParameterizedTypeName.get(ClassName.get(java.util.List.class), TypeVariableName.get("T"))); + assertThat(TypeNames.resolvedTypeName(resolvedCollectionParameterType)) + .isEqualTo(ParameterizedTypeName.get(java.util.List.class, MyType.class)); + + MethodParameter refiedArrayMethodParameter = new MethodParameter(method, 2).withContainingClass(Concrete.class); + ResolvableType resolvedArrayParameterType = ResolvableType.forMethodParameter(refiedArrayMethodParameter); + assertThat(TypeNames.typeName(resolvedArrayParameterType)).extracting(TypeName::toString).isEqualTo("T[]"); + assertThat(TypeNames.resolvedTypeName(resolvedArrayParameterType)).extracting(TypeName::toString) + .isEqualTo("org.springframework.data.javapoet.TypeNamesUnitTests.MyType[]"); + + ResolvableType resolvedReturnType = ResolvableType.forMethodReturnType(method, Concrete.class); + assertThat(TypeNames.typeName(resolvedReturnType)) + .isEqualTo(ParameterizedTypeName.get(ClassName.get(java.util.List.class), TypeVariableName.get("T"))); + assertThat(TypeNames.resolvedTypeName(resolvedReturnType)) + .isEqualTo(ParameterizedTypeName.get(java.util.List.class, MyType.class)); + }); + + ReflectionUtils.doWithMethods(Concrete.class, method -> { + if (!method.getName().contains("otherMethod")) { + return; + } - assertThat(TypeNames.typeName(resolvableType)).isEqualTo(TypeVariableName.get("T")); - assertThat(TypeNames.resolvedTypeName(resolvableType)).isEqualTo(TypeName.get(MyType.class)); + MethodParameter refiedObjectMethodParameter = new MethodParameter(method, 0).withContainingClass(Concrete.class); + ResolvableType resolvedObjectParameterType = ResolvableType.forMethodParameter(refiedObjectMethodParameter); + assertThat(TypeNames.typeName(resolvedObjectParameterType)).isEqualTo(TypeVariableName.get("RT")); + assertThat(TypeNames.resolvedTypeName(resolvedObjectParameterType)).isEqualTo(TypeName.get(Object.class)); + + MethodParameter refiedCollectionMethodParameter = new MethodParameter(method, 1) + .withContainingClass(Concrete.class); + ResolvableType resolvedCollectionParameterType = ResolvableType + .forMethodParameter(refiedCollectionMethodParameter); + assertThat(TypeNames.typeName(resolvedCollectionParameterType)) + .isEqualTo(ParameterizedTypeName.get(ClassName.get(java.util.List.class), TypeVariableName.get("RT"))); + assertThat(TypeNames.resolvedTypeName(resolvedCollectionParameterType)) + .isEqualTo(ClassName.get(java.util.List.class)); + + MethodParameter refiedArrayMethodParameter = new MethodParameter(method, 2).withContainingClass(Concrete.class); + ResolvableType resolvedArrayParameterType = ResolvableType.forMethodParameter(refiedArrayMethodParameter); + assertThat(TypeNames.typeName(resolvedArrayParameterType)).extracting(TypeName::toString).isEqualTo("RT[]"); + assertThat(TypeNames.resolvedTypeName(resolvedArrayParameterType)).extracting(TypeName::toString) + .isEqualTo("java.lang.Object[]"); + + ResolvableType resolvedReturnType = ResolvableType.forMethodReturnType(method, Concrete.class); + assertThat(TypeNames.typeName(resolvedReturnType)).extracting(TypeName::toString).isEqualTo("RT"); + assertThat(TypeNames.resolvedTypeName(resolvedReturnType)).isEqualTo(TypeName.get(Object.class)); }); } interface GenericBase { - java.util.List baseMethod(T arg0); + + java.util.List baseMethod(T arg0, java.util.List arg1, T... arg2); + + RT otherMethod(RT arg0, java.util.List arg1, RT... arg2); } interface Concrete extends GenericBase { } - class MyType {} + static class MyType {} } From a746886e414d519ed0a8b1e17c792b35fdb5987e Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 10 Oct 2025 10:52:54 +0200 Subject: [PATCH 4/7] back of when detecting unresolvable return type --- .../aot/generate/AotRepositoryCreator.java | 15 +++++- .../AotRepositoryCreatorUnitTests.java | 46 +++++++++++++++++-- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java index 4534592ba8..8a6e20469e 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java @@ -27,7 +27,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; - +import org.springframework.core.ResolvableType; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryComposition; @@ -298,6 +298,19 @@ private void contributeMethod(Method method, @Nullable MethodContributorFactory return; } + // TODO: should we do this even before we do something with the method to protect the modules? + if (ResolvableType.forMethodReturnType(method, repositoryInformation.getRepositoryInterface()) + .hasUnresolvableGenerics()) { + + if (logger.isTraceEnabled()) { + logger.trace("Skipping method [%s.%s] contribution, unresolvable generic return" + .formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName())); + } + + generationMetadata.addDelegateMethod(method, contributor); + return; + } + if (contributor.contributesMethodSpec() && !repositoryInformation.isReactiveRepository()) { generationMetadata.addRepositoryMethod(method, contributor); } else { diff --git a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java index 5ed0352f7d..6677b79a52 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java @@ -15,12 +15,14 @@ */ package org.springframework.data.repository.aot.generate; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import example.UserRepository.User; import java.util.List; +import java.util.Map; import java.util.TimeZone; import javax.lang.model.element.Modifier; @@ -28,7 +30,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Answers; - import org.springframework.aot.generate.Generated; import org.springframework.aot.hint.TypeReference; import org.springframework.core.ResolvableType; @@ -40,6 +41,7 @@ import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.AnnotationRepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.repository.query.DefaultParameters; import org.springframework.data.repository.query.QueryMethod; import org.springframework.javapoet.ClassName; import org.springframework.javapoet.JavaFile; @@ -179,8 +181,8 @@ void shouldContributeFragmentImplementationMetadata() { AnnotationRepositoryMetadata.getMetadata(QuerydslUserRepository.class), CrudRepository.class, List.of(RepositoryFragment.structural(QuerydslPredicateExecutor.class, DummyQuerydslPredicateExecutor.class))); - AotRepositoryCreator creator = AotRepositoryCreator - .forRepository(repositoryInformation, "Commons", new SpelAwareProxyProjectionFactory()); + AotRepositoryCreator creator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons", + new SpelAwareProxyProjectionFactory()); creator.contributeMethods(method -> null); AotRepositoryCreator.AotBundle bundle = doCreate(creator); @@ -229,6 +231,38 @@ void usesGenericConstructorArguments() { "public %s(List param1, String param2, Object ctorScoped)".formatted(targetType.getSimpleName())); } + @Test // GH-3374 + void skipsMethodWithUnresolvableGenericReturnType() { + + SpelAwareProxyProjectionFactory spelAwareProxyProjectionFactory = new SpelAwareProxyProjectionFactory(); + AotRepositoryInformation repositoryInformation = new AotRepositoryInformation( + AnnotationRepositoryMetadata.getMetadata(UserRepository.class), CrudRepository.class, + List.of(RepositoryFragment.structural(QuerydslPredicateExecutor.class, DummyQuerydslPredicateExecutor.class))); + + AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons", + spelAwareProxyProjectionFactory); + repositoryCreator.contributeMethods(method -> { + + QueryMethod queryMethod = new QueryMethod(method, repositoryInformation, spelAwareProxyProjectionFactory, + DefaultParameters::new); + return new MethodContributor<>(queryMethod, Map::of) { + @Override + public MethodSpec contribute(AotQueryMethodGenerationContext context) { + return MethodSpec.methodBuilder(context.getMethod().getName()).addCode("// 1 = 1").build(); + } + + @Override + public boolean contributesMethodSpec() { + return true; + } + }; + + }); + + // same package as source repo + assertThat(generate(repositoryCreator)).contains("someMethod()").doesNotContain("findByFirstname()"); + } + private AotRepositoryCreator.AotBundle doCreate(AotRepositoryCreator creator) { return creator.create(getTypeSpecBuilder(creator)); } @@ -263,6 +297,8 @@ private static TypeSpec.Builder getTypeSpecBuilder(ClassName className) { interface UserRepository extends org.springframework.data.repository.Repository { String someMethod(); + + List findByFirstname(String firstname, Class type); } interface QuerydslUserRepository From 048f259eae0a02698b24a9a049d31cf975ff7762 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 10 Oct 2025 12:07:04 +0200 Subject: [PATCH 5/7] Fully Resolve generics to parameterized type name --- .../data/javapoet/TypeNames.java | 5 ++++- .../data/javapoet/TypeNamesUnitTests.java | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/springframework/data/javapoet/TypeNames.java b/src/main/java/org/springframework/data/javapoet/TypeNames.java index 3d38889852..055db183c1 100644 --- a/src/main/java/org/springframework/data/javapoet/TypeNames.java +++ b/src/main/java/org/springframework/data/javapoet/TypeNames.java @@ -15,6 +15,8 @@ */ package org.springframework.data.javapoet; +import java.util.Arrays; + import org.springframework.core.ResolvableType; import org.springframework.javapoet.ArrayTypeName; import org.springframework.javapoet.ClassName; @@ -98,7 +100,8 @@ public static TypeName resolvedTypeName(ResolvableType resolvableType) { } if (resolvableType.hasResolvableGenerics()) { - return ParameterizedTypeName.get(resolvableType.toClass(), resolvableType.resolveGenerics()); + return ParameterizedTypeName.get(ClassName.get(resolvableType.toClass()), + Arrays.stream(resolvableType.getGenerics()).map(TypeNames::resolvedTypeName).toArray(TypeName[]::new)); } return ClassName.get(resolvableType.toClass()); diff --git a/src/test/java/org/springframework/data/javapoet/TypeNamesUnitTests.java b/src/test/java/org/springframework/data/javapoet/TypeNamesUnitTests.java index 0a26df7c1c..9ebab75b83 100644 --- a/src/test/java/org/springframework/data/javapoet/TypeNamesUnitTests.java +++ b/src/test/java/org/springframework/data/javapoet/TypeNamesUnitTests.java @@ -27,6 +27,9 @@ import org.junit.jupiter.params.provider.MethodSource; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.GeoResult; +import org.springframework.data.geo.Point; import org.springframework.javapoet.ClassName; import org.springframework.javapoet.ParameterizedTypeName; import org.springframework.javapoet.TypeName; @@ -136,6 +139,20 @@ void resolvedTypeNamesForMethodParameters() { assertThat(TypeNames.typeName(resolvedReturnType)).extracting(TypeName::toString).isEqualTo("RT"); assertThat(TypeNames.resolvedTypeName(resolvedReturnType)).isEqualTo(TypeName.get(Object.class)); }); + + ReflectionUtils.doWithMethods(Concrete.class, method -> { + if (!method.getName().contains("findByLocationNear")) { + return; + } + + ResolvableType resolvedReturnType = ResolvableType.forMethodReturnType(method, Concrete.class); + + assertThat(TypeNames.typeName(resolvedReturnType)).extracting(TypeName::toString).isEqualTo( + "java.util.List>"); + assertThat(TypeNames.resolvedTypeName(resolvedReturnType)).isEqualTo(ParameterizedTypeName + .get(ClassName.get(java.util.List.class), ParameterizedTypeName.get(GeoResult.class, MyType.class))); + }); + } interface GenericBase { @@ -147,6 +164,7 @@ interface GenericBase { interface Concrete extends GenericBase { + List> findByLocationNear(Point point, Distance maxDistance); } static class MyType {} From 7fb57f47878a295a7384ba6e5850304dc7ac9450 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 10 Oct 2025 15:09:00 +0200 Subject: [PATCH 6/7] Polishing. Split tests into parametrized tests. --- .../data/javapoet/TypeNames.java | 18 +-- .../aot/generate/AotRepositoryCreator.java | 48 +++++- src/test/java/example/BaseRepository.java | 28 ---- src/test/java/example/UserRepository.java | 2 +- .../data/javapoet/TypeNamesUnitTests.java | 152 ++++++++++-------- 5 files changed, 139 insertions(+), 109 deletions(-) delete mode 100644 src/test/java/example/BaseRepository.java diff --git a/src/main/java/org/springframework/data/javapoet/TypeNames.java b/src/main/java/org/springframework/data/javapoet/TypeNames.java index 055db183c1..6112af9e22 100644 --- a/src/main/java/org/springframework/data/javapoet/TypeNames.java +++ b/src/main/java/org/springframework/data/javapoet/TypeNames.java @@ -84,24 +84,24 @@ public static TypeName resolvedTypeName(ResolvableType resolvableType) { return TypeName.get(Object.class); } + if (resolvableType.hasResolvableGenerics()) { + return ParameterizedTypeName.get(ClassName.get(resolvableType.toClass()), + Arrays.stream(resolvableType.getGenerics()).map(TypeNames::resolvedTypeName).toArray(TypeName[]::new)); + } + if (!resolvableType.hasGenerics()) { + Class resolvedType = resolvableType.toClass(); - if (!resolvableType.isArray()) { - return TypeName.get(resolvedType); - } - if (resolvedType.isArray()) { + if (!resolvableType.isArray() || resolvedType.isArray()) { return TypeName.get(resolvedType); } + if (resolvableType.isArray()) { return ArrayTypeName.of(resolvedType); } - return TypeName.get(resolvedType); - } - if (resolvableType.hasResolvableGenerics()) { - return ParameterizedTypeName.get(ClassName.get(resolvableType.toClass()), - Arrays.stream(resolvableType.getGenerics()).map(TypeNames::resolvedTypeName).toArray(TypeName[]::new)); + return TypeName.get(resolvedType); } return ClassName.get(resolvableType.toClass()); diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java index 8a6e20469e..af98a0fc9d 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java @@ -290,6 +290,7 @@ private void contributeMethod(Method method, @Nullable MethodContributorFactory MethodContributor contributor = contributorFactory.create(method); if (contributor == null) { + if (logger.isTraceEnabled()) { logger.trace("Skipping method [%s.%s] contribution, no MethodContributor available" .formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName())); @@ -298,24 +299,55 @@ private void contributeMethod(Method method, @Nullable MethodContributorFactory return; } - // TODO: should we do this even before we do something with the method to protect the modules? - if (ResolvableType.forMethodReturnType(method, repositoryInformation.getRepositoryInterface()) - .hasUnresolvableGenerics()) { + if (hasUnresolvableGenerics(method)) { if (logger.isTraceEnabled()) { - logger.trace("Skipping method [%s.%s] contribution, unresolvable generic return" - .formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName())); + logger.trace( + "Skipping implementation method [%s.%s] contribution. Method uses generics that currently cannot be resolved." + .formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName())); } generationMetadata.addDelegateMethod(method, contributor); return; } - if (contributor.contributesMethodSpec() && !repositoryInformation.isReactiveRepository()) { - generationMetadata.addRepositoryMethod(method, contributor); - } else { + if (!contributor.contributesMethodSpec() || repositoryInformation.isReactiveRepository()) { + + if (repositoryInformation.isReactiveRepository() && logger.isTraceEnabled()) { + logger.trace( + "Skipping implementation method [%s.%s] contribution. AOT repositories are not supported for reactive repositories." + .formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName())); + } + + if (!contributor.contributesMethodSpec() && logger.isTraceEnabled()) { + logger.trace( + "Skipping implementation method [%s.%s] contribution. Spring Data %s did not provide a method implementation." + .formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName(), moduleName)); + } + generationMetadata.addDelegateMethod(method, contributor); + return; + } + + generationMetadata.addRepositoryMethod(method, contributor); + } + + private boolean hasUnresolvableGenerics(Method method) { + + if (ResolvableType.forMethodReturnType(method, repositoryInformation.getRepositoryInterface()) + .hasUnresolvableGenerics()) { + return true; } + + for (int i = 0; i < method.getParameterCount(); i++) { + + if (ResolvableType.forMethodParameter(method, i, repositoryInformation.getRepositoryInterface()) + .hasUnresolvableGenerics()) { + return true; + } + } + + return false; } /** diff --git a/src/test/java/example/BaseRepository.java b/src/test/java/example/BaseRepository.java deleted file mode 100644 index abdd147a35..0000000000 --- a/src/test/java/example/BaseRepository.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2025-present 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 example; - -import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.NoRepositoryBean; - -/** - * @author Christoph Strobl - */ -@NoRepositoryBean -public interface BaseRepository extends CrudRepository { - - T findInBaseRepository(ID id); -} diff --git a/src/test/java/example/UserRepository.java b/src/test/java/example/UserRepository.java index 5370d27a68..d9b35863ef 100644 --- a/src/test/java/example/UserRepository.java +++ b/src/test/java/example/UserRepository.java @@ -24,7 +24,7 @@ /** * @author Christoph Strobl */ -public interface UserRepository extends BaseRepository, UserRepositoryExtension { +public interface UserRepository extends CrudRepository, UserRepositoryExtension { User findByFirstname(String firstname); diff --git a/src/test/java/org/springframework/data/javapoet/TypeNamesUnitTests.java b/src/test/java/org/springframework/data/javapoet/TypeNamesUnitTests.java index 9ebab75b83..af78cd1053 100644 --- a/src/test/java/org/springframework/data/javapoet/TypeNamesUnitTests.java +++ b/src/test/java/org/springframework/data/javapoet/TypeNamesUnitTests.java @@ -15,8 +15,10 @@ */ package org.springframework.data.javapoet; -import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.assertj.core.api.AssertionsForInterfaceTypes.*; +import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.stream.Stream; @@ -25,6 +27,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; + import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.data.geo.Distance; @@ -37,7 +40,10 @@ import org.springframework.util.ReflectionUtils; /** + * Tests for {@link TypeNames}. + * * @author Christoph Strobl + * @author Mark Paluch */ class TypeNamesUnitTests { @@ -75,84 +81,104 @@ void resolvedTypeNamesWithoutGenerics() { assertThat(TypeNames.resolvedTypeName(resolvableType)).extracting(TypeName::toString).isEqualTo("java.util.List"); } - @Test // GH-3374 - void resolvedTypeNamesForMethodParameters() { + static List concreteMethods() { + + List methods = new ArrayList<>(); ReflectionUtils.doWithMethods(Concrete.class, method -> { if (!method.getName().contains("baseMethod")) { return; } - - MethodParameter refiedObjectMethodParameter = new MethodParameter(method, 0).withContainingClass(Concrete.class); - ResolvableType resolvedObjectParameterType = ResolvableType.forMethodParameter(refiedObjectMethodParameter); - assertThat(TypeNames.typeName(resolvedObjectParameterType)).isEqualTo(TypeVariableName.get("T")); - assertThat(TypeNames.resolvedTypeName(resolvedObjectParameterType)).isEqualTo(TypeName.get(MyType.class)); - - MethodParameter refiedCollectionMethodParameter = new MethodParameter(method, 1) - .withContainingClass(Concrete.class); - ResolvableType resolvedCollectionParameterType = ResolvableType - .forMethodParameter(refiedCollectionMethodParameter); - assertThat(TypeNames.typeName(resolvedCollectionParameterType)) - .isEqualTo(ParameterizedTypeName.get(ClassName.get(java.util.List.class), TypeVariableName.get("T"))); - assertThat(TypeNames.resolvedTypeName(resolvedCollectionParameterType)) - .isEqualTo(ParameterizedTypeName.get(java.util.List.class, MyType.class)); - - MethodParameter refiedArrayMethodParameter = new MethodParameter(method, 2).withContainingClass(Concrete.class); - ResolvableType resolvedArrayParameterType = ResolvableType.forMethodParameter(refiedArrayMethodParameter); - assertThat(TypeNames.typeName(resolvedArrayParameterType)).extracting(TypeName::toString).isEqualTo("T[]"); - assertThat(TypeNames.resolvedTypeName(resolvedArrayParameterType)).extracting(TypeName::toString) - .isEqualTo("org.springframework.data.javapoet.TypeNamesUnitTests.MyType[]"); - - ResolvableType resolvedReturnType = ResolvableType.forMethodReturnType(method, Concrete.class); - assertThat(TypeNames.typeName(resolvedReturnType)) - .isEqualTo(ParameterizedTypeName.get(ClassName.get(java.util.List.class), TypeVariableName.get("T"))); - assertThat(TypeNames.resolvedTypeName(resolvedReturnType)) - .isEqualTo(ParameterizedTypeName.get(java.util.List.class, MyType.class)); + methods.add(method); }); + return methods; + } + + static List otherMethods() { + + List methods = new ArrayList<>(); + ReflectionUtils.doWithMethods(Concrete.class, method -> { if (!method.getName().contains("otherMethod")) { return; } - - MethodParameter refiedObjectMethodParameter = new MethodParameter(method, 0).withContainingClass(Concrete.class); - ResolvableType resolvedObjectParameterType = ResolvableType.forMethodParameter(refiedObjectMethodParameter); - assertThat(TypeNames.typeName(resolvedObjectParameterType)).isEqualTo(TypeVariableName.get("RT")); - assertThat(TypeNames.resolvedTypeName(resolvedObjectParameterType)).isEqualTo(TypeName.get(Object.class)); - - MethodParameter refiedCollectionMethodParameter = new MethodParameter(method, 1) - .withContainingClass(Concrete.class); - ResolvableType resolvedCollectionParameterType = ResolvableType - .forMethodParameter(refiedCollectionMethodParameter); - assertThat(TypeNames.typeName(resolvedCollectionParameterType)) - .isEqualTo(ParameterizedTypeName.get(ClassName.get(java.util.List.class), TypeVariableName.get("RT"))); - assertThat(TypeNames.resolvedTypeName(resolvedCollectionParameterType)) - .isEqualTo(ClassName.get(java.util.List.class)); - - MethodParameter refiedArrayMethodParameter = new MethodParameter(method, 2).withContainingClass(Concrete.class); - ResolvableType resolvedArrayParameterType = ResolvableType.forMethodParameter(refiedArrayMethodParameter); - assertThat(TypeNames.typeName(resolvedArrayParameterType)).extracting(TypeName::toString).isEqualTo("RT[]"); - assertThat(TypeNames.resolvedTypeName(resolvedArrayParameterType)).extracting(TypeName::toString) - .isEqualTo("java.lang.Object[]"); - - ResolvableType resolvedReturnType = ResolvableType.forMethodReturnType(method, Concrete.class); - assertThat(TypeNames.typeName(resolvedReturnType)).extracting(TypeName::toString).isEqualTo("RT"); - assertThat(TypeNames.resolvedTypeName(resolvedReturnType)).isEqualTo(TypeName.get(Object.class)); + methods.add(method); }); - ReflectionUtils.doWithMethods(Concrete.class, method -> { - if (!method.getName().contains("findByLocationNear")) { - return; - } + return methods; + } - ResolvableType resolvedReturnType = ResolvableType.forMethodReturnType(method, Concrete.class); + @ParameterizedTest // GH-3374 + @MethodSource("concreteMethods") + void resolvedTypeNamesForMethodParameters(Method method) { + + MethodParameter refiedObjectMethodParameter = new MethodParameter(method, 0).withContainingClass(Concrete.class); + ResolvableType resolvedObjectParameterType = ResolvableType.forMethodParameter(refiedObjectMethodParameter); + assertThat(TypeNames.typeName(resolvedObjectParameterType)).isEqualTo(TypeVariableName.get("T")); + assertThat(TypeNames.resolvedTypeName(resolvedObjectParameterType)).isEqualTo(TypeName.get(MyType.class)); + + MethodParameter refiedCollectionMethodParameter = new MethodParameter(method, 1) + .withContainingClass(Concrete.class); + ResolvableType resolvedCollectionParameterType = ResolvableType.forMethodParameter(refiedCollectionMethodParameter); + assertThat(TypeNames.typeName(resolvedCollectionParameterType)) + .isEqualTo(ParameterizedTypeName.get(ClassName.get(java.util.List.class), TypeVariableName.get("T"))); + assertThat(TypeNames.resolvedTypeName(resolvedCollectionParameterType)) + .isEqualTo(ParameterizedTypeName.get(java.util.List.class, MyType.class)); + + MethodParameter refiedArrayMethodParameter = new MethodParameter(method, 2).withContainingClass(Concrete.class); + ResolvableType resolvedArrayParameterType = ResolvableType.forMethodParameter(refiedArrayMethodParameter); + assertThat(TypeNames.typeName(resolvedArrayParameterType)).extracting(TypeName::toString).isEqualTo("T[]"); + assertThat(TypeNames.resolvedTypeName(resolvedArrayParameterType)).extracting(TypeName::toString) + .isEqualTo("org.springframework.data.javapoet.TypeNamesUnitTests.MyType[]"); + + ResolvableType resolvedReturnType = ResolvableType.forMethodReturnType(method, Concrete.class); + assertThat(TypeNames.typeName(resolvedReturnType)) + .isEqualTo(ParameterizedTypeName.get(ClassName.get(java.util.List.class), TypeVariableName.get("T"))); + assertThat(TypeNames.resolvedTypeName(resolvedReturnType)) + .isEqualTo(ParameterizedTypeName.get(java.util.List.class, MyType.class)); - assertThat(TypeNames.typeName(resolvedReturnType)).extracting(TypeName::toString).isEqualTo( - "java.util.List>"); - assertThat(TypeNames.resolvedTypeName(resolvedReturnType)).isEqualTo(ParameterizedTypeName - .get(ClassName.get(java.util.List.class), ParameterizedTypeName.get(GeoResult.class, MyType.class))); - }); + } + + @ParameterizedTest // GH-3374 + @MethodSource("otherMethods") + void resolvedTypeNamesForOtherMethodParameters(Method method) { + + MethodParameter refiedObjectMethodParameter = new MethodParameter(method, 0).withContainingClass(Concrete.class); + ResolvableType resolvedObjectParameterType = ResolvableType.forMethodParameter(refiedObjectMethodParameter); + assertThat(TypeNames.typeName(resolvedObjectParameterType)).isEqualTo(TypeVariableName.get("RT")); + assertThat(TypeNames.resolvedTypeName(resolvedObjectParameterType)).isEqualTo(TypeName.get(Object.class)); + + MethodParameter refiedCollectionMethodParameter = new MethodParameter(method, 1) + .withContainingClass(Concrete.class); + ResolvableType resolvedCollectionParameterType = ResolvableType.forMethodParameter(refiedCollectionMethodParameter); + assertThat(TypeNames.typeName(resolvedCollectionParameterType)) + .isEqualTo(ParameterizedTypeName.get(ClassName.get(java.util.List.class), TypeVariableName.get("RT"))); + assertThat(TypeNames.resolvedTypeName(resolvedCollectionParameterType)) + .isEqualTo(ClassName.get(java.util.List.class)); + + MethodParameter refiedArrayMethodParameter = new MethodParameter(method, 2).withContainingClass(Concrete.class); + ResolvableType resolvedArrayParameterType = ResolvableType.forMethodParameter(refiedArrayMethodParameter); + assertThat(TypeNames.typeName(resolvedArrayParameterType)).extracting(TypeName::toString).isEqualTo("RT[]"); + assertThat(TypeNames.resolvedTypeName(resolvedArrayParameterType)).extracting(TypeName::toString) + .isEqualTo("java.lang.Object[]"); + + ResolvableType resolvedReturnType = ResolvableType.forMethodReturnType(method, Concrete.class); + assertThat(TypeNames.typeName(resolvedReturnType)).extracting(TypeName::toString).isEqualTo("RT"); + assertThat(TypeNames.resolvedTypeName(resolvedReturnType)).isEqualTo(TypeName.get(Object.class)); + } + + @Test // GH-3374 + void resolvesTypeNamesForMethodParameters() throws NoSuchMethodException { + + Method method = Concrete.class.getDeclaredMethod("findByLocationNear", Point.class, Distance.class); + + ResolvableType resolvedReturnType = ResolvableType.forMethodReturnType(method, Concrete.class); + assertThat(TypeNames.typeName(resolvedReturnType)).extracting(TypeName::toString).isEqualTo( + "java.util.List>"); + assertThat(TypeNames.resolvedTypeName(resolvedReturnType)).isEqualTo(ParameterizedTypeName + .get(ClassName.get(java.util.List.class), ParameterizedTypeName.get(GeoResult.class, MyType.class))); } interface GenericBase { From cfbdafc44bfa7d68722949f568ffeafd63676961 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 10 Oct 2025 18:21:18 +0200 Subject: [PATCH 7/7] Introduce ResolvableGenerics type. Accept simple usages that can be resolved, document resolvable and unresolvable scenarios. --- .../aot/generate/AotRepositoryCreator.java | 210 +++++++++++++++++- .../AotRepositoryCreatorUnitTests.java | 115 ++++++++-- 2 files changed, 303 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java index af98a0fc9d..24dbf83deb 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java @@ -16,17 +16,24 @@ package org.springframework.data.repository.aot.generate; import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.function.Consumer; +import java.util.function.Predicate; import javax.lang.model.element.Modifier; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; + import org.springframework.core.ResolvableType; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryInformation; @@ -34,6 +41,7 @@ import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.util.Lazy; +import org.springframework.data.util.TypeInformation; import org.springframework.javapoet.ClassName; import org.springframework.javapoet.FieldSpec; import org.springframework.javapoet.MethodSpec; @@ -299,7 +307,7 @@ private void contributeMethod(Method method, @Nullable MethodContributorFactory return; } - if (hasUnresolvableGenerics(method)) { + if (ResolvableGenerics.of(method, repositoryInformation.getRepositoryInterface()).hasUnresolvableGenerics()) { if (logger.isTraceEnabled()) { logger.trace( @@ -332,22 +340,208 @@ private void contributeMethod(Method method, @Nullable MethodContributorFactory generationMetadata.addRepositoryMethod(method, contributor); } - private boolean hasUnresolvableGenerics(Method method) { + /** + * Value object to determine whether generics in a given {@link Method} can be resolved. Resolvable generics are e.g. + * declared on the method level (unbounded type variables, type variables using class boundaries). Considers + * collections and map types. + *

+ * Considers resolvable: + *

    + *
  • Unbounded method-level type parameters {@code T foo(Class)}
  • + *
  • Bounded method-level type parameters that resolve to a class + * {@code T foo(Class)}
  • + *
  • Simple references to interface variables {@code T foo(), List foo(…)}
  • + *
  • Unbounded wildcards {@code User foo(GeoJson)}
  • + *
+ * Considers non-resolvable: + *
    + *
  • Parametrized bounds referring to known variables on method-level type parameters + * {@code

    T foo(Class), List foo()}

  • + *
  • Generally unresolvable generics
  • + *
+ *

+ * + * @author Mark Paluch + */ + record ResolvableGenerics(Method method, Class implClass, Set resolvableTypeVariables, + Set unwantedMethodVariables) { + + /** + * Create a new {@code ResolvableGenerics} object for the given {@link Method}. + * + * @param method + * @return + */ + public static ResolvableGenerics of(Method method, Class implClass) { + return new ResolvableGenerics(method, implClass, getResolvableTypeVariables(method), + getUnwantedMethodVariables(method)); + } + + private static Set getResolvableTypeVariables(Method method) { + + Set simpleTypeVariables = new HashSet<>(); + + for (TypeVariable typeParameter : method.getTypeParameters()) { + if (isClassBounded(typeParameter.getBounds())) { + simpleTypeVariables.add(typeParameter); + } + } + + return simpleTypeVariables; + } + + private static Set getUnwantedMethodVariables(Method method) { + + Set unwanted = new HashSet<>(); + + for (TypeVariable typeParameter : method.getTypeParameters()) { + if (!isClassBounded(typeParameter.getBounds())) { + unwanted.add(typeParameter); + } + } + return unwanted; + } + + /** + * Check whether the {@link Method} has unresolvable generics when being considered in the context of the + * implementation class. + * + * @return + */ + public boolean hasUnresolvableGenerics() { + + ResolvableType resolvableType = ResolvableType.forMethodReturnType(method, implClass); + + if (isUnresolvable(resolvableType)) { + return true; + } + + for (int i = 0; i < method.getParameterCount(); i++) { + if (isUnresolvable(ResolvableType.forMethodParameter(method, i, implClass))) { + return true; + } + } + + return false; + } + + private boolean isUnresolvable(TypeInformation typeInformation) { + return isUnresolvable(typeInformation.toResolvableType()); + } + + private boolean isUnresolvable(ResolvableType resolvableType) { + + if (isResolvable(resolvableType)) { + return false; + } + + if (isUnwanted(resolvableType)) { + return true; + } + + if (resolvableType.isAssignableFrom(Class.class)) { + return isUnresolvable(resolvableType.getGeneric(0)); + } + + TypeInformation typeInformation = TypeInformation.of(resolvableType); + if (typeInformation.isMap() || typeInformation.isCollectionLike()) { + + for (ResolvableType type : resolvableType.getGenerics()) { + if (isUnresolvable(type)) { + return true; + } + } + + return false; + } + + if (typeInformation.getActualType() != null && typeInformation.getActualType() != typeInformation) { + return isUnresolvable(typeInformation.getRequiredActualType()); + } + + return resolvableType.hasUnresolvableGenerics(); + } + + private boolean isResolvable(Type[] types) { + + for (Type type : types) { + + if (resolvableTypeVariables.contains(type)) { + continue; + } + + if (isClass(type)) { + continue; + } + + return false; + } - if (ResolvableType.forMethodReturnType(method, repositoryInformation.getRepositoryInterface()) - .hasUnresolvableGenerics()) { return true; } - for (int i = 0; i < method.getParameterCount(); i++) { + private boolean isResolvable(ResolvableType resolvableType) { + + return testGenericType(resolvableType, it -> { + + if (resolvableTypeVariables.contains(it)) { + return true; + } + + if (it instanceof WildcardType wt) { + return isClassBounded(wt.getLowerBounds()) && isClassBounded(wt.getUpperBounds()); + } + + return false; + }); + } + + private boolean isUnwanted(ResolvableType resolvableType) { + + return testGenericType(resolvableType, o -> { + + if (o instanceof WildcardType wt) { + return !isResolvable(wt.getLowerBounds()) || !isResolvable(wt.getUpperBounds()); + } + + return unwantedMethodVariables.contains(o); + }); + } + + private static boolean testGenericType(ResolvableType resolvableType, Predicate predicate) { - if (ResolvableType.forMethodParameter(method, i, repositoryInformation.getRepositoryInterface()) - .hasUnresolvableGenerics()) { + if (predicate.test(resolvableType.getType())) { return true; } + + ResolvableType[] generics = resolvableType.getGenerics(); + for (ResolvableType generic : generics) { + if (testGenericType(generic, predicate)) { + return true; + } + } + + return false; + } + + private static boolean isClassBounded(Type[] bounds) { + + for (Type bound : bounds) { + + if (isClass(bound)) { + continue; + } + + return false; + } + + return true; + } + + private static boolean isClass(Type type) { + return type instanceof Class; } - return false; } /** diff --git a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java index 6677b79a52..393e05bcc4 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java @@ -15,24 +15,31 @@ */ package org.springframework.data.repository.aot.generate; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assumptions.*; +import static org.mockito.Mockito.*; import example.UserRepository.User; +import java.lang.reflect.Method; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.TimeZone; +import java.util.stream.Stream; import javax.lang.model.element.Modifier; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Answers; + import org.springframework.aot.generate.Generated; import org.springframework.aot.hint.TypeReference; import org.springframework.core.ResolvableType; +import org.springframework.data.domain.Range; import org.springframework.data.geo.Metric; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.querydsl.QuerydslPredicateExecutor; @@ -66,7 +73,8 @@ void beforeEach() { doReturn(UserRepository.class).when(repositoryInformation).getRepositoryInterface(); } - @Test // GH-3279 + @Test + // GH-3279 void writesClassSkeleton() { AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons", @@ -79,7 +87,8 @@ void writesClassSkeleton() { .contains("public UserRepositoryImpl"); // default constructor if not arguments to wire } - @Test // GH-3279 + @Test + // GH-3279 void appliesCtorArguments() { AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons", @@ -103,7 +112,8 @@ void appliesCtorArguments() { .doesNotContain("this.ctorScoped = ctorScoped"); } - @Test // GH-3279 + @Test + // GH-3279 void appliesCtorCodeBlock() { AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons", @@ -117,7 +127,8 @@ void appliesCtorCodeBlock() { "UserRepositoryImpl() { throw new IllegalStateException(\"initialization error\"); }"); } - @Test // GH-3279 + @Test + // GH-3279 void appliesClassCustomizations() { AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons", @@ -145,7 +156,8 @@ void appliesClassCustomizations() { .containsIgnoringWhitespaces("void oops() { }"); } - @Test // GH-3279 + @Test + // GH-3279 void appliesQueryMethodContributor() { AotRepositoryInformation repositoryInformation = new AotRepositoryInformation( @@ -174,7 +186,8 @@ public boolean contributesMethodSpec() { .containsIgnoringWhitespaces("void oops() { }"); } - @Test // GH-3279 + @Test + // GH-3279 void shouldContributeFragmentImplementationMetadata() { AotRepositoryInformation repositoryInformation = new AotRepositoryInformation( @@ -194,7 +207,8 @@ void shouldContributeFragmentImplementationMetadata() { assertThat(method.fragment().implementation()).isEqualTo(DummyQuerydslPredicateExecutor.class.getName()); } - @Test // GH-3339 + @Test + // GH-3339 void usesTargetTypeName() { AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons", @@ -212,7 +226,8 @@ void usesTargetTypeName() { .contains("public %s(Metric param1, String param2, Object ctorScoped)".formatted(targetType.getSimpleName())); } - @Test // GH-3339 + @Test + // GH-3339 void usesGenericConstructorArguments() { AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons", @@ -231,7 +246,8 @@ void usesGenericConstructorArguments() { "public %s(List param1, String param2, Object ctorScoped)".formatted(targetType.getSimpleName())); } - @Test // GH-3374 + @Test + // GH-3374 void skipsMethodWithUnresolvableGenericReturnType() { SpelAwareProxyProjectionFactory spelAwareProxyProjectionFactory = new SpelAwareProxyProjectionFactory(); @@ -260,7 +276,55 @@ public boolean contributesMethodSpec() { }); // same package as source repo - assertThat(generate(repositoryCreator)).contains("someMethod()").doesNotContain("findByFirstname()"); + String generated = generate(repositoryCreator); + + assertThat(generated).contains("someMethod").contains("findByFirstname").contains("project1ByFirstname") + .contains("project2ByFirstname").contains("geoQuery").contains("rangeQuery"); + + assertThat(generated).doesNotContain("baseProjection").doesNotContain("upperBoundedProjection") + .doesNotContain("lowerBoundedProjection()"); + } + + static Stream declaredUserRepositoryMethods() { + return Arrays.stream(UserRepository.class.getDeclaredMethods()); + } + + static Stream unresolvedRepositoryMethods() { + return Arrays.stream(UserRepository.class.getMethods()) + .filter(it -> it.getDeclaringClass().equals(BaseRepository.class)) + .filter(it -> it.getName().startsWith("upper") || it.getName().startsWith("lower")); + } + + static Stream resolvedRepositoryMethods() { + return Arrays.stream(UserRepository.class.getMethods()) + .filter(it -> it.getDeclaringClass().equals(BaseRepository.class)) + .filter(it -> it.getName().startsWith("parametrized")); + } + + @ParameterizedTest + @MethodSource("declaredUserRepositoryMethods") + void shouldResolveGenerics(Method method) { + + assertThat(AotRepositoryCreator.ResolvableGenerics.of(method, UserRepository.class).hasUnresolvableGenerics()) + .isFalse(); + } + + @ParameterizedTest + @MethodSource("resolvedRepositoryMethods") + void shouldResolveInterfaceGenerics(Method method) { + + assertThat(AotRepositoryCreator.ResolvableGenerics.of(method, UserRepository.class).hasUnresolvableGenerics()) + .isFalse(); + } + + @ParameterizedTest + @MethodSource("unresolvedRepositoryMethods") + void shouldReportUnresolvedGenerics(Method method) { + + assumeThat(method.getDeclaringClass()).isEqualTo(BaseRepository.class); + + assertThat(AotRepositoryCreator.ResolvableGenerics.of(method, UserRepository.class).hasUnresolvableGenerics()) + .isTrue(); } private AotRepositoryCreator.AotBundle doCreate(AotRepositoryCreator creator) { @@ -294,13 +358,36 @@ private static TypeSpec.Builder getTypeSpecBuilder(ClassName className) { return TypeSpec.classBuilder(className).addAnnotation(Generated.class); } - interface UserRepository extends org.springframework.data.repository.Repository { + interface BaseRepository extends org.springframework.data.repository.Repository { + +

List

upperBoundedProjection(String firstname, Class type); + + List lowerBoundedProjection(String firstname, Class type); + + List parametrizedListProjection(String firstname, Class type); + + T parametrizedSelection(String firstname); + + } + + interface UserRepository extends BaseRepository { String someMethod(); List findByFirstname(String firstname, Class type); + + List project1ByFirstname(String firstname, Class type); + + List project2ByFirstname(String firstname, Class type); + + List geoQuery(GeoJson geoJson); + + List rangeQuery(Range geoJson); + } + public interface GeoJson> {} + interface QuerydslUserRepository extends org.springframework.data.repository.Repository, QuerydslPredicateExecutor {