App下載

6種對(duì)象復(fù)制工具類(lèi),該如何選擇呢?

猿友 2020-08-25 12:01:10 瀏覽數(shù) (3501)
反饋

在日常工作中,我們難免會(huì)碰到對(duì)象屬性復(fù)制的時(shí)候,下面就一個(gè)常見(jiàn)的三層 MVC 架構(gòu)舉例。

三層 MVC 架構(gòu)

當(dāng)我們?cè)谏厦娴募軜?gòu)下編程時(shí),我們通常需要經(jīng)歷對(duì)象轉(zhuǎn)化,比如業(yè)務(wù)請(qǐng)求流程經(jīng)歷三層機(jī)構(gòu)后需要把 DTO 轉(zhuǎn)為DO然后在數(shù)據(jù)庫(kù)中保存。

當(dāng)需要從數(shù)據(jù)查詢(xún)數(shù)據(jù)頁(yè)面展示時(shí),查詢(xún)數(shù)據(jù)經(jīng)過(guò)三層架構(gòu)將會(huì)從 DO 轉(zhuǎn)為 DTO,最后再轉(zhuǎn)為 VO,然后在頁(yè)面中展示。

當(dāng)業(yè)務(wù)簡(jiǎn)單的時(shí)候,我們手寫(xiě)代碼,通過(guò) getter/setter復(fù)制對(duì)象屬性,十分簡(jiǎn)單。但是一旦業(yè)務(wù)變的復(fù)雜,對(duì)象屬性變得很多,那么手寫(xiě)代碼就會(huì)成為程序員的噩夢(mèng)。

不但手寫(xiě)十分繁瑣,非常耗時(shí)間,并且還可能容易出錯(cuò)。

小編之前就經(jīng)歷過(guò)一個(gè)項(xiàng)目,一個(gè)對(duì)象中大概有四五十個(gè)字段屬性,那時(shí)候小編還剛?cè)腴T(mén),什么都不太懂,寫(xiě)了半天 getter/setter復(fù)制對(duì)象屬性。

話外音:一個(gè)對(duì)象屬性這么多,顯然是不太合理的,我們?cè)O(shè)計(jì)過(guò)程應(yīng)該將其拆分。

直到后來(lái),小編了解到了對(duì)象屬性復(fù)制工具類(lèi),使用之后,發(fā)現(xiàn)是真香,再也不用手寫(xiě)代碼。再后來(lái),碰到越來(lái)越多工具類(lèi),雖然核心功能都是一樣的,但是還是存在很多差異。新手看到可能會(huì)一臉懵逼,不知道如何選擇。

所以小編今天這篇介紹一下市面上常用的工具類(lèi):

  • Apache BeanUtils
  • Spring BeanUtils
  • Cglib BeanCopier
  • Dozer
  • orika
  • MapStruct

工具類(lèi)特性

在介紹這些工具類(lèi)之前,我們來(lái)看下一個(gè)好用的屬性復(fù)制工具類(lèi),需要有哪些特性:

  • 基本屬性復(fù)制,這個(gè)是基本功能
  • 不同類(lèi)型的屬性賦值,比如基本類(lèi)型與其包裝類(lèi)型等
  • 不同字段名屬性賦值,當(dāng)然字段名應(yīng)該盡量保持一致,但是實(shí)際業(yè)務(wù)中,由于不同開(kāi)發(fā)人員,或者筆誤拼錯(cuò)單詞,這些原因都可能導(dǎo)致會(huì)字段名不一致的情況
  • 淺拷貝/深拷貝,淺拷貝會(huì)引用同一對(duì)象,如果稍微不慎,同時(shí)改動(dòng)對(duì)象,就會(huì)踩到意想不到的坑

下面我們開(kāi)始介紹工具類(lèi)。

(推薦教程:Java教程

Apache BeanUtils

首先介紹是第一位應(yīng)該是 Java 領(lǐng)域?qū)傩詮?fù)制的最有名的工具類(lèi)「Apache BeanUtils」,這個(gè)工具類(lèi)想必很多人或多或少用過(guò)或則見(jiàn)過(guò)。

