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
pull/3415/head
Mark Paluch 4 weeks ago
parent
commit
0a269466f3
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 12
      src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java
  2. 145
      src/main/java/org/springframework/data/repository/query/ReturnedType.java
  3. 16
      src/test/java/org/springframework/data/repository/query/ReturnedTypeUnitTests.java

12
src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java

@ -99,7 +99,17 @@ public class MethodReturn { @@ -99,7 +99,17 @@ public class MethodReturn {
* @return {@literal true} if the return type is an interface-based projection.
*/
public boolean isInterfaceProjection() {
return isProjecting() && returnedType.getReturnedType().isInterface();
return returnedType.isInterfaceProjection();
}
/**
* Returns whether the method return type is a DTO projection.
*
* @return {@literal true} if the return type is a DTO based projection.
* @since 4.0.1
*/
public boolean isDtoProjection() {
return returnedType.isDtoProjection();
}
/**

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

@ -24,7 +24,6 @@ import java.util.Set; @@ -24,7 +24,6 @@ import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.data.mapping.Parameter;
@ -32,6 +31,7 @@ import org.springframework.data.mapping.PreferredConstructor; @@ -32,6 +31,7 @@ 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.util.Assert;
import org.springframework.util.ClassUtils;
@ -83,7 +83,7 @@ public abstract class ReturnedType { @@ -83,7 +83,7 @@ public abstract class ReturnedType {
}
/**
* Returns the entity type.
* Return the entity type.
*
* @return
*/
@ -92,7 +92,7 @@ public abstract class ReturnedType { @@ -92,7 +92,7 @@ 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
@ -103,46 +103,47 @@ public abstract class ReturnedType { @@ -103,46 +103,47 @@ public abstract class ReturnedType {
}
/**
* 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 4.0.1
*/
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 4.0.1
*/
public abstract @Nullable 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()
*/
@ -150,16 +151,29 @@ public abstract class ReturnedType { @@ -150,16 +151,29 @@ 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.
*/
public abstract @Nullable 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;
/**
@ -176,6 +190,7 @@ public abstract class ReturnedType { @@ -176,6 +190,7 @@ public abstract class ReturnedType {
this.information = information;
this.domainType = domainType;
this.isProjecting = !information.getType().isAssignableFrom(domainType);
this.inputProperties = detectInputProperties(information);
}
@ -198,24 +213,35 @@ public abstract class ReturnedType { @@ -198,24 +213,35 @@ 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();
}
@Override
public @Nullable 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
public @Nullable Class<?> getTypeToRead() {
return isProjecting() && information.isClosed() ? null : domainType;
}
}
/**
@ -223,6 +249,7 @@ public abstract class ReturnedType { @@ -223,6 +249,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 +258,8 @@ public abstract class ReturnedType { @@ -231,7 +258,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 +284,13 @@ public abstract class ReturnedType { @@ -256,7 +284,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 +299,53 @@ public abstract class ReturnedType { @@ -265,33 +299,53 @@ 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
public Class<?> getTypeToRead() {
return type;
}
private boolean isDomainSubtype() {
return getDomainType().equals(type) && getDomainType().isAssignableFrom(type);
}
private boolean isPrimitiveOrWrapper() {
return ClassUtils.isPrimitiveOrWrapper(type);
}
private @Nullable PreferredConstructor<?, ?> detectConstructor(Class<?> type) {
return isDtoProjection() ? PreferredConstructorDiscoverer.discover(type) : null;
}
private List<String> detectConstructorParameterNames() {
if (constructor == null) {
return Collections.emptyList();
@ -310,24 +364,13 @@ public abstract class ReturnedType { @@ -310,24 +364,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 {
@ -396,5 +439,7 @@ public abstract class ReturnedType { @@ -396,5 +439,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