Introduce ParametersSource to Parameters.

We now provide a ParametersSource object to create MethodParameter objects associated with the enclosing class so parameters can resolve generics properly.

Closes #2996
This commit is contained in:
Mark Paluch
2023-12-04 11:01:37 +01:00
parent 0cf2538f97
commit bd05ed1dda
7 changed files with 203 additions and 37 deletions

View File

@@ -18,8 +18,6 @@ package org.springframework.data.repository.query;
import java.lang.reflect.Method;
import java.util.List;
import org.springframework.data.util.TypeInformation;
/**
* Default implementation of {@link Parameters}.
*
@@ -31,7 +29,7 @@ public final class DefaultParameters extends Parameters<DefaultParameters, Param
* Creates a new {@link DefaultParameters} instance from the given {@link Method}.
*
* @param method must not be {@literal null}.
* @deprecated since 3.1, use {@link #DefaultParameters(Method, TypeInformation)} instead.
* @deprecated since 3.1, use {@link #DefaultParameters(ParametersSource)} instead.
*/
@Deprecated(since = "3.1", forRemoval = true)
public DefaultParameters(Method method) {
@@ -39,14 +37,13 @@ public final class DefaultParameters extends Parameters<DefaultParameters, Param
}
/**
* Creates a new {@link DefaultParameters} instance from the given {@link Method} and aggregate
* {@link TypeInformation}.
* Creates a new {@link DefaultParameters} instance from the given {@link ParametersSource}.
*
* @param method must not be {@literal null}.
* @param aggregateType must not be {@literal null}.
* @param parametersSource must not be {@literal null}.
* @since 3.2.1
*/
public DefaultParameters(Method method, TypeInformation<?> aggregateType) {
super(method, param -> new Parameter(param, aggregateType));
public DefaultParameters(ParametersSource parametersSource) {
super(parametersSource, param -> new Parameter(param, parametersSource.getDomainTypeInformation()));
}
private DefaultParameters(List<Parameter> parameters) {

View File

@@ -44,7 +44,8 @@ import org.springframework.util.Assert;
*/
public abstract class Parameters<S extends Parameters<S, T>, T extends Parameter> implements Streamable<T> {
public static final List<Class<?>> TYPES = Arrays.asList(ScrollPosition.class, Pageable.class, Sort.class, Limit.class);
public static final List<Class<?>> TYPES = Arrays.asList(ScrollPosition.class, Pageable.class, Sort.class,
Limit.class);
private static final String PARAM_ON_SPECIAL = format("You must not use @%s on a parameter typed %s or %s",
Param.class.getSimpleName(), Pageable.class.getSimpleName(), Sort.class.getSimpleName());
@@ -82,14 +83,30 @@ public abstract class Parameters<S extends Parameters<S, T>, T extends Parameter
* @param method must not be {@literal null}.
* @param parameterFactory must not be {@literal null}.
* @since 3.0.2
* @deprecated since 3.2.1, use {@link Parameters(ParametersSource, Function)} instead.
*/
@Deprecated(since = "3.2.1", forRemoval = true)
protected Parameters(Method method, Function<MethodParameter, T> parameterFactory) {
this(ParametersSource.of(method), parameterFactory);
}
Assert.notNull(method, "Method must not be null");
/**
* Creates a new {@link Parameters} instance for the given {@link Method} and {@link Function} to create a
* {@link Parameter} instance from a {@link MethodParameter}.
*
* @param parametersSource must not be {@literal null}.
* @param parameterFactory must not be {@literal null}.
* @since 3.2.1
*/
protected Parameters(ParametersSource parametersSource,
Function<MethodParameter, T> parameterFactory) {
Assert.notNull(parametersSource, "ParametersSource must not be null");
// Factory nullability not enforced yet to support falling back to the deprecated
// createParameter(MethodParameter). Add assertion when the deprecation is removed.
Method method = parametersSource.getMethod();
int parameterCount = method.getParameterCount();
this.parameters = new ArrayList<>(parameterCount);
@@ -102,7 +119,9 @@ public abstract class Parameters<S extends Parameters<S, T>, T extends Parameter
for (int i = 0; i < parameterCount; i++) {
MethodParameter methodParameter = new MethodParameter(method, i);
MethodParameter methodParameter = new MethodParameter(method, i)
.withContainingClass(parametersSource.getContainingClass());
methodParameter.initParameterNameDiscovery(PARAMETER_NAME_DISCOVERER);
T parameter = parameterFactory == null //
@@ -421,7 +440,9 @@ public abstract class Parameters<S extends Parameters<S, T>, T extends Parameter
return !TYPES.contains(type);
}
@Override
public Iterator<T> iterator() {
return parameters.iterator();
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2023 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 java.lang.reflect.Method;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
/**
* Interface providing access to the method, containing class and domain type as source object for parameter
* descriptors.
*
* @author Mark Paluch
* @since 3.2.1
*/
public interface ParametersSource {
/**
* Create a new parameter source for the given {@link Method}.
*
* @param method the method to use.
* @return a new parameter source for the given {@link Method}.
*/
static ParametersSource of(Method method) {
Assert.notNull(method, "Method must not be null");
return new ParametersSource() {
@Override
public Class<?> getContainingClass() {
return method.getDeclaringClass();
}
@Override
public Method getMethod() {
return method;
}
@Override
public TypeInformation<?> getDomainTypeInformation() {
return TypeInformation.OBJECT;
}
};
}
/**
* Create a new parameter source for the given {@link Method} in the context of {@link RepositoryMetadata}.
*
* @param method the method to use.
* @return a new parameter source for the given {@link Method}.
*/
static ParametersSource of(RepositoryMetadata metadata, Method method) {
Assert.notNull(metadata, "RepositoryMetadata must not be null");
Assert.notNull(method, "Method must not be null");
return new ParametersSource() {
@Override
public Class<?> getContainingClass() {
return metadata.getRepositoryInterface();
}
@Override
public Method getMethod() {
return method;
}
@Override
public TypeInformation<?> getDomainTypeInformation() {
return metadata.getDomainTypeInformation();
}
};
}
/**
* @return the class that contains the {@link #getMethod()}.
*/
Class<?> getContainingClass();
/**
* @return the method for which the parameter source is defined.
*/
Method getMethod();
/**
* @return the domain type associated with the parameter source.
*/
TypeInformation<?> getDomainTypeInformation();
}

View File

@@ -183,16 +183,29 @@ public class QueryMethod {
* @param method must not be {@literal null}.
* @param domainType must not be {@literal null}.
* @return must not return {@literal null}.
* @deprecated since 3.2.1, use {@link #createParameters(ParametersSource)} instead.
* @since 3.0.2
*/
@Deprecated(since = "3.2.1", forRemoval = true)
protected Parameters<?, ?> createParameters(Method method, TypeInformation<?> domainType) {
return new DefaultParameters(method, domainType);
return createParameters(ParametersSource.of(getMetadata(), method));
}
/**
* Creates a {@link Parameters} instance.
*
* @param parametersSource must not be {@literal null}.
* @return must not return {@literal null}.
* @since 3.2.1
*/
protected Parameters<?, ?> createParameters(ParametersSource parametersSource) {
return new DefaultParameters(parametersSource);
}
/**
* Returns the method's name.
*
* @return
* @return the method's name.
*/
public String getName() {
return method.getName();

View File

@@ -114,7 +114,8 @@ class RepositoryMethodInvokerUnitTests {
repositoryMethodInvoker("findAll").invoke();
assertThat(multicaster.first().getDuration(TimeUnit.MILLISECONDS)).isCloseTo(250, Percentage.withPercentage(10));
assertThat(multicaster.first()
.getDuration(TimeUnit.MILLISECONDS)).isCloseTo(500, Percentage.withPercentage(50));
}
@Test // DATACMNS-1764
@@ -124,7 +125,8 @@ class RepositoryMethodInvokerUnitTests {
repositoryMethodInvokerForReactive("findAll").<Flux<TestDummy>> invoke().subscribe();
assertThat(multicaster.first().getDuration(TimeUnit.MILLISECONDS)).isCloseTo(250, Percentage.withPercentage(10));
assertThat(multicaster.first()
.getDuration(TimeUnit.MILLISECONDS)).isCloseTo(500, Percentage.withPercentage(50));
}
@Test // DATACMNS-1764

View File

@@ -25,6 +25,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.repository.core.RepositoryMetadata;
/**
* Unit tests for {@link ParametersParameterAccessor}.
@@ -36,10 +37,11 @@ import org.springframework.data.domain.Sort.Order;
class ParametersParameterAccessorUnitTests {
Parameters<?, ?> parameters;
RepositoryMetadata metadata;
@BeforeEach
void setUp() throws Exception {
parameters = new DefaultParameters(Sample.class.getMethod("method", String.class, int.class));
parameters = new DefaultParameters(ParametersSource.of(Sample.class.getMethod("method", String.class, int.class)));
}
@Test
@@ -62,7 +64,7 @@ class ParametersParameterAccessorUnitTests {
assertThat(accessor.hasBindableNullValue()).isTrue();
var method = Sample.class.getMethod("method", Pageable.class, String.class);
var parameters = new DefaultParameters(method);
var parameters = new DefaultParameters(ParametersSource.of(method));
accessor = new ParametersParameterAccessor(parameters, new Object[] { null, "Foo" });
assertThat(accessor.hasBindableNullValue()).isFalse();
@@ -72,7 +74,7 @@ class ParametersParameterAccessorUnitTests {
void iteratesonlyOverBindableValues() throws Exception {
var method = Sample.class.getMethod("method", Pageable.class, String.class);
var parameters = new DefaultParameters(method);
var parameters = new DefaultParameters(ParametersSource.of(method));
var accessor = new ParametersParameterAccessor(parameters, new Object[] { PageRequest.of(0, 10), "Foo" });
@@ -84,7 +86,7 @@ class ParametersParameterAccessorUnitTests {
void handlesScrollPositionAsAParameterType() throws NoSuchMethodException {
var method = Sample.class.getMethod("method", ScrollPosition.class, String.class);
var parameters = new DefaultParameters(method);
var parameters = new DefaultParameters(ParametersSource.of(method));
var accessor = new ParametersParameterAccessor(parameters, new Object[] { ScrollPosition.offset(1), "Foo" });
@@ -96,7 +98,7 @@ class ParametersParameterAccessorUnitTests {
void handlesPageRequestAsAParameterType() throws NoSuchMethodException {
var method = Sample.class.getMethod("methodWithPageRequest", PageRequest.class, String.class);
var parameters = new DefaultParameters(method);
var parameters = new DefaultParameters(ParametersSource.of(method));
var accessor = new ParametersParameterAccessor(parameters, new Object[] { PageRequest.of(0, 10), "Foo" });
@@ -108,7 +110,7 @@ class ParametersParameterAccessorUnitTests {
void handlesLimitAsAParameterType() throws NoSuchMethodException {
var method = Sample.class.getMethod("method", Limit.class, String.class);
var accessor = new ParametersParameterAccessor(new DefaultParameters(method),
var accessor = new ParametersParameterAccessor(new DefaultParameters(ParametersSource.of(method)),
new Object[] { Limit.of(100), "spring" });
assertThat(accessor).hasSize(1);
@@ -119,7 +121,7 @@ class ParametersParameterAccessorUnitTests {
void returnsLimitIfAvailable() throws NoSuchMethodException {
var method = Sample.class.getMethod("method", Limit.class, String.class);
var accessor = new ParametersParameterAccessor(new DefaultParameters(method),
var accessor = new ParametersParameterAccessor(new DefaultParameters(ParametersSource.of(method)),
new Object[] { Limit.of(100), "spring" });
assertThat(accessor.getLimit()).extracting(Limit::max).isEqualTo(100);
@@ -129,7 +131,7 @@ class ParametersParameterAccessorUnitTests {
void readsLimitFromPageableIfAvailable() throws NoSuchMethodException {
var method = Sample.class.getMethod("method", Pageable.class, String.class);
var accessor = new ParametersParameterAccessor(new DefaultParameters(method),
var accessor = new ParametersParameterAccessor(new DefaultParameters(ParametersSource.of(method)),
new Object[] { Pageable.ofSize(100), "spring" });
assertThat(accessor.getLimit()).extracting(Limit::max).isEqualTo(100);
@@ -139,7 +141,7 @@ class ParametersParameterAccessorUnitTests {
void returnsUnlimitedIfNoLimitingAvailable() throws NoSuchMethodException {
var method = Sample.class.getMethod("method", Sort.class, String.class);
var accessor = new ParametersParameterAccessor(new DefaultParameters(method),
var accessor = new ParametersParameterAccessor(new DefaultParameters(ParametersSource.of(method)),
new Object[] { Pageable.ofSize(100), "spring" });
assertThat(accessor.getLimit().isUnlimited()).isTrue();
@@ -149,7 +151,7 @@ class ParametersParameterAccessorUnitTests {
void appliesLimitToPageableIfAvailable() throws NoSuchMethodException {
var method = Sample.class.getMethod("method", Limit.class, String.class);
var accessor = new ParametersParameterAccessor(new DefaultParameters(method),
var accessor = new ParametersParameterAccessor(new DefaultParameters(ParametersSource.of(method)),
new Object[] { Limit.of(100), "spring" });
Pageable pageable = accessor.getPageable();
@@ -161,7 +163,7 @@ class ParametersParameterAccessorUnitTests {
void appliesLimitToPageableIfRequested() throws NoSuchMethodException {
var method = Sample.class.getMethod("method", Limit.class, String.class);
var accessor = new ParametersParameterAccessor(new DefaultParameters(method),
var accessor = new ParametersParameterAccessor(new DefaultParameters(ParametersSource.of(method)),
new Object[] { Limit.of(100), "spring" });
assertThat(accessor).hasSize(1);
@@ -172,7 +174,7 @@ class ParametersParameterAccessorUnitTests {
void appliesSortToPageableIfAvailable() throws NoSuchMethodException {
var method = Sample.class.getMethod("method", Sort.class, String.class);
var accessor = new ParametersParameterAccessor(new DefaultParameters(method),
var accessor = new ParametersParameterAccessor(new DefaultParameters(ParametersSource.of(method)),
new Object[] { Sort.by("one", "two"), "spring" });
Pageable pageable = accessor.getPageable();
@@ -184,7 +186,7 @@ class ParametersParameterAccessorUnitTests {
void appliesSortAndLimitToPageableIfAvailable() throws NoSuchMethodException {
var method = Sample.class.getMethod("method", Sort.class, Limit.class, String.class);
var accessor = new ParametersParameterAccessor(new DefaultParameters(method),
var accessor = new ParametersParameterAccessor(new DefaultParameters(ParametersSource.of(method)),
new Object[] { Sort.by("one", "two"), Limit.of(42), "spring" });
Pageable pageable = accessor.getPageable();

View File

@@ -26,12 +26,16 @@ import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.reactivestreams.Publisher;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.test.util.ReflectionTestUtils;
/**
@@ -43,11 +47,13 @@ import org.springframework.test.util.ReflectionTestUtils;
class ParametersUnitTests {
private Method valid;
private RepositoryMetadata metadata;
@BeforeEach
void setUp() throws SecurityException, NoSuchMethodException {
valid = SampleDao.class.getMethod("valid", String.class);
metadata = new DefaultRepositoryMetadata(SampleDao.class);
}
@Test
@@ -56,14 +62,14 @@ class ParametersUnitTests {
var validWithPageable = SampleDao.class.getMethod("validWithPageable", String.class, Pageable.class);
var validWithSort = SampleDao.class.getMethod("validWithSort", String.class, Sort.class);
new DefaultParameters(valid);
new DefaultParameters(validWithPageable);
new DefaultParameters(validWithSort);
new DefaultParameters(ParametersSource.of(valid));
new DefaultParameters(ParametersSource.of(validWithPageable));
new DefaultParameters(ParametersSource.of(validWithSort));
}
@Test
void rejectsNullMethod() {
assertThatIllegalArgumentException().isThrownBy(() -> new DefaultParameters(null));
assertThatIllegalArgumentException().isThrownBy(() -> new DefaultParameters((ParametersSource) null));
}
@Test
@@ -87,12 +93,12 @@ class ParametersUnitTests {
var method = SampleDao.class.getMethod("validWithSortFirst", Sort.class, String.class);
Parameters<?, ?> parameters = new DefaultParameters(method);
Parameters<?, ?> parameters = new DefaultParameters(ParametersSource.of(method));
assertThat(parameters.getBindableParameter(0).getIndex()).isEqualTo(1);
method = SampleDao.class.getMethod("validWithSortInBetween", String.class, Sort.class, String.class);
parameters = new DefaultParameters(method);
parameters = new DefaultParameters(ParametersSource.of(method));
assertThat(parameters.getBindableParameter(0).getIndex()).isEqualTo(0);
assertThat(parameters.getBindableParameter(1).getIndex()).isEqualTo(2);
@@ -202,19 +208,30 @@ class ParametersUnitTests {
assertThat(parameters.getLimitIndex()).isOne();
}
@Test // GH-2995
void considersGenericType() throws Exception {
var method = TypedInterface.class.getMethod("foo", Object.class);
var parameters = new DefaultParameters(
ParametersSource.of(new DefaultRepositoryMetadata(TypedInterface.class), method));
assertThat(parameters.getParameter(0).getType()).isEqualTo(Long.class);
}
private Parameters<?, Parameter> getParametersFor(String methodName, Class<?>... parameterTypes)
throws SecurityException, NoSuchMethodException {
var method = SampleDao.class.getMethod(methodName, parameterTypes);
return new DefaultParameters(method);
return new DefaultParameters(ParametersSource.of(metadata, method));
}
static class User {
}
static interface SampleDao {
interface SampleDao extends Repository<User, String> {
User valid(@Param("username") String username);
@@ -248,4 +265,11 @@ class ParametersUnitTests {
}
interface SomePageable extends Pageable {}
interface Intermediate<T, ID> extends Repository<T, ID> {
void foo(ID id);
}
interface TypedInterface extends Intermediate<User, Long> {}
}