您现在的位置是:首页 >技术交流 >【Accessors注解】记录使用 lombook 注解姿势不对导致无法使用 BeanCopier 复制属性的问题网站首页技术交流

【Accessors注解】记录使用 lombook 注解姿势不对导致无法使用 BeanCopier 复制属性的问题

Amos-Chen 2024-07-01 11:59:52
简介【Accessors注解】记录使用 lombook 注解姿势不对导致无法使用 BeanCopier 复制属性的问题

背景

前几天看同事写的代码,发现不同分层对象之间的转换用的 spring 自带的 BeanUtils.copyProperties(),并且复制的还是对象集合。一时技痒,想优化改造一下,于是乎想当然的把 BeanUtils.copyProperties() 去掉了,然后添加了两个静态 BeanCopier 对象:A 转 B 的 BeanCopier, 和 B 转 A 的 BeanCopier。

public static final BeanCopier BEAN_COPIER_A_B = BeanCopier.create(A.class, B.class, false);
public static final BeanCopier BEAN_COPIER_B_A = BeanCopier.create(B.class, A.class, false);

再把用到 BeanUtils.copyProperties() 的地方相应的改成了 BeanCopier 对象,发布到测试环境。一顿操作下来,发现接口返回出来的对象列表里面的对象属性全是 null。

定位问题

反复检查了代码,并没有会把属性设置为 null 的地方,又因为我只修改了属性复制的方式,所以断定了只有可能是 BeanCopier 复制属性没有生效。
从 A 对象复制成 B 对象,B 对象的字段全是 null,然后查看了一下 B 类的定义,相对以往我个人的使用习惯,多了一个注解 @Accessors(chain = true) ,大胆猜测是因为这个导致,去掉之后果然生效。

不知道为什么同事用这个注解,但是又不用这个注解帮我们生成的方法?
简单说下这个注解的作用:

@Data
@Accessors(chain = true)
public class PersonDO {
   private String name;
   private Integer age;
}

我们知道 lombok 会帮我生成 setName 和 setAge 方法,如果不加 @Accessors(chain = true) 那么就是普通的 setXXX 方法,返回为 void。但是一旦加了 @Accessors(chain = true) 就变成 【请注意区别,后面要考】

public PersonDO setName(final String name) {
       this.name = name;
       return this;
   }

分析原因

为什么 BeanUtils.copyProperties() 可以

  1. 参数说明:

source:源对象,即需要复制属性值的对象。
target:目标对象,即属性值将被复制到的对象。
ignoreProperties(可选):要忽略的属性列表,可以排除某些属性不进行复制。

  1. 实现原理:

BeanUtils.copyProperties() 方法内部通过Java的反射机制实现属性的复制。它通过获取源对象和目标对象的属性描述符(PropertyDescriptor),并使用对应的读取方法和写入方法来获取和设置属性值。对于每个属性,它会使用源对象的读取方法获取属性值,然后使用目标对象的写入方法将属性值设置到目标对象中。

  1. 复制过程:

BeanUtils.copyProperties()方法会自动匹配源对象和目标对象中相同名称的属性,并进行属性值的复制。如果属性名称在源对象和目标对象中都存在,但属性类型不匹配,会尝试进行类型转换。如果存在ignoreProperties参数,可以传入要忽略复制的属性列表,这些属性将不会进行复制操作。

  1. 属性复制的限制:

属性复制过程是基于属性名称的匹配,因此要求源对象和目标对象中的属性名称相同。
属性复制过程是基于属性的读取方法和写入方法,因此要求源对象和目标对象中的属性需要提供对应的读取方法和写入方法。

为什么 BeanCopier 不可以

BeanCopier的原理如下:

  • 首先,BeanCopier通过反射分析源对象和目标对象的属性信息,包括属性名称、类型等。

  • 在第一次复制时,BeanCopier会使用ASM字节码生成库生成源对象和目标对象之间的转换类。该转换类通过直接访问对象的字段而不是使用getter和setter方法来实现属性拷贝,从而提高了性能。

  • 生成的转换类会被加载到内存中,并创建一个实例。

  • 当需要进行属性拷贝时,BeanCopier会调用生成的转换类的拷贝方法,将源对象的属性值复制到目标对象中。

看一下 cglib 通过字节码生成转换类的方法

