Skip to content

Commit 9535a33

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 8bfe5d0 commit 9535a33

File tree

2 files changed

+114
-56
lines changed

2 files changed

+114
-56
lines changed

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

Lines changed: 102 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import org.springframework.data.mapping.model.PreferredConstructorDiscoverer;
3131
import org.springframework.data.projection.ProjectionFactory;
3232
import org.springframework.data.projection.ProjectionInformation;
33+
import org.springframework.data.util.Lazy;
34+
import org.springframework.lang.Contract;
3335
import org.springframework.lang.NonNull;
3436
import org.springframework.lang.Nullable;
3537
import org.springframework.util.Assert;
@@ -82,7 +84,7 @@ public static ReturnedType of(Class<?> returnedType, Class<?> domainType, Projec
8284
}
8385

8486
/**
85-
* Returns the entity type.
87+
* Return the entity type.
8688
*
8789
* @return
8890
*/
@@ -91,74 +93,89 @@ public final Class<?> getDomainType() {
9193
}
9294

9395
/**
94-
* Returns whether the given source object is an instance of the returned type.
96+
* Return whether the given source object is an instance of the returned type.
9597
*
9698
* @param source can be {@literal null}.
9799
* @return
98100
*/
101+
@Contract("null -> false")
99102
public final boolean isInstance(@Nullable Object source) {
100103
return getReturnedType().isInstance(source);
101104
}
102105

103106
/**
104-
* Returns whether the type is projecting, i.e. not of the domain type.
107+
* Return the type of the individual objects to return.
105108
*
106109
* @return
107110
*/
108-
public abstract boolean isProjecting();
111+
public abstract Class<?> getReturnedType();
109112

110113
/**
111-
* Returns the type of the individual objects to return.
114+
* Return whether the type is projecting, i.e. not of the domain type.
112115
*
113116
* @return
114117
*/
115-
public abstract Class<?> getReturnedType();
118+
public abstract boolean isProjecting();
116119

117120
/**
118-
* Returns whether the returned type will require custom construction.
121+
* Return whether the type is an interface-projection.
119122
*
120-
* @return
123+
* @since 3.5.7
121124
*/
122-
public abstract boolean needsCustomConstruction();
125+
public boolean isInterfaceProjection() {
126+
return isProjecting() && getReturnedType().isInterface();
127+
}
123128

124129
/**
125-
* Returns the type that the query execution is supposed to pass to the underlying infrastructure. {@literal null} is
126-
* returned to indicate a generic type (a map or tuple-like type) shall be used.
130+
* Return whether the type is a DTO projection.
127131
*
128-
* @return
132+
* @since 3.5.7
129133
*/
130-
@Nullable
131-
public abstract Class<?> getTypeToRead();
134+
public boolean isDtoProjection() {
135+
return isProjecting() && !getReturnedType().isInterface();
136+
}
132137

133138
/**
134-
* Returns the properties required to be used to populate the result.
139+
* Return the properties required to be used to populate the result.
135140
*
136-
* @return
137141
* @see ProjectionInformation#getInputProperties()
138142
*/
139143
public abstract List<String> getInputProperties();
140144

141145
/**
142-
* Returns whether the returned type has input properties.
146+
* Return whether the returned type has input properties.
143147
*
144-
* @return
145148
* @since 3.3.5
146149
* @see ProjectionInformation#hasInputProperties()
147150
*/
148151
public boolean hasInputProperties() {
149152
return !CollectionUtils.isEmpty(getInputProperties());
150153
}
151154

