DATAJPA-951 - Prevent preemptive conversion to Optional in JpaQueryExecution.

JpaQueryExecution applies a ConversionService to convert between low-level types such as integers, longs and byte arrays. That ConversionService also preemptively converts objects into JDK 8's Optional in case the invoked method's return type is Optional.

We now explicitly remove the converter taking care of the latter to avoid Optionals to be created before the actual result conversion is applied, as it needs to see the raw value to create DTOs or interface based projections correctly.
This commit is contained in:
Oliver Gierke
2016-08-17 16:30:36 +02:00
parent e71e754389
commit 1f677bd166
2 changed files with 51 additions and 4 deletions

View File

@@ -18,6 +18,7 @@ package org.springframework.data.jpa.repository.query;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
@@ -38,6 +39,7 @@ import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.util.CloseableIterator;
import org.springframework.data.util.StreamUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Set of classes to contain query execution strategies. Depending (mostly) on the return type of a
@@ -54,8 +56,10 @@ public abstract class JpaQueryExecution {
static {
ConfigurableConversionService conversionService = new DefaultConversionService();
conversionService.removeConvertible(Collection.class, Object.class);
conversionService.addConverter(JpaResultConverters.BlobToByteArrayConverter.INSTANCE);
conversionService.removeConvertible(Collection.class, Object.class);
potentiallyRemoveOptionalConverter(conversionService);
CONVERSION_SERVICE = conversionService;
}
@@ -325,4 +329,29 @@ public abstract class JpaQueryExecution {
return StreamUtils.createStreamFromIterator(iter);
}
}
/**
* Removes the converter being able to convert any object into an {@link Optional} from the given
* {@link ConversionService} in case we're running on Java 8.
*
* @param conversionService must not be {@literal null}.
*/
public static void potentiallyRemoveOptionalConverter(ConfigurableConversionService conversionService) {
ClassLoader classLoader = JpaQueryExecution.class.getClassLoader();
if (ClassUtils.isPresent("java.util.Optional", classLoader)) {
try {
Class<?> optionalType = ClassUtils.forName("java.util.Optional", classLoader);
conversionService.removeConvertible(Object.class, optionalType);
} catch (ClassNotFoundException e) {
return;
} catch (LinkageError e) {
return;
}
}
}
}

View File

@@ -17,10 +17,11 @@ package org.springframework.data.jpa.repository.query;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import java.util.Optional;
import javax.persistence.EntityManager;
import javax.persistence.Query;
@@ -147,15 +148,32 @@ public class JpaQueryExecutionUnitTests {
verify(jpaQuery, times(0)).createQuery((Object[]) any());
}
public static void sampleMethod(Pageable pageable) {
/**
* @see DATAJPA-951
*/
@Test
public void doesNotPreemtivelyWrapResultIntoOptional() throws Exception {
doReturn(method).when(jpaQuery).getQueryMethod();
doReturn(Optional.class).when(method).getReturnType();
StubQueryExecution execution = new StubQueryExecution() {
protected Object doExecute(AbstractJpaQuery query, Object[] values) {
return "result";
}
};
Object result = execution.execute(jpaQuery, new Object[0]);
assertThat(result, is(instanceOf(String.class)));
}
public static void sampleMethod(Pageable pageable) {}
static class StubQueryExecution extends JpaQueryExecution {
@Override
protected Object doExecute(AbstractJpaQuery query, Object[] values) {
return null;
}
}