Skip to content

Commit 862e299

Browse files
mp911deodrotbohm
authored andcommitted
DATACMNS-836 - Add reactive repository support.
We now expose reactive interfaces to facilitate reactive repository support in store-specific modules. Spring Data modules are free to implement their reactive support using either RxJava1 or Project Reactor (Reactive Streams). We expose a set of base interfaces: * `ReactiveCrudRepository` * `ReactivePagingAndSortingRepository` * `RxJavaCrudRepository` * `RxJavaPagingAndSortingRepository` Reactive repositories provide a similar feature coverage to blocking repositories. Reactive paging support is limited to a `Mono<Page>`/`Single<Page>`. Data is fetched in a deferred way to provide a paging experience similar to blocking paging. A store module can choose either Project Reactor or RxJava 1 to implement reactive repository support. Project Reactor and RxJava types are converted in both directions allowing repositories to be composed of Project Reactor and RxJava query methods. Reactive wrapper type conversion handles wrapper type conversion at repository level. Query/implementation method selection uses multi-pass candidate selection to invoke the most appropriate method (exact arguments, convertible wrappers, assignable arguments). We also provide ReactiveWrappers and ReactiveWrapperConverters to expose metadata about reactive types and their value multiplicity.
1 parent f3ba640 commit 862e299

21 files changed

+2527
-58
lines changed

pom.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@
105105
<optional>true</optional>
106106
</dependency>
107107

108+
<!-- RxJava -->
109+
110+
<dependency>
111+
<groupId>io.reactivex</groupId>
112+
<artifactId>rxjava</artifactId>
113+
<version>${rxjava}</version>
114+
<optional>true</optional>
115+
</dependency>
116+
108117
<!-- Querydsl -->
109118

