Skip to content

Commit 887389d

Browse files
committed
Clarify behavior for generics support in BeanUtils.copyProperties()
Since Spring Framework 5.3, BeanUtils.copyProperties() honors generics in the source and target property types (see gh-24187); however, this refinement of the contract was not properly documented prior to this commit. In addition, the refinement can be a breaking change for users who were relying on the previous unreliable behavior. This commit therefore clarifies the behavior for generics support in BeanUtils.copyProperties() and introduces a table of example matches and mismatches when generics are involved. Closes gh-27259
1 parent d9c22e6 commit 887389d

File tree

2 files changed

+209
-7
lines changed

2 files changed

+209
-7
lines changed

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

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -691,7 +691,25 @@ public static boolean isSimpleValueType(Class<?> type) {
691691
* from each other, as long as the properties match. Any bean properties that the
692692
* source bean exposes but the target bean does not will silently be ignored.
693693
* <p>This is just a convenience method. For more complex transfer needs,
694-
* consider using a full BeanWrapper.
694+
* consider using a full {@link BeanWrapper}.
695+
* <p>As of Spring Framework 5.3, this method honors generic type information
696+
* when matching properties in the source and target objects.
697+
* <p>The following table provides a non-exhaustive set of examples of source
698+
* and target property types that can be copied as well as source and target
699+
* property types that cannot be copied.
700+
* <table border="1">
701+
* <tr><th>source property type</th><th>target property type</th><th>copy supported</th></tr>
702+
* <tr><td>{@code Integer}</td><td>{@code Integer}</td><td>yes</td></tr>
703+
* <tr><td>{@code Integer}</td><td>{@code Number}</td><td>yes</td></tr>
704+
* <tr><td>{@code List<Integer>}</td><td>{@code List<Integer>}</td><td>yes</td></tr>
705+
* <tr><td>{@code List<?>}</td><td>{@code List<?>}</td><td>yes</td></tr>
706+
* <tr><td>{@code List<Integer>}</td><td>{@code List<?>}</td><td>yes</td></tr>
707+
* <tr><td>{@code List<Integer>}</td><td>{@code List<? extends Number>}</td><td>yes</td></tr>
708+
* <tr><td>{@code String}</td><td>{@code Integer}</td><td>no</td></tr>
709+
* <tr><td>{@code Number}</td><td>{@code Integer}</td><td>no</td></tr>
710+
* <tr><td>{@code List<Integer>}</td><td>{@code List<Long>}</td><td>no</td></tr>
711+
* <tr><td>{@code List<Integer>}</td><td>{@code List<Number>}</td><td>no</td></tr>
712+
* </table>
695713
* @param source the source bean
696714
* @param target the target bean
697715
* @throws BeansException if the copying failed
@@ -708,7 +726,10 @@ public static void copyProperties(Object source, Object target) throws BeansExce
708726
* from each other, as long as the properties match. Any bean properties that the
709727
* source bean exposes but the target bean does not will silently be ignored.
710728
* <p>This is just a convenience method. For more complex transfer needs,
711-
* consider using a full BeanWrapper.
729+
* consider using a full {@link BeanWrapper}.
730+
* <p>As of Spring Framework 5.3, this method honors generic type information
731+
* when matching properties in the source and target objects. See the
732+
* documentation for {@link #copyProperties(Object, Object)} for details.
712733
* @param source the source bean
713734
* @param target the target bean
714735
* @param editable the class (or interface) to restrict property setting to
@@ -726,7 +747,10 @@ public static void copyProperties(Object source, Object target, Class<?> editabl
726747
* from each other, as long as the properties match. Any bean properties that the
727748
* source bean exposes but the target bean does not will silently be ignored.
728749
* <p>This is just a convenience method. For more complex transfer needs,
729-
* consider using a full BeanWrapper.
750+
* consider using a full {@link BeanWrapper}.
751+
* <p>As of Spring Framework 5.3, this method honors generic type information
752+
* when matching properties in the source and target objects. See the
753+
* documentation for {@link #copyProperties(Object, Object)} for details.
730754
* @param source the source bean
731755
* @param target the target bean
732756
* @param ignoreProperties array of property names to ignore
@@ -743,7 +767,8 @@ public static void copyProperties(Object source, Object target, String... ignore
743767
* from each other, as long as the properties match. Any bean properties that the
744768
* source bean exposes but the target bean does not will silently be ignored.
745769
* <p>As of Spring Framework 5.3, this method honors generic type information
746-
* when matching properties in the source and target objects.
770+
* when matching properties in the source and target objects. See the
771+
* documentation for {@link #copyProperties(Object, Object)} for details.
747772
* @param source the source bean
748773
* @param target the target bean
749774
* @param editable the class (or interface) to restrict property setting to

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

Lines changed: 179 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -203,8 +203,25 @@ void copyPropertiesWithDifferentTypes2() throws Exception {
203203
assertThat(tb2.getTouchy().equals(tb.getTouchy())).as("Touchy copied").isTrue();
204204
}
205205

206+
/**
207+
* {@code Integer} can be copied to {@code Number}.
208+
*/
206209
@Test
207-
void copyPropertiesHonorsGenericTypeMatches() {
210+
void copyPropertiesFromSubTypeToSuperType() {
211+
IntegerHolder integerHolder = new IntegerHolder();
212+
integerHolder.setNumber(42);
213+
NumberHolder numberHolder = new NumberHolder();
214+
215+
BeanUtils.copyProperties(integerHolder, numberHolder);
216+
assertThat(integerHolder.getNumber()).isEqualTo(42);
217+
assertThat(numberHolder.getNumber()).isEqualTo(42);
218+
}
219+
220+
/**
221+
* {@code List<Integer>} can be copied to {@code List<Integer>}.
222+
*/
223+
@Test
224+
void copyPropertiesHonorsGenericTypeMatchesFromIntegerToInteger() {
208225
IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
209226
integerListHolder1.getList().add(42);
210227
IntegerListHolder2 integerListHolder2 = new IntegerListHolder2();
@@ -214,6 +231,68 @@ void copyPropertiesHonorsGenericTypeMatches() {
214231
assertThat(integerListHolder2.getList()).containsOnly(42);
215232
}
216233

234+
/**
235+
* {@code List<?>} can be copied to {@code List<?>}.
236+
*/
237+
@Test
238+
void copyPropertiesHonorsGenericTypeMatchesFromWildcardToWildcard() {
239+
List<?> list = Arrays.asList("foo", 42);
240+
WildcardListHolder1 wildcardListHolder1 = new WildcardListHolder1();
241+
wildcardListHolder1.setList(list);
242+
WildcardListHolder2 wildcardListHolder2 = new WildcardListHolder2();
243+
assertThat(wildcardListHolder2.getList()).isEmpty();
244+
245+
BeanUtils.copyProperties(wildcardListHolder1, wildcardListHolder2);
246+
assertThat(wildcardListHolder1.getList()).isEqualTo(list);
247+
assertThat(wildcardListHolder2.getList()).isEqualTo(list);
248+
}
249+
250+
/**
251+
* {@code List<Integer>} can be copied to {@code List<?>}.
252+
*/
253+
@Test
254+
void copyPropertiesHonorsGenericTypeMatchesFromIntegerToWildcard() {
255+
IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
256+
integerListHolder1.getList().add(42);
257+
WildcardListHolder2 wildcardListHolder2 = new WildcardListHolder2();
258+
259+
BeanUtils.copyProperties(integerListHolder1, wildcardListHolder2);
260+
assertThat(integerListHolder1.getList()).containsOnly(42);
261+
assertThat(wildcardListHolder2.getList()).isEqualTo(Arrays.asList(42));
262+
}
263+
264+
/**
265+
* {@code List<Integer>} can be copied to {@code List<? extends Number>}.
266+
*/
267+
@Test
268+
void copyPropertiesHonorsGenericTypeMatchesForUpperBoundedWildcard() {
269+
IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
270+
integerListHolder1.getList().add(42);
271+
NumberUpperBoundedWildcardListHolder numberListHolder = new NumberUpperBoundedWildcardListHolder();
272+
273+
BeanUtils.copyProperties(integerListHolder1, numberListHolder);
274+
assertThat(integerListHolder1.getList()).containsOnly(42);
275+
assertThat(numberListHolder.getList()).hasSize(1);
276+
assertThat(numberListHolder.getList().contains(Integer.valueOf(42))).isTrue();
277+
}
278+
279+
/**
280+
* {@code Number} can NOT be copied to {@code Integer}.
281+
*/
282+
@Test
283+
void copyPropertiesDoesNotCopyeFromSuperTypeToSubType() {
284+
NumberHolder numberHolder = new NumberHolder();
285+
numberHolder.setNumber(Integer.valueOf(42));
286+
IntegerHolder integerHolder = new IntegerHolder();
287+
288+
BeanUtils.copyProperties(numberHolder, integerHolder);
289+
assertThat(numberHolder.getNumber()).isEqualTo(42);
290+
assertThat(integerHolder.getNumber()).isNull();
291+
}
292+
293+
/**
294+
* {@code List<Integer>} can NOT be copied to {@code List<Long>}.
295+
*/
217296
@Test
218297
void copyPropertiesDoesNotHonorGenericTypeMismatches() {
219298
IntegerListHolder1 integerListHolder = new IntegerListHolder1();
@@ -225,6 +304,20 @@ void copyPropertiesDoesNotHonorGenericTypeMismatches() {
225304
assertThat(longListHolder.getList()).isEmpty();
226305
}
227306

307+
/**
308+
* {@code List<Integer>} can NOT be copied to {@code List<Number>}.
309+
*/
310+
@Test
311+
void copyPropertiesDoesNotHonorGenericTypeMismatchesFromSubTypeToSuperType() {
312+
IntegerListHolder1 integerListHolder = new IntegerListHolder1();
313+
integerListHolder.getList().add(42);
314+
NumberListHolder numberListHolder = new NumberListHolder();
315+
316+
BeanUtils.copyProperties(integerListHolder, numberListHolder);
317+
assertThat(integerListHolder.getList()).containsOnly(42);
318+
assertThat(numberListHolder.getList()).isEmpty();
319+
}
320+
228321
@Test // gh-26531
229322
void copyPropertiesIgnoresGenericsIfSourceOrTargetHasUnresolvableGenerics() throws Exception {
230323
Order original = new Order("test", Arrays.asList("foo", "bar"));
@@ -413,6 +506,90 @@ private void assertSignatureEquals(Method desiredMethod, String signature) {
413506
}
414507

415508

509+
@SuppressWarnings("unused")
510+
private static class NumberHolder {
511+
512+
private Number number;
513+
514+
public Number getNumber() {
515+
return number;
516+
}
517+
518+
public void setNumber(Number number) {
519+
this.number = number;
520+
}
521+
}
522+
523+
@SuppressWarnings("unused")
524+
private static class IntegerHolder {
525+
526+
private Integer number;
527+
528+
public Integer getNumber() {
529+
return number;
530+
}
531+
532+
public void setNumber(Integer number) {
533+
this.number = number;
534+
}
535+
}
536+
537+
@SuppressWarnings("unused")
538+
private static class WildcardListHolder1 {
539+
540+
private List<?> list = new ArrayList<>();
541+
542+
public List<?> getList() {
543+
return list;
544+
}
545+
546+
public void setList(List<?> list) {
547+
this.list = list;
548+
}
549+
}
550+
551+
@SuppressWarnings("unused")
552+
private static class WildcardListHolder2 {
553+
554+
private List<?> list = new ArrayList<>();
555+
556+
public List<?> getList() {
557+
return list;
558+
}
559+
560+
public void setList(List<?> list) {
561+
this.list = list;
562+
}
563+
}
564+
565+
@SuppressWarnings("unused")
566+
private static class NumberUpperBoundedWildcardListHolder {
567+
568+
private List<? extends Number> list = new ArrayList<>();
569+
570+
public List<? extends Number> getList() {
571+
return list;
572+
}
573+
574+
public void setList(List<? extends Number> list) {
575+
this.list = list;
576+
}
577+
}
578+
579+
@SuppressWarnings("unused")
580+
private static class NumberListHolder {
581+
582+
private List<Number> list = new ArrayList<>();
583+
584+
public List<Number> getList() {
585+
return list;
586+
}
587+
588+
public void setList(List<Number> list) {
589+
this.list = list;
590+
}
591+
}
592+
416593
@SuppressWarnings("unused")
417594
private static class IntegerListHolder1 {
418595

0 commit comments

Comments
 (0)