最近项目中使用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后的大写字母也改成了小写。但这应该没有影响,因为获取源对象属性也是通过目标对象属性的名称,如果得到这个名称的机制是一致的话,理论上是能够找到源对象的相应属性。
进一步Debug跟踪,发现所有目标属性均能找到对应的源属性,证明上面的设想是正确的。
然后却发现仅有isDeleted这个属性存在get方法,其它属性的readMethod均为null,这与一开始的测试结果是一致的。实际上不仅仅源属性中的get方法获取不到,目标对象的部分get方法也是为空的。
到了这一步,又变得一头雾水了,只知道没有正确获取属性描述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的对象属性复制工具!