public void generateClass(ClassVisitor v) {
     Type sourceType = Type.getType(source);
     Type targetType = Type.getType(target);
     // 创建一个 ClassEmitter 对象 ce,用于生成类的字节码
     ClassEmitter ce = new ClassEmitter(v);
     // 调用 begin_class 方法,开始定义类的基本信息,包括类的修饰符、名称、父类、接口等。
     ce.begin_class(Constants.V1_8,
                    Constants.ACC_PUBLIC,
                    getClassName(),
                    BEAN_COPIER,
                    null,
                    Constants.SOURCE_FILE);
	 // 生成默认的无参构造函数
     EmitUtils.null_constructor(ce);
     // 生成 public 修饰符,名字为 copy 的方法
     CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, COPY, null);
     // 获取源对象的所有 get 方法
     PropertyDescriptor[] getters = ReflectUtils.getBeanGetters(source);
     // 获取目标对象的所有 set 方法
     // 这里非常重要
     // 这里非常重要 
     // 假如还是上面的 PersonDO 的例子,如果该类加入了 @Accessors(chain = true) 注解
     // 则 setters 为 空 ,至于为什么为空,下面的代码另外分析
     PropertyDescriptor[] setters = ReflectUtils.getBeanSetters(target);

     Map names = new HashMap();
     for (int i = 0; i < getters.length; i++) {
         names.put(getters[i].getName(), getters[i]);
     }
     Local targetLocal = e.make_local();
     Local sourceLocal = e.make_local();
     // 判断有没有用到自定义的转换器
     if (useConverter) {
         e.load_arg(1);
         e.checkcast(targetType);
         e.store_local(targetLocal);
         e.load_arg(0);                
         e.checkcast(sourceType);
         e.store_local(sourceLocal);
     } else {
         e.load_arg(1);
         e.checkcast(targetType);
         e.load_arg(0);
         e.checkcast(sourceType);
     }
     // 既然 setters 为空 则这里肯定不会执行
     for (int i = 0; i < setters.length; i++) {
         PropertyDescriptor setter = setters[i];
         PropertyDescriptor getter = (PropertyDescriptor)names.get(setter.getName());
         if (getter != null) {
             MethodInfo read = ReflectUtils.getMethodInfo(getter.getReadMethod());
             MethodInfo write = ReflectUtils.getMethodInfo(setter.getWriteMethod());
             if (useConverter) {
                 Type setterType = write.getSignature().getArgumentTypes()[0];
                 e.load_local(targetLocal);
                 e.load_arg(2);
                 e.load_local(sourceLocal);
                 e.invoke(read);
                 e.box(read.getSignature().getReturnType());
                 EmitUtils.load_class(e, setterType);
                 e.push(write.getSignature().getName());
                 e.invoke_interface(CONVERTER, CONVERT);
                 e.unbox_or_zero(setterType);
                 e.invoke(write);
             } else if (compatible(getter, setter)) {
                 e.dup2();
                 e.invoke(read);
                 e.invoke(write);
             }
         }
     }
     e.return_value();
     e.end_method();
     ce.end_class();
 }

再看下 ReflectUtils.getBeanSetters()

public static PropertyDescriptor[] getBeanSetters(Class type) {
	return getPropertiesHelper(type, false, true);
}
// type 目标类
// read --- true 代表获取该类的 read 方法 , getBeanSetters 传入的是 false 
// write -- true 代表获取该类的 write 方法,  getBeanSetters 传入的是 true 
private static PropertyDescriptor[] getPropertiesHelper(Class type, boolean read, boolean write) {
	try {
		BeanInfo info = Introspector.getBeanInfo(type, Object.class);
		PropertyDescriptor[] all = info.getPropertyDescriptors();
		if (read && write) {
			return all;
		}
		List properties = new ArrayList(all.length);
		for (int i = 0; i < all.length; i++) {
			PropertyDescriptor pd = all[i];
			if ((read && pd.getReadMethod() != null) ||
			        // 重点在这里 pd.getWriteMethod() 为 null 
					(write && pd.getWriteMethod() != null)) {
				properties.add(pd);
			}
		}
		return (PropertyDescriptor[]) properties.toArray(new PropertyDescriptor[properties.size()]);
	}
	catch (IntrospectionException e) {
		throw new CodeGenerationException(e);
	}
}

这里是 pd.getWriteMethod() 对应的源码,不过这里有点难度没看懂。

public synchronized Method getWriteMethod() {
     Method writeMethod = this.writeMethodRef.get();
     if (writeMethod == null) {
         Class<?> cls = getClass0();
         if (cls == null || (writeMethodName == null && !this.writeMethodRef.isSet())) {
             // The write method was explicitly set to null.
             return null;
         }

         // We need the type to fetch the correct method.
         Class<?> type = getPropertyType0();
         if (type == null) {
             try {
                 // Can't use getPropertyType since it will lead to recursive loop.
                 type = findPropertyType(getReadMethod(), null);
                 setPropertyType(type);
             } catch (IntrospectionException ex) {
                 // Without the correct property type we can't be guaranteed
                 // to find the correct method.
                 return null;
             }
         }

         if (writeMethodName == null) {
             writeMethodName = Introspector.SET_PREFIX + getBaseName();
         }

         Class<?>[] args = (type == null) ? null : new Class<?>[] { type };
         writeMethod = Introspector.findMethod(cls, writeMethodName, 1, args);
         if (writeMethod != null) {
             if (!writeMethod.getReturnType().equals(void.class)) {
                 writeMethod = null;
             }
         }
         try {
             setWriteMethod(writeMethod);
         } catch (IntrospectionException ex) {
             // fall through
         }
     }
     return writeMethod;
 }

总结

Accessors注解后的实体类作为目标类,在进行 BeanCopier 复制属性的时候,由于获取到的 writeMethod 方法是空,所以通过字节码生成 copy 方法是不包含类的属性的,于是乎复制无效。

java.beans.PropertyDescriptor 中的方法 public synchronized Method getWriteMethod() 看不太懂,有知道的望告知。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。