沒(méi)用過(guò)也沒(méi)關(guān)系,我們來(lái)展示這個(gè)類(lèi)的用法,用法非常簡(jiǎn)單。

首先我們引入依賴(lài),這里使用最新版本:

<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.9.4</version>
</dependency>

假設(shè)我們項(xiàng)目中有如下類(lèi):

6種對(duì)象復(fù)制工具類(lèi)

此時(shí)我們需要完成 DTO 對(duì)象轉(zhuǎn)化到 DO 對(duì)象,我們只需要簡(jiǎn)單調(diào)用BeanUtils#copyProperties 方法就可以完成對(duì)象屬性的復(fù)制。

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);

不過(guò),上面的代碼如果你這么寫(xiě),我們會(huì)碰到第一個(gè)問(wèn)題,BeanUtils默認(rèn)不支持 String轉(zhuǎn)為 Date 類(lèi)型。

BeanUtils 默認(rèn)不支持 String轉(zhuǎn)為 Date 類(lèi)型

為了解決這個(gè)問(wèn)題,我們需要自己構(gòu)造一個(gè) Converter 轉(zhuǎn)換類(lèi),然后使用 ConvertUtils注冊(cè),使用方法如下:

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);

此時(shí),我們觀察 studentDOstudentDTO對(duì)象屬性值:

studentDO與 studentDTO對(duì)象屬性值

從上面的圖我們可以得出BeanUtils一些結(jié)論:

  • 普通字段名不一致的屬性無(wú)法被復(fù)制
  • 嵌套對(duì)象字段,將會(huì)與源對(duì)象使用同一對(duì)象,即使用淺拷貝
  • 類(lèi)型不一致的字段,將會(huì)進(jìn)行默認(rèn)類(lèi)型轉(zhuǎn)化。

雖然 BeanUtils 使用起來(lái)很方便,不過(guò)其底層源碼為了追求完美,加了過(guò)多的包裝,使用了很多反射,做了很多校驗(yàn),所以導(dǎo)致性能較差,所以并阿里巴巴開(kāi)發(fā)手冊(cè)上強(qiáng)制規(guī)定避免使用 Apache BeanUtils。

強(qiáng)制規(guī)定避免使用 Apache BeanUtils

Spring BeanUtils

Spring 屬性復(fù)制工具類(lèi)類(lèi)名與 Apache 一樣,基本用法也差不多。我先來(lái)看下 Spring BeanUtils 基本用法。

同樣,我們先引入依賴(lài),從名字我們可以看出,BeanUtils 位于 Spring-Beans 模塊,這里我們依然使用最新模塊。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>

這里我們使用 DTO 與 DO 復(fù)用上面的例子,轉(zhuǎn)換代碼如下:

// 省略上面賦值代碼,與上面一致
StudentDO studentDO = new StudentDO();


BeanUtils.copyProperties(studentDTO, studentDO);

從用法可以看到,Spring BeanUtils 與 Apache 有一個(gè)最大的不同,兩者源對(duì)象與目標(biāo)對(duì)象參數(shù)位置不一樣,小編之前沒(méi)注意,用了 Spring 工具類(lèi),但是卻是按照 Apache 的用法使用。

此時(shí)對(duì)比studentDOstudentDTO對(duì)象:

對(duì)比studentDO與 studentDTO對(duì)象

從上面的對(duì)比圖我們可以得到一些結(jié)論:

  • 字段名不一致,屬性無(wú)法復(fù)制
  • 類(lèi)型不一致,屬性無(wú)法復(fù)制。但是注意,如果類(lèi)型為基本類(lèi)型以及基本類(lèi)型的包裝類(lèi),這種可以轉(zhuǎn)化
  • 嵌套對(duì)象字段,將會(huì)與源對(duì)象使用同一對(duì)象,即使用淺拷貝

除了這個(gè)方法之外,Spring BeanUtils 還提供了一個(gè)重載方法:

public static void copyProperties(Object source, Object target, String... ignoreProperties) 

