Skip to content

Commit

Permalink
Refine record canonical constructor support in BeanUtils
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sdeleuze committed Oct 16, 2024
1 parent 514d600 commit effe606
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 24 deletions.
40 changes: 21 additions & 19 deletions spring-beans/src/main/java/org/springframework/beans/BeanUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,10 @@ public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws

/**
* Return a resolvable constructor for the provided class, either a primary or single
* public constructor with arguments, or a single non-public constructor with arguments,
* or simply a default constructor. Callers have to be prepared to resolve arguments
* for the returned constructor's parameters, if any.
* public constructor with arguments, a single non-public constructor with arguments
* or simply a default constructor.
* <p>Callers have to be prepared to resolve arguments for the returned constructor's
* parameters, if any.
* @param clazz the class to check
* @throws IllegalStateException in case of no unique constructor found at all
* @since 5.3
Expand All @@ -253,19 +254,6 @@ else if (ctors.length == 0) {
return (Constructor<T>) ctors[0];
}
}
else if (clazz.isRecord()) {
try {
// if record -> use canonical constructor, which is always presented
Class<?>[] paramTypes
= Arrays.stream(clazz.getRecordComponents())
.map(RecordComponent::getType)
.toArray(Class<?>[]::new);
return clazz.getDeclaredConstructor(paramTypes);
}
catch (NoSuchMethodException ex) {
// Giving up with record...
}
}

// Several constructors -> let's try to take the default constructor
try {
Expand All @@ -282,18 +270,32 @@ else if (clazz.isRecord()) {
/**
* Return the primary constructor of the provided class. For Kotlin classes, this
* returns the Java constructor corresponding to the Kotlin primary constructor
* (as defined in the Kotlin specification). Otherwise, in particular for non-Kotlin
* classes, this simply returns {@code null}.
* (as defined in the Kotlin specification). For Java records, this returns the
* canonical constructor. Otherwise, this simply returns {@code null}.
* @param clazz the class to check
* @since 5.0
* @see <a href="https://kotlinlang.org/docs/reference/classes.html#constructors">Kotlin docs</a>
* @see <a href="https://kotlinlang.org/docs/reference/classes.html#constructors">Kotlin constructors</a>
* @see <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-8.html#jls-8.10.4">Record constructor declarations</a>
*/
@Nullable
public static <T> Constructor<T> findPrimaryConstructor(Class<T> clazz) {
Assert.notNull(clazz, "Class must not be null");
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(clazz)) {
return KotlinDelegate.findPrimaryConstructor(clazz);
}
if (clazz.isRecord()) {
try {
// Use the canonical constructor which is always present
RecordComponent[] components = clazz.getRecordComponents();
Class<?>[] paramTypes = new Class<?>[components.length];
for (int i = 0; i < components.length; i++) {
paramTypes[i] = components[i].getType();
}
return clazz.getDeclaredConstructor(paramTypes);
}
catch (NoSuchMethodException ignored) {
}
}
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -522,26 +522,36 @@ void isNotSimpleProperty(Class<?> type) {
}

@Test
void resolveRecordConstructor() throws NoSuchMethodException {
void resolveMultipleRecordPublicConstructor() throws NoSuchMethodException {
assertThat(BeanUtils.getResolvableConstructor(RecordWithMultiplePublicConstructors.class))
.isEqualTo(getRecordWithMultipleVariationsConstructor());
.isEqualTo(RecordWithMultiplePublicConstructors.class.getDeclaredConstructor(String.class, String.class));
}

@Test
void resolveMultipleRecordePackagePrivateConstructor() throws NoSuchMethodException {
assertThat(BeanUtils.getResolvableConstructor(RecordWithMultiplePackagePrivateConstructors.class))
.isEqualTo(RecordWithMultiplePackagePrivateConstructors.class.getDeclaredConstructor(String.class, String.class));
}

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


public record RecordWithMultiplePublicConstructors(String value, String name) {
@SuppressWarnings("unused")
public RecordWithMultiplePublicConstructors(String value) {
this(value, "default value");
}
}

private Constructor<RecordWithMultiplePublicConstructors> getRecordWithMultipleVariationsConstructor() throws NoSuchMethodException {
return RecordWithMultiplePublicConstructors.class.getConstructor(String.class, String.class);
record RecordWithMultiplePackagePrivateConstructors(String value, String name) {
@SuppressWarnings("unused")
RecordWithMultiplePackagePrivateConstructors(String value) {
this(value, "default value");
}
}


@SuppressWarnings("unused")
private static class NumberHolder {

Expand Down

0 comments on commit effe606

Please sign in to comment.