BeanUtils.copyProperties no longer copies generic type properties from a base class that has been enhanced #32888
Closed
Description
With a recent release the BeanUtils.copyProperties method stopped working for generic properties of enhanced classes. The breaking change was introduced with this commit. 09aa59f
Below is a Unit Test which fail when using the latest version BeanUtils implementation and the former version.
import static org.junit.Assert.assertEquals;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.junit.Test;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
public class TestCopyProperties {
public static class BaseModel<T> {
public BaseModel() {
}
private T id;
private String name;
/**
* @return the id
*/
public T getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(T id) {
this.id = id;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
}
public static class User extends BaseModel<Integer> {
private String address;
public User() {
super();
}
/**
* @return the address
*/
public String getAddress() {
return address;
}
/**
* @param address the address to set
*/
public void setAddress(String address) {
this.address = address;
}
}
@Test
public void testCopyFailed() throws Exception {
User f = createCglibProxy(User.class);
f.setId(1);
f.setName("proxy");
f.setAddress("addr");
User copy = new User();
// copyProperties(f, copy, null, (String[]) null);
BeanUtils.copyProperties(f, copy);
assertEquals(f.getName(), copy.getName());
assertEquals(f.getAddress(), copy.getAddress());
assertEquals(f.getId(), copy.getId());
}
@Test
public void testCopyPrevious() throws Exception {
User f = createCglibProxy(User.class);
f.setId(1);
f.setName("proxy");
f.setAddress("addr");
User copy = new User();
copyProperties(f, copy, null, (String[]) null);
assertEquals(f.getName(), copy.getName());
assertEquals(f.getAddress(), copy.getAddress());
assertEquals(f.getId(), copy.getId());
}
@SuppressWarnings("unchecked")
private <T> T createCglibProxy(Class<T> clazz)
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clazz);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
return proxy.invokeSuper(obj, args);
});
return (T) enhancer.create();
}
private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
@Nullable String... ignoreProperties) throws BeansException {
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
Class<?> actualEditable = target.getClass();
if (editable != null) {
if (!editable.isInstance(target)) {
throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
"] not assignable to editable class [" + editable.getName() + "]");
}
actualEditable = editable;
}
PropertyDescriptor[] targetPds = BeanUtils.getPropertyDescriptors(actualEditable);
Set<String> ignoredProps = (ignoreProperties != null ? new HashSet<>(Arrays.asList(ignoreProperties)) : null);
for (PropertyDescriptor targetPd : targetPds) {
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoredProps == null || !ignoredProps.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = BeanUtils.getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null) {
ResolvableType sourceResolvableType = ResolvableType.forMethodReturnType(readMethod);
ResolvableType targetResolvableType = ResolvableType.forMethodParameter(writeMethod, 0);
boolean isAssignable = (sourceResolvableType.hasUnresolvableGenerics()
|| targetResolvableType.hasUnresolvableGenerics()
? ClassUtils.isAssignable(writeMethod.getParameterTypes()[0],
readMethod.getReturnType())
: targetResolvableType.isAssignableFrom(sourceResolvableType));
if (isAssignable) {
try {
ReflectionUtils.makeAccessible(readMethod);
Object value = readMethod.invoke(source);
ReflectionUtils.makeAccessible(writeMethod);
writeMethod.invoke(target, value);
} catch (Throwable ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target",
ex);
}
}
}
}
}
}
}
}