From 667256adf97fd08ae70d1eec87216eba2d441416 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 11 Feb 2021 18:18:57 +0100 Subject: [PATCH] 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 --- .../org/springframework/beans/BeanUtils.java | 9 +- .../springframework/beans/BeanUtilsTests.java | 102 +++++++++++++++++- 2 files changed, 109 insertions(+), 2 deletions(-) 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 5a46e8d08a4c..053393e6c438 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 @@ 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); 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 ef1bbf4616bf..f3e9d2eaa9a1 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 @@ 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 @@ 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"); + 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 @@ private PrivateBeanWithPrivateConstructor() { } } + @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(); + } + } + } + }