Skip to content

Commit e74f868

Browse files
committed
Revise generics support in BeanUtils.copyProperties()
Closes gh-24187
1 parent 89ee0b0 commit e74f868

File tree

2 files changed

+60
-43
lines changed

2 files changed

+60
-43
lines changed

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

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.beans.PropertyDescriptor;
2020
import java.beans.PropertyEditor;
2121
import java.lang.reflect.Constructor;
22-
import java.lang.reflect.Field;
2322
import java.lang.reflect.InvocationTargetException;
2423
import java.lang.reflect.Method;
2524
import java.lang.reflect.Modifier;
@@ -46,7 +45,7 @@
4645

4746
import org.springframework.core.KotlinDetector;
4847
import org.springframework.core.MethodParameter;
49-
import org.springframework.core.convert.TypeDescriptor;
48+
import org.springframework.core.ResolvableType;
5049
import org.springframework.lang.Nullable;
5150
import org.springframework.util.Assert;
5251
import org.springframework.util.ClassUtils;
@@ -683,11 +682,12 @@ public static void copyProperties(Object source, Object target, String... ignore
683682
}
684683

685684
/**
686-
* Copy the property values of the given source bean into the given target bean
687-
* and ignored if
685+
* Copy the property values of the given source bean into the given target bean.
688686
* <p>Note: The source and target classes do not have to match or even be derived
689687
* from each other, as long as the properties match. Any bean properties that the
690688
* source bean exposes but the target bean does not will silently be ignored.
689+
* <p>As of Spring Framework 5.3, this method honors generic type information
690+
* when matching properties in the source and target objects.
691691
* @param source the source bean
692692
* @param target the target bean
693693
* @param editable the class (or interface) to restrict property setting to
@@ -717,30 +717,25 @@ private static void copyProperties(Object source, Object target, @Nullable Class
717717
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
718718
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
719719
if (sourcePd != null) {
720-
Field sourcefield = ReflectionUtils.findField(source.getClass(), sourcePd.getName());
721-
Field targetfield = ReflectionUtils.findField(target.getClass(), targetPd.getName());
722-
723-
TypeDescriptor sourceTypeDescriptor = new TypeDescriptor(sourcefield);
724-
TypeDescriptor targetTypeDescriptor = new TypeDescriptor(targetfield);
725-
726720
Method readMethod = sourcePd.getReadMethod();
727-
728-
if (readMethod != null &&
729-
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType()) &&
730-
sourceTypeDescriptor.getResolvableType().equals(targetTypeDescriptor.getResolvableType())) {
731-
try {
732-
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
733-
readMethod.setAccessible(true);
721+
if (readMethod != null) {
722+
ResolvableType sourceResolvableType = ResolvableType.forMethodReturnType(readMethod);
723+
ResolvableType targetResolvableType = ResolvableType.forMethodParameter(writeMethod, 0);
724+
if (targetResolvableType.isAssignableFrom(sourceResolvableType)) {
725+
try {
726+
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
727+
readMethod.setAccessible(true);
728+
}
729+
Object value = readMethod.invoke(source);
730+
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
731+
writeMethod.setAccessible(true);
732+
}
733+
writeMethod.invoke(target, value);
734+
}
735+
catch (Throwable ex) {
736+
throw new FatalBeanException(
737+
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
734738
}
735-
Object value = readMethod.invoke(source);
736-
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
737-
writeMethod.setAccessible(true);
738-
}
739-
writeMethod.invoke(target, value);
740-
}
741-
catch (Throwable ex) {
742-
throw new FatalBeanException(
743-
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
744739
}
745740
}
746741
}

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

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,28 @@ void copyPropertiesWithDifferentTypes2() throws Exception {
174174
assertThat(tb2.getTouchy().equals(tb.getTouchy())).as("Touchy copied").isTrue();
175175
}
176176

177+
@Test
178+
void copyPropertiesHonorsGenericTypeMatches() {
179+
IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
180+
integerListHolder1.getList().add(42);
181+
IntegerListHolder2 integerListHolder2 = new IntegerListHolder2();
182+
183+
BeanUtils.copyProperties(integerListHolder1, integerListHolder2);
184+
assertThat(integerListHolder1.getList()).containsOnly(42);
185+
assertThat(integerListHolder2.getList()).containsOnly(42);
186+
}
187+
188+
@Test
189+
void copyPropertiesDoesNotHonorGenericTypeMismatches() {
190+
IntegerListHolder1 integerListHolder = new IntegerListHolder1();
191+
integerListHolder.getList().add(42);
192+
LongListHolder longListHolder = new LongListHolder();
193+
194+
BeanUtils.copyProperties(integerListHolder, longListHolder);
195+
assertThat(integerListHolder.getList()).containsOnly(42);
196+
assertThat(longListHolder.getList()).isEmpty();
197+
}
198+
177199
@Test
178200
void testCopyPropertiesWithEditable() throws Exception {
179201
TestBean tb = new TestBean();
@@ -337,24 +359,24 @@ void isNotSimpleProperty(Class<?> type) {
337359
private void assertSignatureEquals(Method desiredMethod, String signature) {
338360
assertThat(BeanUtils.resolveSignature(signature, MethodSignatureBean.class)).isEqualTo(desiredMethod);
339361
}
340-
341-
@Test
342-
void testCopiedParametersType() {
343-
344-
A a = new A();
345-
a.getList().add(42);
346-
B b = new B();
347362

348-
BeanUtils.copyProperties(a, b);
349363

350-
assertThat(a.getList()).containsOnly(42);
364+
@SuppressWarnings("unused")
365+
private static class IntegerListHolder1 {
366+
367+
private List<Integer> list = new ArrayList<>();
351368

352-
b.getList().forEach(n -> assertThat(n).isInstanceOf(Long.class));
353-
assertThat(b.getList()).isEmpty();
354-
369+
public List<Integer> getList() {
370+
return list;
371+
}
372+
373+
public void setList(List<Integer> list) {
374+
this.list = list;
375+
}
355376
}
356-
357-
class A {
377+
378+
@SuppressWarnings("unused")
379+
private static class IntegerListHolder2 {
358380

359381
private List<Integer> list = new ArrayList<>();
360382

@@ -365,9 +387,10 @@ public List<Integer> getList() {
365387
public void setList(List<Integer> list) {
366388
this.list = list;
367389
}
368-
369390
}
370-
class B {
391+
392+
@SuppressWarnings("unused")
393+
private static class LongListHolder {
371394

372395
private List<Long> list = new ArrayList<>();
373396

@@ -378,7 +401,6 @@ public List<Long> getList() {
378401
public void setList(List<Long> list) {
379402
this.list = list;
380403
}
381-
382404
}
383405

384406

0 commit comments

Comments
 (0)