Skip to content

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

Closed
@namwonmkw

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

}

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