Skip to content

Commit 0a26946

Browse files
committed
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
1 parent 039ce57 commit 0a26946

File tree

3 files changed

+119
-56
lines changed

3 files changed

+119
-56
lines changed

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,17 @@ public boolean isProjecting() {
9999
* @return {@literal true} if the return type is an interface-based projection.
100100
*/
101101
public boolean isInterfaceProjection() {
102-
return isProjecting() && returnedType.getReturnedType().isInterface();
102+
return returnedType.isInterfaceProjection();
103+
}
104+
105+
/**
106+
* Returns whether the method return type is a DTO projection.
107+
*
108+
* @return {@literal true} if the return type is a DTO based projection.
109+
* @since 4.0.1
110+
*/
111+
public boolean isDtoProjection() {
112+
return returnedType.isDtoProjection();
103113
}
104114

105115
/**

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

Lines changed: 96 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@
2424

2525
import org.apache.commons.logging.Log;
2626
import org.apache.commons.logging.LogFactory;
27-
import org.jspecify.annotations.NonNull;
2827
import org.jspecify.annotations.Nullable;
2928

3029
import org.springframework.data.mapping.Parameter;
3130
import org.springframework.data.mapping.PreferredConstructor;
3231
import org.springframework.data.mapping.model.PreferredConstructorDiscoverer;
3332
import org.springframework.data.projection.ProjectionFactory;
3433
import org.springframework.data.projection.ProjectionInformation;
34+
import org.springframework.data.util.Lazy;
3535
import org.springframework.lang.Contract;
3636
import org.springframework.util.Assert;
3737
import org.springframework.util.ClassUtils;
@@ -83,7 +83,7 @@ public static ReturnedType of(Class<?> returnedType, Class<?> domainType, Projec
8383
}
8484

8585
/**
86-
* Returns the entity type.
86+
* Return the entity type.
8787
*
8888
* @return
8989
*/
@@ -92,7 +92,7 @@ public final Class<?> getDomainType() {
9292
}
9393

9494
/**
95-
* Returns whether the given source object is an instance of the returned type.
95+
* Return whether the given source object is an instance of the returned type.
9696
*
9797
* @param source can be {@literal null}.
9898
* @return
@@ -103,63 +103,77 @@ public final boolean isInstance(@Nullable Object source) {
103103
}
104104

105105
/**
106-
* Returns whether the type is projecting, i.e. not of the domain type.
106+
* Return the type of the individual objects to return.
107107
*
108108
* @return
109109
*/
110-
public abstract boolean isProjecting();
110+
public abstract Class<?> getReturnedType();
111111

112112
/**
113-
* Returns the type of the individual objects to return.
113+
* Return whether the type is projecting, i.e. not of the domain type.
114114
*
115115
* @return
116116
*/
117-
public abstract Class<?> getReturnedType();
117+
public abstract boolean isProjecting();
118118

119119
/**
120-
* Returns whether the returned type will require custom construction.
120+
* Return whether the type is an interface-projection.
121121
*
122-
* @return
122+
* @since 4.0.1
123123
*/
124-
public abstract boolean needsCustomConstruction();
124+
public boolean isInterfaceProjection() {
125+
return isProjecting() && getReturnedType().isInterface();
126+
}
125127

126128
/**
127-
* Returns the type that the query execution is supposed to pass to the underlying infrastructure. {@literal null} is
128-
* returned to indicate a generic type (a map or tuple-like type) shall be used.
129+
* Return whether the type is a DTO projection.
129130
*
130-
* @return
131+
* @since 4.0.1
131132
*/
132-
public abstract @Nullable Class<?> getTypeToRead();
133+
public boolean isDtoProjection() {
134+
return isProjecting() && !getReturnedType().isInterface();
135+
}
133136

134137
/**
135-
* Returns the properties required to be used to populate the result.
138+
* Return the properties required to be used to populate the result.
136139
*
137-
* @return
138140
* @see ProjectionInformation#getInputProperties()
139141
*/
140142
public abstract List<String> getInputProperties();
141143

142144
/**
143-
* Returns whether the returned type has input properties.
145+
* Return whether the returned type has input properties.
144146
*
145-
* @return
146147
* @since 3.3.5
147148
* @see ProjectionInformation#hasInputProperties()
148149
*/
149150
public boolean hasInputProperties() {
150151
return !CollectionUtils.isEmpty(getInputProperties());
151152
}
152153

154+
/**
155+
* Return whether the returned type will require custom construction.
156+
*/
157+
public abstract boolean needsCustomConstruction();
158+
159+
/**
160+
* Return the type that the query execution is supposed to pass to the underlying infrastructure. {@literal null} is
161+
* returned to indicate a generic type (a map or tuple-like type) shall be used.
162+
*/
163+
public abstract @Nullable Class<?> getTypeToRead();
164+
153165
/**
154166
* A {@link ReturnedType} that's backed by an interface.
155167
*
156168
* @author Oliver Gierke
169+
* @author Mark Paluch
157170
* @since 1.12
158171
*/
159172
private static final class ReturnedInterface extends ReturnedType {
160173

161174
private final ProjectionInformation information;
162175
private final Class<?> domainType;
176+
private final boolean isProjecting;
163177
private final List<String> inputProperties;
164178

165179
/**
@@ -176,6 +190,7 @@ public ReturnedInterface(ProjectionInformation information, Class<?> domainType)
176190

177191
this.information = information;
178192
this.domainType = domainType;
193+
this.isProjecting = !information.getType().isAssignableFrom(domainType);
179194
this.inputProperties = detectInputProperties(information);
180195
}
181196

@@ -198,31 +213,43 @@ public Class<?> getReturnedType() {
198213
}
199214

200215
@Override
201-
public boolean needsCustomConstruction() {
202-
return isProjecting() && information.isClosed();
216+
public boolean isProjecting() {
217+
return isProjecting;
203218
}
204219

205220
@Override
206-
public boolean isProjecting() {
207-
return !information.getType().isAssignableFrom(domainType);
221+
public boolean isInterfaceProjection() {
222+
return isProjecting();
208223
}
209224

210225
@Override
211-
public @Nullable Class<?> getTypeToRead() {
212-
return isProjecting() && information.isClosed() ? null : domainType;
226+
public boolean isDtoProjection() {
227+
return false;
213228
}
214229

215230
@Override
216231
public List<String> getInputProperties() {
217232
return inputProperties;
218233
}
234+
235+
@Override
236+
public boolean needsCustomConstruction() {
237+
return isProjecting() && information.isClosed();
238+
}
239+
240+
@Override
241+
public @Nullable Class<?> getTypeToRead() {
242+
return isProjecting() && information.isClosed() ? null : domainType;
243+
}
244+
219245
}
220246

221247
/**
222248
* A {@link ReturnedType} that's backed by an actual class.
223249
*
224250
* @author Oliver Gierke
225251
* @author Mikhail Polivakha
252+
* @author Mark Paluch
226253
* @since 1.12
227254
*/
228255
private static final class ReturnedClass extends ReturnedType {
@@ -231,7 +258,8 @@ private static final class ReturnedClass extends ReturnedType {
231258

232259
private final Class<?> type;
233260
private final boolean isDto;
234-
private final List<String> inputProperties;
261+
private final @Nullable PreferredConstructor<?, ?> constructor;
262+
private final Lazy<List<String>> inputProperties;
235263

236264
/**
237265
* Creates a new {@link ReturnedClass} instance for the given returned type and domain type.
@@ -256,7 +284,13 @@ public ReturnedClass(Class<?> returnedType, Class<?> domainType) {
256284
!VOID_TYPES.contains(type) && //
257285
!type.getPackage().getName().startsWith("java.");
258286

259-
this.inputProperties = detectConstructorParameterNames(returnedType);
287+
this.constructor = detectConstructor(type);
288+
289+
if (this.constructor == null) {
290+
this.inputProperties = Lazy.of(Collections.emptyList());
291+
} else {
292+
this.inputProperties = Lazy.of(this::detectConstructorParameterNames);
293+
}
260294
}
261295

262296
@Override
@@ -265,33 +299,53 @@ public Class<?> getReturnedType() {
265299
}
266300

267301
@Override
268-
@NonNull
269-
public Class<?> getTypeToRead() {
270-
return type;
302+
public boolean isProjecting() {
303+
return isDto;
271304
}
272305

273306
@Override
274-
public boolean isProjecting() {
275-
return isDto();
307+
public boolean isInterfaceProjection() {
308+
return false;
276309
}
277310

278311
@Override
279-
public boolean needsCustomConstruction() {
280-
return isDto() && !inputProperties.isEmpty();
312+
public boolean isDtoProjection() {
313+
return isProjecting();
281314
}
282315

283316
@Override
284317
public List<String> getInputProperties() {
285-
return inputProperties;
318+
return inputProperties.get();
286319
}
287320

288-
private List<String> detectConstructorParameterNames(Class<?> type) {
321+
@Override
322+
public boolean hasInputProperties() {
323+
return this.constructor != null && this.constructor.getParameterCount() > 0 && super.hasInputProperties();
324+
}
289325

290-
if (!isDto()) {
291-
return Collections.emptyList();
292-
}
326+
@Override
327+
public boolean needsCustomConstruction() {
328+
return isDtoProjection() && hasInputProperties();
329+
}
293330

294-
PreferredConstructor<?, ?> constructor = PreferredConstructorDiscoverer.discover(type);
331+
@Override
332+
public Class<?> getTypeToRead() {
333+
return type;
334+
}
335+
336+
private boolean isDomainSubtype() {
337+
return getDomainType().equals(type) && getDomainType().isAssignableFrom(type);
338+
}
339+
340+
private boolean isPrimitiveOrWrapper() {
341+
return ClassUtils.isPrimitiveOrWrapper(type);
342+
}
343+
344+
private @Nullable PreferredConstructor<?, ?> detectConstructor(Class<?> type) {
345+
return isDtoProjection() ? PreferredConstructorDiscoverer.discover(type) : null;
346+
}
347+
348+
private List<String> detectConstructorParameterNames() {
295349

296350
if (constructor == null) {
297351
return Collections.emptyList();
@@ -310,24 +364,13 @@ private List<String> detectConstructorParameterNames(Class<?> type) {
310364
if (logger.isWarnEnabled()) {
311365
logger.warn(("No constructor parameter names discovered. "
312366
+ "Compile the affected code with '-parameters' instead or avoid its introspection: %s")
313-
.formatted(type.getName()));
367+
.formatted(constructor.getConstructor().getDeclaringClass().getName()));
314368
}
315369
}
316370

317371
return Collections.unmodifiableList(properties);
318372
}
319373

320-
private boolean isDto() {
321-
return isDto;
322-
}
323-
324-
private boolean isDomainSubtype() {
325-
return getDomainType().equals(type) && getDomainType().isAssignableFrom(type);
326-
}
327-
328-
private boolean isPrimitiveOrWrapper() {
329-
return ClassUtils.isPrimitiveOrWrapper(type);
330-
}
331374
}
332375

333376
private static final class CacheKey {
@@ -396,5 +439,7 @@ public String toString() {
396439
return "ReturnedType.CacheKey(returnedType=" + this.getReturnedType() + ", domainType=" + this.getDomainType()
397440
+ ", projectionFactoryHashCode=" + this.getProjectionFactoryHashCode() + ")";
398441
}
442+
399443
}
444+
400445
}

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,20 @@
3737
*/
3838
class ReturnedTypeUnitTests {
3939

40-
@Test // DATACMNS-89
40+
@Test // DATACMNS-89, GH-3410
4141
void treatsSimpleDomainTypeAsIs() throws Exception {
4242

4343
var type = getReturnedType("findAll");
4444

4545
assertThat(type.getTypeToRead()).isEqualTo(Sample.class);
4646
assertThat(type.getInputProperties()).isEmpty();
4747
assertThat(type.isProjecting()).isFalse();
48+
assertThat(type.isDtoProjection()).isFalse();
49+
assertThat(type.isInterfaceProjection()).isFalse();
4850
assertThat(type.needsCustomConstruction()).isFalse();
4951
}
5052

51-
@Test // DATACMNS-89
53+
@Test // DATACMNS-89, GH-3410
5254
void detectsDto() throws Exception {
5355

5456
var type = getReturnedType("findAllDtos");
@@ -57,6 +59,8 @@ void detectsDto() throws Exception {
5759
assertThat(type.getInputProperties()).contains("firstname");
5860
assertThat(type.isInstance(new SampleDto("firstname"))).isTrue();
5961
assertThat(type.isProjecting()).isTrue();
62+
assertThat(type.isDtoProjection()).isTrue();
63+
assertThat(type.isInterfaceProjection()).isFalse();
6064
assertThat(type.needsCustomConstruction()).isTrue();
6165
}
6266

@@ -78,13 +82,15 @@ void detectsVoidMethod() throws Exception {
7882
assertThat(type.getReturnedType()).isEqualTo(void.class);
7983
}
8084

81-
@Test // DATACMNS-89
85+
@Test // DATACMNS-89, GH-3410
8286
void detectsClosedProjection() throws Exception {
8387

8488
var type = getReturnedType("findOneProjection");
8589

8690
assertThat(type.getReturnedType()).isEqualTo(SampleProjection.class);
8791
assertThat(type.isProjecting()).isTrue();
92+
assertThat(type.isInterfaceProjection()).isTrue();
93+
assertThat(type.isDtoProjection()).isFalse();
8894
assertThat(type.needsCustomConstruction()).isTrue();
8995
}
9096

@@ -109,12 +115,14 @@ void detectsComplexNumberTypes() throws Exception {
109115
assertThat(type.getTypeToRead()).isEqualTo(BigInteger.class);
110116
}
111117

112-
@Test // DATACMNS-840
118+
@Test // DATACMNS-840, GH-3410
113119
void detectsSampleDtoWithDefaultConstructor() throws Exception {
114120

115121
var type = getReturnedType("dtoWithMultipleConstructors");
116122

117123
assertThat(type.getInputProperties()).isEmpty();
124+
assertThat(type.isDtoProjection()).isTrue();
125+
assertThat(type.isInterfaceProjection()).isFalse();
118126
assertThat(type.needsCustomConstruction()).isFalse();
119127
}
120128

0 commit comments

Comments
 (0)