diff --git a/src/main/java/org/springframework/data/repository/query/ReturnedType.java b/src/main/java/org/springframework/data/repository/query/ReturnedType.java index 3bfb31cbc..cb76cf9b3 100644 --- a/src/main/java/org/springframework/data/repository/query/ReturnedType.java +++ b/src/main/java/org/springframework/data/repository/query/ReturnedType.java @@ -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 { } /** - * Returns the entity type. + * Return the entity type. * * @return */ @@ -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 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 { 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 inputProperties; /** @@ -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 { } @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 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 { * * @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 { private final Class type; private final boolean isDto; - private final List inputProperties; + private final @Nullable PreferredConstructor constructor; + private final Lazy> inputProperties; /** * Creates a new {@link ReturnedClass} instance for the given returned type and domain type. @@ -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 { } @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 getInputProperties() { - return inputProperties; + return inputProperties.get(); } - private List 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 detectConstructorParameterNames() { if (constructor == null) { return Collections.emptyList(); @@ -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 { return "ReturnedType.CacheKey(returnedType=" + this.getReturnedType() + ", domainType=" + this.getDomainType() + ", projectionFactoryHashCode=" + this.getProjectionFactoryHashCode() + ")"; } + } + } diff --git a/src/test/java/org/springframework/data/repository/query/ReturnedTypeUnitTests.java b/src/test/java/org/springframework/data/repository/query/ReturnedTypeUnitTests.java index 2bcaa24e5..6e6204e94 100755 --- a/src/test/java/org/springframework/data/repository/query/ReturnedTypeUnitTests.java +++ b/src/test/java/org/springframework/data/repository/query/ReturnedTypeUnitTests.java @@ -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 { 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 { 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 { 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 { 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(); }