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 5bfcba6da..27837a7b5 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 @@ -33,6 +33,7 @@ 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.ReflectionUtils; import org.springframework.javapoet.ClassName; import org.springframework.javapoet.FieldSpec; import org.springframework.javapoet.MethodSpec; @@ -238,7 +239,9 @@ class AotRepositoryCreator { } catch (RuntimeException e) { if (logger.isErrorEnabled()) { logger.error("Failed to contribute Repository method [%s.%s]" - .formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName()), e); + .formatted(repositoryInformation.getRepositoryInterface().getName(), + ReflectionUtils.toString(method)), + e); } } }); @@ -263,7 +266,7 @@ class AotRepositoryCreator { if (logger.isTraceEnabled()) { logger.trace("Skipping %s method [%s.%s] contribution".formatted( (method.isBridge() ? "bridge" : method.isDefault() ? "default" : "static"), - repositoryInformation.getRepositoryInterface().getName(), method.getName())); + repositoryInformation.getRepositoryInterface().getName(), ReflectionUtils.toString(method))); } return; } @@ -272,7 +275,7 @@ class AotRepositoryCreator { if (logger.isTraceEnabled()) { logger.trace("Skipping method [%s.%s] contribution, not a query method" - .formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName())); + .formatted(repositoryInformation.getRepositoryInterface().getName(), ReflectionUtils.toString(method))); } return; } @@ -281,7 +284,7 @@ class AotRepositoryCreator { if (logger.isTraceEnabled()) { logger.trace("Skipping method [%s.%s] contribution, no MethodContributorFactory available" - .formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName())); + .formatted(repositoryInformation.getRepositoryInterface().getName(), ReflectionUtils.toString(method))); } return; } @@ -292,7 +295,7 @@ class AotRepositoryCreator { if (logger.isTraceEnabled()) { logger.trace("Skipping method [%s.%s] contribution, no MethodContributor available" - .formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName())); + .formatted(repositoryInformation.getRepositoryInterface().getName(), ReflectionUtils.toString(method))); } return; @@ -303,7 +306,7 @@ class AotRepositoryCreator { if (logger.isTraceEnabled()) { logger.trace( "Skipping implementation method [%s.%s] contribution. Method uses generics that currently cannot be resolved." - .formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName())); + .formatted(repositoryInformation.getRepositoryInterface().getName(), ReflectionUtils.toString(method))); } generationMetadata.addDelegateMethod(method, contributor); @@ -315,13 +318,14 @@ class AotRepositoryCreator { if (repositoryInformation.isReactiveRepository() && logger.isTraceEnabled()) { logger.trace( "Skipping implementation method [%s.%s] contribution. AOT repositories are not supported for reactive repositories." - .formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName())); + .formatted(repositoryInformation.getRepositoryInterface().getName(), ReflectionUtils.toString(method))); } if (!contributor.contributesMethodSpec() && logger.isTraceEnabled()) { logger.trace( "Skipping implementation method [%s.%s] contribution. Spring Data %s did not provide a method implementation." - .formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName(), moduleName)); + .formatted(repositoryInformation.getRepositoryInterface().getName(), ReflectionUtils.toString(method), + moduleName)); } generationMetadata.addDelegateMethod(method, contributor); 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 e4ae9c13d..f9733773c 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 @@ -20,15 +20,14 @@ import java.lang.reflect.TypeVariable; import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Function; -import java.util.stream.Collectors; import javax.lang.model.element.Modifier; import org.springframework.data.javapoet.TypeNames; +import org.springframework.data.util.ReflectionUtils; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.ParameterSpec; -import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeVariableName; /** @@ -111,9 +110,8 @@ class AotRepositoryMethodBuilder { MethodMetadata methodMetadata = context.getTargetMethodMetadata(); Map methodArguments = methodMetadata.getMethodArguments(); - builder.addJavadoc("AOT generated implementation of {@link $T#$L($L)}.", context.getMethod().getDeclaringClass(), - context.getMethod().getName(), - methodArguments.values().stream().map(it -> it.type().toString()).collect(Collectors.joining(", "))); + builder.addJavadoc("AOT generated implementation of {@link $T#$L}.", context.getMethod().getDeclaringClass(), + ReflectionUtils.toString(context.getMethod())); methodArguments.forEach((name, spec) -> builder.addParameter(spec)); diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvocationListener.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvocationListener.java index 350973e5d..f711ff682 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvocationListener.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvocationListener.java @@ -16,13 +16,13 @@ package org.springframework.data.repository.core.support; import java.lang.reflect.Method; -import java.util.Arrays; import java.util.concurrent.TimeUnit; import org.jspecify.annotations.Nullable; +import org.springframework.data.util.ReflectionUtils; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; +import org.springframework.util.ClassUtils; /** * Interface to be implemented by listeners that want to be notified upon repository method invocation. Listeners are @@ -89,9 +89,8 @@ public interface RepositoryMethodInvocationListener { @Override public String toString() { - return String.format("Invocation %s.%s(%s): %s ms - %s", repositoryInterface.getSimpleName(), method.getName(), - StringUtils.arrayToCommaDelimitedString( - Arrays.stream(method.getParameterTypes()).map(Class::getSimpleName).toArray()), + return String.format("Invocation %s.%s: %s ms - %s", repositoryInterface.getSimpleName(), + ReflectionUtils.toString(method, ClassUtils::getShortName), getDuration(TimeUnit.MILLISECONDS), result.getState()); } } diff --git a/src/main/java/org/springframework/data/repository/query/QueryCreationException.java b/src/main/java/org/springframework/data/repository/query/QueryCreationException.java index 7e7c3750f..65e9144e8 100644 --- a/src/main/java/org/springframework/data/repository/query/QueryCreationException.java +++ b/src/main/java/org/springframework/data/repository/query/QueryCreationException.java @@ -17,13 +17,12 @@ package org.springframework.data.repository.query; import java.io.Serial; import java.lang.reflect.Method; -import java.lang.reflect.Type; -import java.util.Arrays; -import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; import org.springframework.data.repository.core.RepositoryCreationException; +import org.springframework.data.util.ReflectionUtils; +import org.springframework.util.ClassUtils; /** * Exception to be thrown if a query cannot be created from a {@link Method}. @@ -141,7 +140,6 @@ public final class QueryCreationException extends RepositoryCreationException { cause, repositoryInterface, method); } - /** * @return the method causing the exception. * @since 2.5 @@ -152,17 +150,9 @@ public final class QueryCreationException extends RepositoryCreationException { @Override public String getLocalizedMessage() { - - StringBuilder sb = new StringBuilder(); - sb.append(method.getDeclaringClass().getSimpleName()).append('.'); - sb.append(method.getName()); - - sb.append(method.getName()); - sb.append(Arrays.stream(method.getParameterTypes()) // - .map(Type::getTypeName) // - .collect(Collectors.joining(",", "(", ")"))); - - return "Cannot create query for method [%s]; %s".formatted(sb.toString(), getMessage()); + return "Cannot create query for method [%s.%s]; %s".formatted(ClassUtils.getShortName(method.getDeclaringClass()), + ReflectionUtils.toString(getMethod()), getMessage()); } + } diff --git a/src/main/java/org/springframework/data/repository/query/QueryMethod.java b/src/main/java/org/springframework/data/repository/query/QueryMethod.java index 37a9b5f1a..fed6d530f 100644 --- a/src/main/java/org/springframework/data/repository/query/QueryMethod.java +++ b/src/main/java/org/springframework/data/repository/query/QueryMethod.java @@ -150,17 +150,20 @@ public class QueryMethod { } Assert.notNull(this.parameters, - () -> String.format("Parameters extracted from method '%s' must not be null", method.getName())); + () -> String.format("Parameters extracted from method '%s' must not be null", + ReflectionUtils.toString(method))); if (isPageQuery()) { Assert.isTrue(this.parameters.hasPageableParameter(), - String.format("Paging query needs to have a Pageable parameter; Offending method: %s", method)); + String.format("Paging query needs to have a Pageable parameter; Offending method: %s", + ReflectionUtils.toString(method))); } if (isScrollQuery()) { Assert.isTrue(this.parameters.hasScrollPositionParameter() || this.parameters.hasPageableParameter(), - String.format("Scroll query needs to have a ScrollPosition parameter; Offending method: %s", method)); + String.format("Scroll query needs to have a ScrollPosition parameter; Offending method: %s", + ReflectionUtils.toString(method))); } } diff --git a/src/main/java/org/springframework/data/util/NullnessMethodInvocationValidator.java b/src/main/java/org/springframework/data/util/NullnessMethodInvocationValidator.java index 3a6e65ee5..1a86d3c41 100644 --- a/src/main/java/org/springframework/data/util/NullnessMethodInvocationValidator.java +++ b/src/main/java/org/springframework/data/util/NullnessMethodInvocationValidator.java @@ -115,7 +115,7 @@ public class NullnessMethodInvocationValidator implements MethodInterceptor { */ protected RuntimeException argumentIsNull(Method method, String parameterName) { return new IllegalArgumentException(String.format("Parameter %s in %s.%s must not be null", parameterName, - ClassUtils.getShortName(method.getDeclaringClass()), method.getName())); + ClassUtils.getShortName(method.getDeclaringClass()), ReflectionUtils.toString(method))); } /** diff --git a/src/main/java/org/springframework/data/util/ReflectionUtils.java b/src/main/java/org/springframework/data/util/ReflectionUtils.java index 01bdb01d5..dc514e7a8 100644 --- a/src/main/java/org/springframework/data/util/ReflectionUtils.java +++ b/src/main/java/org/springframework/data/util/ReflectionUtils.java @@ -19,10 +19,12 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -442,6 +444,40 @@ public final class ReflectionUtils { return true; } + /** + * Returns a string representation of the given method including its name and parameter using fully-qualified class + * names. + *

+ * In contrast to {@link Method#toString()} this method omits the declaring type, the return type, any generics and + * modifiers. + * + * @param method the method to render to string. + * @return a string representation of the given method, i.e. {@code toString(java.lang.reflect.Method)}. + * @since 4.0 + */ + public static String toString(Method method) { + return toString(method, Type::getTypeName); + } + + /** + * Returns a string representation of the given method including its name and parameter types. + *

+ * In contrast to {@link Method#toString()} this method omits the declaring type, the return type, any generics and + * modifiers. + * + * @param method the method to render to string. + * @param typeNameMapper mapping function to obtain the type name from a {@link Class}. + * @return a string representation of the given method, i.e. {@code toString(java.lang.reflect.Method)} when using a + * {@code Type::getTypeName typeNameMapper}. + * @since 4.0 + */ + public static String toString(Method method, Function, String> typeNameMapper) { + + return method.getName() + Arrays.stream(method.getParameterTypes()) // + .map(typeNameMapper) // + .collect(Collectors.joining(",", "(", ")")); + } + /** * Returns {@literal} whether the given {@link MethodParameter} is nullable. Nullable parameters are reference types * and ones that are defined in Kotlin as such. diff --git a/src/test/java/org/springframework/data/repository/query/QueryCreationExceptionUnitTests.java b/src/test/java/org/springframework/data/repository/query/QueryCreationExceptionUnitTests.java new file mode 100644 index 000000000..8573fbdbe --- /dev/null +++ b/src/test/java/org/springframework/data/repository/query/QueryCreationExceptionUnitTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2025 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.query; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link QueryCreationException}. + * + * @author Mark Paluch + */ +class QueryCreationExceptionUnitTests { + + @Test // GH-3396 + void getMessageReturnsPlainMessage() throws NoSuchMethodException { + + QueryCreationException exception = QueryCreationException.create("message", null, Object.class, + getClass().getDeclaredMethod("getMessageReturnsPlainMessage")); + + assertThat(exception.getMessage()).isEqualTo("message"); + } + + @Test // GH-3396 + void getLocalizedMessageReturnsContextualMessage() throws NoSuchMethodException { + + QueryCreationException exception = QueryCreationException.create("message", null, Object.class, + getClass().getDeclaredMethod("getMessageReturnsPlainMessage")); + + assertThat(exception.getLocalizedMessage()).isEqualTo( + "Cannot create query for method [" + getClass().getSimpleName() + ".getMessageReturnsPlainMessage()]; message"); + } + + @Test // GH-3396 + void toStringReturnsContextualMessage() throws NoSuchMethodException { + + QueryCreationException exception = QueryCreationException.create("message", null, Object.class, + getClass().getDeclaredMethod("getMessageReturnsPlainMessage")); + + assertThat(exception.toString()).contains( + "Cannot create query for method [" + getClass().getSimpleName() + ".getMessageReturnsPlainMessage()]; message"); + } + +}