使用這個(gè)方法,我們可以忽略某些不想被復(fù)制過(guò)去的屬性:

BeanUtils.copyProperties(studentDTO, studentDO,"name");

這樣,name 屬性就不會(huì)被復(fù)制到 DO 對(duì)象中。

6種對(duì)象復(fù)制工具類(lèi)

雖然 Spring BeanUtils 與 Apache BeanUtils 功能差不多,但是在性能上 Spring BeanUtils 還是完爆 Apache BeanUtils。主要原因還是在于 Spring 并沒(méi)有與 Apache 一樣使用反射做了過(guò)多校驗(yàn),另外 Spring BeanUtils 內(nèi)部使用了緩存,加快轉(zhuǎn)換的速度。

所以?xún)烧哌x擇,還是推薦使用 Spring BeanUtils。

Cglib BeanCopier

上面兩個(gè)是小編日常工作經(jīng)常使用,而下面的這些都是小編最近才開(kāi)始接觸的,比如 Cglib BeanCopier。這個(gè)使用方法,可能比上面兩個(gè)類(lèi)稍微復(fù)雜一點(diǎn),下面我們來(lái)看下具體用法:

首先我們引入 Cglib 依賴(lài):

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

畫(huà)外音:如果你工程內(nèi)還有 Spring-Core 的話,如果查找BeanCopier 這個(gè)類(lèi),可以發(fā)現(xiàn)兩個(gè)不同的包的同名類(lèi)。

一個(gè)屬于 Cglib,另一個(gè)屬于 Spring-Core。

其實(shí) Spring-Core 內(nèi)BeanCopier 實(shí)際就是引入了 Cglib 中的類(lèi),這么做的目的是為包了保證 Spring 使用長(zhǎng)度 Cglib 相關(guān)類(lèi)的穩(wěn)定性,防止外部 Cglib 依賴(lài)不一致,導(dǎo)致 Spring 運(yùn)行異常。

轉(zhuǎn)換代碼如下:

// 省略賦值語(yǔ)句
StudentDO studentDO = new StudentDO();
BeanCopier beanCopier = BeanCopier.create(StudentDTO.class, StudentDO.class, false);
beanCopier.copy(studentDTO, studentDO, null);

使用方法相比 BeanUtils, BeanCopier 稍微多了一步。 對(duì)比studentDOstudentDTO對(duì)象:

對(duì)比studentDO與 studentDTO對(duì)象

從上面可以得到與 Spring Beanutils 基本一致的結(jié)論:

  • 字段名不一致,屬性無(wú)法復(fù)制
  • 類(lèi)型不一致,屬性無(wú)法復(fù)制。不過(guò)有點(diǎn)不一樣,如果類(lèi)型為基本類(lèi)型/基本類(lèi)型的包裝類(lèi)型,這兩者無(wú)法被拷貝。
  • 嵌套對(duì)象字段,將會(huì)與源對(duì)象使用同一對(duì)象,即使用淺拷貝

上面我們使用 Beanutils,遇到這種字段名,類(lèi)型不一致的這種情況,我們沒(méi)有什么好辦法,只能手寫(xiě)硬編碼。

不過(guò)在 BeanCopier 下,我們可以引入轉(zhuǎn)換器,進(jìn)行類(lèi)型轉(zhuǎn)換。

// 注意最后一個(gè)屬性設(shè)置為 true
BeanCopier beanCopier = BeanCopier.create(StudentDTO.class, StudentDO.class, true);
// 自定義轉(zhuǎn)換器
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;
    }
});

不過(guò)吐槽一下這個(gè)轉(zhuǎn)換器,一旦我們自己打開(kāi)使用轉(zhuǎn)換器,所有屬性復(fù)制都需要我們自己來(lái)了。比如上面的例子中,我們只處理當(dāng)源對(duì)象字段類(lèi)型為 Integer,這種情況,其他都沒(méi)處理。我們得到 DO 對(duì)象將會(huì)只有 name 屬性才能被復(fù)制。

6種對(duì)象復(fù)制工具類(lèi)

