您好,登錄后才能下訂單哦!
今天小編給大家分享一下Java的對象復制工具類有哪些及怎么使用的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
在介紹這些工具類之前,我們來看下一個好用的屬性復制工具類,需要有哪些特性:
基本屬性復制,這個是基本功能
不同類型的屬性賦值,比如基本類型與其包裝類型等
不同字段名屬性賦值,當然字段名應該盡量保持一致,但是實際業務中,由于不同開發人員,或者筆誤拼錯單詞,這些原因都可能導致會字段名不一致的情況
淺拷貝/深拷貝,淺拷貝會引用同一對象,如果稍微不慎,同時改動對象,就會踩到意想不到的坑
下面我們開始介紹工具類。
首先介紹是第一位應該是 Java 領域屬性復制的最有名的工具類「Apache BeanUtils」,這個工具類想必很多人或多或少用過或則見過。
沒用過也沒關系,我們來展示這個類的用法,用法非常簡單。
首先我們引入依賴,這里使用最新版本:
<dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.9.4</version> </dependency>
此時我們需要完成 DTO 對象轉化到 DO 對象,我們只需要簡單調用BeanUtils#copyProperties
方法就可以完成對象屬性的復制。
StudentDTO studentDTO = new StudentDTO(); studentDTO.setName("小編"); studentDTO.setAge(18); studentDTO.setNo("6666"); List<String> subjects = new ArrayList<>(); subjects.add("math"); subjects.add("english"); studentDTO.setSubjects(subjects); studentDTO.setCourse(new Course("CS-1")); studentDTO.setCreateDate("2020-08-08"); StudentDO studentDO = new StudentDO(); BeanUtils.copyProperties(studentDO, studentDTO);
不過,上面的代碼如果你這么寫,我們會碰到第一個問題,BeanUtils默認不支持 String
轉為 Date 類型。
為了解決這個問題,我們需要自己構造一個 Converter
轉換類,然后使用 ConvertUtils
注冊,使用方法如下:
ConvertUtils.register(new Converter() { @SneakyThrows @Override public <Date> Date convert(Class<Date> type, Object value) { if (value == null) { return null; } if (value instanceof String) { String str = (String) value; return (Date) DateUtils.parseDate(str, "yyyy-MM-dd"); } return null; } }, Date.class);
此時,我們觀察 studentDO
與 studentDTO
對象屬性值:
從上面的我們可以得出BeanUtils一些結論:
普通字段名不一致的屬性無法被復制
嵌套對象字段,將會與源對象使用同一對象,即使用淺拷貝
類型不一致的字段,將會進行默認類型轉化。
雖然 BeanUtils 使用起來很方便,不過其底層源碼為了追求完美,加了過多的包裝,使用了很多反射,做了很多校驗,所以導致性能較差,所以并阿里巴巴開發手冊上強制規定避免使用 Apache BeanUtils。
Spring 屬性復制工具類類名與 Apache
一樣,基本用法也差不多。我先來看下 Spring BeanUtils 基本用法。
同樣,我們先引入依賴,從名字我們可以看出,BeanUtils 位于 Spring-Beans
模塊,這里我們依然使用最新模塊。
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.2.8.RELEASE</version> </dependency>
這里我們使用 DTO 與 DO 復用上面的例子,轉換代碼如下:
// 省略上面賦值代碼,與上面一致 StudentDO studentDO = new StudentDO(); BeanUtils.copyProperties(studentDTO, studentDO);
從用法可以看到,Spring BeanUtils 與 Apache 有一個最大的不同,兩者源對象與目標對象參數位置不一樣,小編之前沒注意,用了 Spring 工具類,但是卻是按照 Apache 的用法使用。
從上面的對比我們可以得到一些結論:
字段名不一致,屬性無法復制
類型不一致,屬性無法復制。但是注意,如果類型為基本類型以及基本類型的包裝類,這種可以轉化
嵌套對象字段,將會與源對象使用同一對象,即使用淺拷貝
除了這個方法之外,Spring BeanUtils 還提供了一個重載方法:
public static void copyProperties(Object source, Object target, String... ignoreProperties)
使用這個方法,我們可以忽略某些不想被復制過去的屬性:
BeanUtils.copyProperties(studentDTO, studentDO,"name");
這樣,name
屬性就不會被復制到 DO 對象中。
雖然 Spring BeanUtils 與 Apache BeanUtils 功能差不多,但是在性能上 Spring BeanUtils 還是完爆 Apache BeanUtils。主要原因還是在于 Spring 并沒有與 Apache 一樣使用反射做了過多校驗,另外 Spring BeanUtils 內部使用了緩存,加快轉換的速度。
所以兩者選擇,還是推薦使用 Spring BeanUtils。
上面兩個是小編日常工作經常使用,而下面的這些都是小編最近才開始接觸的,比如 Cglib BeanCopier。這個使用方法,可能比上面兩個類稍微復雜一點,下面我們來看下具體用法:
首先我們引入 Cglib 依賴:
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
畫外音:如果你工程內還有 Spring-Core 的話,如果查找
BeanCopier
這個類,可以發現兩個不同的包的同名類。
一個屬于 Cglib,另一個屬于 Spring-Core。
其實 Spring-Core 內BeanCopier
實際就是引入了 Cglib 中的類,這么做的目的是為包了保證 Spring 使用長度 Cglib 相關類的穩定性,防止外部 Cglib 依賴不一致,導致 Spring 運行異常。
轉換代碼如下:
// 省略賦值語句 StudentDO studentDO = new StudentDO(); BeanCopier beanCopier = BeanCopier.create(StudentDTO.class, StudentDO.class, false); beanCopier.copy(studentDTO, studentDO, null);
使用方法相比 BeanUtils
, BeanCopier 稍微多了一步。 對比studentDO
與 studentDTO
對象:
從上面可以得到與 Spring Beanutils 基本一致的結論:
字段名不一致,屬性無法復制
類型不一致,屬性無法復制。不過有點不一樣,如果類型為基本類型/基本類型的包裝類型,這兩者無法被拷貝。
嵌套對象字段,將會與源對象使用同一對象,即使用淺拷貝
上面我們使用 Beanutils,遇到這種字段名,類型不一致的這種情況,我們沒有什么好辦法,只能手寫硬編碼。
不過在 BeanCopier 下,我們可以引入轉換器,進行類型轉換。
// 注意最后一個屬性設置為 true BeanCopier beanCopier = BeanCopier.create(StudentDTO.class, StudentDO.class, true); // 自定義轉換器 beanCopier.copy(studentDTO, studentDO, new Converter() { @Override public Object convert(Object source, Class target, Object context) { if (source instanceof Integer) { Integer num = (Integer) source; return num.toString(); } return null; } });
不過吐槽一下這個轉換器,一旦我們自己打開使用轉換器,所有屬性復制都需要我們自己來了。比如上面的例子中,我們只處理當源對象字段類型為 Integer,這種情況,其他都沒處理。我們得到 DO 對象將會只有 name 屬性才能被復制。
Cglib BeanCopier 的原理與上面兩個 Beanutils 原理不太一樣,其主要使用 字節碼技術動態生成一個代理類,代理類實現get 和 set方法。生成代理類過程存在一定開銷,但是一旦生成,我們可以緩存起來重復使用,所有 Cglib 性能相比以上兩種 Beanutils 性能比較好。
Dozer ,中文直譯為挖土機 ,這是一個「重量級」屬性復制工具類,相比于上面介紹三個工具類,Dozer 具有很多強大的功能。
畫外音:重量級/輕量級其實只是一個相對的說法,由于 Dozer 相對 BeanUtils 這類工具類來說,擁有許多高級功能,所以相對來說這是一個重量級工具類。
小編剛碰到這個工具類,就被深深折服,真的太強大了,上面我們期望的功能,Dozer 都給你實現了。
下面我們來看下使用方法,首先我們引入 Dozer 依賴:
<dependency> <groupId>net.sf.dozer</groupId> <artifactId>dozer</artifactId> <version>5.4.0</version> </dependency>
使用方法如下:
// 省略屬性的代碼 DozerBeanMapper mapper = new DozerBeanMapper(); StudentDO studentDO = mapper.map(studentDTO, StudentDO.class); System.out.println(studentDO);
Dozer 需要我們新建一個DozerBeanMapper
,這個類作用等同與 BeanUtils,負責對象之間的映射,屬性復制。
畫外音:下面的代碼我們可以看到,生成
DozerBeanMapper
實例需要加載配置文件,隨意生成代價比較高。在我們應用程序中,應該使用單例模式,重復使用DozerBeanMapper
。
如果屬性都是一些簡單基本類型,那我們只要使用上面代碼,可以快速完成屬性復制。
不過很不幸,我們的代碼中有字符串與 Date 類型轉化,如果我們直接使用上面的代碼,程序運行將會拋出異常。
所以這里我們要用到 Dozer 強大的配置功能,我們總共可以使用下面三種方式:
XML
API
注解
其中,API 的方式比較繁瑣,目前大部分使用 XML 進行,另外注解功能的是在 Dozer 5.3.2 之后增加的新功能,不過功能相較于 XML 來說較弱。
下面我們使用 XML 配置方式,配置 DTO 與 DO 關系,首先我們新建一個dozer/dozer-mapping.xml
文件:
<?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 date-format="yyyy-MM-dd HH:mm:ss"> <class-a>com.just.doone.example.domain.StudentDTO</class-a> <class-b>com.just.doone.example.domain.StudentDO</class-b> <!-- 在下面指定字段名不一致的映射關系 --> <field> <a>no</a> <b>number</b> </field> <field> <!-- 字段級別的日期轉換,將會覆蓋字段上的轉換 --> <a date-format="yy-MM-dd">createDate</a> <b>createDate</b> </field> </mapping> </mappings>
然后修改我們的 Java 代碼,增加讀取 Dozer 的配置文件:
DozerBeanMapper mapper = new DozerBeanMapper(); List<String> mappingFiles = new ArrayList<>(); // 讀取配置文件 mappingFiles.add("dozer/dozer-mapping.xml"); mapper.setMappingFiles(mappingFiles); StudentDO studentDO = mapper.map(studentDTO, StudentDO.class); System.out.println(studentDO);
運行之后,對比studentDO
與 studentDTO
對象:
從上面我們可以發現:
類型不一致的字段,屬性被復制
DO 與 DTO 對象字段不是同一個對象,也就是深拷貝
通過配置字段名的映射關系,不一樣字段的屬性也被復制
除了上述這些相對簡單的屬性以外,Dozer 還支持很多額外的功能,比如枚舉屬性復制,Map 等集合屬性復制等。
有些小伙伴剛看到 Dozer 的用法,可能覺得這個工具類比較繁瑣,不像 BeanUtils 工具類一樣一行代碼就可以解。
其實 Dozer 可以很好跟 Spring 框架整合,我們可以在 Spring 配置文件提前配置,后續我們只要引用 Dozer 的相應的 Bean ,使用方式也是一行代碼。
Dozer 與 Spring 整合,我們可以使用其 DozerBeanMapperFactoryBean
,配置如下:
<bean class="org.dozer.spring.DozerBeanMapperFactoryBean"> <property name="mappingFiles" value="classpath*:/*mapping.xml"/> <!--自定義轉換器--> <property name="customConverters"> <list> <bean class= "org.dozer.converters.CustomConverter"/> </list> </property> </bean>
DozerBeanMapperFactoryBean
支持設置屬性比較多,可以自定義設置類型轉換,還可以設置其他屬性。
另外還有一種簡單的方法,我們可以在 XML 中配置DozerBeanMapper
:
<bean id="org.dozer.Mapper" class="org.dozer.DozerBeanMapper"> <property name="mappingFiles"> <list> <value>dozer/dozer-Mapperpping.xml</value> </list> </property> </bean>
Spring 配置完成之后,我們在代碼中可以直接注入:
@Autowired Mapper mapper; public void objMapping(StudentDTO studentDTO) { // 直接使用 StudentDO studentDO = mapper.map(studentDTO, StudentDO.class); }
Dozer 注解方式相比 XML 配置來說功能很弱,只能完成字段名不一致的映射。
上面的代碼中,我們可以在 DTO 的 no
字段上使用 @Mapping
注解,這樣我們在使用 Dozer 完成轉換時,該字段屬性將會被復制。
@Data public class StudentDTO { private String name; private Integer age; @Mapping("number") private String no; private List<String> subjects; private Course course; private String createDate; }
雖然目前注解功能有點薄弱,不過后看版本官方可能增加新的注解功能,另外 XML 與注解可以一起使用。
最后 Dozer 底層本質上還是使用了反射完成屬性的復制,所以執行速度并不是那么理想。
orika也是一個跟 Dozer 類似的重量級屬性復制工具類,也提供諸如 Dozer 類似的功能。但是 orika 無需使用繁瑣 XML 配置,它自身提供一套非常簡潔的 API 用法,非常容易上手。
首先我們引入其最新的依賴:
<dependency> <groupId>ma.glasnost.orika</groupId> <artifactId>orika-core</artifactId> <version>1.5.4</version> </dependency>
基本使用方法如下:
// 省略其他設值代碼 // 這里先不要設值時間 // studentDTO.setCreateDate("2020-08-08"); MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build(); MapperFacade mapper = mapperFactory.getMapperFacade(); StudentDO studentDO = mapper.map(studentDTO, StudentDO.class);
這里我們引入兩個類 MapperFactory
與 MapperFacade
,其中 MapperFactory
可以用于字段映射,配置轉換器等,而 MapperFacade
的作用就與 Beanutils 一樣,用于負責對象的之間的映射。
上面的代碼中,我們故意注釋了 DTO 對象中的 createDate 時間屬性的設值,這是因為默認情況下如果沒有單獨設置時間類型的轉換器,上面的代碼將會拋錯。
另外,上面的代碼中,對于字段名不一致的屬性,是不會復制的,所以我們需要單獨設置。
下面我們就設置一個時間轉換器,并且指定一下字段名:
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build(); ConverterFactory converterFactory = mapperFactory.getConverterFactory(); converterFactory.registerConverter(new DateToStringConverter("yyyy-MM-dd")); mapperFactory.classMap(StudentDTO.class, StudentDO.class) .field("no", "number") // 一定要調用下 byDefault .byDefault() .register(); MapperFacade mapper = mapperFactory.getMapperFacade(); StudentDO studentDO = mapper.map(studentDTO, StudentDO.class);
上面的代碼中,首先我們需要在 ConverterFactory
注冊一個時間類型的轉換器,其次我們還需要再 MapperFactory
指定不同字段名的之間的映射關系。
這里我們要注意,在我們使用 classMap
之后,如果想要相同字段名屬性默認被復制,那么一定調用 byDefault
方法。
簡單對比一下 DTO 與 DO 對象
可以發現 orika 的一些特性:
默認支持類型不一致(基本類型/包裝類型)轉換
支持深拷貝
指定不同字段名映射關系,屬性可以被成功復制。
另外 orika 還支持集合映射:
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build(); List<Person> persons = new ArrayList<>(); List<PersonDto> personDtos = mapperFactory.getMapperFacade().mapAsList(persons, PersonDto.class);
最后聊下 orika 實現原理,orika 與 dozer 底層原理不太一樣,底層其使用了 javassist 生成字段屬性的映射的字節碼,然后直接動態加載執行字節碼文件,相比于 Dozer 的這種使用反射原來的工具類,速度上會快很多。
不知不覺,一口氣已經寫了 5 個屬性復制工具類,小伙伴都看到這里,那就不要放棄了,堅持看完,下面將介紹一個與上面這些都不太一樣的工具類「MapStruct」。
上面介紹的這些工具類,不管使用反射,還是使用字節碼技術,這些都需要在代碼運行期間動態執行,所以相對于手寫硬編碼這種方式,上面這些工具類執行速度都會慢很多。
那有沒有一個工具類的運行速度與硬編碼這種方式差不多那?
這就要介紹 MapStruct 這個工具類,這個工具類之所以運行速度與硬編碼差不多,這是因為他在編譯期間就生成了 Java Bean 屬性復制的代碼,運行期間就無需使用反射或者字節碼技術,所以確保了高性能。
另外,由于編譯期間就生成了代碼,所以如果有任何問題,編譯期間就可以提前暴露,這對于開發人員來講就可以提前解決問題,而不用等到代碼應用上線了,運行之后才發現錯誤。
下面我們來看下,怎么使用這個工具類,首先我們先引入這個依賴:
<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.3.1.Final</version> </dependency>
其次,由于 MapStruct 需要在編譯器期間生成代碼,所以我們需要 maven-compiler-plugin
插件中配置:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <!-- depending on your project --> <target>1.8</target> <!-- depending on your project --> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.3.1.Final</version> </path> <!-- other annotation processors --> </annotationProcessorPaths> </configuration> </plugin>
接下來我們需要定義映射接口,代碼如下:
@Mapper public interface StudentMapper { StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class); @Mapping(source = "no", target = "number") @Mapping(source = "createDate", target = "createDate", dateFormat = "yyyy-MM-dd") StudentDO dtoToDo(StudentDTO studentDTO); }
我們需要使用 MapStruct 注解 @Mapper
定義一個轉換接口,這樣定義之后,StudentMapper
的功能就與 BeanUtils 等工具類一樣了。
其次,由于我們 DTO 與 DO 對象中存在字段名不一致的情況,所以我們還在在轉換方法上使用 @Mapping
注解指定字段映射。另外我們 createDate
字段類型不一致,這里我們還需要指定時間格式化類型。
上面定義完成之后,我們就可以直接使用 StudentMapper
一行代碼搞定對象轉換。
// 忽略其他代碼 StudentDO studentDO = StudentMapper.INSTANCE.dtoToDo(studentDTO);
如果我們對象使用 Lombok 的話,使用 @Mapping
指定不同字段名,編譯期間可能會拋出如下的錯誤:
這個原因主要是因為 Lombok 也需要編譯期間自動生成代碼,這就可能導致兩者沖突,當 MapStruct 生成代碼時,還不存在 Lombok 生成的代碼。
解決辦法可以在 maven-compiler-plugin
插件配置中加入 Lombok,如下:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <!-- depending on your project --> <target>1.8</target> <!-- depending on your project --> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.3.1.Final</version> </path> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> </path> <!-- other annotation processors --> </annotationProcessorPaths> </configuration> </plugin>
部分類型不一致,可以自動轉換,比如
基本類型與包裝類型
基本類型的包裝類型與 String
深拷貝
上面介紹的例子介紹一些簡單字段映射,如果小伙伴在工作總共還碰到其他的場景,可以先查看一下這個工程,查看一下有沒有結局解決辦法
上面我們已經知道 MapStruct 在編譯期間就生成了代碼,下面我們來看下自動生成代碼:
public class StudentMapperImpl implements StudentMapper { public StudentMapperImpl() { } public StudentDO dtoToDo(StudentDTO studentDTO) { if (studentDTO == null) { return null; } else { StudentDO studentDO = new StudentDO(); studentDO.setNumber(studentDTO.getNo()); try { if (studentDTO.getCreateDate() != null) { studentDO.setCreateDate((new SimpleDateFormat("yyyy-MM-dd")).parse(studentDTO.getCreateDate())); } } catch (ParseException var4) { throw new RuntimeException(var4); } studentDO.setName(studentDTO.getName()); if (studentDTO.getAge() != null) { studentDO.setAge(String.valueOf(studentDTO.getAge())); } List<String> list = studentDTO.getSubjects(); if (list != null) { studentDO.setSubjects(new ArrayList(list)); } studentDO.setCourse(studentDTO.getCourse()); return studentDO; } } }
從生成的代碼來看,里面并沒有什么黑魔法,MapStruct 自動生成了一個實現類 StudentMapperImpl
,里面實現了 dtoToDo
,方法里面調用getter/setter
設值。
從這個可以看出,MapStruct 作用就相當于幫我們手寫getter/setter
設值,所以它的性能會很好。
以上就是“Java的對象復制工具類有哪些及怎么使用”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。