DATACMNS-483 - Support for wrapper types as return types for repository methods.

The repository method invocation mechanism now post-processes the result of the actual method invocation in case it's not type-compatible with the return type of the invoked method and invokes a conversion if applicable. This allows us to register custom converters for well-known wrapper types. We currently support Optional from both Google Guava and JDK 8.

Implementation notice

We use some JDK 8 type stubs from the Spring Data Build project to be able to compile using JDKs < 8. This is necessary as we need to remain compatible with Spring 3.2.x for the Dijkstra release train and our test executions still run into some issues with it a Java 8 runtime (mostly ASM related). These issues were reported in [0, 1] and we'll probably get off the hack for Dijkstra GA by upgrading to Spring 3.2.9.

[0] https://jira.spring.io/browse/SPR-11719
[1] https://jira.spring.io/browse/SPR-11718
This commit is contained in:
Oliver Gierke
2014-04-22 14:20:54 +02:00
parent 165c0595e3
commit c141fb771d
9 changed files with 368 additions and 22 deletions

View File

@@ -21,6 +21,7 @@ import java.util.Arrays;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.core.CrudMethods;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
@@ -60,7 +61,8 @@ public abstract class AbstractRepositoryMetadata implements RepositoryMetadata {
TypeInformation<?> returnTypeInfo = typeInformation.getReturnType(method);
Class<?> rawType = returnTypeInfo.getType();
boolean needToUnwrap = Iterable.class.isAssignableFrom(rawType) || rawType.isArray();
boolean needToUnwrap = Iterable.class.isAssignableFrom(rawType) || rawType.isArray()
|| QueryExecutionConverters.supports(rawType);
return needToUnwrap ? returnTypeInfo.getComponentType().getType() : rawType;
}

View File

