Browse Source

Defer `ReturnedClass.inputProperties` lookup.

We now deferr input properties lookup to avoid eager parameter introspection and therefore potentially logging of missing property names even in case these are not required.

Also, introduce isDtoProjection() and isInterfaceProjection() methods to simplify calling code typically introspecting ReturnedType.getReturnType().isInterface().

Closes #3410
3.5.x
Mark Paluch 4 weeks ago
parent
commit
21809f7ac9
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 154
      src/main/java/org/springframework/data/repository/query/ReturnedType.java
  2. 16
      src/test/java/org/springframework/data/repository/query/ReturnedTypeUnitTests.java

154
src/main/java/org/springframework/data/repository/query/ReturnedType.java

@ -30,6 +30,8 @@ import org.springframework.data.mapping.PreferredConstructor; @@ -30,6 +30,8 @@ import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.model.PreferredConstructorDiscoverer;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.ProjectionInformation;
import org.springframework.data.util.Lazy;
import org.springframework.lang.Contract;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@ -82,7 +84,7 @@ public abstract class ReturnedType { @@ -82,7 +84,7 @@ public abstract class ReturnedType {
}
/**
* Returns the entity type.
* Return the entity type.
*
* @return
*/
@ -91,57 +93,58 @@ public abstract class ReturnedType { @@ -91,57 +93,58 @@ public abstract class ReturnedType {
}
/**
* Returns whether the given source object is an instance of the returned type.
* Return whether the given source object is an instance of the returned type.
*
* @param source can be {@literal null}.
* @return
*/
@Contract("null -> false")
public final boolean isInstance(@Nullable Object source) {
return getReturnedType().isInstance(source);
}
/**
* Returns whether the type is projecting, i.e. not of the domain type.
* Return the type of the individual objects to return.
*
* @return
*/
public abstract boolean isProjecting();
public abstract Class<?> getReturnedType();
/**
* Returns the type of the individual objects to return.
* Return whether the type is projecting, i.e. not of the domain type.
*
* @return
*/
public abstract Class<?> getReturnedType();
public abstract boolean isProjecting();
/**
* Returns whether the returned type will require custom construction.
* Return whether the type is an interface-projection.
*
* @return
* @since 3.5.7
*/
public abstract boolean needsCustomConstruction();
public boolean isInterfaceProjection() {
return isProjecting() && getReturnedType().isInterface();
}
/**
* Returns the type that the query execution is supposed to pass to the underlying infrastructure. {@literal null} is
* returned to indicate a generic type (a map or tuple-like type) shall be used.
* Return whether the type is a DTO projection.
*
* @return
* @since 3.5.7
*/
@Nullable
public abstract Class<?> getTypeToRead();
public boolean isDtoProjection() {
return isProjecting() && !getReturnedType().isInterface();
}
/**
* Returns the properties required to be used to populate the result.
* Return the properties required to be used to populate the result.
*
* @return
* @see ProjectionInformation#getInputProperties()
*/
public abstract List<String> getInputProperties();
/**
* Returns whether the returned type has input properties.
* Return whether the returned type has input properties.
*
* @return
* @since 3.3.5
* @see ProjectionInformation#hasInputProperties()
*/
@ -149,16 +152,30 @@ public abstract class ReturnedType { @@ -149,16 +152,30 @@ public abstract class ReturnedType {
return !CollectionUtils.isEmpty(getInputProperties());
}
/**
* Return whether the returned type will require custom construction.
*/
public abstract boolean needsCustomConstruction();
/**
* Return the type that the query execution is supposed to pass to the underlying infrastructure. {@literal null} is
* returned to indicate a generic type (a map or tuple-like type) shall be used.
*/
@Nullable
public abstract Class<?> getTypeToRead();
/**
* A {@link ReturnedType} that's backed by an interface.
*
* @author Oliver Gierke
* @author Mark Paluch
* @since 1.12
*/
private static final class ReturnedInterface extends ReturnedType {
private final ProjectionInformation information;
private final Class<?> domainType;
private final boolean isProjecting;
private final List<String> inputProperties;
/**
@ -175,6 +192,7 @@ public abstract class ReturnedType { @@ -175,6 +192,7 @@ public abstract class ReturnedType {
this.information = information;
this.domainType = domainType;
this.isProjecting = !information.getType().isAssignableFrom(domainType);
this.inputProperties = detectInputProperties(information);
}
@ -197,25 +215,36 @@ public abstract class ReturnedType { @@ -197,25 +215,36 @@ public abstract class ReturnedType {
}
@Override
public boolean needsCustomConstruction() {
return isProjecting() && information.isClosed();
public boolean isProjecting() {
return isProjecting;
}
@Override
public boolean isProjecting() {
return !information.getType().isAssignableFrom(domainType);
public boolean isInterfaceProjection() {
return isProjecting();
}
@Nullable
@Override
public Class<?> getTypeToRead() {
return isProjecting() && information.isClosed() ? null : domainType;
public boolean isDtoProjection() {
return false;
}
@Override
public List<String> getInputProperties() {
return inputProperties;
}
@Override
public boolean needsCustomConstruction() {
return isProjecting() && information.isClosed();
}
@Override
@Nullable
public Class<?> getTypeToRead() {
return isProjecting() && information.isClosed() ? null : domainType;
}
}
/**
@ -223,6 +252,7 @@ public abstract class ReturnedType { @@ -223,6 +252,7 @@ public abstract class ReturnedType {
*
* @author Oliver Gierke
* @author Mikhail Polivakha
* @author Mark Paluch
* @since 1.12
*/
private static final class ReturnedClass extends ReturnedType {
@ -231,7 +261,8 @@ public abstract class ReturnedType { @@ -231,7 +261,8 @@ public abstract class ReturnedType {
private final Class<?> type;
private final boolean isDto;
private final List<String> inputProperties;
private final @Nullable PreferredConstructor<?, ?> constructor;
private final Lazy<List<String>> inputProperties;
/**
* Creates a new {@link ReturnedClass} instance for the given returned type and domain type.
@ -256,7 +287,13 @@ public abstract class ReturnedType { @@ -256,7 +287,13 @@ public abstract class ReturnedType {
!VOID_TYPES.contains(type) && //
!type.getPackage().getName().startsWith("java.");
this.inputProperties = detectConstructorParameterNames(returnedType);
this.constructor = detectConstructor(type);
if (this.constructor == null) {
this.inputProperties = Lazy.of(Collections.emptyList());
} else {
this.inputProperties = Lazy.of(this::detectConstructorParameterNames);
}
}
@Override
@ -265,33 +302,55 @@ public abstract class ReturnedType { @@ -265,33 +302,55 @@ public abstract class ReturnedType {
}
@Override
@NonNull
public Class<?> getTypeToRead() {
return type;
public boolean isProjecting() {
return isDto;
}
@Override
public boolean isProjecting() {
return isDto();
public boolean isInterfaceProjection() {
return false;
}
@Override
public boolean needsCustomConstruction() {
return isDto() && !inputProperties.isEmpty();
public boolean isDtoProjection() {
return isProjecting();
}
@Override
public List<String> getInputProperties() {
return inputProperties;
return inputProperties.get();
}
private List<String> detectConstructorParameterNames(Class<?> type) {
@Override
public boolean hasInputProperties() {
return this.constructor != null && this.constructor.getParameterCount() > 0 && super.hasInputProperties();
}
if (!isDto()) {
return Collections.emptyList();
}
@Override
public boolean needsCustomConstruction() {
return isDtoProjection() && hasInputProperties();
}
PreferredConstructor<?, ?> constructor = PreferredConstructorDiscoverer.discover(type);
@Override
@NonNull
public Class<?> getTypeToRead() {
return type;
}
private boolean isDomainSubtype() {
return getDomainType().equals(type) && getDomainType().isAssignableFrom(type);
}
private boolean isPrimitiveOrWrapper() {
return ClassUtils.isPrimitiveOrWrapper(type);
}
@Nullable
private PreferredConstructor<?, ?> detectConstructor(Class<?> type) {
return isDtoProjection() ? PreferredConstructorDiscoverer.discover(type) : null;
}
private List<String> detectConstructorParameterNames() {
if (constructor == null) {
return Collections.emptyList();
@ -310,24 +369,13 @@ public abstract class ReturnedType { @@ -310,24 +369,13 @@ public abstract class ReturnedType {
if (logger.isWarnEnabled()) {
logger.warn(("No constructor parameter names discovered. "
+ "Compile the affected code with '-parameters' instead or avoid its introspection: %s")
.formatted(type.getName()));
.formatted(constructor.getConstructor().getDeclaringClass().getName()));
}
}
return Collections.unmodifiableList(properties);
}
private boolean isDto() {
return isDto;
}
private boolean isDomainSubtype() {
return getDomainType().equals(type) && getDomainType().isAssignableFrom(type);
}
private boolean isPrimitiveOrWrapper() {
return ClassUtils.isPrimitiveOrWrapper(type);
}
}
private static final class CacheKey {
@ -394,5 +442,7 @@ public abstract class ReturnedType { @@ -394,5 +442,7 @@ public abstract class ReturnedType {
return "ReturnedType.CacheKey(returnedType=" + this.getReturnedType() + ", domainType=" + this.getDomainType()
+ ", projectionFactoryHashCode=" + this.getProjectionFactoryHashCode() + ")";
}
}
}

16
src/test/java/org/springframework/data/repository/query/ReturnedTypeUnitTests.java

@ -37,7 +37,7 @@ import org.springframework.data.repository.core.support.DefaultRepositoryMetadat @@ -37,7 +37,7 @@ import org.springframework.data.repository.core.support.DefaultRepositoryMetadat
*/
class ReturnedTypeUnitTests {
@Test // DATACMNS-89
@Test // DATACMNS-89, GH-3410
void treatsSimpleDomainTypeAsIs() throws Exception {
var type = getReturnedType("findAll");
@ -45,10 +45,12 @@ class ReturnedTypeUnitTests { @@ -45,10 +45,12 @@ class ReturnedTypeUnitTests {
assertThat(type.getTypeToRead()).isEqualTo(Sample.class);
assertThat(type.getInputProperties()).isEmpty();
assertThat(type.isProjecting()).isFalse();
assertThat(type.isDtoProjection()).isFalse();
assertThat(type.isInterfaceProjection()).isFalse();
assertThat(type.needsCustomConstruction()).isFalse();
}
@Test // DATACMNS-89
@Test // DATACMNS-89, GH-3410
void detectsDto() throws Exception {
var type = getReturnedType("findAllDtos");
@ -57,6 +59,8 @@ class ReturnedTypeUnitTests { @@ -57,6 +59,8 @@ class ReturnedTypeUnitTests {
assertThat(type.getInputProperties()).contains("firstname");
assertThat(type.isInstance(new SampleDto("firstname"))).isTrue();
assertThat(type.isProjecting()).isTrue();
assertThat(type.isDtoProjection()).isTrue();
assertThat(type.isInterfaceProjection()).isFalse();
assertThat(type.needsCustomConstruction()).isTrue();
}
@ -78,13 +82,15 @@ class ReturnedTypeUnitTests { @@ -78,13 +82,15 @@ class ReturnedTypeUnitTests {
assertThat(type.getReturnedType()).isEqualTo(void.class);
}
@Test // DATACMNS-89
@Test // DATACMNS-89, GH-3410
void detectsClosedProjection() throws Exception {
var type = getReturnedType("findOneProjection");
assertThat(type.getReturnedType()).isEqualTo(SampleProjection.class);
assertThat(type.isProjecting()).isTrue();
assertThat(type.isInterfaceProjection()).isTrue();
assertThat(type.isDtoProjection()).isFalse();
assertThat(type.needsCustomConstruction()).isTrue();
}
@ -109,12 +115,14 @@ class ReturnedTypeUnitTests { @@ -109,12 +115,14 @@ class ReturnedTypeUnitTests {
assertThat(type.getTypeToRead()).isEqualTo(BigInteger.class);
}
@Test // DATACMNS-840
@Test // DATACMNS-840, GH-3410
void detectsSampleDtoWithDefaultConstructor() throws Exception {
var type = getReturnedType("dtoWithMultipleConstructors");
assertThat(type.getInputProperties()).isEmpty();
assertThat(type.isDtoProjection()).isTrue();
assertThat(type.isInterfaceProjection()).isFalse();
assertThat(type.needsCustomConstruction()).isFalse();
}

Loading…
Cancel
Save