日常編程中,經常會碰到對象屬性復制的場景,比如 VO、DTO、PO、VO 等之間的轉換,關于什么是VO、DTO、PO、VO 等可以看上篇文章,VO、DTO、PO、VO 等對象具體有哪些方式可以使用呢?
set/get 方式
性能最好的方式,但是當類的屬性數量只有簡單的幾個,通過手寫set/get即可完成,但是屬性有十幾個,甚至幾十個的時候,通過set/get的方式,可能會占用大量的編程時間,關鍵是像這樣的代碼,基本上是機械式的操作。面對這種重復又枯燥的編程工作,可以使用一些通用的對象屬性復制工具,常用的有如下幾種
ApacheBeanUtils
Apache 提供的一個用于 bean 拷貝的工具,早期使用的非常廣泛,使用上也非常簡單,首先項目中導入 apache beanutils 包,如下
<!--Apache BeanUtils-->
<dependency><groupId>commons-beanutils</groupId><artifactId>commons-beanutils</artifactId><version>1.9.4</version>
</dependency>
然后直接使用工具類即可,如下
// 原始對象
UserInfo source = new UserInfo();
// set...// 目標對象
UserInfo target = new UserInfo();
BeanUtils.copyProperties(target, source);
System.out.println(target.toString());
Apache BeanUtils 工具從操作使用上還是非常方便的,不過其底層源碼為了追求完美,加了過多的包裝,使用了很多反射,做了很多校驗,導致屬性復制時性能較差,因此阿里巴巴開發手冊上強制規定避免使用 Apache BeanUtils
SpringBeanUtils
spring 提供的一個 bean 轉換工具,首先項目中導入依賴
<!--spring BeanUtils-->
<dependency><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId><version>4.3.30.RELEASE</version>
</dependency>
在代碼中直接導入org.springframework.beans.BeanUtils工具進行對象屬性復制
/*** 對象屬性拷貝 <br>* 將源對象的屬性拷貝到目標對象** @param source 源對象* @param target 目標對象*/
public static void copyProperties(Object source, Object target) {try {BeanUtils.copyProperties(source, target);} catch (BeansException e) {LOGGER.error("BeanUtil property copy failed :BeansException", e);} catch (Exception e) {LOGGER.error("BeanUtil property copy failed:Exception", e);}
}
初次之外,spring BeanUtils 還提供了重載方法
public static void copyProperties(Object source, Object target, String... ignoreProperties);
如果不想某些屬性復制過去,可以使用如下方式實現
BeanUtils.copyProperties(source, target, "userPwd");
也可以實現 List 集合之間的對象屬性賦值
/*** @param input 輸入集合* @param clzz 輸出集合類型* @param <E> 輸入集合類型* @param <T> 輸出集合類型* @return 返回集合*/
public static <E, T> List<T> convertList2List(List<E> input, Class<T> clzz) {List<T> output = Lists.newArrayList();if (CollectionUtils.isNotEmpty(input)) {for (E source : input) {T target = BeanUtils.instantiate(clzz);BeanUtil.copyProperties(source, target);output.add(target);}}return output;
}@RunWith(PowerMockRunner.class)
public class TestUtil
{@Testpublic void test(){Employee ee1=new Employee("A",33,"abc");Employee ee2=new Employee("B",44,"abcd");User user=new User();BeanUtil.copyProperties(ee1, user);System.out.println(user);List<User> output=new ArrayList<>();List<Employee> source= Arrays.asList(ee1,ee2);output=BeanUtil.convertList2List(source,User.class);for (User str:output) {System.out.println(str);}}
}
雖然Apache BeanUtils和Spring BeanUtils使用起來都很方便,但是兩者性能差異非常大,Spring BeanUtils的對象屬性復制速度比Apache BeanUtils要快很多,主要原因在于 Spring 并沒有像 Apache 一樣使用反射做過多的參數校驗,Spring BeanUtils的實現原理也比較簡答,就是通過Java的Introspector獲取到兩個類的PropertyDescriptor,對比兩個屬性具有相同的名字和類型,如果是,則進行賦值(通過ReadMethod獲取值,通過WriteMethod賦值),否則忽略。為了提高性能Spring對BeanInfo和PropertyDescriptor進行了緩存,源碼如下(4.3.9 版本)
/** * Copy the property values of the given source bean into the given target bean. * <p>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, 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; } //獲取target類的屬性(有緩存) 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()))) { //獲取source類的屬性(有緩存) PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName()); if (sourcePd != null) { Method readMethod = sourcePd.getReadMethod(); if (readMethod != null && //判斷target的setter方法入參和source的getter方法返回類型是否一致 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); } //賦值到target writeMethod.invoke(target, value); } catch (Throwable ex) { throw new FatalBeanException( "Could not copy property '" + targetPd.getName() + "' from source to target", ex); } } } } }
}
還有一個需要注意的地方是,Apache BeanUtils和Spring BeanUtils的類名和方法基本上相同,但是它們的原始對象和目標對象的參數位置是相反的,如果直接從Apache BeanUtils切換到Spring BeanUtils有巨大的風險
雖然使用起來很方便,但是有幾個坑得注意
1、類型不匹配
@Data
public class SourceBean {private Long age;
}@Data
public class TargetBean {private String age;
}public class Test {public static void main(String[] args) {SourceBean source = new SourceBean();source.setAge(25L);TargetBean target = new TargetBean();BeanUtils.copyProperties(source, target);System.out.println(target.getAge()); //拷貝賦值失敗,輸出null}
}
2、是淺拷貝
什么是深拷貝?什么是淺拷貝?
● 淺拷貝是指創建一個新對象,該對象的屬性值與原始對象相同,但對于引用類型的屬性,仍然共享相同的引用。換句話說,淺拷貝只復制對象及其引用,而不復制引用指向的對象本身。
● 深拷貝是指創建一個新對象,該對象的屬性值與原始對象相同,包括引用類型的屬性。深拷貝會遞歸復制引用對象,創建全新的對象,以確保拷貝后的對象與原始對象完全獨立
public class Address {private String city;//getter 和 setter 方法省略
}public class Person {private String name;private Address address;//getter 和 setter 方法省略
}Person sourcePerson = new Person();
sourcePerson.setName("John");
Address address = new Address();
address.setCity("New York");
sourcePerson.setAddress(address);Person targetPerson = new Person();
BeanUtils.copyProperties(sourcePerson, targetPerson);sourcePerson.getAddress().setCity("London");System.out.println(targetPerson.getAddress().getCity()); // 輸出為 "London"
3、屬性名稱不一致
public class SourceBean {private String username;// getter 和 setter 方法省略
}public class TargetBean {private String userName;// getter 和 setter 方法省略
}SourceBean source = new SourceBean();
source.setUsername("男孩");TargetBean target = new TargetBean();
BeanUtils.copyProperties(source, target);System.out.println(target.getUserName()); // 輸出為 null
4、null 覆蓋
Hutool BeanUtil
hutool是平常使用比較頻繁的一個工具包,對文件、加密解密、轉碼、正則、線程、XML等JDK方法進行封裝,并且也可以進行對象的拷貝。在使用前引入坐標:
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.1.0</version>
</dependency>
使用方式
BeanUtil.copyProperties(UserPo,UserDto);
也可以忽略指定的屬性
void copyProperties(Object source, Object target, String... ignoreProperties);
除此之外,hutool的BeanUtil還提供了很多其他實用的方法
Cglib BeanCopier
Cglib BeanCopier 對象屬性復制工具,首先項目中導入 cglib 包
<!--cglib-->
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version>
</dependency>
然后在代碼中直接導入net.sf.cglib.beans.BeanCopier工具進行對象屬性復制,樣例代碼如下:
// 原始對象
UserInfo source = new UserInfo();
// set...// 獲取一個復制工具
BeanCopier beanCopier = BeanCopier.create(UserInfo.class, UserInfo.class, false);// 對象屬性值復制
UserInfo target = new UserInfo();
beanCopier.copy(source, target, null);
System.out.println(target.toString());
如果遇到字段名相同,但是類型不一致的對象復制,可以引入轉換器,進行類型轉換,比如這樣:
UserInfo source = new UserInfo();
// set...// 創建一個復制工具
BeanCopier beanCopier = BeanCopier.create(UserInfo.class, UserInfo.class, true);// 自定義對象屬性值復制
UserInfo target = new UserInfo();
beanCopier.copy(source, target, new Converter() {@Overridepublic Object convert(Object source, Class target, Object context) {if(source instanceof Integer){return String.valueOf(source);}return source;}
});
System.out.println(target.toString());
Cglib BeanCopier 的工作原理與 apache Beanutils 和 spring beanutils 原理不太一樣,其主要使用字節碼技術動態生成一個代理類,通過代理類來實現get/set方法。
雖然生成代理類過程存在一定開銷,但是一旦生成可以重復使用,因此 Cglib 性能相比以上兩種 Beanutils 性能都要好。另外就是,如果工程是基于 Spring 框架開發的,查找 BeanCopier 這個類的時候,可以發現兩個不同的包,一個屬于Cglib,另一個屬于Spring-Core。
其實Spring-Core內置的BeanCopier引入了 Cglib 中的類,這么做的目的是為保證 Spring 中使用 Cglib 相關類的穩定性,防止外部 Cglib 依賴不一致,導致 Spring 運行異常,因此無論你引用那個包,本質都是使用 Cglib
MapStuct
MapStruct官網:MapStruct – Java bean mappings, the easy way!
MapStruct官網示例:https://github.com/mapstruct/mapstruct-examples
MapStruct 也是一款對象屬性復制的工具,但是它跟上面介紹的幾款工具技術實現思路都不一樣,主要區別在于無論是Beanutils還是BeanCopier,都是程序運行期間去執行對象屬性復制操作。而MapStruct是在程序編譯期間,就已經生成好了對象屬性復制相關的邏輯。因此可以想象的到,MapStruct的復制性能要快很多!MapStruct工具的使用參考第二篇文章
總結
幾種方式性能測試對比
以上幾種對象屬性復制的方式
- 如果當前類只有簡單的幾個屬性,建議直接使用set/get,原生編程性能最好
- 如果類屬性很多,可以使用Spring BeanUtils或者Cglib BeanCopier工具,可以省下很多的機械式編程工作
- 如果當前類屬性很多,同時對復制性能有要求,推薦使用MapStruct
最后,以上的對象屬性復制工具都是淺拷貝的實現方式,如果要深拷貝,可以使用對象序列戶和反序列化技術實現!
更多文章:https://codeyb.top/