Cglib BeanCopier 的原理與上面兩個(gè) Beanutils 原理不太一樣,其主要使用 字節(jié)碼技術(shù)動(dòng)態(tài)生成一個(gè)代理類(lèi),代理類(lèi)實(shí)現(xiàn)get 和 set方法。生成代理類(lèi)過(guò)程存在一定開(kāi)銷(xiāo),但是一旦生成,我們可以緩存起來(lái)重復(fù)使用,所有 Cglib 性能相比以上兩種 Beanutils 性能比較好。

Dozer

Dozer ,中文直譯為挖土機(jī) ,這是一個(gè)「重量級(jí)」屬性復(fù)制工具類(lèi),相比于上面介紹三個(gè)工具類(lèi),Dozer 具有很多強(qiáng)大的功能。

畫(huà)外音:重量級(jí)/輕量級(jí)其實(shí)只是一個(gè)相對(duì)的說(shuō)法,由于 Dozer 相對(duì) BeanUtils 這類(lèi)工具類(lèi)來(lái)說(shuō),擁有許多高級(jí)功能,所以相對(duì)來(lái)說(shuō)這是一個(gè)重量級(jí)工具類(lèi)。

小編剛碰到這個(gè)工具類(lèi),就被深深折服,真的太強(qiáng)大了,上面我們期望的功能,Dozer 都給你實(shí)現(xiàn)了。

下面我們來(lái)看下使用方法,首先我們引入 Dozer 依賴(lài):

<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 需要我們新建一個(gè)DozerBeanMapper,這個(gè)類(lèi)作用等同與 BeanUtils,負(fù)責(zé)對(duì)象之間的映射,屬性復(fù)制。

畫(huà)外音:下面的代碼我們可以看到,生成 DozerBeanMapper實(shí)例需要加載配置文件,隨意生成代價(jià)比較高。在我們應(yīng)用程序中,應(yīng)該使用單例模式,重復(fù)使用DozerBeanMapper。

如果屬性都是一些簡(jiǎn)單基本類(lèi)型,那我們只要使用上面代碼,可以快速完成屬性復(fù)制。

不過(guò)很不幸,我們的代碼中有字符串與 Date 類(lèi)型轉(zhuǎn)化,如果我們直接使用上面的代碼,程序運(yùn)行將會(huì)拋出異常。

字符串與 Date 類(lèi)型轉(zhuǎn)化

所以這里我們要用到 Dozer 強(qiáng)大的配置功能,我們總共可以使用下面三種方式:

  • XML
  • API
  • 注解

其中,API 的方式比較繁瑣,目前大部分使用 XML 進(jìn)行,另外注解功能的是在 Dozer 5.3.2 之后增加的新功能,不過(guò)功能相較于 XML 來(lái)說(shuō)較弱。

XML 使用方式

下面我們使用 XML 配置方式,配置 DTO 與 DO 關(guān)系,首先我們新建一個(gè)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">
    <!-- 類(lèi)級(jí)別的日期轉(zhuǎn)換,默認(rèn)使用這個(gè)格式轉(zhuǎn)換    -->
    <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>
        <!-- 在下面指定字段名不一致的映射關(guān)系       -->
        <field>
            <a>no</a>
            <b>number</b>
        </field>


        <field>
            <!-- 字段級(jí)別的日期轉(zhuǎn)換,將會(huì)覆蓋字段上的轉(zhuǎn)換            -->
            <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);

運(yùn)行之后,對(duì)比studentDOstudentDTO對(duì)象:

對(duì)比studentDO與 studentDTO對(duì)象

從上面的圖我們可以發(fā)現(xiàn):

  • 類(lèi)型不一致的字段,屬性被復(fù)制
  • DO 與 DTO 對(duì)象字段不是同一個(gè)對(duì)象,也就是深拷貝
  • 通過(guò)配置字段名的映射關(guān)系,不一樣字段的屬性也被復(fù)制

除了上述這些相對(duì)簡(jiǎn)單的屬性以外,Dozer 還支持很多額外的功能,比如枚舉屬性復(fù)制,Map 等集合屬性復(fù)制等。

