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;
import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.util.Lazy; import org.springframework.data.util.Lazy;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.javapoet.ClassName; import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.FieldSpec; import org.springframework.javapoet.FieldSpec;
import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.MethodSpec;
@ -238,7 +239,9 @@ class AotRepositoryCreator {
} catch (RuntimeException e) { } catch (RuntimeException e) {
if (logger.isErrorEnabled()) { if (logger.isErrorEnabled()) {
logger.error("Failed to contribute Repository method [%s.%s]" 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()) { if (logger.isTraceEnabled()) {
logger.trace("Skipping %s method [%s.%s] contribution".formatted( logger.trace("Skipping %s method [%s.%s] contribution".formatted(
(method.isBridge() ? "bridge" : method.isDefault() ? "default" : "static"), (method.isBridge() ? "bridge" : method.isDefault() ? "default" : "static"),
repositoryInformation.getRepositoryInterface().getName(), method.getName())); repositoryInformation.getRepositoryInterface().getName(), ReflectionUtils.toString(method)));
} }
return; return;
} }
@ -272,7 +275,7 @@ class AotRepositoryCreator {
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace("Skipping method [%s.%s] contribution, not a query method" 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; return;
} }
@ -281,7 +284,7 @@ class AotRepositoryCreator {
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace("Skipping method [%s.%s] contribution, no MethodContributorFactory available" logger.trace("Skipping method [%s.%s] contribution, no MethodContributorFactory available"
.formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName())); .formatted(repositoryInformation.getRepositoryInterface().getName(), ReflectionUtils.toString(method)));
} }
return; return;
} }
@ -292,7 +295,7 @@ class AotRepositoryCreator {
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace("Skipping method [%s.%s] contribution, no MethodContributor available" logger.trace("Skipping method [%s.%s] contribution, no MethodContributor available"
.formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName())); .formatted(repositoryInformation.getRepositoryInterface().getName(), ReflectionUtils.toString(method)));
} }
return; return;
@ -303,7 +306,7 @@ class AotRepositoryCreator {
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace( logger.trace(
"Skipping implementation method [%s.%s] contribution. Method uses generics that currently cannot be resolved." "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); generationMetadata.addDelegateMethod(method, contributor);
@ -315,13 +318,14 @@ class AotRepositoryCreator {
if (repositoryInformation.isReactiveRepository() && logger.isTraceEnabled()) { if (repositoryInformation.isReactiveRepository() && logger.isTraceEnabled()) {
logger.trace( logger.trace(
"Skipping implementation method [%s.%s] contribution. AOT repositories are not supported for reactive repositories." "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()) { if (!contributor.contributesMethodSpec() && logger.isTraceEnabled()) {
logger.trace( logger.trace(
"Skipping implementation method [%s.%s] contribution. Spring Data %s did not provide a method implementation." "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); 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;
import java.util.Map; import java.util.Map;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
import javax.lang.model.element.Modifier; import javax.lang.model.element.Modifier;
import org.springframework.data.javapoet.TypeNames; import org.springframework.data.javapoet.TypeNames;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.ParameterSpec; import org.springframework.javapoet.ParameterSpec;
import org.springframework.javapoet.TypeName;
import org.springframework.javapoet.TypeVariableName; import org.springframework.javapoet.TypeVariableName;
/** /**
@ -111,9 +110,8 @@ class AotRepositoryMethodBuilder {
MethodMetadata methodMetadata = context.getTargetMethodMetadata(); MethodMetadata methodMetadata = context.getTargetMethodMetadata();
Map<String, ParameterSpec> methodArguments = methodMetadata.getMethodArguments(); Map<String, ParameterSpec> methodArguments = methodMetadata.getMethodArguments();
builder.addJavadoc("AOT generated implementation of {@link $T#$L($L)}.", context.getMethod().getDeclaringClass(), builder.addJavadoc("AOT generated implementation of {@link $T#$L}.", context.getMethod().getDeclaringClass(),
context.getMethod().getName(), ReflectionUtils.toString(context.getMethod()));
methodArguments.values().stream().map(it -> it.type().toString()).collect(Collectors.joining(", ")));
methodArguments.forEach((name, spec) -> builder.addParameter(spec)); methodArguments.forEach((name, spec) -> builder.addParameter(spec));

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

@ -16,13 +16,13 @@
package org.springframework.data.repository.core.support; package org.springframework.data.repository.core.support;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.util.Assert; 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 * 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 @Override
public String toString() { public String toString() {
return String.format("Invocation %s.%s(%s): %s ms - %s", repositoryInterface.getSimpleName(), method.getName(), return String.format("Invocation %s.%s: %s ms - %s", repositoryInterface.getSimpleName(),
StringUtils.arrayToCommaDelimitedString( ReflectionUtils.toString(method, ClassUtils::getShortName),
Arrays.stream(method.getParameterTypes()).map(Class::getSimpleName).toArray()),
getDuration(TimeUnit.MILLISECONDS), result.getState()); 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;
import java.io.Serial; import java.io.Serial;
import java.lang.reflect.Method; 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.jspecify.annotations.Nullable;
import org.springframework.data.repository.core.RepositoryCreationException; 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}. * 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); cause, repositoryInterface, method);
} }
/** /**
* @return the method causing the exception. * @return the method causing the exception.
* @since 2.5 * @since 2.5
@ -152,17 +150,9 @@ public final class QueryCreationException extends RepositoryCreationException {
@Override @Override
public String getLocalizedMessage() { public String getLocalizedMessage() {
return "Cannot create query for method [%s.%s]; %s".formatted(ClassUtils.getShortName(method.getDeclaringClass()),
StringBuilder sb = new StringBuilder(); ReflectionUtils.toString(getMethod()), getMessage());
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());
} }
} }

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

@ -150,17 +150,20 @@ public class QueryMethod {
} }
Assert.notNull(this.parameters, 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()) { if (isPageQuery()) {
Assert.isTrue(this.parameters.hasPageableParameter(), 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()) { if (isScrollQuery()) {
Assert.isTrue(this.parameters.hasScrollPositionParameter() || this.parameters.hasPageableParameter(), 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 {
*/ */
protected RuntimeException argumentIsNull(Method method, String parameterName) { protected RuntimeException argumentIsNull(Method method, String parameterName) {
return new IllegalArgumentException(String.format("Parameter %s in %s.%s must not be null", 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;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -442,6 +444,40 @@ public final class ReflectionUtils {
return true; 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 * Returns {@literal} whether the given {@link MethodParameter} is nullable. Nullable parameters are reference types
* and ones that are defined in Kotlin as such. * and ones that are defined in Kotlin as such.

58
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");
}
}
Loading…
Cancel
Save