Skip to content

Commit 05f5059

Browse files
committed
Move Method string rendering from QueryCreationException to ReflectionUtils.
Closes #3396
1 parent c2f8cf0 commit 05f5059

File tree

8 files changed

+125
-37
lines changed

8 files changed

+125
-37
lines changed

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.data.repository.core.support.RepositoryFragment;
3434
import org.springframework.data.repository.query.QueryMethod;
3535
import org.springframework.data.util.Lazy;
36+
import org.springframework.data.util.ReflectionUtils;
3637
import org.springframework.javapoet.ClassName;
3738
import org.springframework.javapoet.FieldSpec;
3839
import org.springframework.javapoet.MethodSpec;
@@ -238,7 +239,9 @@ void contributeMethods(@Nullable MethodContributorFactory methodContributorFacto
238239
} catch (RuntimeException e) {
239240
if (logger.isErrorEnabled()) {
240241
logger.error("Failed to contribute Repository method [%s.%s]"
241-
.formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName()), e);
242+
.formatted(repositoryInformation.getRepositoryInterface().getName(),
243+
ReflectionUtils.toString(method)),
244+
e);
242245
}
243246
}
244247
});
@@ -263,7 +266,7 @@ private void contributeMethod(Method method, @Nullable MethodContributorFactory
263266
if (logger.isTraceEnabled()) {
264267
logger.trace("Skipping %s method [%s.%s] contribution".formatted(
265268
(method.isBridge() ? "bridge" : method.isDefault() ? "default" : "static"),
266-
repositoryInformation.getRepositoryInterface().getName(), method.getName()));
269+
repositoryInformation.getRepositoryInterface().getName(), ReflectionUtils.toString(method)));
267270
}
268271
return;
269272
}
@@ -272,7 +275,7 @@ private void contributeMethod(Method method, @Nullable MethodContributorFactory
272275

273276
if (logger.isTraceEnabled()) {
274277
logger.trace("Skipping method [%s.%s] contribution, not a query method"
275-
.formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName()));
278+
.formatted(repositoryInformation.getRepositoryInterface().getName(), ReflectionUtils.toString(method)));
276279
}
277280
return;
278281
}
@@ -281,7 +284,7 @@ private void contributeMethod(Method method, @Nullable MethodContributorFactory
281284

282285
if (logger.isTraceEnabled()) {
283286
logger.trace("Skipping method [%s.%s] contribution, no MethodContributorFactory available"
284-
.formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName()));
287+
.formatted(repositoryInformation.getRepositoryInterface().getName(), ReflectionUtils.toString(method)));
285288
}
286289
return;
287290
}
@@ -292,7 +295,7 @@ private void contributeMethod(Method method, @Nullable MethodContributorFactory
292295

293296
if (logger.isTraceEnabled()) {
294297
logger.trace("Skipping method [%s.%s] contribution, no MethodContributor available"
295-
.formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName()));
298+
.formatted(repositoryInformation.getRepositoryInterface().getName(), ReflectionUtils.toString(method)));
296299
}
297300

298301
return;
@@ -303,7 +306,7 @@ private void contributeMethod(Method method, @Nullable MethodContributorFactory
303306
if (logger.isTraceEnabled()) {
304307
logger.trace(
305308
"Skipping implementation method [%s.%s] contribution. Method uses generics that currently cannot be resolved."
306-
.formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName()));
309+
.formatted(repositoryInformation.getRepositoryInterface().getName(), ReflectionUtils.toString(method)));
307310
}
308311

309312
generationMetadata.addDelegateMethod(method, contributor);
@@ -315,13 +318,14 @@ private void contributeMethod(Method method, @Nullable MethodContributorFactory
315318
if (repositoryInformation.isReactiveRepository() && logger.isTraceEnabled()) {
316319
logger.trace(
317320
"Skipping implementation method [%s.%s] contribution. AOT repositories are not supported for reactive repositories."
318-
.formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName()));
321+
.formatted(repositoryInformation.getRepositoryInterface().getName(), ReflectionUtils.toString(method)));
319322
}
320323

321324
if (!contributor.contributesMethodSpec() && logger.isTraceEnabled()) {
322325
logger.trace(
323326
"Skipping implementation method [%s.%s] contribution. Spring Data %s did not provide a method implementation."
324-
.formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName(), moduleName));
327+
.formatted(repositoryInformation.getRepositoryInterface().getName(), ReflectionUtils.toString(method),
328+
moduleName));
325329
}
326330

327331
generationMetadata.addDelegateMethod(method, contributor);

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,14 @@
2020
import java.util.Map;
2121
import java.util.function.BiConsumer;
2222
import java.util.function.Function;
23-
import java.util.stream.Collectors;
2423

2524
import javax.lang.model.element.Modifier;
2625

