Browse Source

Move `Method` string rendering from `QueryCreationException` to `ReflectionUtils`.

Closes #3396
pull/3399/head
Mark Paluch 1 month ago
parent
commit
05f5059930
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 20
      src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java
  2. 8
      src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java
  3. 9
      src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvocationListener.java
  4. 20
      src/main/java/org/springframework/data/repository/query/QueryCreationException.java
  5. 9
      src/main/java/org/springframework/data/repository/query/QueryMethod.java
  6. 2
      src/main/java/org/springframework/data/util/NullnessMethodInvocationValidator.java
  7. 36
      src/main/java/org/springframework/data/util/ReflectionUtils.java
  8. 58
      src/test/java/org/springframework/data/repository/query/QueryCreationExceptionUnitTests.java

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

@ -33,6 +33,7 @@ import org.springframework.data.repository.core.support.RepositoryComposition; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);

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

@ -20,15 +20,14 @@ import java.lang.reflect.TypeVariable; @@ -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 { @@ -111,9 +110,8 @@ class AotRepositoryMethodBuilder {
MethodMetadata methodMetadata = context.getTargetMethodMetadata();
Map<String, ParameterSpec> 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));

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

@ -16,13 +16,13 @@ @@ -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 { @@ -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());
}
}

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

@ -17,13 +17,12 @@ package org.springframework.data.repository.query; @@ -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 { @@ -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 { @@ -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());
}
}

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

@ -150,17 +150,20 @@ public class QueryMethod { @@ -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)));
}
}

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

@ -115,7 +115,7 @@ public class NullnessMethodInvocationValidator implements MethodInterceptor { @@ -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)));
}
/**

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

@ -19,10 +19,12 @@ import java.lang.annotation.Annotation; @@ -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 { @@ -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.
* <p>
* 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.
* <p>
* 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<Class<?>, 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.

58
src/test/java/org/springframework/data/repository/query/QueryCreationExceptionUnitTests.java

@ -0,0 +1,58 @@ @@ -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");
}
}
Loading…
Cancel
Save