Skip to content

Commit 667256a

Browse files
committed
Ignore generics when Proxy is supplied to BeanUtils.copyProperties()
gh-24281 introduced support to honor generic type information in BeanUtils.copyProperties(), but that introduced a regression. Specifically, if the supplied source or target object lacked generic type information for the return type of the read-method or the parameter type of the write-method for a given property, respectively, the two properties would be considered a mismatch and ignored. This can occur if the source or target object is a java.lang.reflect.Proxy since the dynamically generated class for the proxy loses the generic type information from interfaces that the proxy implements. This commit fixes this regression by ignoring generic type information if either the source or target property is lacking generic type information. Closes gh-26531
1 parent 8791928 commit 667256a

File tree

2 files changed

+109
-2
lines changed

2 files changed

+109
-2
lines changed

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,14 @@ private static void copyProperties(Object source, Object target, @Nullable Class
776776
if (readMethod != null) {
777777
ResolvableType sourceResolvableType = ResolvableType.forMethodReturnType(readMethod);
778778
ResolvableType targetResolvableType = ResolvableType.forMethodParameter(writeMethod, 0);
779-
if (targetResolvableType.isAssignableFrom(sourceResolvableType)) {
779+
780+
// Ignore generic types in assignable check if either ResolvableType has unresolvable generics.
781+
boolean isAssignable =
782+
(sourceResolvableType.hasUnresolvableGenerics() || targetResolvableType.hasUnresolvableGenerics() ?
783+
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType()) :
784+
targetResolvableType.isAssignableFrom(sourceResolvableType));
785+
786+
if (isAssignable) {
780787
try {
781788
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
782789
readMethod.setAccessible(true);

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

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2021 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.
@@ -19,12 +19,16 @@
1919
import java.beans.Introspector;
2020
import java.beans.PropertyDescriptor;
2121
import java.lang.reflect.Constructor;
22+
import java.lang.reflect.InvocationHandler;
23+
import java.lang.reflect.InvocationTargetException;
2224
import java.lang.reflect.Method;
25+
import java.lang.reflect.Proxy;
2326
import java.net.URI;
2427
import java.net.URL;
2528
import java.time.DayOfWeek;
2629
import java.time.LocalDateTime;
2730
import java.util.ArrayList;
31+
import java.util.Arrays;
2832
import java.util.Date;
2933
import java.util.List;
3034
import java.util.Locale;
@@ -196,6 +200,29 @@ void copyPropertiesDoesNotHonorGenericTypeMismatches() {
196200
assertThat(longListHolder.getList()).isEmpty();
197201
}
198202

203+
@Test // gh-26531
204+
void copyPropertiesIgnoresGenericsIfSourceOrTargetHasUnresolvableGenerics() throws Exception {
205+
Order original = new Order("test", Arrays.asList("foo", "bar"));
206+
207+
// Create a Proxy that loses the generic type information for the getLineItems() method.
208+
OrderSummary proxy = proxyOrder(original);
209+
assertThat(OrderSummary.class.getDeclaredMethod("getLineItems").toGenericString())
210+
.contains("java.util.List<java.lang.String>");
211+
assertThat(proxy.getClass().getDeclaredMethod("getLineItems").toGenericString())
212+
.contains("java.util.List")
213+
.doesNotContain("<java.lang.String>");
214+
215+
// Ensure that our custom Proxy works as expected.
216+
assertThat(proxy.getId()).isEqualTo("test");
217+
assertThat(proxy.getLineItems()).containsExactly("foo", "bar");
218+
219+
// Copy from proxy to target.
220+
Order target = new Order();
221+
BeanUtils.copyProperties(proxy, target);
222+
assertThat(target.getId()).isEqualTo("test");
223+
assertThat(target.getLineItems()).containsExactly("foo", "bar");
224+
}
225+
199226
@Test
200227
void copyPropertiesWithEditable() throws Exception {
201228
TestBean tb = new TestBean();
@@ -633,4 +660,77 @@ private PrivateBeanWithPrivateConstructor() {
633660
}
634661
}
635662

663+
@SuppressWarnings("unused")
664+
private static class Order {
665+
666+
private String id;
667+
private List<String> lineItems;
668+
669+
670+
Order() {
671+
}
672+
673+
Order(String id, List<String> lineItems) {
674+
this.id = id;
675+
this.lineItems = lineItems;
676+
}
677+
678+
public String getId() {
679+
return id;
680+
}
681+
682+
public void setId(String id) {
683+
this.id = id;
684+
}
685+
686+
public List<String> getLineItems() {
687+
return this.lineItems;
688+
}
689+
690+
public void setLineItems(List<String> lineItems) {
691+
this.lineItems = lineItems;
692+
}
693+
694+
@Override
695+
public String toString() {
696+
return "Order [id=" + this.id + ", lineItems=" + this.lineItems + "]";
697+
}
698+
}
699+
700+
private interface OrderSummary {
701+
702+
String getId();
703+
704+
List<String> getLineItems();
705+
}
706+
707+
708+
private OrderSummary proxyOrder(Order order) {
709+
return (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(),
710+
new Class<?>[] { OrderSummary.class }, new OrderInvocationHandler(order));
711+
}
712+
713+
714+
private static class OrderInvocationHandler implements InvocationHandler {
715+
716+
private final Order order;
717+
718+
719+
OrderInvocationHandler(Order order) {
720+
this.order = order;
721+
}
722+
723+
@Override
724+
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
725+
try {
726+
// Ignore args since OrderSummary doesn't declare any methods with arguments,
727+
// and we're not supporting equals(Object), etc.
728+
return Order.class.getDeclaredMethod(method.getName()).invoke(this.order);
729+
}
730+
catch (InvocationTargetException ex) {
731+
throw ex.getTargetException();
732+
}
733+
}
734+
}
735+
636736
}

0 commit comments

Comments
 (0)