2726
import org.springframework.data.javapoet.TypeNames;
27+
import org.springframework.data.util.ReflectionUtils;
2828
import org.springframework.javapoet.CodeBlock;
2929
import org.springframework.javapoet.MethodSpec;
3030
import org.springframework.javapoet.ParameterSpec;
31-
import org.springframework.javapoet.TypeName;
3231
import org.springframework.javapoet.TypeVariableName;
3332

3433
/**
@@ -111,9 +110,8 @@ private MethodSpec.Builder initializeMethodBuilder() {
111110

112111
MethodMetadata methodMetadata = context.getTargetMethodMetadata();
113112
Map<String, ParameterSpec> methodArguments = methodMetadata.getMethodArguments();
114-
builder.addJavadoc("AOT generated implementation of {@link $T#$L($L)}.", context.getMethod().getDeclaringClass(),
115-
context.getMethod().getName(),
116-
methodArguments.values().stream().map(it -> it.type().toString()).collect(Collectors.joining(", ")));
113+
builder.addJavadoc("AOT generated implementation of {@link $T#$L}.", context.getMethod().getDeclaringClass(),
114+
ReflectionUtils.toString(context.getMethod()));
117115

118116
methodArguments.forEach((name, spec) -> builder.addParameter(spec));
119117

src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvocationListener.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
package org.springframework.data.repository.core.support;
1717

1818
import java.lang.reflect.Method;
19-
import java.util.Arrays;
2019
import java.util.concurrent.TimeUnit;
2120

2221
import org.jspecify.annotations.Nullable;
2322

23+
import org.springframework.data.util.ReflectionUtils;
2424
import org.springframework.util.Assert;
25-
import org.springframework.util.StringUtils;
25+
import org.springframework.util.ClassUtils;
2626

2727
/**
2828
* Interface to be implemented by listeners that want to be notified upon repository method invocation. Listeners are
@@ -89,9 +89,8 @@ public Method getMethod() {
8989
@Override
9090
public String toString() {
9191

92-
return String.format("Invocation %s.%s(%s): %s ms - %s", repositoryInterface.getSimpleName(), method.getName(),
93-
StringUtils.arrayToCommaDelimitedString(
94-
Arrays.stream(method.getParameterTypes()).map(Class::getSimpleName).toArray()),
92+
return String.format("Invocation %s.%s: %s ms - %s", repositoryInterface.getSimpleName(),
93+
ReflectionUtils.toString(method, ClassUtils::getShortName),
9594
getDuration(TimeUnit.MILLISECONDS), result.getState());
9695
}
9796
}

src/main/java/org/springframework/data/repository/query/QueryCreationException.java

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@
1717

1818
import java.io.Serial;
1919
import java.lang.reflect.Method;
20-
import java.lang.reflect.Type;
21-
import java.util.Arrays;
22-
import java.util.stream.Collectors;
2320

2421
import org.jspecify.annotations.Nullable;
2522

2623
import org.springframework.data.repository.core.RepositoryCreationException;
24+
import org.springframework.data.util.ReflectionUtils;
25+
import org.springframework.util.ClassUtils;
2726

2827
/**
2928
* Exception to be thrown if a query cannot be created from a {@link Method}.
@@ -141,7 +140,6 @@ public static QueryCreationException create(@Nullable String message, @Nullable
141140
cause, repositoryInterface, method);
142141
}
143142

144-
145143
/**
146144
* @return the method causing the exception.
147145
* @since 2.5
@@ -152,17 +150,9 @@ public Method getMethod() {
152150

153151
@Override
154152
public String getLocalizedMessage() {
155-
156-
StringBuilder sb = new StringBuilder();
157-
sb.append(method.getDeclaringClass().getSimpleName()).append('.');
158-
sb.append(method.getName());
159-
160-
sb.append(method.getName());
161-
sb.append(Arrays.stream(method.getParameterTypes()) //
162-
.map(Type::getTypeName) //
163-
.collect(Collectors.joining(",", "(", ")")));
164-
165-
return "Cannot create query for method [%s]; %s".formatted(sb.toString(), getMessage());
153+
return "Cannot create query for method [%s.%s]; %s".formatted(ClassUtils.getShortName(method.getDeclaringClass()),
154+
ReflectionUtils.toString(getMethod()), getMessage());
166155
}
167156

157+
168158
}

src/main/java/org/springframework/data/repository/query/QueryMethod.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,17 +150,20 @@ private void validate() {
150150
}
151151

152152
Assert.notNull(this.parameters,
153-
() -> String.format("Parameters extracted from method '%s' must not be null", method.getName()));
153+
() -> String.format("Parameters extracted from method '%s' must not be null",
154+
ReflectionUtils.toString(method)));
154155

155156
if (isPageQuery()) {
156157
Assert.isTrue(this.parameters.hasPageableParameter(),
157-
String.format("Paging query needs to have a Pageable parameter; Offending method: %s", method));
158+
String.format("Paging query needs to have a Pageable parameter; Offending method: %s",
159+
ReflectionUtils.toString(method)));
158160
}
159161

160162
if (isScrollQuery()) {
161163

162164
Assert.isTrue(this.parameters.hasScrollPositionParameter() || this.parameters.hasPageableParameter(),
163-
String.format("Scroll query needs to have a ScrollPosition parameter; Offending method: %s", method));
165+
String.format("Scroll query needs to have a ScrollPosition parameter; Offending method: %s",
166+
ReflectionUtils.toString(method)));
164167
}
165168
}
166169

src/main/java/org/springframework/data/util/NullnessMethodInvocationValidator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public static boolean supports(Class<?> type) {
115115
*/
116116
protected RuntimeException argumentIsNull(Method method, String parameterName) {
117117
return new IllegalArgumentException(String.format("Parameter %s in %s.%s must not be null", parameterName,
118-
ClassUtils.getShortName(method.getDeclaringClass()), method.getName()));
118+
ClassUtils.getShortName(method.getDeclaringClass()), ReflectionUtils.toString(method)));
119119
}
120120

