六種常用Bean拷貝工具一覽
在我們?nèi)粘5墓ぷ髦校?jīng)常需要做對象的拷貝或轉(zhuǎn)化,例如在傳遞參數(shù)時,把入?yún)⒌腄TO轉(zhuǎn)化為PO存入數(shù)據(jù)庫,在返回前端時把PO再轉(zhuǎn)化為VO。如果再分的細一點,可能還會有DO(Domain Object),TO(Transfer Object) ,BO(business object)等對象,隨著業(yè)務(wù)的劃分越來越細,對象的拷貝工作也越來越頻繁,所以本文就來梳理一下常用的對象拷貝工具和它們的差異。
常用的工具大概有以下幾種:
- Apache BeanUtils
- Spring BeanUtils
- cglib BeanCopier
- Hutool BeanUtil
- Mapstruct
- Dozer
準(zhǔn)備工作,創(chuàng)建兩個類PO和DTO:
- @Data
- public class OrderPO {
- Integer id;
- String orderNumber;
- List<String> proId;
- }
- @Data
- public class OrderDTO {
- int id;
- String orderNumber;
- List<String> proId;
- }
01.Apache BeanUtils
引入依賴坐標(biāo):
- <dependency>
- <groupId>commons-beanutils</groupId>
- <artifactId>commons-beanutils</artifactId>
- <version>1.9.3</version>
- </dependency>
進行測試,初始化PO對象,并創(chuàng)建DTO空對象,使用BeanUtils進行:
- @org.junit.Test
- public void test(){
- OrderPO orderPO=new OrderPO();
- orderPO.setId(1);
- orderPO.setOrderNumber("orderNumber");
- ArrayList<String> list = new ArrayList<String>() {{
- add("1");
- add("2");
- }};
- orderPO.setProId(list);
- OrderDTO orderDTO=new OrderDTO();
- BeanUtils.copyProperties(orderDTO,orderPO);
- }
打印兩個對象,具有相同的屬性:
- OrderPO(id=1, orderNumberorderNumber=orderNumber, proId=[1, 2])
- OrderDTO(id=1, orderNumberorderNumber=orderNumber, proId=[1, 2])
可以看出,在Bean中具有相同名稱的屬性分別是基本數(shù)據(jù)類型和包裝類時,比如分別是int和Integer時,可以正常進行拷貝。那么再深究一點,拷貝Bean過程中,使用的是深拷貝還是淺拷貝呢?
兩個List對象使用的是同一個對象,因此在拷貝中,如果存在引用對象,那么使用的是淺拷貝。在完成拷貝后,如果再修改這個對象:
- list.add("3");
- log.info(orderDTO.getProId());
再次打印DTO對象,發(fā)現(xiàn)即使不再次重新拷貝,修改的值也會被添加過去
- OrderDTO(id=1, orderNumberorderNumber=orderNumber, proId=[1, 2, 3])
02.Spring BeanUtils
如果使用的spring項目時不需要單獨引入依賴,單獨使用時需要引入坐標(biāo):
- <dependency>
- <groupId>org.springframework</groupId>
- <artifactId>spring-beans</artifactId>
- <version>5.2.2.RELEASE</version>
- </dependency>
使用方式與apache的BeanUtils方法名相同,但參數(shù)順序相反,第一個參數(shù)是源對象,第二個參數(shù)是目標(biāo)對象:
- BeanUtils.copyProperties(orderPO,orderDTO);
過程省略,這里使用的還是淺拷貝。spring的BeanUtils還提供了額外的方法,這個可變參數(shù)的方法可以忽略某些屬性進行拷貝:
- void copyProperties(Object source, Object target, String... ignoreProperties);
忽略orderNumber屬性進行拷貝:
- BeanUtils.copyProperties(orderPO,orderDTO,"orderNumber");
輸出結(jié)果:
- OrderPO(id=1, orderNumberorderNumber=orderNumber, proId=[1, 2])
- OrderDTO(id=1, orderNumber=null, proId=[1, 2])
此外,在阿里巴巴的開發(fā)手冊中,強制避免使用apache BeanUtils進行拷貝,建議使用Spring BeanUtils或下面要介紹的BeanCopier。主要原因還是在于Spring并沒有與 apache一樣對反射做了過多校驗,另外Spring BeanUtils內(nèi)部使用了緩存,加快轉(zhuǎn)換的速度。此外,由于我們的大多項目已經(jīng)集成了Spring ,如果沒有其他特殊的需求,直接使用它的BeanUtils就能滿足我們的基本需求。
03.cglib BeanCopier
如果工程內(nèi)含有spring-core包的依賴,也不需要額外引入依賴,否則需要引入坐標(biāo):
- <dependency>
- <groupId>cglib</groupId>
- <artifactId>cglib</artifactId>
- <version>3.3.0</version>
- </dependency>
使用示例:
- BeanCopier beanCopier = BeanCopier.create(
- orderPO.getClass(),
- orderDTO.getClass(), false);
- beanCopier.copy(orderPO,orderDTO,null);
測試結(jié)果:
- OrderPO(id=1, orderNumberorderNumber=orderNumber, proId=[1, 2])
- OrderDTO(id=0, orderNumberorderNumber=orderNumber, proId=[1, 2])
在上面的例子中,id字段沒有被正常拷貝,兩個字段不同的是在PO中使用的是包裝類型Integer,但DTO中使用的是基本類型int。因此,使用BeanCopier時,如果存在基本類型和包裝類,是無法被正??截悾臑橄嗤愋秃蟛拍鼙徽?截?。另外,BeanCopier使用的仍然是淺拷貝,驗證過程大家可以自己進行實驗。
04.Hutool BeanUtil
hutool是個人平常使用比較頻繁的一個工具包,對文件、加密解密、轉(zhuǎn)碼、正則、線程、XML等JDK方法進行封裝,并且也可以進行對象的拷貝。在使用前引入坐標(biāo):
- <dependency>
- <groupId>cn.hutool</groupId>
- <artifactId>hutool-all</artifactId>
- <version>5.1.0</version>
- </dependency>
使用方法如下,并且使用的也是淺拷貝方式:
- BeanUtil.copyProperties(orderPO,orderDTO);
和Spring BeanUtils相同,也可以進行屬性的忽略:
- void copyProperties(Object source, Object target, String... ignoreProperties);
除此之外,hutool的BeanUtil還提供了很多其他實用的方法:
個人在使用中感覺Bean和Map的互相轉(zhuǎn)換還是很常用的,有時在使用Map接收參數(shù)時,后期能夠很方便的把Map轉(zhuǎn)換為Bean
05.Mapstruct
Mapstruct的使用和上面幾種方式有些不同,因為上面的幾種方式,spring和apache,hutool使用的都是反射,cglib是基于字節(jié)碼文件的操作,都是在都代碼運行期間動態(tài)執(zhí)行的,但是Mapstruct不同,它在編譯期間就生成了 Bean屬性復(fù)制的代碼,運行期間就無需使用反射或者字節(jié)碼技術(shù),所以具有很高的性能。
使用Mapstruct需要需要引入下面的依賴:
- <dependency>
- <groupId>org.mapstruct</groupId>
- <artifactId>mapstruct-jdk8</artifactId>
- <version>1.3.0.Final</version>
- </dependency>
- <dependency>
- <groupId>org.mapstruct</groupId>
- <artifactId>mapstruct-processor</artifactId>
- <version>1.3.0.Final</version>
- </dependency>
需要額外寫一個接口來實現(xiàn):
- @Mapper
- public interface ConvertMapper {
- OrderDTO po2Dto(OrderPO orderPO);
- }
這里的@Mapper注解不是用于mybatis的注解,而是org.mapstruct.Mapper。使用起來也非常簡單:
- ConvertMapper mapper = Mappers.getMapper(ConvertMapper.class);
- OrderDTO orderDTO=mapper.po2Dto(orderPO);
查看編譯后的target目錄,編譯時將我們定義的ConvertMapper 接口,生成了ConvertMapperImpl實現(xiàn)類,并實現(xiàn)了po2Dto方法??匆幌戮幾g生成的文件:
可以看到方法中為每一個屬性生成了set方法,并且對于引用對象,生成了一個新的對象,使用深拷貝的方式,所以修改之前的引用對象,這里的值也不會改變。并且,這種使用set/get的方式比使用反射的速度更快。
06.Dozer
Dozer是一個Bean到Bean映射器,它以遞歸方式將數(shù)據(jù)從一個對象復(fù)制到另一個對象,并且這些Bean可以具有不同的復(fù)雜類型。使用前引入依賴坐標(biāo):
- <dependency>
- <groupId>net.sf.dozer</groupId>
- <artifactId>dozer</artifactId>
- <version>5.4.0</version>
- </dependency>
調(diào)用方式非常簡單:
- DozerBeanMapper mapper = new DozerBeanMapper();
- OrderDTO orderDTO=mapper.map(orderPO,OrderDTO.class);
查看運行時生成的對象,可以看見使用的深拷貝的方式:
除此之外,還可以配置不同屬性名稱的映射,修改DTO和PO,在PO中添加一個name屬性,在DTO中添加value屬性:
- @Data
- public class OrderPO {
- Integer id;
- String orderNumber;
- List<String> proId;
- String name;
- }
- @Data
- public class OrderDTO {
- int id;
- String orderNumber;
- List<String> proId;
- String value;
- }
新建一個配置文件,在mapping中可以添加字段的映射關(guān)系:
- <?xml version="1.0" encoding="UTF-8"?>
- <mappings xmlns="http://dozer.sourceforge.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://dozer.sourceforge.net
- http://dozer.sourceforge.net/schema/beanmapping.xsd">
- <mapping>
- <class-a>com.cn.entity.OrderPO</class-a>
- <class-b>com.cn.entity.OrderDTO</class-b>
- <field>
- <a>name</a>
- <b>value</b>
- </field>
- </mapping>
- </mappings>
DozerBeanMapper使用上面的配置文件進行配置,再次拷貝對象:
- ...
- orderPO.setName("hydra");
- DozerBeanMapper mapper = new DozerBeanMapper();
- List<String> mappingFiles = new ArrayList<>();
- mappingFiles.add("dozer.xml");
- mapper.setMappingFiles(mappingFiles);
- OrderDTO orderDTO=mapper.map(orderPO,OrderDTO.class);
查看測試結(jié)果,不同名稱的字段也可以進行拷貝了:
- OrderPO(id=1, orderNumberorderNumber=orderNumber, proId=[1, 2], name=hydra)
- OrderDTO(id=1, orderNumberorderNumber=orderNumber, proId=[1, 2], value=hydra)
如果業(yè)務(wù)場景中的Bean具有很多不同的屬性,這么配置起來還是很麻煩的,需要額外手寫很多xml文件。以上就是工作中常被接觸到的幾種對象拷貝工具,在具體的使用中,更多的要結(jié)合拷貝效率等要求,以及工作場景中需要使用的是深拷貝還是淺拷貝等諸多因素。