155+
/**
156+
* Return whether the returned type will require custom construction.
157+
*/
158+
public abstract boolean needsCustomConstruction();
159+
160+
/**
161+
* Return the type that the query execution is supposed to pass to the underlying infrastructure. {@literal null} is
162+
* returned to indicate a generic type (a map or tuple-like type) shall be used.
163+
*/
164+
@Nullable
165+
public abstract Class<?> getTypeToRead();
166+
152167
/**
153168
* A {@link ReturnedType} that's backed by an interface.
154169
*
155170
* @author Oliver Gierke
171+
* @author Mark Paluch
156172
* @since 1.12
157173
*/
158174
private static final class ReturnedInterface extends ReturnedType {
159175

160176
private final ProjectionInformation information;
161177
private final Class<?> domainType;
178+
private final boolean isProjecting;
162179
private final List<String> inputProperties;
163180

164181
/**
@@ -175,6 +192,7 @@ public ReturnedInterface(ProjectionInformation information, Class<?> domainType)
175192

176193
this.information = information;
177194
this.domainType = domainType;
195+
this.isProjecting = !information.getType().isAssignableFrom(domainType);
178196
this.inputProperties = detectInputProperties(information);
179197
}
180198

@@ -197,32 +215,44 @@ public Class<?> getReturnedType() {
197215
}
198216

199217
@Override
200-
public boolean needsCustomConstruction() {
201-
return isProjecting() && information.isClosed();
218+
public boolean isProjecting() {
219+
return isProjecting;
202220
}
203221

204222
@Override
205-
public boolean isProjecting() {
206-
return !information.getType().isAssignableFrom(domainType);
223+
public boolean isInterfaceProjection() {
224+
return isProjecting();
207225
}
208226

209-
@Nullable
210227
@Override
211-
public Class<?> getTypeToRead() {
212-
return isProjecting() && information.isClosed() ? null : domainType;
228+
public boolean isDtoProjection() {
229+
return false;
213230
}
214231

215232
@Override
216233
public List<String> getInputProperties() {
217234
return inputProperties;
218235
}
236+
237+
@Override
238+
public boolean needsCustomConstruction() {
239+
return isProjecting() && information.isClosed();
240+
}
241+
242+
@Override
243+
@Nullable
244+
public Class<?> getTypeToRead() {
245+
return isProjecting() && information.isClosed() ? null : domainType;
246+
}
247+
219248
}
220249

221250
/**
222251
* A {@link ReturnedType} that's backed by an actual class.
223252
*
224253
* @author Oliver Gierke
225254
* @author Mikhail Polivakha
255+
* @author Mark Paluch
226256
* @since 1.12
227257
*/
228258
private static final class ReturnedClass extends ReturnedType {
@@ -231,7 +261,8 @@ private static final class ReturnedClass extends ReturnedType {
231261

232262
private final Class<?> type;
233263
private final boolean isDto;
234-
private final List<String> inputProperties;
264+
private final @Nullable PreferredConstructor<?, ?> constructor;
265+
private final Lazy<List<String>> inputProperties;
235266

236267
/**
237268
* Creates a new {@link ReturnedClass} instance for the given returned type and domain type.
@@ -256,7 +287,13 @@ public ReturnedClass(Class<?> returnedType, Class<?> domainType) {
256287
!VOID_TYPES.contains(type) && //
257288
!type.getPackage().getName().startsWith("java.");
258289

259-
this.inputProperties = detectConstructorParameterNames(returnedType);
290+
this.constructor = detectConstructorParameterNames(type);
291+
292+
if (this.constructor == null) {
293+
this.inputProperties = Lazy.of(Collections.emptyList());
294+
} else {
295+
this.inputProperties = Lazy.of(this::detectConstructorParameterNames);
296+
}
260297
}
261298

262299
@Override
@@ -265,33 +302,55 @@ public Class<?> getReturnedType() {
265302
}
266303

267304
@Override
268-
@NonNull
269-
public Class<?> getTypeToRead() {
270-
return type;
305+
public boolean isProjecting() {
306+
return isDto;
271307
}
272308

273309
@Override
274-
public boolean isProjecting() {
275-
return isDto();
310+
public boolean isInterfaceProjection() {
311+
return false;
276312
}
277313

278314
@Override
279-
public boolean needsCustomConstruction() {
280-
return isDto() && !inputProperties.isEmpty();
315+
public boolean isDtoProjection() {
316+
return isProjecting();
281317
}
282318

283319
@Override
284320
public List<String> getInputProperties() {
285-
return inputProperties;
321+
return inputProperties.get();
286322
}
287323

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

290-
if (!isDto()) {
291-
return Collections.emptyList();
292-
}
329+
@Override
330+
public boolean needsCustomConstruction() {
331+
return isDtoProjection() && hasInputProperties();
332+
}
293333

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

296355
if (constructor == null) {
297356
return Collections.emptyList();
@@ -310,24 +369,13 @@ private List<String> detectConstructorParameterNames(Class<?> type) {
310369
if (logger.isWarnEnabled()) {
311370
logger.warn(("No constructor parameter names discovered. "
312371
+ "Compile the affected code with '-parameters' instead or avoid its introspection: %s")
313-
.formatted(type.getName()));
372+
.formatted(constructor.getConstructor().getDeclaringClass().getName()));
314373
}
315374
}
316375

317376
return Collections.unmodifiableList(properties);
318377
}
319378

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-
}
331379
}
332380

333381
private static final class CacheKey {
@@ -394,5 +442,7 @@ public String toString() {
394442
return "ReturnedType.CacheKey(returnedType=" + this.getReturnedType() + ", domainType=" + this.getDomainType()
395443
+ ", projectionFactoryHashCode=" + this.getProjectionFactoryHashCode() + ")";
396444
}
445+
397446
}
447+
398448
}

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)