Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b8c0a2f

Browse files
mp911deodrotbohm
authored andcommittedJul 25, 2019
DATACMNS-1556 - Deeply inspect query results deeply for the need of query post-processing.
We now inspect returned Collections whether we need to convert these at all. If not, we skip conversion. Also, allocate GenericConversionService only once for memory-efficiency.
1 parent 94c4713 commit b8c0a2f

File tree

4 files changed

+139
-17
lines changed

4 files changed

+139
-17
lines changed
 

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

Lines changed: 127 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,20 @@
1717

1818
import java.lang.reflect.Method;
1919
import java.util.Collection;
20+
import java.util.Collections;
21+
import java.util.HashMap;
2022
import java.util.Map;
2123
import java.util.Optional;
2224

2325
import org.springframework.core.CollectionFactory;
2426
import org.springframework.core.MethodParameter;
2527
import org.springframework.core.convert.ConversionService;
2628
import org.springframework.core.convert.TypeDescriptor;
27-
import org.springframework.core.convert.support.DefaultConversionService;
2829
import org.springframework.core.convert.support.GenericConversionService;
2930
import org.springframework.data.repository.util.NullableWrapper;
3031
import org.springframework.data.repository.util.QueryExecutionConverters;
3132
import org.springframework.data.repository.util.ReactiveWrapperConverters;
33+
import org.springframework.data.util.Streamable;
3234
import org.springframework.lang.Nullable;
3335

3436
/**
@@ -44,15 +46,15 @@ class QueryExecutionResultHandler {
4446

4547
private final GenericConversionService conversionService;
4648

49+
private final Object mutex = new Object();
50+
51+
// concurrent access guarded by mutex.
52+
private Map<Method, ReturnTypeDescriptor> descriptorCache = Collections.emptyMap();
53+
4754
/**
4855
* Creates a new {@link QueryExecutionResultHandler}.
4956
*/
50-
public QueryExecutionResultHandler() {
51-
52-
GenericConversionService conversionService = new DefaultConversionService();
53-
QueryExecutionConverters.registerConvertersIn(conversionService);
54-
conversionService.removeConvertible(Object.class, Object.class);
55-
57+
public QueryExecutionResultHandler(GenericConversionService conversionService) {
5658
this.conversionService = conversionService;
5759
}
5860

@@ -70,24 +72,51 @@ public Object postProcessInvocationResult(@Nullable Object result, Method method
7072
return result;
7173
}
7274

73-
MethodParameter parameter = new MethodParameter(method, -1);
75+
ReturnTypeDescriptor descriptor = getOrCreateReturnTypeDescriptor(method);
76+
77+
return postProcessInvocationResult(result, 0, descriptor);
78+
}
79+
80+
private ReturnTypeDescriptor getOrCreateReturnTypeDescriptor(Method method) {
81+
82+
Map<Method, ReturnTypeDescriptor> descriptorCache = this.descriptorCache;
83+
ReturnTypeDescriptor descriptor = descriptorCache.get(method);
84+
85+
if (descriptor == null) {
7486

75-
return postProcessInvocationResult(result, 0, parameter);
87+
descriptor = ReturnTypeDescriptor.of(method);
7688

89+
Map<Method, ReturnTypeDescriptor> updatedDescriptorCache;
90+
91+
if (descriptorCache.isEmpty()) {
92+
updatedDescriptorCache = Collections.singletonMap(method, descriptor);
93+
} else {
94+
updatedDescriptorCache = new HashMap<>(descriptorCache.size() + 1, 1);
95+
updatedDescriptorCache.putAll(descriptorCache);
96+
updatedDescriptorCache.put(method, descriptor);
97+
98+
}
99+
100+
synchronized (mutex) {
101+
this.descriptorCache = updatedDescriptorCache;
102+
}
103+
}
104+
105+
return descriptor;
77106
}
78107