額外的功能

有些小伙伴剛看到 Dozer 的用法,可能覺(jué)得這個(gè)工具類(lèi)比較繁瑣,不像 BeanUtils 工具類(lèi)一樣一行代碼就可以解。

其實(shí) Dozer 可以很好跟 Spring 框架整合,我們可以在 Spring 配置文件提前配置,后續(xù)我們只要引用 Dozer 的相應(yīng)的 Bean ,使用方式也是一行代碼。

Dozer 與 Spring 整合,我們可以使用其 DozerBeanMapperFactoryBean,配置如下:

    <bean class="org.dozer.spring.DozerBeanMapperFactoryBean">
        <property name="mappingFiles" 
                  value="classpath*:/*mapping.xml"/>
      <!--自定義轉(zhuǎn)換器-->
        <property name="customConverters">
            <list>
                <bean class=
                      "org.dozer.converters.CustomConverter"/>      
            </list>
        </property>
    </bean>

DozerBeanMapperFactoryBean支持設(shè)置屬性比較多,可以自定義設(shè)置類(lèi)型轉(zhuǎn)換,還可以設(shè)置其他屬性。

另外還有一種簡(jiǎn)單的方法,我們可以在 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 配置完成之后,我們?cè)诖a中可以直接注入:

@Autowired
Mapper mapper;


public void objMapping(StudentDTO studentDTO) {
// 直接使用
StudentDO studentDO =
mapper.map(studentDTO, StudentDO.class);
}

注解方式

Dozer 注解方式相比 XML 配置來(lái)說(shuō)功能很弱,只能完成字段名不一致的映射。

上面的代碼中,我們可以在 DTO 的 no 字段上使用 @Mapping 注解,這樣我們?cè)谑褂?Dozer 完成轉(zhuǎn)換時(shí),該字段屬性將會(huì)被復(fù)制。

@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;
}

雖然目前注解功能有點(diǎn)薄弱,不過(guò)后看版本官方可能增加新的注解功能,另外 XML 與注解可以一起使用。

最后 Dozer 底層本質(zhì)上還是使用了反射完成屬性的復(fù)制,所以執(zhí)行速度并不是那么理想。

orika

orika也是一個(gè)跟 Dozer 類(lèi)似的重量級(jí)屬性復(fù)制工具類(lèi),也提供諸如 Dozer 類(lèi)似的功能。但是 orika 無(wú)需使用繁瑣 XML 配置,它自身提供一套非常簡(jiǎn)潔的 API 用法,非常容易上手。

首先我們引入其最新的依賴(lài):

<dependency>
    <groupId>ma.glasnost.orika</groupId>
    <artifactId>orika-core</artifactId>
    <version>1.5.4</version>
</dependency>

基本使用方法如下:

// 省略其他設(shè)值代碼


// 這里先不要設(shè)值時(shí)間
// studentDTO.setCreateDate("2020-08-08");


MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
MapperFacade mapper = mapperFactory.getMapperFacade();
StudentDO studentDO = mapper.map(studentDTO, StudentDO.class);

這里我們引入兩個(gè)類(lèi) MapperFactoryMapperFacade,其中 MapperFactory 可以用于字段映射,配置轉(zhuǎn)換器等,而 MapperFacade 的作用就與 Beanutils 一樣,用于負(fù)責(zé)對(duì)象的之間的映射。

上面的代碼中,我們故意注釋了 DTO 對(duì)象中的 createDate 時(shí)間屬性的設(shè)值,這是因?yàn)槟J(rèn)情況下如果沒(méi)有單獨(dú)設(shè)置時(shí)間類(lèi)型的轉(zhuǎn)換器,上面的代碼將會(huì)拋錯(cuò)。

另外,上面的代碼中,對(duì)于字段名不一致的屬性,是不會(huì)復(fù)制的,所以我們需要單獨(dú)設(shè)置。

下面我們就設(shè)置一個(gè)時(shí)間轉(zhuǎn)換器,并且指定一下字段名:

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")
      // 一定要調(diào)用下 byDefault
        .byDefault()
        .register();