121121
/**

src/main/java/org/springframework/data/util/ReflectionUtils.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
import java.lang.reflect.Constructor;
2020
import java.lang.reflect.Field;
2121
import java.lang.reflect.Method;
22+
import java.lang.reflect.Type;
2223
import java.util.ArrayList;
2324
import java.util.Arrays;
2425
import java.util.Collections;
2526
import java.util.List;
27+
import java.util.function.Function;
2628
import java.util.function.Predicate;
2729
import java.util.stream.Collectors;
2830
import java.util.stream.Stream;
@@ -442,6 +444,40 @@ private static boolean argumentsMatch(Class<?>[] parameterTypes, Object[] argume
442444
return true;
443445
}
444446

447+
/**
448+
* Returns a string representation of the given method including its name and parameter using fully-qualified class
449+
* names.
450+
* <p>
451+
* In contrast to {@link Method#toString()} this method omits the declaring type, the return type, any generics and
452+
* modifiers.
453+
*
454+
* @param method the method to render to string.
455+
* @return a string representation of the given method, i.e. {@code toString(java.lang.reflect.Method)}.
456+
* @since 4.0
457+
*/
458+
public static String toString(Method method) {
459+
return toString(method, Type::getTypeName);
460+
}
461+
462+
/**
463+
* Returns a string representation of the given method including its name and parameter types.
464+
* <p>
465+
* In contrast to {@link Method#toString()} this method omits the declaring type, the return type, any generics and
466+
* modifiers.
467+
*
468+
* @param method the method to render to string.
469+
* @param typeNameMapper mapping function to obtain the type name from a {@link Class}.
470+
* @return a string representation of the given method, i.e. {@code toString(java.lang.reflect.Method)} when using a
471+
* {@code Type::getTypeName typeNameMapper}.
472+
* @since 4.0
473+
*/
474+
public static String toString(Method method, Function<Class<?>, String> typeNameMapper) {
475+
476+
return method.getName() + Arrays.stream(method.getParameterTypes()) //
477+
.map(typeNameMapper) //
478+
.collect(Collectors.joining(",", "(", ")"));
479+
}
480+
445481
/**
446482
* Returns {@literal} whether the given {@link MethodParameter} is nullable. Nullable parameters are reference types
447483
* and ones that are defined in Kotlin as such.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2025 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.query;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import org.junit.jupiter.api.Test;
21+
22+
/**
23+
* Unit tests for {@link QueryCreationException}.
24+
*
25+
* @author Mark Paluch
26+
*/
27+
class QueryCreationExceptionUnitTests {
28+
29+
@Test // GH-3396
30+
void getMessageReturnsPlainMessage() throws NoSuchMethodException {
31+
32+
QueryCreationException exception = QueryCreationException.create("message", null, Object.class,
33+
getClass().getDeclaredMethod("getMessageReturnsPlainMessage"));
34+
35+
assertThat(exception.getMessage()).isEqualTo("message");
36+
}
37+
38+
@Test // GH-3396
39+
void getLocalizedMessageReturnsContextualMessage() throws NoSuchMethodException {
40+
41+
QueryCreationException exception = QueryCreationException.create("message", null, Object.class,
42+
getClass().getDeclaredMethod("getMessageReturnsPlainMessage"));
43+
44+
assertThat(exception.getLocalizedMessage()).isEqualTo(
45+
"Cannot create query for method [" + getClass().getSimpleName() + ".getMessageReturnsPlainMessage()]; message");
46+
}
47+
48+
@Test // GH-3396
49+
void toStringReturnsContextualMessage() throws NoSuchMethodException {
50+
51+
QueryCreationException exception = QueryCreationException.create("message", null, Object.class,
52+
getClass().getDeclaredMethod("getMessageReturnsPlainMessage"));
53+
54+
assertThat(exception.toString()).contains(
55+
"Cannot create query for method [" + getClass().getSimpleName() + ".getMessageReturnsPlainMessage()]; message");
56+
}
57+
58+
}

0 commit comments

Comments
 (0)