diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java
index 58349d86b89..1ba55688b19 100644
--- a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java
+++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -686,7 +686,25 @@ public abstract class BeanUtils {
* from each other, as long as the properties match. Any bean properties that the
* source bean exposes but the target bean does not will silently be ignored.
*
This is just a convenience method. For more complex transfer needs,
- * consider using a full BeanWrapper.
+ * consider using a full {@link BeanWrapper}.
+ *
As of Spring Framework 5.3, this method honors generic type information
+ * when matching properties in the source and target objects.
+ *
The following table provides a non-exhaustive set of examples of source
+ * and target property types that can be copied as well as source and target
+ * property types that cannot be copied.
+ *
+ * | source property type | target property type | copy supported |
+ * | {@code Integer} | {@code Integer} | yes |
+ * | {@code Integer} | {@code Number} | yes |
+ * | {@code List} | {@code List} | yes |
+ * | {@code List>} | {@code List>} | yes |
+ * | {@code List} | {@code List>} | yes |
+ * | {@code List} | {@code List extends Number>} | yes |
+ * | {@code String} | {@code Integer} | no |
+ * | {@code Number} | {@code Integer} | no |
+ * | {@code List} | {@code List} | no |
+ * | {@code List} | {@code List} | no |
+ *
* @param source the source bean
* @param target the target bean
* @throws BeansException if the copying failed
@@ -703,7 +721,10 @@ public abstract class BeanUtils {
* from each other, as long as the properties match. Any bean properties that the
* source bean exposes but the target bean does not will silently be ignored.
* This is just a convenience method. For more complex transfer needs,
- * consider using a full BeanWrapper.
+ * consider using a full {@link BeanWrapper}.
+ *
As of Spring Framework 5.3, this method honors generic type information
+ * when matching properties in the source and target objects. See the
+ * documentation for {@link #copyProperties(Object, Object)} for details.
* @param source the source bean
* @param target the target bean
* @param editable the class (or interface) to restrict property setting to
@@ -721,7 +742,10 @@ public abstract class BeanUtils {
* from each other, as long as the properties match. Any bean properties that the
* source bean exposes but the target bean does not will silently be ignored.
*
This is just a convenience method. For more complex transfer needs,
- * consider using a full BeanWrapper.
+ * consider using a full {@link BeanWrapper}.
+ *
As of Spring Framework 5.3, this method honors generic type information
+ * when matching properties in the source and target objects. See the
+ * documentation for {@link #copyProperties(Object, Object)} for details.
* @param source the source bean
* @param target the target bean
* @param ignoreProperties array of property names to ignore
@@ -738,7 +762,8 @@ public abstract class BeanUtils {
* from each other, as long as the properties match. Any bean properties that the
* source bean exposes but the target bean does not will silently be ignored.
*
As of Spring Framework 5.3, this method honors generic type information
- * when matching properties in the source and target objects.
+ * when matching properties in the source and target objects. See the
+ * documentation for {@link #copyProperties(Object, Object)} for details.
* @param source the source bean
* @param target the target bean
* @param editable the class (or interface) to restrict property setting to
diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java
index b90fd515dc3..711189cdf1c 100644
--- a/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java
+++ b/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -203,8 +203,25 @@ class BeanUtilsTests {
assertThat(tb2.getTouchy().equals(tb.getTouchy())).as("Touchy copied").isTrue();
}
+ /**
+ * {@code Integer} can be copied to {@code Number}.
+ */
@Test
- void copyPropertiesHonorsGenericTypeMatches() {
+ void copyPropertiesFromSubTypeToSuperType() {
+ IntegerHolder integerHolder = new IntegerHolder();
+ integerHolder.setNumber(42);
+ NumberHolder numberHolder = new NumberHolder();
+
+ BeanUtils.copyProperties(integerHolder, numberHolder);
+ assertThat(integerHolder.getNumber()).isEqualTo(42);
+ assertThat(numberHolder.getNumber()).isEqualTo(42);
+ }
+
+ /**
+ * {@code List} can be copied to {@code List}.
+ */
+ @Test
+ void copyPropertiesHonorsGenericTypeMatchesFromIntegerToInteger() {
IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
integerListHolder1.getList().add(42);
IntegerListHolder2 integerListHolder2 = new IntegerListHolder2();
@@ -214,6 +231,68 @@ class BeanUtilsTests {
assertThat(integerListHolder2.getList()).containsOnly(42);
}
+ /**
+ * {@code List>} can be copied to {@code List>}.
+ */
+ @Test
+ void copyPropertiesHonorsGenericTypeMatchesFromWildcardToWildcard() {
+ List> list = Arrays.asList("foo", 42);
+ WildcardListHolder1 wildcardListHolder1 = new WildcardListHolder1();
+ wildcardListHolder1.setList(list);
+ WildcardListHolder2 wildcardListHolder2 = new WildcardListHolder2();
+ assertThat(wildcardListHolder2.getList()).isEmpty();
+
+ BeanUtils.copyProperties(wildcardListHolder1, wildcardListHolder2);
+ assertThat(wildcardListHolder1.getList()).isEqualTo(list);
+ assertThat(wildcardListHolder2.getList()).isEqualTo(list);
+ }
+
+ /**
+ * {@code List} can be copied to {@code List>}.
+ */
+ @Test
+ void copyPropertiesHonorsGenericTypeMatchesFromIntegerToWildcard() {
+ IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
+ integerListHolder1.getList().add(42);
+ WildcardListHolder2 wildcardListHolder2 = new WildcardListHolder2();
+
+ BeanUtils.copyProperties(integerListHolder1, wildcardListHolder2);
+ assertThat(integerListHolder1.getList()).containsOnly(42);
+ assertThat(wildcardListHolder2.getList()).isEqualTo(Arrays.asList(42));
+ }
+
+ /**
+ * {@code List} can be copied to {@code List extends Number>}.
+ */
+ @Test
+ void copyPropertiesHonorsGenericTypeMatchesForUpperBoundedWildcard() {
+ IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
+ integerListHolder1.getList().add(42);
+ NumberUpperBoundedWildcardListHolder numberListHolder = new NumberUpperBoundedWildcardListHolder();
+
+ BeanUtils.copyProperties(integerListHolder1, numberListHolder);
+ assertThat(integerListHolder1.getList()).containsOnly(42);
+ assertThat(numberListHolder.getList()).hasSize(1);
+ assertThat(numberListHolder.getList().contains(Integer.valueOf(42))).isTrue();
+ }
+
+ /**
+ * {@code Number} can NOT be copied to {@code Integer}.
+ */
+ @Test
+ void copyPropertiesDoesNotCopyeFromSuperTypeToSubType() {
+ NumberHolder numberHolder = new NumberHolder();
+ numberHolder.setNumber(Integer.valueOf(42));
+ IntegerHolder integerHolder = new IntegerHolder();
+
+ BeanUtils.copyProperties(numberHolder, integerHolder);
+ assertThat(numberHolder.getNumber()).isEqualTo(42);
+ assertThat(integerHolder.getNumber()).isNull();
+ }
+
+ /**
+ * {@code List} can NOT be copied to {@code List}.
+ */
@Test
void copyPropertiesDoesNotHonorGenericTypeMismatches() {
IntegerListHolder1 integerListHolder = new IntegerListHolder1();
@@ -225,6 +304,20 @@ class BeanUtilsTests {
assertThat(longListHolder.getList()).isEmpty();
}
+ /**
+ * {@code List} can NOT be copied to {@code List}.
+ */
+ @Test
+ void copyPropertiesDoesNotHonorGenericTypeMismatchesFromSubTypeToSuperType() {
+ IntegerListHolder1 integerListHolder = new IntegerListHolder1();
+ integerListHolder.getList().add(42);
+ NumberListHolder numberListHolder = new NumberListHolder();
+
+ BeanUtils.copyProperties(integerListHolder, numberListHolder);
+ assertThat(integerListHolder.getList()).containsOnly(42);
+ assertThat(numberListHolder.getList()).isEmpty();
+ }
+
@Test // gh-26531
void copyPropertiesIgnoresGenericsIfSourceOrTargetHasUnresolvableGenerics() throws Exception {
Order original = new Order("test", Arrays.asList("foo", "bar"));
@@ -413,6 +506,90 @@ class BeanUtilsTests {
}
+ @SuppressWarnings("unused")
+ private static class NumberHolder {
+
+ private Number number;
+
+ public Number getNumber() {
+ return number;
+ }
+
+ public void setNumber(Number number) {
+ this.number = number;
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static class IntegerHolder {
+
+ private Integer number;
+
+ public Integer getNumber() {
+ return number;
+ }
+
+ public void setNumber(Integer number) {
+ this.number = number;
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static class WildcardListHolder1 {
+
+ private List> list = new ArrayList<>();
+
+ public List> getList() {
+ return list;
+ }
+
+ public void setList(List> list) {
+ this.list = list;
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static class WildcardListHolder2 {
+
+ private List> list = new ArrayList<>();
+
+ public List> getList() {
+ return list;
+ }
+
+ public void setList(List> list) {
+ this.list = list;
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static class NumberUpperBoundedWildcardListHolder {
+
+ private List extends Number> list = new ArrayList<>();
+
+ public List extends Number> getList() {
+ return list;
+ }
+
+ public void setList(List extends Number> list) {
+ this.list = list;
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static class NumberListHolder {
+
+ private List list = new ArrayList<>();
+
+ public List getList() {
+ return list;
+ }
+
+ public void setList(List list) {
+ this.list = list;
+ }
+ }
+
@SuppressWarnings("unused")
private static class IntegerListHolder1 {