BeanUtils.copyProerties()分析:关于Kotlin中无法复制is开头属性的问题

最近项目中使用Kotlin、Java混合开发,其中使用了BeanUtils的copyProerties方法来复制对象属性,偶然发现is开头的属性复制时会丢失。

下面演示测试过程:

定义两个Kotlin数据类T1、T2以及一个用于测试的main函数

data class T1(var name: String? = null, var isDeleted: Boolean = true, var isVisible: Boolean? = null, var isActive: Int = 1)
data class T2(var name: String? = null, var isDeleted: Boolean = true, var isVisible: Boolean? = null, var isActive: Int = 1, var other: String)

fun main(args: Array) {
    val t1 = T1()
    val t2 = T2("name-value", false, false, 0, "other-value")
    println(t2.toString())
    BeanUtils.copyProperties(t2, t1)
    println(t1.toString())
}

输出结果如下

T2(name=name-value, isDeleted=false, isVisible=false, isActive=0, other=other-value)
T1(name=name-value, isDeleted=false, isVisible=null, isActive=1)

经分析得到以下结论

使用BeanUtils复制Kotlin数据类对象时,遇到is开头的属性,仅能成功复制类型为Boolean的属性(如:isDeleted: Boolean),并且不可为空(如:isVisible: Boolean?)

到这里虽然有了结果,但究竟为何呢?接下来我们尝试分析一下原因!

首先把利用IDEA中的Kotlin工具(Kotlin > Show Kotlin Bytecode)将Kotlin代码转成字节码,然后再通过Decompile将字节码反编译成Java代码,下面贴出部分代码

public final class T1 {
   @Nullable
   private String name;
   private boolean isDeleted;
   @Nullable
   private Boolean isVisible;
   private int isActive;

   @Nullable
   public final String getName() {
      return this.name;
   }

   public final void setName(@Nullable String var1) {
      this.name = var1;
   }

   public final boolean isDeleted() {
      return this.isDeleted;
   }

   public final void setDeleted(boolean var1) {
      this.isDeleted = var1;
   }

   @Nullable
   public final Boolean isVisible() {
      return this.isVisible;
   }

   public final void setVisible(@Nullable Boolean var1) {
      this.isVisible = var1;
   }

   public final int isActive() {
      return this.isActive;
   }

   public final void setActive(int var1) {
      this.isActive = var1;
   }
}

可以发现Kotlin对is开头的属性做了特殊处理:生成get和set方法时,isXxx对应的get和set方法分别为isXxx和setXxx,而不是getIsXxx和setIsXxx。

到这里,似乎可以猜想到大致的原因了,但还不是特别清晰,继续查看下BeanUtils的相关代码,为了便于分析,在重要代码添加了注释便于理解。

    /**
     * Copy the property values of the given source bean into the given target bean.
     * 

Note: The source and target classes do not have to match or even be derived * from each other, as long as the properties match. Any bean properties that the * source bean exposes but the target bean does not will silently be ignored. * @param source the source bean * @param target the target bean * @param editable the class (or interface) to restrict property setting to * @param ignoreProperties array of property names to ignore * @throws BeansException if the copying failed * @see BeanWrapper */ 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[] PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable); List ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null); //遍历目标对象的各个属性描述PropertyDescriptor for (PropertyDescriptor targetPd : targetPds) { //获取目标对象属性的set方法 Method writeMethod = targetPd.getWriteMethod(); if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) { //获取源对象中与目标对象同名的属性的描述PropertyDescriptor PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName()); if (sourcePd != null) { //获取源对象属性的get方法 Method readMethod = sourcePd.getReadMethod(); //判断get方法是否存在 && 目标对象属性的set方法参数类型是否与源对象属性的get方法返回类型一致 if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) { try { if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) { readMethod.setAccessible(true); } //执行源对象属性的get方法,获取属性值 Object value = readMethod.invoke(source); if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) { writeMethod.setAccessible(true); } //执行目标对象属性的set方法,写入属性值 writeMethod.invoke(target, value); } catch (Throwable ex) { throw new FatalBeanException( "Could not copy property '" + targetPd.getName() + "' from source to target", ex); } } } } } }

这个copyProperties函数中有4个参数,这里我们只使用到前2个参数,所以涉及后面2个参数的代码就忽略,并不影响分析过程。

再结合Debug,观察下详细的运行情况,发现is开头的几个属性都发生了变化,is被截去,并且is后的大写字母也改成了小写。
BeanUtils.copyProerties()分析:关于Kotlin中无法复制is开头属性的问题_第1张图片
BeanUtils-debug-01.png

但这应该没有影响,因为获取源对象属性也是通过目标对象属性的名称,如果得到这个名称的机制是一致的话,理论上是能够找到源对象的相应属性。

进一步Debug跟踪,发现所有目标属性均能找到对应的源属性,证明上面的设想是正确的。
然后却发现仅有isDeleted这个属性存在get方法,其它属性的readMethod均为null,这与一开始的测试结果是一致的。实际上不仅仅源属性中的get方法获取不到,目标对象的部分get方法也是为空的。


BeanUtils.copyProerties()分析:关于Kotlin中无法复制is开头属性的问题_第2张图片
BeanUtils-debug-02.png