MapperFacade mapper = mapperFactory.getMapperFacade();
StudentDO studentDO = mapper.map(studentDTO, StudentDO.class);

上面的代碼中,首先我們需要在 ConverterFactory 注冊(cè)一個(gè)時(shí)間類(lèi)型的轉(zhuǎn)換器,其次我們還需要再 MapperFactory 指定不同字段名的之間的映射關(guān)系。

這里我們要注意,在我們使用 classMap 之后,如果想要相同字段名屬性默認(rèn)被復(fù)制,那么一定調(diào)用 byDefault方法。

簡(jiǎn)單對(duì)比一下 DTO 與 DO 對(duì)象:

對(duì)比DTO 與 DO 對(duì)象

上圖可以發(fā)現(xiàn) orika 的一些特性:

  • 默認(rèn)支持類(lèi)型不一致(基本類(lèi)型/包裝類(lèi)型)轉(zhuǎn)換
  • 支持深拷貝
  • 指定不同字段名映射關(guān)系,屬性可以被成功復(fù)制。

另外 orika 還支持集合映射:

MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
List<Person> persons = new ArrayList<>();
List<PersonDto> personDtos = mapperFactory.getMapperFacade().mapAsList(persons, PersonDto.class);

最后聊下 orika 實(shí)現(xiàn)原理,orika 與 dozer 底層原理不太一樣,底層其使用了 javassist 生成字段屬性的映射的字節(jié)碼,然后直接動(dòng)態(tài)加載執(zhí)行字節(jié)碼文件,相比于 Dozer 的這種使用反射原來(lái)的工具類(lèi),速度上會(huì)快很多。

 orika 實(shí)現(xiàn)原理

MapStruct

不知不覺(jué),一口氣已經(jīng)寫(xiě)了 5 個(gè)屬性復(fù)制工具類(lèi),小伙伴都看到這里,那就不要放棄了,堅(jiān)持看完,下面將介紹一個(gè)與上面這些都不太一樣的工具類(lèi)「MapStruct」。

上面介紹的這些工具類(lèi),不管使用反射,還是使用字節(jié)碼技術(shù),這些都需要在代碼運(yùn)行期間動(dòng)態(tài)執(zhí)行,所以相對(duì)于手寫(xiě)硬編碼這種方式,上面這些工具類(lèi)執(zhí)行速度都會(huì)慢很多。

那有沒(méi)有一個(gè)工具類(lèi)的運(yùn)行速度與硬編碼這種方式差不多那?

這就要介紹 MapStruct 這個(gè)工具類(lèi),這個(gè)工具類(lèi)之所以運(yùn)行速度與硬編碼差不多,這是因?yàn)樗诰幾g期間就生成了 Java Bean 屬性復(fù)制的代碼,運(yùn)行期間就無(wú)需使用反射或者字節(jié)碼技術(shù),所以確保了高性能。

另外,由于編譯期間就生成了代碼,所以如果有任何問(wèn)題,編譯期間就可以提前暴露,這對(duì)于開(kāi)發(fā)人員來(lái)講就可以提前解決問(wèn)題,而不用等到代碼應(yīng)用上線了,運(yùn)行之后才發(fā)現(xiàn)錯(cuò)誤。

下面我們來(lái)看下,怎么使用這個(gè)工具類(lèi),首先我們先引入這個(gè)依賴(lài):

<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>

接下來(lái)我們需要定義映射接口,代碼如下:

@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 定義一個(gè)轉(zhuǎn)換接口,這樣定義之后,StudentMapper 的功能就與 BeanUtils 等工具類(lèi)一樣了。

其次,由于我們 DTO 與 DO 對(duì)象中存在字段名不一致的情況,所以我們還在在轉(zhuǎn)換方法上使用 @Mapping 注解指定字段映射。另外我們 createDate 字段類(lèi)型不一致,這里我們還需要指定時(shí)間格式化類(lèi)型。

上面定義完成之后,我們就可以直接使用 StudentMapper 一行代碼搞定對(duì)象轉(zhuǎn)換。

