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 5a46e8d08a..053393e6c4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -776,7 +776,14 @@ public abstract class BeanUtils { 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); 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 ef1bbf4616..f3e9d2eaa9 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-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. @@ -19,12 +19,16 @@ package org.springframework.beans; 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; @@ -196,6 +200,29 @@ class BeanUtilsTests { 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"); + assertThat(proxy.getClass().getDeclaredMethod("getLineItems").toGenericString()) + .contains("java.util.List") + .doesNotContain(""); + + // 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(); @@ -633,4 +660,77 @@ class BeanUtilsTests { } } + @SuppressWarnings("unused") + private static class Order { + + private String id; + private List lineItems; + + + Order() { + } + + Order(String id, List lineItems) { + this.id = id; + this.lineItems = lineItems; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public List getLineItems() { + return this.lineItems; + } + + public void setLineItems(List lineItems) { + this.lineItems = lineItems; + } + + @Override + public String toString() { + return "Order [id=" + this.id + ", lineItems=" + this.lineItems + "]"; + } + } + + private interface OrderSummary { + + String getId(); + + List 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(); + } + } + } + }