Skip to content

BeanUtils.copyProperties no longer copies generic type properties from a base class that has been enhanced #32888

@namwonmkw

Description

@namwonmkw

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); } } } } } } } }

Metadata

Metadata

Assignees

Labels

in: coreIssues in core modules (aop, beans, core, context, expression)type: regressionA bug that is also a regression

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions