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. 147
      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 {
* @return {@literal true} if the return type is an interface-based projection. * @return {@literal true} if the return type is an interface-based projection.
*/ */
public boolean isInterfaceProjection() { 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();
} }
/** /**

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

@ -24,7 +24,6 @@ import java.util.Set;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.data.mapping.Parameter; import org.springframework.data.mapping.Parameter;
@ -32,6 +31,7 @@ import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.model.PreferredConstructorDiscoverer; import org.springframework.data.mapping.model.PreferredConstructorDiscoverer;
import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.ProjectionInformation; import org.springframework.data.projection.ProjectionInformation;
import org.springframework.data.util.Lazy;
import org.springframework.lang.Contract; import org.springframework.lang.Contract;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
@ -83,7 +83,7 @@ public abstract class ReturnedType {
} }
/** /**
* Returns the entity type. * Return the entity type.
* *
* @return * @return
*/ */
@ -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}. * @param source can be {@literal null}.
* @return * @return
@ -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 * @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 * @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 * Return whether the type is a DTO projection.
* returned to indicate a generic type (a map or tuple-like type) shall be used.
* *
* @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() * @see ProjectionInformation#getInputProperties()
*/ */
public abstract List<String> 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 * @since 3.3.5
* @see ProjectionInformation#hasInputProperties() * @see ProjectionInformation#hasInputProperties()
*/ */
@ -150,16 +151,29 @@ public abstract class ReturnedType {
return !CollectionUtils.isEmpty(getInputProperties()); 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. * A {@link ReturnedType} that's backed by an interface.
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Mark Paluch
* @since 1.12 * @since 1.12
*/ */
private static final class ReturnedInterface extends ReturnedType { private static final class ReturnedInterface extends ReturnedType {
private final ProjectionInformation information; private final ProjectionInformation information;
private final Class<?> domainType; private final Class<?> domainType;
private final boolean isProjecting;
private final List<String> inputProperties; private final List<String> inputProperties;
/** /**
@ -176,6 +190,7 @@ public abstract class ReturnedType {
this.information = information; this.information = information;
this.domainType = domainType; this.domainType = domainType;
this.isProjecting = !information.getType().isAssignableFrom(domainType);
this.inputProperties = detectInputProperties(information); this.inputProperties = detectInputProperties(information);
} }
@ -198,24 +213,35 @@ public abstract class ReturnedType {
} }
@Override @Override
public boolean needsCustomConstruction() { public boolean isProjecting() {
return isProjecting() && information.isClosed(); return isProjecting;
} }
@Override @Override
public boolean isProjecting() { public boolean isInterfaceProjection() {
return !information.getType().isAssignableFrom(domainType); return isProjecting();
} }
@Override @Override
public @Nullable Class<?> getTypeToRead() { public boolean isDtoProjection() {
return isProjecting() && information.isClosed() ? null : domainType; return false;
} }
@Override @Override
public List<String> getInputProperties() { public List<String> getInputProperties() {
return inputProperties; 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 {
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Mikhail Polivakha * @author Mikhail Polivakha
* @author Mark Paluch
* @since 1.12 * @since 1.12
*/ */
private static final class ReturnedClass extends ReturnedType { private static final class ReturnedClass extends ReturnedType {
@ -231,7 +258,8 @@ public abstract class ReturnedType {
private final Class<?> type; private final Class<?> type;
private final boolean isDto; 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. * Creates a new {@link ReturnedClass} instance for the given returned type and domain type.
@ -256,7 +284,13 @@ public abstract class ReturnedType {
!VOID_TYPES.contains(type) && // !VOID_TYPES.contains(type) && //
!type.getPackage().getName().startsWith("java."); !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 @Override
@ -265,33 +299,53 @@ public abstract class ReturnedType {
} }
@Override @Override
@NonNull public boolean isProjecting() {
public Class<?> getTypeToRead() { return isDto;
return type;
} }
@Override @Override
public boolean isProjecting() { public boolean isInterfaceProjection() {
return isDto(); return false;
} }
@Override @Override
public boolean needsCustomConstruction() { public boolean isDtoProjection() {
return isDto() && !inputProperties.isEmpty(); return isProjecting();
} }
@Override @Override
public List<String> getInputProperties() { 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()) { @Override
return Collections.emptyList(); 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) { if (constructor == null) {
return Collections.emptyList(); return Collections.emptyList();
@ -310,24 +364,13 @@ public abstract class ReturnedType {
if (logger.isWarnEnabled()) { if (logger.isWarnEnabled()) {
logger.warn(("No constructor parameter names discovered. " logger.warn(("No constructor parameter names discovered. "
+ "Compile the affected code with '-parameters' instead or avoid its introspection: %s") + "Compile the affected code with '-parameters' instead or avoid its introspection: %s")
.formatted(type.getName())); .formatted(constructor.getConstructor().getDeclaringClass().getName()));
} }
} }
return Collections.unmodifiableList(properties); 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 { private static final class CacheKey {
@ -396,5 +439,7 @@ public abstract class ReturnedType {
return "ReturnedType.CacheKey(returnedType=" + this.getReturnedType() + ", domainType=" + this.getDomainType() return "ReturnedType.CacheKey(returnedType=" + this.getReturnedType() + ", domainType=" + this.getDomainType()
+ ", projectionFactoryHashCode=" + this.getProjectionFactoryHashCode() + ")"; + ", 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
*/ */
class ReturnedTypeUnitTests { class ReturnedTypeUnitTests {
@Test // DATACMNS-89 @Test // DATACMNS-89, GH-3410
void treatsSimpleDomainTypeAsIs() throws Exception { void treatsSimpleDomainTypeAsIs() throws Exception {
var type = getReturnedType("findAll"); var type = getReturnedType("findAll");
@ -45,10 +45,12 @@ class ReturnedTypeUnitTests {
assertThat(type.getTypeToRead()).isEqualTo(Sample.class); assertThat(type.getTypeToRead()).isEqualTo(Sample.class);
assertThat(type.getInputProperties()).isEmpty(); assertThat(type.getInputProperties()).isEmpty();
assertThat(type.isProjecting()).isFalse(); assertThat(type.isProjecting()).isFalse();
assertThat(type.isDtoProjection()).isFalse();
assertThat(type.isInterfaceProjection()).isFalse();
assertThat(type.needsCustomConstruction()).isFalse(); assertThat(type.needsCustomConstruction()).isFalse();
} }
@Test // DATACMNS-89 @Test // DATACMNS-89, GH-3410
void detectsDto() throws Exception { void detectsDto() throws Exception {
var type = getReturnedType("findAllDtos"); var type = getReturnedType("findAllDtos");
@ -57,6 +59,8 @@ class ReturnedTypeUnitTests {
assertThat(type.getInputProperties()).contains("firstname"); assertThat(type.getInputProperties()).contains("firstname");
assertThat(type.isInstance(new SampleDto("firstname"))).isTrue(); assertThat(type.isInstance(new SampleDto("firstname"))).isTrue();
assertThat(type.isProjecting()).isTrue(); assertThat(type.isProjecting()).isTrue();
assertThat(type.isDtoProjection()).isTrue();
assertThat(type.isInterfaceProjection()).isFalse();
assertThat(type.needsCustomConstruction()).isTrue(); assertThat(type.needsCustomConstruction()).isTrue();
} }
@ -78,13 +82,15 @@ class ReturnedTypeUnitTests {
assertThat(type.getReturnedType()).isEqualTo(void.class); assertThat(type.getReturnedType()).isEqualTo(void.class);
} }
@Test // DATACMNS-89 @Test // DATACMNS-89, GH-3410
void detectsClosedProjection() throws Exception { void detectsClosedProjection() throws Exception {
var type = getReturnedType("findOneProjection"); var type = getReturnedType("findOneProjection");
assertThat(type.getReturnedType()).isEqualTo(SampleProjection.class); assertThat(type.getReturnedType()).isEqualTo(SampleProjection.class);
assertThat(type.isProjecting()).isTrue(); assertThat(type.isProjecting()).isTrue();
assertThat(type.isInterfaceProjection()).isTrue();
assertThat(type.isDtoProjection()).isFalse();
assertThat(type.needsCustomConstruction()).isTrue(); assertThat(type.needsCustomConstruction()).isTrue();
} }
@ -109,12 +115,14 @@ class ReturnedTypeUnitTests {
assertThat(type.getTypeToRead()).isEqualTo(BigInteger.class); assertThat(type.getTypeToRead()).isEqualTo(BigInteger.class);
} }
@Test // DATACMNS-840 @Test // DATACMNS-840, GH-3410
void detectsSampleDtoWithDefaultConstructor() throws Exception { void detectsSampleDtoWithDefaultConstructor() throws Exception {
var type = getReturnedType("dtoWithMultipleConstructors"); var type = getReturnedType("dtoWithMultipleConstructors");
assertThat(type.getInputProperties()).isEmpty(); assertThat(type.getInputProperties()).isEmpty();
assertThat(type.isDtoProjection()).isTrue();
assertThat(type.isInterfaceProjection()).isFalse();
assertThat(type.needsCustomConstruction()).isFalse(); assertThat(type.needsCustomConstruction()).isFalse();
} }

Loading…
Cancel
Save