79108
/**
80109
* Post-processes the given result of a query invocation to the given type.
81110
*
82111
* @param result can be {@literal null}.
83112
* @param nestingLevel
84-
* @param parameter must not be {@literal null}.
113+
* @param descriptor must not be {@literal null}.
85114
* @return
86115
*/
87116
@Nullable
88-
Object postProcessInvocationResult(@Nullable Object result, int nestingLevel, MethodParameter parameter) {
117+
Object postProcessInvocationResult(@Nullable Object result, int nestingLevel, ReturnTypeDescriptor descriptor) {
89118

90-
TypeDescriptor returnTypeDescriptor = TypeDescriptor.nested(parameter, nestingLevel);
119+
TypeDescriptor returnTypeDescriptor = descriptor.getReturnTypeDescriptor(nestingLevel);
91120

92121
if (returnTypeDescriptor == null) {
93122
return result;
@@ -100,7 +129,7 @@ Object postProcessInvocationResult(@Nullable Object result, int nestingLevel, Me
100129
if (QueryExecutionConverters.supports(expectedReturnType)) {
101130

102131
// For a wrapper type, try nested resolution first
103-
result = postProcessInvocationResult(result, nestingLevel + 1, parameter);
132+
result = postProcessInvocationResult(result, nestingLevel + 1, descriptor);
104133

105134
if (conversionRequired(WRAPPER_TYPE, returnTypeDescriptor)) {
106135
return conversionService.convert(new NullableWrapper(result), returnTypeDescriptor);
@@ -122,14 +151,48 @@ Object postProcessInvocationResult(@Nullable Object result, int nestingLevel, Me
122151
return ReactiveWrapperConverters.toWrapper(result, expectedReturnType);
123152
}
124153

125-
return conversionService.canConvert(TypeDescriptor.forObject(result), returnTypeDescriptor)
154+
if (result instanceof Collection<?>) {
155+
156+
TypeDescriptor elementDescriptor = descriptor.getReturnTypeDescriptor(nestingLevel + 1);
157+
boolean requiresConversion = requiresConversion((Collection<?>) result, expectedReturnType, elementDescriptor);
158+
159+
if (!requiresConversion) {
160+
return result;
161+
}
162+
}
163+
164+
TypeDescriptor resultDescriptor = TypeDescriptor.forObject(result);
165+
return conversionService.canConvert(resultDescriptor, returnTypeDescriptor)
126166
? conversionService.convert(result, returnTypeDescriptor)
127167
: result;
128168
}
129169

130170
return Map.class.equals(expectedReturnType) //
131171
? CollectionFactory.createMap(expectedReturnType, 0) //
132172
: null;
173+
174+
}
175+
private boolean requiresConversion(Collection<?> collection, Class<?> expectedReturnType,
176+
@Nullable TypeDescriptor elementDescriptor) {
177+
178+
if (Streamable.class.isAssignableFrom(expectedReturnType) || !expectedReturnType.isInstance(collection)) {
179+
return true;
180+
}
181+
182+
if (elementDescriptor == null || !Iterable.class.isAssignableFrom(expectedReturnType)) {
183+
return false;
184+
}
185+
186+
Class<?> type = elementDescriptor.getType();
187+
188+
for (Object o : collection) {
189+
190+
if (!type.isInstance(o)) {
191+
return true;
192+
}
193+
}
194+
195+
return false;
133196
}
134197

135198
/**
@@ -178,4 +241,54 @@ private static boolean processingRequired(@Nullable Object source, Class<?> targ
178241
|| source == null //
179242
|| Collection.class.isInstance(source);
180243
}
244+
245+
/**
246+
* Value object capturing {@link MethodParameter} and {@link TypeDescriptor}s for top and nested levels.
247+
*/
248+
static class ReturnTypeDescriptor {
249+
250+
private final MethodParameter methodParameter;
251+
private final TypeDescriptor typeDescriptor;
252+
private final @Nullable TypeDescriptor nestedTypeDescriptor;
253+
254+
private ReturnTypeDescriptor(Method method) {
255+
this.methodParameter = new MethodParameter(method, -1);
256+
this.typeDescriptor = TypeDescriptor.nested(this.methodParameter, 0);
257+
this.nestedTypeDescriptor = TypeDescriptor.nested(this.methodParameter, 1);
258+
}
259+
260+
/**
261+
* Create a {@link ReturnTypeDescriptor} from a {@link Method}.
262+
*
263+
* @param method
264+
* @return
265+
*/
266+
public static ReturnTypeDescriptor of(Method method) {
267+
return new ReturnTypeDescriptor(method);
268+
}
269+
270+
/**
271+
* Return the {@link TypeDescriptor} for a nested type declared within the method parameter described by
272+
* {@code nestingLevel} .
273+
*
274+
* @param nestingLevel the nesting level. {@code 0} is the first level, {@code 1} the next inner one.
275+
* @return the {@link TypeDescriptor} or {@literal null} if it could not be obtained.
276+
* @see TypeDescriptor#nested(MethodParameter, int)
277+
*/
278+
@Nullable
279+
public TypeDescriptor getReturnTypeDescriptor(int nestingLevel) {
280+
281+
// optimizing for nesting level 0 and 1 (Optional<T>, List<T>)
282+
// nesting level 2 (Optional<List<T>>) uses the slow path.
283+
284+
switch (nestingLevel) {
285+
case 0:
286+
return typeDescriptor;
287+
case 1:
288+
return nestedTypeDescriptor;
289+
default:
290+
return TypeDescriptor.nested(this.methodParameter, nestingLevel);
291+
}
292+
}
293+
}
181294
}

‎src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
import org.springframework.beans.factory.BeanFactory;
4545
import org.springframework.beans.factory.BeanFactoryAware;
4646
import org.springframework.core.ResolvableType;
47+
import org.springframework.core.convert.support.DefaultConversionService;
48+
import org.springframework.core.convert.support.GenericConversionService;
4749
import org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor;
4850
import org.springframework.data.projection.ProjectionFactory;
4951
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
@@ -114,6 +116,13 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
114116
return o;
115117
};
116118

119+
final static GenericConversionService CONVERSION_SERVICE = new DefaultConversionService();
120+
121+
static {
122+
QueryExecutionConverters.registerConvertersIn(CONVERSION_SERVICE);
123+
CONVERSION_SERVICE.removeConvertible(Object.class, Object.class);
124+
}
125+
117126
private final Map<RepositoryInformationCacheKey, RepositoryInformation> repositoryInformationCache;
118127
private final List<RepositoryProxyPostProcessor> postProcessors;
119128

@@ -534,7 +543,7 @@ public class QueryExecutorMethodInterceptor implements MethodInterceptor {
534543
public QueryExecutorMethodInterceptor(RepositoryInformation repositoryInformation,
535544
ProjectionFactory projectionFactory) {
536545

537-
this.resultHandler = new QueryExecutionResultHandler();
546+
this.resultHandler = new QueryExecutionResultHandler(CONVERSION_SERVICE);
538547

539548
Optional<QueryLookupStrategy> lookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey,
540549
RepositoryFactorySupport.this.evaluationContextProvider);

‎src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public static boolean supports(Class<?> type) {
178178
return SUPPORTS_CACHE.computeIfAbsent(type, key -> {
179179

180180
for (WrapperType candidate : WRAPPER_TYPES) {
181-
if (candidate.getType().isAssignableFrom(type)) {
181+
if (candidate.getType().isAssignableFrom(key)) {
182182
return true;
183183
}
184184
}

‎src/test/java/org/springframework/data/repository/core/support/QueryExecutionResultHandlerUnitTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
*/
5555
public class QueryExecutionResultHandlerUnitTests {
5656

57-
QueryExecutionResultHandler handler = new QueryExecutionResultHandler();
57+
QueryExecutionResultHandler handler = new QueryExecutionResultHandler(RepositoryFactorySupport.CONVERSION_SERVICE);
5858

5959
@Test // DATACMNS-610
6060
public void convertsListsToSet() throws Exception {

0 commit comments

Comments
 (0)
Please sign in to comment.