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
This commit is contained in:
Sébastien Deleuze 2024-10-16 15:05:47 +02:00
parent 514d6000d1
commit effe606b28
2 changed files with 36 additions and 24 deletions

View File

@ -225,9 +225,10 @@ public abstract class BeanUtils {
/**
* 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
@ -253,19 +254,6 @@ public abstract class BeanUtils {
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 {
@ -282,11 +270,12 @@ public abstract class BeanUtils {
/**
* 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) {
@ -294,6 +283,19 @@ public abstract class BeanUtils {
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;
}

View File

@ -522,26 +522,36 @@ class BeanUtilsTests {
}
@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 {