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);
+
+
+ *
+ * @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 super T> 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 super T> type);
+
+ List project2ByFirstname(String firstname, Class extends T> type);
+
+ List geoQuery(GeoJson> geoJson);
+
+ List rangeQuery(Range> geoJson);
+
}
+ public interface GeoJson> {}
+
interface QuerydslUserRepository
extends org.springframework.data.repository.Repository, QuerydslPredicateExecutor {