Skip to content

Commit

Permalink
Ignore generics when Proxy is supplied to BeanUtils.copyProperties()
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sbrannen committed Feb 11, 2021
1 parent 8791928 commit 667256a
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,14 @@ private static void copyProperties(Object source, Object target, @Nullable Class
if (readMethod != null) {
ResolvableType sourceResolvableType = ResolvableType.forMethodReturnType(readMethod);
ResolvableType targetResolvableType = ResolvableType.forMethodParameter(writeMethod, 0);
if (targetResolvableType.isAssignableFrom(sourceResolvableType)) {

// Ignore generic types in assignable check if either ResolvableType has unresolvable generics.
boolean isAssignable =
(sourceResolvableType.hasUnresolvableGenerics() || targetResolvableType.hasUnresolvableGenerics() ?
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType()) :
targetResolvableType.isAssignableFrom(sourceResolvableType));

if (isAssignable) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
Expand All @@ -19,12 +19,16 @@
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URI;
import java.net.URL;
import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -196,6 +200,29 @@ void copyPropertiesDoesNotHonorGenericTypeMismatches() {
assertThat(longListHolder.getList()).isEmpty();
}

@Test // gh-26531
void copyPropertiesIgnoresGenericsIfSourceOrTargetHasUnresolvableGenerics() throws Exception {
Order original = new Order("test", Arrays.asList("foo", "bar"));

// Create a Proxy that loses the generic type information for the getLineItems() method.
OrderSummary proxy = proxyOrder(original);
assertThat(OrderSummary.class.getDeclaredMethod("getLineItems").toGenericString())
.contains("java.util.List<java.lang.String>");
assertThat(proxy.getClass().getDeclaredMethod("getLineItems").toGenericString())
.contains("java.util.List")
.doesNotContain("<java.lang.String>");

// Ensure that our custom Proxy works as expected.
assertThat(proxy.getId()).isEqualTo("test");
assertThat(proxy.getLineItems()).containsExactly("foo", "bar");

// Copy from proxy to target.
Order target = new Order();
BeanUtils.copyProperties(proxy, target);
assertThat(target.getId()).isEqualTo("test");
assertThat(target.getLineItems()).containsExactly("foo", "bar");
}

@Test
void copyPropertiesWithEditable() throws Exception {
TestBean tb = new TestBean();
Expand Down Expand Up @@ -633,4 +660,77 @@ private PrivateBeanWithPrivateConstructor() {
}
}

@SuppressWarnings("unused")
private static class Order {

private String id;
private List<String> lineItems;


Order() {
}

Order(String id, List<String> lineItems) {
this.id = id;
this.lineItems = lineItems;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public List<String> getLineItems() {
return this.lineItems;
}

public void setLineItems(List<String> lineItems) {
this.lineItems = lineItems;
}

@Override
public String toString() {
return "Order [id=" + this.id + ", lineItems=" + this.lineItems + "]";
}
}

private interface OrderSummary {

String getId();

List<String> getLineItems();
}


private OrderSummary proxyOrder(Order order) {
return (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(),
new Class<?>[] { OrderSummary.class }, new OrderInvocationHandler(order));
}


private static class OrderInvocationHandler implements InvocationHandler {

private final Order order;


OrderInvocationHandler(Order order) {
this.order = order;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// Ignore args since OrderSummary doesn't declare any methods with arguments,
// and we're not supporting equals(Object), etc.
return Order.class.getDeclaredMethod(method.getName()).invoke(this.order);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
}

}

0 comments on commit 667256a

Please sign in to comment.