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 24dbf83de..5bfcba6da 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,32 +16,23 @@ 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; import org.springframework.data.repository.core.support.RepositoryComposition; 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; @@ -340,210 +331,6 @@ class AotRepositoryCreator { generationMetadata.addRepositoryMethod(method, contributor); } - /** - * 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: - *

- * Considers non-resolvable: - * - *

- * - * @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; - } - - return true; - } - - 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 (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; - } - - } - /** * Factory interface to conditionally create {@link MethodContributor} instances. An implementation may decide whether * to return a {@link MethodContributor} or {@literal null}, if no method (code or metadata) should be contributed. 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 cebc6013e..a41729d2e 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 @@ -26,7 +26,6 @@ import java.util.Map.Entry; import java.util.function.Function; import org.jspecify.annotations.Nullable; - import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; @@ -36,6 +35,7 @@ import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.util.TypeInformation; import org.springframework.javapoet.ParameterSpec; import org.springframework.javapoet.TypeName; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** @@ -79,14 +79,17 @@ class MethodMetadata { ResolvableType repositoryInterface, Map methodArguments, Map methodParameters) { + Class repositoryInterfaceType = repositoryInterface.toClass(); + for (Parameter parameter : method.getParameters()) { - MethodParameter methodParameter = MethodParameter.forParameter(parameter).withContainingClass(repositoryInterface.resolve()); + MethodParameter methodParameter = MethodParameter.forParameter(parameter) + .withContainingClass(repositoryInterfaceType); methodParameter.initParameterNameDiscovery(nameDiscoverer); - ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(methodParameter); - TypeName parameterType = TypeNames.resolvedTypeName(resolvableParameterType); + TypeName parameterType = parameterTypeName(methodParameter, repositoryInterfaceType); + Assert.notNull(methodParameter.getParameterName(), "MethodParameter.getParameterName() must not be null"); ParameterSpec parameterSpec = ParameterSpec.builder(parameterType, methodParameter.getParameterName()).build(); if (methodArguments.containsKey(parameterSpec.name())) { @@ -98,6 +101,20 @@ class MethodMetadata { } } + @SuppressWarnings("NullAway") + static TypeName parameterTypeName(MethodParameter methodParameter, Class repositoryInterface) { + + ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(methodParameter); + if (ClassUtils.isPrimitiveOrWrapper(resolvableParameterType.toClass())) { + return TypeNames.className(resolvableParameterType); + } + + ResolvableGenerics resolvableGenerics = ResolvableGenerics.of(methodParameter.getMethod(), repositoryInterface); + return resolvableGenerics.isFullyResolvableParameter(methodParameter.getParameter()) + ? TypeNames.resolvedTypeName(resolvableParameterType) + : TypeNames.className(resolvableParameterType); + } + ResolvableType getReturnType() { return returnType; } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/ResolvableGenerics.java b/src/main/java/org/springframework/data/repository/aot/generate/ResolvableGenerics.java new file mode 100644 index 000000000..f865d7366 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/ResolvableGenerics.java @@ -0,0 +1,259 @@ +/* + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.aot.generate; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.data.util.TypeInformation; + +/** + * 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)); + } + + /** + * Checks if a given {@link Parameter} can be resolved fully. Unresolvable or unwanted type signatures should fall + * back to using raw types. + * + * @param parameter + * @return + */ + public boolean isFullyResolvableParameter(Parameter parameter) { + + MethodParameter methodParameter = MethodParameter.forParameter(parameter).withContainingClass(implClass); + ResolvableType resolvableType = ResolvableType.forMethodParameter(methodParameter); + + return testGenericType(resolvableType, o -> { + + if (o instanceof WildcardType wt) { + return !isResolvable(wt.getLowerBounds()) || !isResolvable(wt.getUpperBounds()); + } + + if (o instanceof ParameterizedType pt) { + return isResolvable(pt.getActualTypeArguments()); + } + + return unwantedMethodVariables.contains(o); + }); + } + + 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; + } + + return true; + } + + 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 (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; + } +} 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 393e05bcc..72cf43f24 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,9 +15,10 @@ */ package org.springframework.data.repository.aot.generate; -import static org.assertj.core.api.Assertions.*; -import static org.assertj.core.api.Assumptions.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import example.UserRepository.User; @@ -35,7 +36,6 @@ 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; @@ -51,10 +51,12 @@ 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.CodeBlock; import org.springframework.javapoet.JavaFile; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.TypeSpec; import org.springframework.stereotype.Repository; +import org.springframework.util.ClassUtils; /** * Unit tests for {@link AotRepositoryCreator}. @@ -73,8 +75,7 @@ class AotRepositoryCreatorUnitTests { doReturn(UserRepository.class).when(repositoryInformation).getRepositoryInterface(); } - @Test - // GH-3279 + @Test // GH-3279 void writesClassSkeleton() { AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons", @@ -87,8 +88,7 @@ class AotRepositoryCreatorUnitTests { .contains("public UserRepositoryImpl"); // default constructor if not arguments to wire } - @Test - // GH-3279 + @Test // GH-3279 void appliesCtorArguments() { AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons", @@ -112,8 +112,7 @@ class AotRepositoryCreatorUnitTests { .doesNotContain("this.ctorScoped = ctorScoped"); } - @Test - // GH-3279 + @Test // GH-3279 void appliesCtorCodeBlock() { AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons", @@ -127,8 +126,7 @@ class AotRepositoryCreatorUnitTests { "UserRepositoryImpl() { throw new IllegalStateException(\"initialization error\"); }"); } - @Test - // GH-3279 + @Test // GH-3279 void appliesClassCustomizations() { AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons", @@ -156,8 +154,7 @@ class AotRepositoryCreatorUnitTests { .containsIgnoringWhitespaces("void oops() { }"); } - @Test - // GH-3279 + @Test // GH-3279 void appliesQueryMethodContributor() { AotRepositoryInformation repositoryInformation = new AotRepositoryInformation( @@ -186,8 +183,7 @@ class AotRepositoryCreatorUnitTests { .containsIgnoringWhitespaces("void oops() { }"); } - @Test - // GH-3279 + @Test // GH-3279 void shouldContributeFragmentImplementationMetadata() { AotRepositoryInformation repositoryInformation = new AotRepositoryInformation( @@ -207,8 +203,7 @@ class AotRepositoryCreatorUnitTests { assertThat(method.fragment().implementation()).isEqualTo(DummyQuerydslPredicateExecutor.class.getName()); } - @Test - // GH-3339 + @Test // GH-3339 void usesTargetTypeName() { AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons", @@ -226,8 +221,7 @@ class AotRepositoryCreatorUnitTests { .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", @@ -246,8 +240,7 @@ class AotRepositoryCreatorUnitTests { "public %s(List param1, String param2, Object ctorScoped)".formatted(targetType.getSimpleName())); } - @Test - // GH-3374 + @Test // GH-3374 void skipsMethodWithUnresolvableGenericReturnType() { SpelAwareProxyProjectionFactory spelAwareProxyProjectionFactory = new SpelAwareProxyProjectionFactory(); @@ -261,27 +254,48 @@ class AotRepositoryCreatorUnitTests { 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; + return MethodContributor.forQueryMethod(queryMethod).withMetadata(Map::of).contribute(context -> { + + CodeBlock.Builder builder = CodeBlock.builder(); + if (!ClassUtils.isVoidType(method.getReturnType())) { + builder.addStatement("return null"); } - }; + return builder.build(); + }); }); // same package as source repo String generated = generate(repositoryCreator); - assertThat(generated).contains("someMethod").contains("findByFirstname").contains("project1ByFirstname") - .contains("project2ByFirstname").contains("geoQuery").contains("rangeQuery"); + assertThat(generated) // + // very simple ones with fixed type signature + .contains("findByFirstname") // + .contains("rangeQuery") // + .contains("public String someMethod()") // + .contains("public String findByPrimitive(int value)") // + .contains("public String findByPrimitiveArray(int[] values)") // + .containsSubsequence("public", "User typedInputParameter(", "User value)") // + .containsSubsequence("List<", "User> findByFirstnameIn(List firstname)") // - assertThat(generated).doesNotContain("baseProjection").doesNotContain("upperBoundedProjection") + // List findByFirstname(String firstname, Class type); + .contains("public List findByFirstname(String firstname, Class type)") + + // List project1ByFirstname(String firstname, Class type); + .contains("public List project2ByFirstname(String firstname, Class type)") + + // List project2ByFirstname(String firstname, Class type) + .contains("public List project2ByFirstname(String firstname, Class type)") + + // List geoQuery(GeoJson geoJson); + .containsSubsequence("public List<", "User> geoQuery(", "GeoJson geoJson)") + + // List genInputType(String firstname, T value); + .contains("public List genInputType(String firstname, Object value)") + + .doesNotContain("baseProjection") // + .doesNotContain("upperBoundedProjection") // .doesNotContain("lowerBoundedProjection()"); } @@ -305,16 +319,14 @@ class AotRepositoryCreatorUnitTests { @MethodSource("declaredUserRepositoryMethods") void shouldResolveGenerics(Method method) { - assertThat(AotRepositoryCreator.ResolvableGenerics.of(method, UserRepository.class).hasUnresolvableGenerics()) - .isFalse(); + assertThat(ResolvableGenerics.of(method, UserRepository.class).hasUnresolvableGenerics()).isFalse(); } @ParameterizedTest @MethodSource("resolvedRepositoryMethods") void shouldResolveInterfaceGenerics(Method method) { - assertThat(AotRepositoryCreator.ResolvableGenerics.of(method, UserRepository.class).hasUnresolvableGenerics()) - .isFalse(); + assertThat(ResolvableGenerics.of(method, UserRepository.class).hasUnresolvableGenerics()).isFalse(); } @ParameterizedTest @@ -323,8 +335,7 @@ class AotRepositoryCreatorUnitTests { assumeThat(method.getDeclaringClass()).isEqualTo(BaseRepository.class); - assertThat(AotRepositoryCreator.ResolvableGenerics.of(method, UserRepository.class).hasUnresolvableGenerics()) - .isTrue(); + assertThat(ResolvableGenerics.of(method, UserRepository.class).hasUnresolvableGenerics()).isTrue(); } private AotRepositoryCreator.AotBundle doCreate(AotRepositoryCreator creator) { @@ -368,14 +379,24 @@ class AotRepositoryCreatorUnitTests { T parametrizedSelection(String firstname); + T typedInputParameter(T value); + } interface UserRepository extends BaseRepository { String someMethod(); + List findByFirstnameIn(List firstname); + + String findByPrimitive(int value); + + String findByPrimitiveArray(int[] values); + List findByFirstname(String firstname, Class type); + List genInputType(String firstname, T value); + List project1ByFirstname(String firstname, Class type); List project2ByFirstname(String firstname, Class type);