// 忽略其他代碼
StudentDO studentDO = StudentMapper.INSTANCE.dtoToDo(studentDTO);

如果我們對(duì)象使用 Lombok 的話,使用 @Mapping指定不同字段名,編譯期間可能會(huì)拋出如下的錯(cuò)誤:

拋出如下的錯(cuò)誤

這個(gè)原因主要是因?yàn)?Lombok 也需要編譯期間自動(dòng)生成代碼,這就可能導(dǎo)致兩者沖突,當(dāng) MapStruct 生成代碼時(shí),還不存在 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>

輸出 DO 與 DTO 如下:

輸出 DO 與 DTO

從上圖中我們可以得到一些結(jié)論:

  • 部分類(lèi)型不一致,可以自動(dòng)轉(zhuǎn)換,比如

    • 基本類(lèi)型與包裝類(lèi)型
    • 基本類(lèi)型的包裝類(lèi)型與 String

  • 深拷貝

上面介紹的例子介紹一些簡(jiǎn)單字段映射,如果小伙伴在工作總共還碰到其他的場(chǎng)景,可以先查看一下這個(gè)工程,查看一下有沒(méi)有結(jié)局解決辦法:github.com/mapstruct/mapstruct-examples

上面我們已經(jīng)知道 MapStruct 在編譯期間就生成了代碼,下面我們來(lái)看下自動(dòng)生成代碼:

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;
        }
    }
}

從生成的代碼來(lái)看,里面并沒(méi)有什么黑魔法,MapStruct 自動(dòng)生成了一個(gè)實(shí)現(xiàn)類(lèi) StudentMapperImpl,里面實(shí)現(xiàn)了 dtoToDo,方法里面調(diào)用getter/setter設(shè)值。

從這個(gè)可以看出,MapStruct 作用就相當(dāng)于幫我們手寫(xiě)getter/setter設(shè)值,所以它的性能會(huì)很好。

(推薦微課:Java微課

總結(jié)

看文這篇文章,我們一共學(xué)習(xí)了 7 個(gè)屬性復(fù)制工具類(lèi),這么多工具類(lèi)我們?cè)撊绾芜x擇那?小編講講自己的一些見(jiàn)解:

第一,首先我們直接拋棄 Apache Beanutils ,這個(gè)不用說(shuō)了,阿里巴巴規(guī)范都這樣定了,我們就不要使用好了。

第二,當(dāng)然是看工具類(lèi)的性能,這些工具類(lèi)的性能,網(wǎng)上文章介紹的比較多,小編就復(fù)制過(guò)來(lái),大家可以比較一下。

性能比較

性能比較

可以看到 MapStruct 的性能可以說(shuō)還是相當(dāng)優(yōu)秀。那么如果你的業(yè)務(wù)對(duì)于性能,響應(yīng)等要求比較高,或者你的業(yè)務(wù)存在大數(shù)據(jù)量導(dǎo)入/導(dǎo)出的場(chǎng)景,而這個(gè)代碼存在對(duì)象轉(zhuǎn)化,那就切勿使用 Apache Beanutils, Dozer 這兩個(gè)工具類(lèi)。

第三,其實(shí)很大一部分應(yīng)用是沒(méi)有很高的性能的要求,只要工具類(lèi)能提供足夠的便利,就可以接受。如果你的業(yè)務(wù)中沒(méi)有很復(fù)雜的的需求,那么直接使用 Spring Beanutils 就好了,畢竟 Spring 的包大部分應(yīng)用都在使用,我們都無(wú)需導(dǎo)入其他包了。

那么如果業(yè)務(wù)存在不同類(lèi)型,不同的字段名,那么可以考慮使用 orika 等這種重量級(jí)工具類(lèi)。

文章來(lái)源:公眾號(hào)--Java極客技術(shù) 作者:鴨血粉絲

以上就是W3Cschool編程獅關(guān)于 6種對(duì)象復(fù)制工具類(lèi),該如何選擇呢?的相關(guān)介紹了,希望對(duì)大家有所幫助。

0 人點(diǎn)贊