From 6e5a18609f416ccb1cdcceef1ef8de1f00d58d97 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 19 Feb 2026 08:51:53 +0100 Subject: [PATCH] Render method parameter annotations in AOT repositories. We now include annotations of repository query method parameters to allow introspection through Parameters for reflective checks. See spring-projects/spring-data-relational#2245 --- .../aot/generate/MethodMetadata.java | 20 +++++++++++++++++-- src/test/java/example/UserRepository.java | 3 ++- .../AotRepositoryMethodBuilderUnitTests.java | 6 +++--- .../aot/generate/MethodMetadataUnitTests.java | 13 +++++++++++- 4 files changed, 35 insertions(+), 7 deletions(-) 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 dea656a64..f9b657c31 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 @@ -15,6 +15,7 @@ */ package org.springframework.data.repository.aot.generate; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.ArrayList; @@ -29,9 +30,12 @@ import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.data.core.TypeInformation; import org.springframework.data.javapoet.TypeNames; import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.javapoet.AnnotationSpec; import org.springframework.javapoet.ParameterSpec; import org.springframework.javapoet.TypeName; import org.springframework.util.Assert; @@ -87,7 +91,7 @@ class MethodMetadata { TypeName parameterType = parameterTypeName(methodParameter, repositoryInterfaceType); Assert.notNull(methodParameter.getParameterName(), "MethodParameter.getParameterName() must not be null"); - ParameterSpec parameterSpec = ParameterSpec.builder(parameterType, methodParameter.getParameterName()).build(); + ParameterSpec parameterSpec = buildParameter(parameterType, methodParameter); if (methodArguments.containsKey(parameterSpec.name())) { throw new IllegalStateException("Parameter with name '" + parameterSpec.name() + "' already exists."); @@ -98,7 +102,19 @@ class MethodMetadata { } } - @SuppressWarnings("NullAway") + private static ParameterSpec buildParameter(TypeName parameterType, MethodParameter methodParameter) { + + ParameterSpec.Builder builder = ParameterSpec.builder(parameterType, methodParameter.getParameterName()); + MergedAnnotations annotations = MergedAnnotations.from(methodParameter.getParameterAnnotations()); + + for (MergedAnnotation annotation : annotations) { + builder.addAnnotation(AnnotationSpec.get(annotation.synthesize())); + } + + return builder.build(); + } + + @SuppressWarnings("NullAway") static TypeName parameterTypeName(MethodParameter methodParameter, Class repositoryInterface) { ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(methodParameter); diff --git a/src/test/java/example/UserRepository.java b/src/test/java/example/UserRepository.java index ef224823e..4e1ede930 100644 --- a/src/test/java/example/UserRepository.java +++ b/src/test/java/example/UserRepository.java @@ -20,13 +20,14 @@ import example.UserRepository.User; import java.util.List; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; /** * @author Christoph Strobl */ public interface UserRepository extends CrudRepository, UserRepositoryExtension { - User findByFirstname(String firstname); + User findByFirstname(@Param("hello") String firstname); List findByFirstnameIn(List firstnames); 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 76e5fd541..0fdfaa6f3 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 @@ -55,7 +55,7 @@ class AotRepositoryMethodBuilderUnitTests { when(methodGenerationContext.getExpressionMarker()).thenReturn(new ExpressionMarker()); } - @Test // GH-3279 + @Test // GH-3279, GH-3458 void generatesMethodSkeletonBasedOnGenerationMetadata() throws NoSuchMethodException { Method method = UserRepository.class.getMethod("findByFirstname", String.class); @@ -66,8 +66,8 @@ class AotRepositoryMethodBuilderUnitTests { when(methodGenerationContext.getTargetMethodMetadata()).thenReturn(methodMetadata); AotRepositoryMethodBuilder builder = new AotRepositoryMethodBuilder(methodGenerationContext); - assertThat(builder.buildMethod().toString()) // - .containsPattern("public .*User findByFirstname\\(.*String firstname\\)"); + assertThat(builder.buildMethod().toString().replaceAll(System.lineSeparator(), " ")) // + .containsPattern("findByFirstname\\(.*@.*Param\\(\"hello\"\\).*String firstname\\)"); } @Test // GH-3279 diff --git a/src/test/java/org/springframework/data/repository/aot/generate/MethodMetadataUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/MethodMetadataUnitTests.java index 8e5123d97..aa726d396 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/MethodMetadataUnitTests.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/MethodMetadataUnitTests.java @@ -26,6 +26,8 @@ import org.mockito.Mockito; import org.springframework.data.core.TypeInformation; import org.springframework.data.domain.Pageable; import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.query.Param; +import org.springframework.javapoet.ParameterSpec; /** * Unit tests for {@link MethodMetadata}. @@ -44,6 +46,15 @@ class MethodMetadataUnitTests { assertThat(metadata.getParameterName(2)).isEqualTo("arg2"); } + @Test // GH-3458 + void addsAnnotations() throws NoSuchMethodException { + + MethodMetadata metadata = methodMetadataFor("threeArgsMethod"); + + ParameterSpec spec = metadata.getMethodArguments().get("arg2"); + assertThat(spec.annotations()).hasSize(1); + } + @Test // GH-3270 void getParameterNameByNonExistingIndex() throws NoSuchMethodException { @@ -82,6 +93,6 @@ class MethodMetadataUnitTests { String noArgsMethod(); - String threeArgsMethod(Object arg0, Pageable arg1, Object arg2); + String threeArgsMethod(Object arg0, Pageable arg1, @Param("foo") Object arg2); } }