到了这一步,又变得一头雾水了,只知道没有正确获取属性描述PropertyDescriptor,只能去查看getopertyDescriptor这个方法具体是如何处理的。

BeanUtils中有一个方法getPropertyDescriptors来获取对象属性描述数组

    /**
     * Retrieve the JavaBeans {@code PropertyDescriptor}s of a given class.
     * @param clazz the Class to retrieve the PropertyDescriptors for
     * @return an array of {@code PropertyDescriptors} for the given class
     * @throws BeansException if PropertyDescriptor look fails
     */
    public static PropertyDescriptor[] getPropertyDescriptors(Class clazz) throws BeansException {
        CachedIntrospectionResults cr = CachedIntrospectionResults.forClass(clazz);
        return cr.getPropertyDescriptors();
    }

PropertyDescriptor[] 是从CachedIntrospectionResults获取的,这是它的构造方法

    /**
     * Create a new CachedIntrospectionResults instance for the given class.
     * @param beanClass the bean class to analyze
     * @throws BeansException in case of introspection failure
     */
    private CachedIntrospectionResults(Class beanClass) throws BeansException {
        try {
            if (logger.isTraceEnabled()) {
                logger.trace("Getting BeanInfo for class [" + beanClass.getName() + "]");
            }
            this.beanInfo = getBeanInfo(beanClass, shouldIntrospectorIgnoreBeaninfoClasses);

            if (logger.isTraceEnabled()) {
                logger.trace("Caching PropertyDescriptors for class [" + beanClass.getName() + "]");
            }
            this.propertyDescriptorCache = new LinkedHashMap<>();

            // This call is slow so we do it once.
            PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors();
            for (PropertyDescriptor pd : pds) {
                if (Class.class == beanClass &&
                        ("classLoader".equals(pd.getName()) ||  "protectionDomain".equals(pd.getName()))) {
                    // Ignore Class.getClassLoader() and getProtectionDomain() methods - nobody needs to bind to those
                    continue;
                }
                if (logger.isTraceEnabled()) {
                    logger.trace("Found bean property '" + pd.getName() + "'" +
                            (pd.getPropertyType() != null ? " of type [" + pd.getPropertyType().getName() + "]" : "") +
                            (pd.getPropertyEditorClass() != null ?
                                    "; editor [" + pd.getPropertyEditorClass().getName() + "]" : ""));
                }
                pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
                this.propertyDescriptorCache.put(pd.getName(), pd);
            }

            // Explicitly check implemented interfaces for setter/getter methods as well,
            // in particular for Java 8 default methods...
            Class clazz = beanClass;
            while (clazz != null && clazz != Object.class) {
                Class[] ifcs = clazz.getInterfaces();
                for (Class ifc : ifcs) {
                    if (!ClassUtils.isJavaLanguageInterface(ifc)) {
                        BeanInfo ifcInfo = getBeanInfo(ifc, true);
                        PropertyDescriptor[] ifcPds = ifcInfo.getPropertyDescriptors();
                        for (PropertyDescriptor pd : ifcPds) {
                            if (!this.propertyDescriptorCache.containsKey(pd.getName())) {
                                pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
                                this.propertyDescriptorCache.put(pd.getName(), pd);
                            }
                        }
                    }
                }
                clazz = clazz.getSuperclass();
            }

            this.typeDescriptorCache = new ConcurrentReferenceHashMap<>();
        }
        catch (IntrospectionException ex) {
            throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass.getName() + "]", ex);
        }
    }

其中关键的一行代码用到了getBeanInfo()这个方法

/**
     * Retrieve a {@link BeanInfo} descriptor for the given target class.
     * @param beanClass the target class to introspect
     * @param ignoreBeaninfoClasses whether to apply {@link Introspector#IGNORE_ALL_BEANINFO} mode
     * @return the resulting {@code BeanInfo} descriptor (never {@code null})
     * @throws IntrospectionException from the underlying {@link Introspector}
     */
    private static BeanInfo getBeanInfo(Class beanClass, boolean ignoreBeaninfoClasses)
            throws IntrospectionException {

        for (BeanInfoFactory beanInfoFactory : beanInfoFactories) {
            BeanInfo beanInfo = beanInfoFactory.getBeanInfo(beanClass);
            if (beanInfo != null) {
                return beanInfo;
            }
        }
        return (ignoreBeaninfoClasses ?
                Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) :
                Introspector.getBeanInfo(beanClass));
    }

通过Debug发现,getBeanInfo()的返回结果中,部分属性的readMethod方法就是null,而getBeanInfo()方法最终调用的是Introspector.getBeanInfo(beanClass));这行代码,继续深入,发现Introspector已经是jdk中java.beans包中的文件了...

那么获取对象属性这部分代码就无法修改了,除非自己写底层实现替换之,所以涉及Kotlin属性复制的时候,遇到is开头的属性,遵循前面测试的结论吧!

使用BeanUtils复制Kotlin数据类对象时,遇到is开头的属性,仅能成功复制类型为Boolean的属性(如:isDeleted: Boolean),并且不可为空(如:isVisible: Boolean?)

或者,自己另外实现一个兼容Kotlin的对象属性复制工具!

你可能感兴趣的:(BeanUtils.copyProerties()分析:关于Kotlin中无法复制is开头属性的问题)