前言
作為一個JAVA后端開發(fā)疯趟,日常工作中不免會經(jīng)常用到對象拷貝晦譬,本篇就從實際案例出發(fā)叭首,進行各種方案的實踐對比习勤。
場景重現(xiàn)
一日,糖哥接到需求焙格,需要新寫一個學(xué)生信息列表獲取的接口姻报,數(shù)據(jù)庫的獲取的方法底層已有封裝,但是考慮到信息保密需要隱藏一部分敏感字段〖涿現(xiàn)在我們來看下已有的StudentDO和新添加的StudentTO類吴旋。
@Data
Class StudentDO {
private Long id;
private String name;
private String idCard;
private String tel;
private Integer age;
}
@Data
Class StudentTO {
private Long id;
private String name;
private Integer age;
}
根據(jù)已有的方法可以獲取到StudentDO的List,但是在實際輸出時需要將其轉(zhuǎn)換成StudentTO的List厢破。
方案和思路
1.遍歷然后get/set
這是最容易想到的辦法荣瑟。具體實現(xiàn)如下:
public List<StudentTO> copyList(List<StudentDO> doList) {
List<StudentTO> toList = new ArrayList<>();
for (StudentDO item : doList) {
StudentTO to = new StudentTO();
to.setId(item.getId());
to.setName(item.getName());
to.setAge(item.getAge());
toList.add(to);
}
return toList;
}
從代碼性能來說,這種方式是最高效的摩泪,但是缺點是每次都要去基于不同的類實現(xiàn)不同的轉(zhuǎn)換方法笆焰,在編碼效率上是極低的。
2.反射實現(xiàn)通用性對象拷貝(Spring BeanUtils)
反射是java的一種特性见坑,一般我們都會用它去解決一些通用性的問題嚷掠。針對當(dāng)前的問題,通用的解決思路就是將源對象與目標(biāo)對象的相同屬性值設(shè)置到目標(biāo)對象中荞驴。
基于反射去實現(xiàn)對象拷貝有很多種不皆,我們拿其中使用較為普遍的Spring BeanUtils舉例。
我們先來看看基于Spring BeanUtils怎么解決上述問題熊楼。
public void studentCopyList(List<StudentDO> dolist) {
// spring BeanUtils實現(xiàn)
List<StudentTO> studentTOList1 = springBeanUtilsCopyList(dolist, StudentTO.class);
}
public static <T> List<T> springBeanUtilsCopyList(List<?> objects, Class<T> class1) {
try {
if (objects == null || objects.isEmpty()) {
return Collections.emptyList();
}
List<T> res = new ArrayList<>();
for (Object s : objects) {
T t = class1.newInstance();
BeanUtils.copyProperties(s, t);
res.add(t);
}
return res;
} catch (InstantiationException | IllegalAccessException e) {
return Collections.emptyList();
}
}
再來看看Spring BeanUtils的部分核心源碼霹娄。
private static void copyProperties(Object source, Object target, Class<?> editable, 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 = getPropertyDescriptors(actualEditable);
List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
for (PropertyDescriptor targetPd : targetPds) {
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null &&
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, value);
}
catch (Throwable ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
}
可以看到其主要就是利用了反射機制,先遍歷目標(biāo)對象的屬性值鲫骗,當(dāng)發(fā)現(xiàn)源對象中有相同屬性值時進行設(shè)置犬耻。
這種做法的好處就是通用性很強,但是缺點是反射會降低性能执泰,尤其在調(diào)用量大的時候越發(fā)明顯枕磁。
3.即時編譯實現(xiàn)對象拷貝(cglib BeanCopier)
我們知道java不僅僅是一門靜態(tài)編譯語言,還帶有即時編譯的特性术吝。思路是我們可以根據(jù)入?yún)韯討B(tài)生成相應(yīng)的get/set代碼處理邏輯计济,并即時編譯運行晴楔。
這里我們舉例基于cglib實現(xiàn)的BeanCopier,該工具類目前也引入在spring的core包中峭咒。先來看看如何使用税弃。
public void studentCopyList(List<StudentDO> dolist) {
// cglib BeanCopier實現(xiàn)
List<StudentTO> studentTOList2 = cglibBeanCopierCopyList(dolist, StudentTO.class);
}
public static <T> List<T> cglibBeanCopierCopyList(List<?> objects, Class<T> targetClass) {
try {
if (objects == null || objects.isEmpty()) {
return Collections.emptyList();
}
List<T> res = new ArrayList<>();
for (Object s : objects) {
T t = targetClass.newInstance();
BeanCopier copier = BeanCopier.create(s.getClass(), t.getClass(), false);
copier.copy(s, t, null);
res.add(t);
}
return res;
} catch (InstantiationException | IllegalAccessException e) {
return Collections.emptyList();
}
}
再來看看其源碼實現(xiàn),其主要邏輯可以參考生成class部分的代碼:
public void generateClass(ClassVisitor v) {
Type sourceType = Type.getType(this.source);
Type targetType = Type.getType(this.target);
ClassEmitter ce = new ClassEmitter(v);
ce.begin_class(46, 1, this.getClassName(), BeanCopier.BEAN_COPIER, (Type[])null, "<generated>");
EmitUtils.null_constructor(ce);
CodeEmitter e = ce.begin_method(1, BeanCopier.COPY, (Type[])null);
PropertyDescriptor[] getters = ReflectUtils.getBeanGetters(this.source);
PropertyDescriptor[] setters = ReflectUtils.getBeanSetters(this.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 (this.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);
}
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 (this.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(BeanCopier.CONVERTER, BeanCopier.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();
}
邏輯可以看到和基于反射的spring BeanUtils是一致的凑队,只是實現(xiàn)方式不同则果。(cglib主要是利用了 Asm 字節(jié)碼技術(shù))
該種方式即解決了日常使用的編碼效率問題,又優(yōu)化了整個執(zhí)行過程中的性能損耗漩氨。
4.注解處理器實現(xiàn)對象拷貝(mapstruct)
java源碼編譯由以下3個過程組成
- 分析和輸入到符號表
- 注解處理
- 語義分析和生成class文件
很多工具其實都會基于注解處理器來實現(xiàn)相應(yīng)的功能西壮,例如常用的lombok等。
本次介紹的mapstruct也是同樣的原理叫惊。
使用mapstruct會比之前的兩種方法多一個步驟就是需要創(chuàng)建一個interface類款青,具體實現(xiàn)如下:
@Resource
private StudentMapper studentMapper;
public void studentCopyList(List<StudentDO> dolist) {
// 基于mapstruct實現(xiàn)
List<StudentTO> studentTOList3 = studentMapper.toTOList(dolist);
}
對應(yīng)需要創(chuàng)建的接口類:
@Mapper(componentModel = "spring")
public interface StudentMapper {
List<StudentTO> toTOList(List<StudentDO> doList);
}
在源碼編譯階段,注解處理器根據(jù)@Mapper注解會自動生成StudentMapper對應(yīng)的實現(xiàn)類霍狰。
@Component
public class StudentMapperImpl implements StudentMapper {
public StudentMapperImpl() {
}
public List<StudentTO> toTOList(List<StudentDO> doList) {
if (doList == null) {
return null;
} else {
List<StudentTO> list = new ArrayList(doList.size());
Iterator var3 = doList.iterator();
while(var3.hasNext()) {
StudentDO studentDO = (StudentDO)var3.next();
list.add(this.studentDOToStudentTO(studentDO));
}
return list;
}
}
protected StudentTO studentDOToStudentTO(StudentDO studentDO) {
if (studentDO == null) {
return null;
} else {
StudentTO studentTO = new StudentTO();
studentTO.setId(studentDO.getId());
studentTO.setName(studentDO.getName());
studentTO.setAge(studentDO.getAge());
return studentTO;
}
}
}
相較之下抡草,mapstruct每次實現(xiàn)調(diào)用的復(fù)雜度上會高一點,但是從性能上看是最優(yōu)的蔗坯,最接近原生的get/set調(diào)用實現(xiàn)康震。
性能對比
參考上面的案例,按list中元素個數(shù)宾濒,單次拷貝的耗時(單位:ms)橫向?qū)Ρ热缦拢?/p>
方案 | 10個 | 100個 | 10000個 | 1000000個 |
---|---|---|---|---|
Spring BeanUtils(反射) | 650 | 723 | 770 | 950 |
cglib BeanCopier(asm字節(jié)碼技術(shù)) | 48 | 60 | 65 | 300 |
mapstruct(注解處理器) | 3 | 4 | 5 | 40 |
可以看到mapstruct確實是性能最好的腿短。但是另外還發(fā)現(xiàn)基于反射實現(xiàn)的Spring BeanUtils并沒有隨著調(diào)用次數(shù)的增大而大大提升耗時,與預(yù)期不符绘梦。這個其實不難想象是spring做了一層緩存橘忱,對于同一個類會緩存住反射獲取的信息,參考CachedIntrospectionResults中的代碼卸奉。
總結(jié)
從綜合角度來看钝诚,mapstruct是最優(yōu)解,但是日常使用中如果對于性能要求并沒有那么高择卦,其實其他的方案也是可以選擇的敲长,畢竟可以實現(xiàn)更好的封裝復(fù)用。