With a recent release the BeanUtils.copyProperties method stopped working for generic properties of enhanced classes. The breaking change was introduced with this commit. https://github.com/spring-projects/spring-framework/commit/09aa59f9e79e19a2f09e66002c665b6a5a03ae20

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

}

Comment From: jhoeller

This is available in current 6.1.9 snapshots already. Feel free to give it an early try!

Comment From: namwonmkw

Thanks for the quick turnaround. I have tested it with a snapshot and it is working. How soon will 6.1.9 be released?On Jun 4, 2024, at 3:08 PM, Juergen Hoeller @.***> wrote: This is available in current 6.1.9 snapshots already. Feel free to give it an early try!

—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you authored the thread.Message ID: @.***>

Comment From: jhoeller

It's scheduled for release next week: June 13. You can always see the current target dates on https://github.com/spring-projects/spring-framework/milestones