110119
<dependency>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ public Set<Class<?>> getAlternativeDomainTypes() {
336336

337337
/**
338338
* Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments
339-
* agains the ones bound in the given repository interface.
339+
* against the ones bound in the given repository interface.
340340
*
341341
* @param method
342342
* @param baseClassMethod
@@ -381,7 +381,7 @@ private boolean parametersMatch(Method method, Method baseClassMethod) {
381381
* @param parameterType must not be {@literal null}.
382382
* @return
383383
*/
384-
private boolean matchesGenericType(TypeVariable<?> variable, ResolvableType parameterType) {
384+
protected boolean matchesGenericType(TypeVariable<?> variable, ResolvableType parameterType) {
385385

386386
GenericDeclaration declaration = variable.getGenericDeclaration();
387387

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

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
* Simple domain service to convert query results into a dedicated type.
2929
*
3030
* @author Oliver Gierke
31+
* @author Mark Paluch
3132
*/
3233
class QueryExecutionResultHandler {
3334

@@ -50,25 +51,36 @@ public QueryExecutionResultHandler() {
5051
* Post-processes the given result of a query invocation to the given type.
5152
*
5253
* @param result can be {@literal null}.
53-
* @param returnTypeDesciptor can be {@literal null}, if so, no conversion is performed.
54+
* @param returnTypeDescriptor can be {@literal null}, if so, no conversion is performed.
5455
* @return
5556
*/
56-
public Object postProcessInvocationResult(Object result, TypeDescriptor returnTypeDesciptor) {
57+
public Object postProcessInvocationResult(Object result, TypeDescriptor returnTypeDescriptor) {
5758

58-
if (returnTypeDesciptor == null) {
59+
if (returnTypeDescriptor == null) {
5960
return result;
6061
}
6162

62-
Class<?> expectedReturnType = returnTypeDesciptor.getType();
63+
Class<?> expectedReturnType = returnTypeDescriptor.getType();
6364

6465
if (result != null && expectedReturnType.isInstance(result)) {
6566
return result;
6667
}
6768

68-
if (QueryExecutionConverters.supports(expectedReturnType)
69-
&& conversionService.canConvert(WRAPPER_TYPE, returnTypeDesciptor)
70-
&& !conversionService.canBypassConvert(WRAPPER_TYPE, TypeDescriptor.valueOf(expectedReturnType))) {
71-
return conversionService.convert(new NullableWrapper(result), expectedReturnType);
69+
if (QueryExecutionConverters.supports(expectedReturnType)) {
70+
71+
TypeDescriptor targetType = TypeDescriptor.valueOf(expectedReturnType);
72+
73+
if(conversionService.canConvert(WRAPPER_TYPE, returnTypeDescriptor)
74+
&& !conversionService.canBypassConvert(WRAPPER_TYPE, targetType)) {
75+
76+
return conversionService.convert(new NullableWrapper(result), expectedReturnType);
77+
}
78+
79+
if(result != null && conversionService.canConvert(TypeDescriptor.valueOf(result.getClass()), returnTypeDescriptor)
80+
&& !conversionService.canBypassConvert(TypeDescriptor.valueOf(result.getClass()), targetType)) {
81+
82+
return conversionService.convert(result, expectedReturnType);
83+
}
7284
}
7385

7486
if (result != null) {
@@ -82,4 +94,5 @@ public Object postProcessInvocationResult(Object result, TypeDescriptor returnTy
8294

8395
return null;
8496
}
97+
8598
}
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/*
2+
* Copyright 2016 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+
* http://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.core.support;
17+
18+
import static org.springframework.core.GenericTypeResolver.*;
19+
20+
import java.lang.reflect.Method;
21+
import java.lang.reflect.Type;
22+
import java.util.function.BiPredicate;
23+
24+
import org.springframework.core.MethodParameter;
25+
import org.springframework.core.convert.ConversionService;
26+
import org.springframework.data.repository.core.RepositoryInformation;
27+
import org.springframework.data.repository.core.RepositoryMetadata;
28+
import org.springframework.data.repository.util.QueryExecutionConverters;
29+
import org.springframework.util.Assert;
30+
31+
/**
32+
* This {@link RepositoryInformation} uses a {@link ConversionService} to check whether method arguments can be
33+
* converted for invocation of implementation methods.
34+
*
35+
* @author Mark Paluch
36+
*/
37+
public class ReactiveRepositoryInformation extends DefaultRepositoryInformation {
38+
39+
private final ConversionService conversionService;
40+
41+
/**
42+
* Creates a new {@link ReactiveRepositoryInformation} for the given repository interface and repository base class
43+
* using a {@link ConversionService}.
44+
*
45+
* @param metadata must not be {@literal null}.
46+
* @param repositoryBaseClass must not be {@literal null}.
47+
* @param customImplementationClass
48+
* @param conversionService must not be {@literal null}.
49+
*/
50+
public ReactiveRepositoryInformation(RepositoryMetadata metadata, Class<?> repositoryBaseClass,
51+
Class<?> customImplementationClass, ConversionService conversionService) {
52+
53+
super(metadata, repositoryBaseClass, customImplementationClass);
54+
55+
Assert.notNull(conversionService, "Conversion service must not be null!");
56+
57+
this.conversionService = conversionService;
58+
}
59+
60+
/**
61+
* Returns the given target class' method if the given method (declared in the repository interface) was also declared
62+
* at the target class. Returns the given method if the given base class does not declare the method given. Takes
63+
* generics into account.
64+
*
65+
* @param method must not be {@literal null}
66+
* @param baseClass
67+
* @return
68+
*/
69+
Method getTargetClassMethod(Method method, Class<?> baseClass) {
70+
71+
if (baseClass == null) {
72+
return method;
73+
}
74+
75+
boolean wantsWrappers = wantsMethodUsingReactiveWrapperParameters(method);
76+
77+
if (wantsWrappers) {
78+
Method candidate = getMethodCandidate(method, baseClass, new ExactWrapperMatch(method));
79+
80+
if (candidate != null) {
81+
return candidate;
82+
}
83+
84+
candidate = getMethodCandidate(method, baseClass, new WrapperConversionMatch(method, conversionService));
85+
86+
if (candidate != null) {
87+
return candidate;
88+
}
89+
}
90+
91+
Method candidate = getMethodCandidate(method, baseClass,
92+
new MatchParameterOrComponentType(method, getRepositoryInterface()));
93+
94+
if (candidate != null) {
95+
return candidate;
96+
}
97+
98+
return method;
99+
}
100+
101+
private boolean wantsMethodUsingReactiveWrapperParameters(Method method) {
102+
103+
boolean wantsWrappers = false;
104+
105+
for (Class<?> parameterType : method.getParameterTypes()) {
106+
if (isNonunwrappingWrapper(parameterType)) {
107+
wantsWrappers = true;
108+
break;
109+
}
110+
}
111+
112+
return wantsWrappers;
113+
}
114+
115+
private Method getMethodCandidate(Method method, Class<?> baseClass, BiPredicate<Class<?>, Integer> predicate) {
116+
117+
for (Method baseClassMethod : baseClass.getMethods()) {
118+
119+
// Wrong name
120+
if (!method.getName().equals(baseClassMethod.getName())) {
121+
continue;
122+
}
123+
124+
// Wrong number of arguments
125+
if (!(method.getParameterTypes().length == baseClassMethod.getParameterTypes().length)) {
126+
continue;
127+
}
128+
129+
// Check whether all parameters match
130+
if (!parametersMatch(method, baseClassMethod, predicate)) {
131+
continue;
132+
}
133+
134+
return baseClassMethod;
135+
}
136+
137+
return null;
138+
}
139+
140+
/**
141+
* Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments
142+
* against the ones bound in the given repository interface.
143+
*
144+
* @param method
145+
* @param baseClassMethod
146+
* @param predicate
147+
* @return
148+
*/
149+
private boolean parametersMatch(Method method, Method baseClassMethod, BiPredicate<Class<?>, Integer> predicate) {
150+
151+
Type[] genericTypes = baseClassMethod.getGenericParameterTypes();
152+
Class<?>[] types = baseClassMethod.getParameterTypes();
153+
154+
for (int i = 0; i < genericTypes.length; i++) {
155+
if (!predicate.test(types[i], i)) {
156+
return false;
157+
}
158+
}
159+
160+
return true;
161+
}
162+
163+
/**
164+
* Checks whether the type is a wrapper without unwrapping support. Reactive wrappers don't like to be unwrapped.
165+
*
166+
* @param parameterType
167+
* @return
168+
*/
169+
static boolean isNonunwrappingWrapper(Class<?> parameterType) {
170+
return QueryExecutionConverters.supports(parameterType)
171+
&& !QueryExecutionConverters.supportsUnwrapping(parameterType);
172+
}
173+
174+
static class WrapperConversionMatch implements BiPredicate<Class<?>, Integer> {
175+
176+
final Method declaredMethod;
177+
final Class<?>[] declaredParameterTypes;
178+
final ConversionService conversionService;
179+
180+
public WrapperConversionMatch(Method declaredMethod, ConversionService conversionService) {
181+
182+
this.declaredMethod = declaredMethod;
183+
this.declaredParameterTypes = declaredMethod.getParameterTypes();
184+
this.conversionService = conversionService;
185+
}
186+
187+
@Override
188+
public boolean test(Class<?> candidateParameterType, Integer index) {
189+
190+
// TODO: should check for component type
191+
if (isNonunwrappingWrapper(candidateParameterType) && isNonunwrappingWrapper(declaredParameterTypes[index])) {
192+
193+
if (conversionService.canConvert(declaredParameterTypes[index], candidateParameterType)) {
194+
return true;
195+
}
196+
}
197+
198+
return false;
199+
}
200+
201+
}
202+
203+
static class ExactWrapperMatch implements BiPredicate<Class<?>, Integer> {
204+
205+
final Method declaredMethod;
206+
final Class<?>[] declaredParameterTypes;
207+
208+
public ExactWrapperMatch(Method declaredMethod) {
209+
210+
this.declaredMethod = declaredMethod;
211+
this.declaredParameterTypes = declaredMethod.getParameterTypes();
212+
}
213+
214+
@Override
215+
public boolean test(Class<?> candidateParameterType, Integer index) {
216+
217+
// TODO: should check for component type
218+
if (isNonunwrappingWrapper(candidateParameterType) && isNonunwrappingWrapper(declaredParameterTypes[index])) {
219+
220+
if (declaredParameterTypes[index].isAssignableFrom(candidateParameterType)) {
221+
return true;
222+
}
223+
}
224+
225+
return false;
226+
}
227+
228+
}
229+
230+
static class MatchParameterOrComponentType implements BiPredicate<Class<?>, Integer> {
231+
232+
final Method declaredMethod;
233+
final Class<?>[] declaredParameterTypes;
234+
final Class<?> repositoryInterface;
235+
236+
public MatchParameterOrComponentType(Method declaredMethod, Class<?> repositoryInterface) {
237+
238+
this.declaredMethod = declaredMethod;
239+
this.declaredParameterTypes = declaredMethod.getParameterTypes();
240+
this.repositoryInterface = repositoryInterface;
241+
}
242+
243+
@Override
244+
public boolean test(Class<?> candidateParameterType, Integer index) {
245+
246+
MethodParameter parameter = new MethodParameter(declaredMethod, index);
247+
Class<?> parameterType = resolveParameterType(parameter, repositoryInterface);
248+
249+
if (!candidateParameterType.isAssignableFrom(parameterType)
250+
|| !candidateParameterType.equals(declaredParameterTypes[index])) {
251+
return false;
252+
}
253+
254+
return true;
255+
}
256+
257+
}
258+
259+
}

0 commit comments

Comments
 (0)