Skip to content

Commit 5bc8d22

Browse files
christophstroblmp911de
authored andcommitted
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. Closes #3338
1 parent 5cacf95 commit 5bc8d22

File tree

4 files changed

+142
-5
lines changed

4 files changed

+142
-5
lines changed

src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.util.List;
2222

2323
import org.jspecify.annotations.Nullable;
24-
2524
import org.springframework.core.MethodParameter;
2625
import org.springframework.core.ResolvableType;
2726
import org.springframework.core.annotation.MergedAnnotation;
@@ -50,6 +49,7 @@ public class AotQueryMethodGenerationContext {
5049
private final AotRepositoryFragmentMetadata targetTypeMetadata;
5150
private final MethodMetadata targetMethodMetadata;
5251
private final VariableNameFactory variableNameFactory;
52+
private final ExpressionMarker expressionMarker;
5353

5454
protected AotQueryMethodGenerationContext(RepositoryInformation repositoryInformation, Method method,
5555
QueryMethod queryMethod, AotRepositoryFragmentMetadata targetTypeMetadata) {
@@ -61,6 +61,7 @@ protected AotQueryMethodGenerationContext(RepositoryInformation repositoryInform
6161
this.targetTypeMetadata = targetTypeMetadata;
6262
this.targetMethodMetadata = new MethodMetadata(repositoryInformation, method);
6363
this.variableNameFactory = LocalVariableNameFactory.forMethod(targetMethodMetadata);
64+
this.expressionMarker = new ExpressionMarker();
6465
}
6566

6667
MethodMetadata getTargetMethodMetadata() {
@@ -342,4 +343,13 @@ public String localVariable(String variableName) {
342343
return getParameterName(queryMethod.getParameters().getScoreRangeIndex());
343344
}
344345

346+
/**
347+
* Obtain the {@link ExpressionMarker} for the current method. Will add a local class within the method that can be
348+
* referenced via {@link ExpressionMarker#enclosingMethod()}.
349+
*
350+
* @return the {@link ExpressionMarker} for this particular method.
351+
*/
352+
public ExpressionMarker getExpressionMarker() {
353+
return expressionMarker;
354+
}
345355
}

src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ public MethodSpec buildMethod() {
9696
context.getMethod().getName(), StringUtils.collectionToCommaDelimitedString(context.getTargetMethodMetadata()
9797
.getMethodArguments().values().stream().map(it -> it.type().toString()).collect(Collectors.toList())));
9898
context.getTargetMethodMetadata().getMethodArguments().forEach((name, spec) -> builder.addParameter(spec));
99+
if(context.getExpressionMarker().isInUse()) {
100+
builder.addCode(context.getExpressionMarker().declaration());
101+
}
99102
builder.addCode(methodBody);
100103
customizer.accept(context, builder);
101104

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.repository.aot.generate;
17+
18+
import org.springframework.javapoet.CodeBlock;
19+
20+
/**
21+
* ExpressionMarker is used to add a dedicated type to AOT generated methods that can be used to determine the current
22+
* method by calling {@link Class#getEnclosingMethod()} on it. This can be useful when working with expressions (eg.
23+
* SpEL) that need to be evaluated in a given context.
24+
* <p>
25+
* {@link ExpressionMarker} is intended to be used via {@link AotQueryMethodGenerationContext} to maintain usage info,
26+
* making sure the code is only added ({@link #isInUse()}) when {@link #enclosingMethod()} was called for generating
27+
* code.
28+
*
29+
* <pre class="code">
30+
* ExpressionMarker marker = context.getExpressionMarker();
31+
* CodeBlock.builder().add("evaluate($L, $S, $L)", marker.enclosingMethod(), queryString, parameters);
32+
* </pre>
33+
*
34+
* @author Christoph Strobl
35+
* @since 4.0
36+
*/
37+
public class ExpressionMarker {
38+
39+
private final String typeName;
40+
private boolean inUse = false;
41+
42+
ExpressionMarker() {
43+
this("ExpressionMarker");
44+
}
45+
46+
ExpressionMarker(String typeName) {
47+
this.typeName = typeName;
48+
}
49+
50+
/**
51+
* @return {@code class ExpressionMarker}.
52+
*/
53+
CodeBlock declaration() {
54+
return CodeBlock.of("class $L{};\n", typeName);
55+
}
56+
57+
/**
58+
* Calling this method sets the {@link ExpressionMarker} as {@link #isInUse() in-use}.
59+
*
60+
* @return {@code ExpressionMarker.class}.
61+
*/
62+
public CodeBlock marker() {
63+
64+
if (!inUse) {
65+
inUse = true;
66+
}
67+
return CodeBlock.of("$L.class", typeName);
68+
}
69+
70+
/**
71+
* Calling this method sets the {@link ExpressionMarker} as {@link #isInUse() in-use}.
72+
*
73+
* @return {@code ExpressionMarker.class.getEnclosingMethod()}
74+
*/
75+
public CodeBlock enclosingMethod() {
76+
return CodeBlock.of("$L.getEnclosingMethod()", marker());
77+
}
78+
79+
/**
80+
* @return if the marker is in use.
81+
*/
82+
public boolean isInUse() {
83+
return inUse;
84+
}
85+
}

src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilderUnitTests.java

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,24 @@
1515
*/
1616
package org.springframework.data.repository.aot.generate;
1717

18-
import static org.assertj.core.api.Assertions.*;
19-
import static org.mockito.ArgumentMatchers.*;
20-
import static org.mockito.Mockito.*;
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.mockito.ArgumentMatchers.any;
20+
import static org.mockito.Mockito.doReturn;
21+
import static org.mockito.Mockito.when;
2122

2223
import example.UserRepository;
2324
import example.UserRepository.User;
2425

2526
import java.lang.reflect.Method;
2627
import java.util.List;
28+
import java.util.stream.Stream;
2729

2830
import org.junit.jupiter.api.BeforeEach;
2931
import org.junit.jupiter.api.Test;
32+
import org.junit.jupiter.params.ParameterizedTest;
33+
import org.junit.jupiter.params.provider.Arguments;
34+
import org.junit.jupiter.params.provider.MethodSource;
3035
import org.mockito.Mockito;
31-
3236
import org.springframework.core.ResolvableType;
3337
import org.springframework.data.repository.core.RepositoryInformation;
3438
import org.springframework.data.util.TypeInformation;
@@ -45,10 +49,12 @@ class AotRepositoryMethodBuilderUnitTests {
4549

4650
@BeforeEach
4751
void beforeEach() {
52+
4853
repositoryInformation = Mockito.mock(RepositoryInformation.class);
4954
methodGenerationContext = Mockito.mock(AotQueryMethodGenerationContext.class);
5055

5156
when(methodGenerationContext.getRepositoryInformation()).thenReturn(repositoryInformation);
57+
when(methodGenerationContext.getExpressionMarker()).thenReturn(new ExpressionMarker());
5258
}
5359

5460
@Test // GH-3279
@@ -87,4 +93,37 @@ void generatesMethodWithGenerics() throws NoSuchMethodException {
8793
.containsPattern("public .*List<.*User> findByFirstnameIn\\(") //
8894
.containsPattern(".*List<.*String> firstnames\\)");
8995
}
96+
97+
@ParameterizedTest // GH-3279
98+
@MethodSource(value = { "expressionMarkers" })
99+
void generatesExpressionMarkerIfInUse(ExpressionMarker expressionMarker) throws NoSuchMethodException {
100+
101+
Method method = UserRepository.class.getMethod("findByFirstname", String.class);
102+
when(methodGenerationContext.getMethod()).thenReturn(method);
103+
when(methodGenerationContext.getReturnType()).thenReturn(ResolvableType.forClass(User.class));
104+
doReturn(TypeInformation.of(User.class)).when(repositoryInformation).getReturnType(any());
105+
doReturn(TypeInformation.of(User.class)).when(repositoryInformation).getReturnedDomainTypeInformation(any());
106+
MethodMetadata methodMetadata = new MethodMetadata(repositoryInformation, method);
107+
methodMetadata.addParameter(ParameterSpec.builder(String.class, "firstname").build());
108+
when(methodGenerationContext.getTargetMethodMetadata()).thenReturn(methodMetadata);
109+
when(methodGenerationContext.getExpressionMarker()).thenReturn(expressionMarker);
110+
111+
AotRepositoryMethodBuilder builder = new AotRepositoryMethodBuilder(methodGenerationContext);
112+
String methodCode = builder.buildMethod().toString();
113+
if (expressionMarker.isInUse()) {
114+
assertThat(methodCode).contains("class ExpressionMarker{};");
115+
} else {
116+
assertThat(methodCode).doesNotContain("class ExpressionMarker{};");
117+
}
118+
}
119+
120+
static Stream<Arguments> expressionMarkers() {
121+
122+
ExpressionMarker unused = new ExpressionMarker();
123+
124+
ExpressionMarker used = new ExpressionMarker();
125+
used.marker();
126+
127+
return Stream.of(Arguments.of(unused), Arguments.of(used));
128+
}
90129
}

0 commit comments

Comments
 (0)