Browse Source

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
pull/33729/head
Sébastien Deleuze 1 year ago
parent
commit
effe606b28
  1. 40
      spring-beans/src/main/java/org/springframework/beans/BeanUtils.java
  2. 20
      spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java

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

@ -225,9 +225,10 @@ public abstract class BeanUtils {
/** /**
* Return a resolvable constructor for the provided class, either a primary or single * 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, * public constructor with arguments, a single non-public constructor with arguments
* or simply a default constructor. Callers have to be prepared to resolve arguments * or simply a default constructor.
* for the returned constructor's parameters, if any. * <p>Callers have to be prepared to resolve arguments for the returned constructor's
* parameters, if any.
* @param clazz the class to check * @param clazz the class to check
* @throws IllegalStateException in case of no unique constructor found at all * @throws IllegalStateException in case of no unique constructor found at all
* @since 5.3 * @since 5.3
@ -253,19 +254,6 @@ public abstract class BeanUtils {
return (Constructor<T>) ctors[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 // Several constructors -> let's try to take the default constructor
try { try {
@ -282,11 +270,12 @@ public abstract class BeanUtils {
/** /**
* Return the primary constructor of the provided class. For Kotlin classes, this * Return the primary constructor of the provided class. For Kotlin classes, this
* returns the Java constructor corresponding to the Kotlin primary constructor * returns the Java constructor corresponding to the Kotlin primary constructor
* (as defined in the Kotlin specification). Otherwise, in particular for non-Kotlin * (as defined in the Kotlin specification). For Java records, this returns the
* classes, this simply returns {@code null}. * canonical constructor. Otherwise, this simply returns {@code null}.
* @param clazz the class to check * @param clazz the class to check
* @since 5.0 * @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 @Nullable
public static <T> Constructor<T> findPrimaryConstructor(Class<T> clazz) { public static <T> Constructor<T> findPrimaryConstructor(Class<T> clazz) {
@ -294,6 +283,19 @@ public abstract class BeanUtils {
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(clazz)) { if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(clazz)) {
return KotlinDelegate.findPrimaryConstructor(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; return null;
} }

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

@ -522,26 +522,36 @@ class BeanUtilsTests {
} }
@Test @Test
void resolveRecordConstructor() throws NoSuchMethodException { void resolveMultipleRecordPublicConstructor() throws NoSuchMethodException {
assertThat(BeanUtils.getResolvableConstructor(RecordWithMultiplePublicConstructors.class)) 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) { private void assertSignatureEquals(Method desiredMethod, String signature) {
assertThat(BeanUtils.resolveSignature(signature, MethodSignatureBean.class)).isEqualTo(desiredMethod); assertThat(BeanUtils.resolveSignature(signature, MethodSignatureBean.class)).isEqualTo(desiredMethod);
} }
public record RecordWithMultiplePublicConstructors(String value, String name) { public record RecordWithMultiplePublicConstructors(String value, String name) {
@SuppressWarnings("unused")
public RecordWithMultiplePublicConstructors(String value) { public RecordWithMultiplePublicConstructors(String value) {
this(value, "default value"); this(value, "default value");
} }
} }
private Constructor<RecordWithMultiplePublicConstructors> getRecordWithMultipleVariationsConstructor() throws NoSuchMethodException { record RecordWithMultiplePackagePrivateConstructors(String value, String name) {
return RecordWithMultiplePublicConstructors.class.getConstructor(String.class, String.class); @SuppressWarnings("unused")
RecordWithMultiplePackagePrivateConstructors(String value) {
this(value, "default value");
}
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static class NumberHolder { private static class NumberHolder {

Loading…
Cancel
Save