@@ -30,6 +30,9 @@ import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.NamedQueries;
@@ -40,6 +43,8 @@ import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.util.ClassUtils;
import org.springframework.data.repository.util.NullableWrapper;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -52,6 +57,8 @@ import org.springframework.util.ObjectUtils;
*/
public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {
private static final TypeDescriptor WRAPPER_TYPE = TypeDescriptor.valueOf(NullableWrapper.class);
private final Map<RepositoryInformationCacheKey, RepositoryInformation> REPOSITORY_INFORMATION_CACHE = new HashMap<RepositoryInformationCacheKey, RepositoryInformation>();
private final List<RepositoryProxyPostProcessor> postProcessors = new ArrayList<RepositoryProxyPostProcessor>();
@@ -273,6 +280,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {
private final Object customImplementation;
private final RepositoryInformation repositoryInformation;
private final GenericConversionService conversionService;
private final Object target;
/**
@@ -282,6 +290,13 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {
public QueryExecutorMethodInterceptor(RepositoryInformation repositoryInformation, Object customImplementation,
Object target) {
Assert.notNull(repositoryInformation, "RepositoryInformation must not be null!");
Assert.notNull(target, "Target must not be null!");
DefaultConversionService conversionService = new DefaultConversionService();
QueryExecutionConverters.registerConvertersIn(conversionService);
this.conversionService = conversionService;
this.repositoryInformation = repositoryInformation;
this.customImplementation = customImplementation;
this.target = target;
@@ -325,22 +340,45 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {
*/
public Object invoke(MethodInvocation invocation) throws Throwable {
Object result = doInvoke(invocation);
Class<?> expectedReturnType = invocation.getMethod().getReturnType();
if (result != null && expectedReturnType.isInstance(result)) {
return result;
}
if (conversionService.canConvert(NullableWrapper.class, expectedReturnType)
&& !conversionService.canBypassConvert(WRAPPER_TYPE, TypeDescriptor.valueOf(expectedReturnType))) {
return conversionService.convert(new NullableWrapper(result), expectedReturnType);
}
if (result == null) {
return null;
}
return conversionService.canConvert(result.getClass(), expectedReturnType) ? conversionService.convert(result,
expectedReturnType) : result;
}
private Object doInvoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
Object[] arguments = invocation.getArguments();
if (isCustomMethodInvocation(invocation)) {
Method actualMethod = repositoryInformation.getTargetClassMethod(method);
makeAccessible(actualMethod);
return executeMethodOn(customImplementation, actualMethod, invocation.getArguments());
return executeMethodOn(customImplementation, actualMethod, arguments);
}
if (hasQueryFor(method)) {
return queries.get(method).execute(invocation.getArguments());
return queries.get(method).execute(arguments);
}
// Lookup actual method as it might be redeclared in the interface
// and we have to use the repository instance nevertheless
Method actualMethod = repositoryInformation.getTargetClassMethod(method);
return executeMethodOn(target, actualMethod, invocation.getArguments());
return executeMethodOn(target, actualMethod, arguments);
}
/**
@@ -370,7 +408,6 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {
* @return
*/
private boolean hasQueryFor(Method method) {
return queries.containsKey(method);
}

View File

@@ -40,6 +40,8 @@ public class QueryMethod {
private final Method method;
private final Parameters<?, ?> parameters;
private Class<?> domainClass;
/**
* Creates a new {@link QueryMethod} from the given parameters. Looks up the correct query to use for following
* invocations of the method given.
@@ -129,11 +131,16 @@ public class QueryMethod {
*/
protected Class<?> getDomainClass() {
Class<?> repositoryDomainClass = metadata.getDomainType();
Class<?> methodDomainClass = metadata.getReturnedDomainClass(method);
if (domainClass == null) {
return repositoryDomainClass == null || repositoryDomainClass.isAssignableFrom(methodDomainClass) ? methodDomainClass
: repositoryDomainClass;
Class<?> repositoryDomainClass = metadata.getDomainType();
Class<?> methodDomainClass = metadata.getReturnedDomainClass(method);
this.domainClass = repositoryDomainClass == null || repositoryDomainClass.isAssignableFrom(methodDomainClass) ? methodDomainClass
: repositoryDomainClass;
}
return domainClass;
}
/**

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2014 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
*
* http://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.util;
import org.springframework.core.convert.converter.Converter;
/**
* Simple value object to wrap a nullable delegate. Used to be able to write {@link Converter} implementations that
* convert {@literal null} into an object of some sort.
*
* @author Oliver Gierke
* @since 1.8
* @see QueryExecutionConverters
*/
public class NullableWrapper {
private final Object value;
/**
* Creates a new {@link NullableWrapper} for the given value.
*
* @param value can be {@literal null}.
*/
public NullableWrapper(Object value) {
this.value = value;
}
/**
* Returns the type of the contained value. WIll fall back to {@link Object} in case the value is {@literal null}.
*
* @return will never be {@literal null}.
*/
public Class<?> getValueType() {
return value == null ? Object.class : value.getClass();
}
/**
* Returns the backing valie
*
* @return the value can be {@literal null}.
*/
public Object getValue() {
return value;
}
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright 2014 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
*
* http://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.util;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import com.google.common.base.Optional;
/**
* Converters to potentially wrap the execution of a repository method into a variety of wrapper types potentially being
* available on the classpath.
*
* @author Oliver Gierke
* @since 1.8
*/
public class QueryExecutionConverters {
private static final boolean GUAVA_PRESENT = ClassUtils.isPresent("com.google.common.base.Optional",
QueryExecutionConverters.class.getClassLoader());
private static final boolean JDK_PRESENT = ClassUtils.isPresent("java.util.Optional",
QueryExecutionConverters.class.getClassLoader());
private static final Set<Class<?>> WRAPPER_TYPES;
private static final Set<Converter<?, ?>> CONVERTERS;
static {
Set<Class<?>> wrapperTypes = new HashSet<Class<?>>();
Set<Converter<?, ?>> converters = new HashSet<Converter<?, ?>>();
if (GUAVA_PRESENT) {
wrapperTypes.add(ObjectToGuavaOptionalConverter.INSTANCE.getWrapperType());
converters.add(ObjectToGuavaOptionalConverter.INSTANCE);
}
if (JDK_PRESENT) {
wrapperTypes.add(ObjectToJdk8OptionalConverter.INSTANCE.getWrapperType());
converters.add(ObjectToJdk8OptionalConverter.INSTANCE);
}
WRAPPER_TYPES = Collections.unmodifiableSet(wrapperTypes);
CONVERTERS = Collections.unmodifiableSet(converters);
}
/**
* Returns whether the given type is a supported wrapper type.
*
* @param type must not be {@literal null}.
* @return
*/
public static boolean supports(Class<?> type) {
Assert.notNull(type, "Type must not be null!");
return WRAPPER_TYPES.contains(type);
}
/**
* Registers converters for wrapper types found on the classpath.
*
* @param conversionService must not be {@literal null}.
*/
public static void registerConvertersIn(ConfigurableConversionService conversionService) {
Assert.notNull(conversionService, "ConversionService must not be null!");
for (Converter<?, ?> converter : CONVERTERS) {
conversionService.addConverter(converter);
}
}
/**
* A Spring {@link Converter} to support Google Guava's {@link Optional}.
*
* @author Oliver Gierke
*/
private static enum ObjectToGuavaOptionalConverter implements Converter<NullableWrapper, Optional<Object>> {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Override
public Optional<Object> convert(NullableWrapper source) {
return Optional.fromNullable(source.getValue());
}
public Class<?> getWrapperType() {
return Optional.class;
}
}
/**
* A Spring {@link Converter} to support JDK 8's {@link java.util.Optional}.
*
* @author Oliver Gierke
*/
private static enum ObjectToJdk8OptionalConverter implements Converter<NullableWrapper, java.util.Optional<Object>> {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Override
public java.util.Optional<Object> convert(NullableWrapper source) {
return java.util.Optional.ofNullable(source.getValue());
}
public Class<?> getWrapperType() {
return java.util.Optional.class;
}
}
}