From 5460d4dfc1e536b0379ef8677f5f10bb8220f9ad Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 30 Jul 2025 12:19:17 +0200 Subject: [PATCH 1/2] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index db0fdf4333..86a83ec605 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 4.0.0-SNAPSHOT + 4.0.x-AOT-EXPRESSION-MARKER-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. From 83ba7319703186c33904800508facc993d52c952 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 30 Jul 2025 13:12:36 +0200 Subject: [PATCH 2/2] Add ExpressionMarker abstraction for obtaining Method in AOT generated code. ExpressionMarker is a stateful abstraction that helps creating local classes used to obtain the enclosing method. The code generation will only add the local class when needed. Prior to this change markers had been added unconditionally to each and every method. --- .../AotQueryMethodGenerationContext.java | 12 ++- .../generate/AotRepositoryMethodBuilder.java | 3 + .../aot/generate/ExpressionMarker.java | 85 +++++++++++++++++++ .../AotRepositoryMethodBuilderUnitTests.java | 47 +++++++++- 4 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/aot/generate/ExpressionMarker.java diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java b/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java index a927b61ee8..69bd86c536 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java @@ -21,7 +21,6 @@ import java.util.List; import org.jspecify.annotations.Nullable; - import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.MergedAnnotation; @@ -50,6 +49,7 @@ public class AotQueryMethodGenerationContext { private final AotRepositoryFragmentMetadata targetTypeMetadata; private final MethodMetadata targetMethodMetadata; private final VariableNameFactory variableNameFactory; + private final ExpressionMarker expressionMarker; protected AotQueryMethodGenerationContext(RepositoryInformation repositoryInformation, Method method, QueryMethod queryMethod, AotRepositoryFragmentMetadata targetTypeMetadata) { @@ -61,6 +61,7 @@ protected AotQueryMethodGenerationContext(RepositoryInformation repositoryInform this.targetTypeMetadata = targetTypeMetadata; this.targetMethodMetadata = new MethodMetadata(repositoryInformation, method); this.variableNameFactory = LocalVariableNameFactory.forMethod(targetMethodMetadata); + this.expressionMarker = new ExpressionMarker(); } MethodMetadata getTargetMethodMetadata() { @@ -342,4 +343,13 @@ public String localVariable(String variableName) { return getParameterName(queryMethod.getParameters().getScoreRangeIndex()); } + /** + * Obtain the {@link ExpressionMarker} for the current method. Will add a local class within the method that can be + * referenced via {@link ExpressionMarker#enclosingMethod()}. + * + * @return the {@link ExpressionMarker} for this particular method. + */ + public ExpressionMarker getExpressionMarker() { + return expressionMarker; + } } 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 0994f234cd..c265d098c8 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 @@ -96,6 +96,9 @@ public MethodSpec buildMethod() { context.getMethod().getName(), StringUtils.collectionToCommaDelimitedString(context.getTargetMethodMetadata() .getMethodArguments().values().stream().map(it -> it.type.toString()).collect(Collectors.toList()))); context.getTargetMethodMetadata().getMethodArguments().forEach((name, spec) -> builder.addParameter(spec)); + if(context.getExpressionMarker().isInUse()) { + builder.addCode(context.getExpressionMarker().declaration()); + } builder.addCode(methodBody); customizer.accept(context, builder); diff --git a/src/main/java/org/springframework/data/repository/aot/generate/ExpressionMarker.java b/src/main/java/org/springframework/data/repository/aot/generate/ExpressionMarker.java new file mode 100644 index 0000000000..6397b7bdd1 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/ExpressionMarker.java @@ -0,0 +1,85 @@ +/* + * 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 org.springframework.javapoet.CodeBlock; + +/** + * ExpressionMarker is used to add a dedicated type to AOT generated methods that can be used to determine the current + * method by calling {@link Class#getEnclosingMethod()} on it. This can be useful when working with expressions (eg. + * SpEL) that need to be evaluated in a given context. + *

+ * {@link ExpressionMarker} is intended to be used via {@link AotQueryMethodGenerationContext} to maintain usage info, + * making sure the code is only added ({@link #isInUse()}) when {@link #enclosingMethod()} was called for generating + * code. + * + *

+ * ExpressionMarker marker = context.getExpressionMarker();
+ * CodeBlock.builder().add("evaluate($L, $S, $L)", marker.enclosingMethod(), queryString, parameters);
+ * 
+ * + * @author Christoph Strobl + * @since 4.0 + */ +public class ExpressionMarker { + + private final String typeName; + private boolean inUse = false; + + ExpressionMarker() { + this("ExpressionMarker"); + } + + ExpressionMarker(String typeName) { + this.typeName = typeName; + } + + /** + * @return {@code class ExpressionMarker}. + */ + CodeBlock declaration() { + return CodeBlock.of("class $L{};\n", typeName); + } + + /** + * Calling this method sets the {@link ExpressionMarker} as {@link #isInUse() in-use}. + * + * @return {@code ExpressionMarker.class}. + */ + public CodeBlock marker() { + + if (!inUse) { + inUse = true; + } + return CodeBlock.of("$L.class", typeName); + } + + /** + * Calling this method sets the {@link ExpressionMarker} as {@link #isInUse() in-use}. + * + * @return {@code ExpressionMarker.class.getEnclosingMethod()} + */ + public CodeBlock enclosingMethod() { + return CodeBlock.of("$L.getEnclosingMethod()", marker()); + } + + /** + * @return if the marker is in use. + */ + public boolean isInUse() { + return inUse; + } +} 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 a80559ebe8..fd8ad840b7 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 @@ -15,20 +15,24 @@ */ package org.springframework.data.repository.aot.generate; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; import example.UserRepository; import example.UserRepository.User; import java.lang.reflect.Method; import java.util.List; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; 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.mockito.Mockito; - import org.springframework.core.ResolvableType; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.util.TypeInformation; @@ -45,10 +49,12 @@ class AotRepositoryMethodBuilderUnitTests { @BeforeEach void beforeEach() { + repositoryInformation = Mockito.mock(RepositoryInformation.class); methodGenerationContext = Mockito.mock(AotQueryMethodGenerationContext.class); when(methodGenerationContext.getRepositoryInformation()).thenReturn(repositoryInformation); + when(methodGenerationContext.getExpressionMarker()).thenReturn(new ExpressionMarker()); } @Test // GH-3279 @@ -87,4 +93,37 @@ void generatesMethodWithGenerics() throws NoSuchMethodException { .containsPattern("public .*List<.*User> findByFirstnameIn\\(") // .containsPattern(".*List<.*String> firstnames\\)"); } + + @ParameterizedTest // GH-3279 + @MethodSource(value = { "expressionMarkers" }) + void generatesExpressionMarkerIfInUse(ExpressionMarker expressionMarker) throws NoSuchMethodException { + + 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); + methodMetadata.addParameter(ParameterSpec.builder(String.class, "firstname").build()); + when(methodGenerationContext.getTargetMethodMetadata()).thenReturn(methodMetadata); + when(methodGenerationContext.getExpressionMarker()).thenReturn(expressionMarker); + + AotRepositoryMethodBuilder builder = new AotRepositoryMethodBuilder(methodGenerationContext); + String methodCode = builder.buildMethod().toString(); + if (expressionMarker.isInUse()) { + assertThat(methodCode).contains("class ExpressionMarker{};"); + } else { + assertThat(methodCode).doesNotContain("class ExpressionMarker{};"); + } + } + + static Stream expressionMarkers() { + + ExpressionMarker unused = new ExpressionMarker(); + + ExpressionMarker used = new ExpressionMarker(); + used.marker(); + + return Stream.of(Arguments.of(unused), Arguments.of(used)); + } }