Skip to content

Commit effe606

Browse files
committed
Refine record canonical constructor support in BeanUtils
This commit refines the contribution with the following changes: - Move the support to findPrimaryConstructor - Use a for loop instead of a Stream for more efficiency - Support other visibilities than public - Polishing Closes gh-33707
1 parent 514d600 commit effe606

File tree

2 files changed

+36
-24
lines changed

2 files changed

+36
-24
lines changed

spring-beans/src/main/java/org/springframework/beans/BeanUtils.java

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,10 @@ public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws
225225

226226
/**
227227
* Return a resolvable constructor for the provided class, either a primary or single
228-
* public constructor with arguments, or a single non-public constructor with arguments,
229-
* or simply a default constructor. Callers have to be prepared to resolve arguments
230-
* for the returned constructor's parameters, if any.
228+
* public constructor with arguments, a single non-public constructor with arguments
229+
* or simply a default constructor.
230+
* <p>Callers have to be prepared to resolve arguments for the returned constructor's
231+
* parameters, if any.
231232
* @param clazz the class to check
232233
* @throws IllegalStateException in case of no unique constructor found at all
233234
* @since 5.3
@@ -253,19 +254,6 @@ else if (ctors.length == 0) {
253254
return (Constructor<T>) ctors[0];
254255
}
255256
}
256-
else if (clazz.isRecord()) {
257-
try {
258-
// if record -> use canonical constructor, which is always presented
259-
Class<?>[] paramTypes
260-
= Arrays.stream(clazz.getRecordComponents())
261-
.map(RecordComponent::getType)
262-
.toArray(Class<?>[]::new);
263-
return clazz.getDeclaredConstructor(paramTypes);
264-
}
265-
catch (NoSuchMethodException ex) {
266-
// Giving up with record...
267-
}
268-
}
269257

270258
// Several constructors -> let's try to take the default constructor
271259
try {
@@ -282,18 +270,32 @@ else if (clazz.isRecord()) {
282270
/**
283271
* Return the primary constructor of the provided class. For Kotlin classes, this
284272
* returns the Java constructor corresponding to the Kotlin primary constructor
285-
* (as defined in the Kotlin specification). Otherwise, in particular for non-Kotlin
286-
* classes, this simply returns {@code null}.
273+
* (as defined in the Kotlin specification). For Java records, this returns the
274+
* canonical constructor. Otherwise, this simply returns {@code null}.
287275
* @param clazz the class to check
288276
* @since 5.0
289-
* @see <a href="https://kotlinlang.org/docs/reference/classes.html#constructors">Kotlin docs</a>
277+
* @see <a href="https://kotlinlang.org/docs/reference/classes.html#constructors">Kotlin constructors</a>
278+
* @see <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-8.html#jls-8.10.4">Record constructor declarations</a>
290279
*/
291280
@Nullable
292281
public static <T> Constructor<T> findPrimaryConstructor(Class<T> clazz) {
293282
Assert.notNull(clazz, "Class must not be null");
294283
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(clazz)) {
295284
return KotlinDelegate.findPrimaryConstructor(clazz);
296285
}
286+
if (clazz.isRecord()) {
287+
try {
288+
// Use the canonical constructor which is always present
289+
RecordComponent[] components = clazz.getRecordComponents();
290+
Class<?>[] paramTypes = new Class<?>[components.length];
291+
for (int i = 0; i < components.length; i++) {
292+
paramTypes[i] = components[i].getType();
293+
}
294+
return clazz.getDeclaredConstructor(paramTypes);
295+
}
296+
catch (NoSuchMethodException ignored) {
297+
}
298+
}
297299
return null;
298300
}
299301

spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -522,26 +522,36 @@ void isNotSimpleProperty(Class<?> type) {
522522
}
523523

524524
@Test
525-
void resolveRecordConstructor() throws NoSuchMethodException {
525+
void resolveMultipleRecordPublicConstructor() throws NoSuchMethodException {
526526
assertThat(BeanUtils.getResolvableConstructor(RecordWithMultiplePublicConstructors.class))
527-
.isEqualTo(getRecordWithMultipleVariationsConstructor());
527+
.isEqualTo(RecordWithMultiplePublicConstructors.class.getDeclaredConstructor(String.class, String.class));
528+
}
529+
530+
@Test
531+
void resolveMultipleRecordePackagePrivateConstructor() throws NoSuchMethodException {
532+
assertThat(BeanUtils.getResolvableConstructor(RecordWithMultiplePackagePrivateConstructors.class))
533+
.isEqualTo(RecordWithMultiplePackagePrivateConstructors.class.getDeclaredConstructor(String.class, String.class));
528534
}
529535

530536
private void assertSignatureEquals(Method desiredMethod, String signature) {
531537
assertThat(BeanUtils.resolveSignature(signature, MethodSignatureBean.class)).isEqualTo(desiredMethod);
532538
}
533539

540+
534541
public record RecordWithMultiplePublicConstructors(String value, String name) {
542+
@SuppressWarnings("unused")
535543
public RecordWithMultiplePublicConstructors(String value) {
536544
this(value, "default value");
537545
}
538546
}
539547

540-
private Constructor<RecordWithMultiplePublicConstructors> getRecordWithMultipleVariationsConstructor() throws NoSuchMethodException {
541-
return RecordWithMultiplePublicConstructors.class.getConstructor(String.class, String.class);
548+
record RecordWithMultiplePackagePrivateConstructors(String value, String name) {
549+
@SuppressWarnings("unused")
550+
RecordWithMultiplePackagePrivateConstructors(String value) {
551+
this(value, "default value");
552+
}
542553
}
543554

544-
545555
@SuppressWarnings("unused")
546556
private static class NumberHolder {
547557

0 commit comments

Comments
 (0)