性能高功能齊全的java bean映射工具mapstruct

0 前言

業(yè)務(wù)變的越來越龐大復(fù)雜后,整個業(yè)務(wù)也被劃分為很多層級功能,各層級功能各司其職,共同實現(xiàn)業(yè)務(wù)目標(biāo)。代表各層級的數(shù)據(jù)對象如PO、DAO、DTO、VO、BO、QO等在這些層級間傳遞、轉(zhuǎn)換、提取、組合和維護(hù)管理。所以這些數(shù)據(jù)對象之間的映射在業(yè)務(wù)主干數(shù)據(jù)流程中出現(xiàn)的非常頻繁,也出現(xiàn)了非常多的對象映射組件,如apache BeanUtils、spring BeanUtils、CGLIB BeanCopier、Dozer、Orika、ModelMapper、JMapper、Selma等。

mapstruct由于基于注解和注解處理器在編譯期自動生成java代碼文件,編譯后直接調(diào)用java bean的getter()setter()方法,實現(xiàn)bean的字段提取和賦值操作。跟其他bean mapping組件相比,mapstruct無需進(jìn)行反射、運行時生成字節(jié)碼等操作,效果上相當(dāng)于手工編寫代碼,只是mapstruct把代碼編碼過程自動化了。所以mapstruct在性能上也基本相當(dāng)于人工編寫代碼(只細(xì)微低于),但是節(jié)省了工程師大量的時間,也避免了低級錯誤發(fā)生的概率。由于mapstruct在編譯期生成代碼文件,我們甚至可以對生成的文件進(jìn)一步進(jìn)行手工修改,以追求極致預(yù)期,但是我沒看到需要這樣做的場景。同時mapstruct考慮各種應(yīng)用場景,提供了非常豐富的操作。

1 基本功能介紹

1.1 package依賴

要使用mapstruct,需要引入它的package依賴:

    <!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>1.4.2.Final</version>
    </dependency>

同時,由于mapstruct是基于注解處理器在編譯期自動生成代碼的,除了引入mapstruct的基本功能包依賴,還需要配置相應(yīng)的compiler注解處理器。

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>1.18.22</version>
                        </path>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok-mapstruct-binding</artifactId>
                            <version>0.2.0</version>
                        </path>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>1.4.2.Final</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>

由于項目中使用lombok,lombok也是基于注解處理器在編譯期自動生成class文件字節(jié)碼的,所以同時配置了lombok和lombok-mapstruct-binding。

PS:

  1. 據(jù)個人一點經(jīng)驗,如果修改了java bean或mapstruct的mapper文件內(nèi)容,再次編譯前一定要先執(zhí)行mvn clean將之前的package內(nèi)容清除掉,否則會編譯報錯或生成的java文件功能不符合預(yù)期。
  2. java注解處理器的使用有點麻煩,注解處理器功能也是我們編寫的代碼,需要先編譯成字節(jié)碼再處理注解,但是編譯時處理注解又要能找到注解處理器的字節(jié)碼,是一個雞生蛋、蛋生雞的問題。如果項目中有多個注解處理器,它們之間可能會相互影響,比如項目中同時有l(wèi)ombok和mapstruct,一定要配置一個lombok-mapstruct-binding。如果項目中還有其他的使用了注解處理器的組件,需要特別留意這類問題。
  3. 看到網(wǎng)上有人介紹踩的坑,說lombok的注解處理器配置需要寫在mapstruct之前,雖然看不出其中的邏輯,大家如果遇到一些奇怪的問題,也可以參考一下

1.2 一個簡單的例子

先定義一個SrcObj和DstObj類:

package com.javatest.mapstruct;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SrcObj {

    private String name;
    private int age;
    private double x;
    private String y;

}
package com.javatest.mapstruct;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DstObj {
    private String name;
    private Integer age;
    private String x;
    private Double y;

}

然后定義一個mapstruct映射類

package com.javatest.mapstruct;

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface BaseMapper {
    BaseMapper MAPPER = Mappers.getMapper(BaseMapper.class);

    DstObj toDst(SrcObj src);

}

@Mapper注解是mapstruct定義的一個注解,mapstruct在編譯期掃描所有的添加了這個注解的接口(也可以是抽象類),會自動生成一個這個接口的實現(xiàn)類。BaseMapper MAPPER = Mappers.getMapper(BaseMapper.class)則是獲取這個接口實現(xiàn)類實例的靜態(tài)接口,它可以放在任何地方,只是為了代碼管理方便,一般放在mapstruct映射接口里。

可以看到,實現(xiàn)從SrcObj映射到DstObj的功能,我們不需要寫任何代碼,只要定義一個接口就可以了(跟spring data repository類似)。下面我們看下測試代碼和結(jié)果:

package com.javatest.mapstruct;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@Slf4j
@SpringBootApplication
public class MapstructApplication {

    public static void main(String[] args) {
        SpringApplication.run(MapstructApplication.class, args);
    }

    @Bean
    @SuppressWarnings("unchecked")
    public CommandLineRunner runner() {
        return (args) -> {
            SrcObj src = new SrcObj("john", 30, 80.6D, "3.5");
            DstObj dst = BaseMapper.MAPPER.toDst(src);

            log.info("src: {}", new ObjectMapper().writeValueAsString(src));
            log.info("dst: {}", new ObjectMapper().writeValueAsString(dst));
        };

    }
}

編譯執(zhí)行上面代碼后,打印結(jié)果如下:

2021-11-10 16:38:51.224  INFO 99570 --- [           main] c.j.mapstruct.MapstructApplication       : src: {"name":"john","age":30,"x":80.6,"y":"3.5"}
2021-11-10 16:38:51.225  INFO 99570 --- [           main] c.j.mapstruct.MapstructApplication       : dst: {"name":"john","age":30,"x":"80.6","y":3.5}

同時我們可以在項目的編譯打包結(jié)果目錄target/generated-sources/annotations/com/javatest/mapstruct下看到mapstruct自動生成的接口實現(xiàn)文件BaseMapperImpl.java,文件內(nèi)容為:

package com.javatest.mapstruct;

import com.javatest.mapstruct.DstObj.DstObjBuilder;
import javax.annotation.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2021-11-10T16:38:48+0800",
    comments = "version: 1.4.2.Final, compiler: javac, environment: Java 1.8.0_292 (Azul Systems, Inc.)"
)
public class BaseMapperImpl implements BaseMapper {

    @Override
    public DstObj toDst(SrcObj src) {
        if ( src == null ) {
            return null;
        }

        DstObjBuilder dstObj = DstObj.builder();

        dstObj.name( src.getName() );
        dstObj.age( src.getAge() );
        dstObj.x( String.valueOf( src.getX() ) );
        if ( src.getY() != null ) {
            dstObj.y( Double.parseDouble( src.getY() ) );
        }

        return dstObj.build();
    }
}

1.3 基礎(chǔ)功能

從 1.2 節(jié)的例子我可以看到,mapstruct不僅自動實現(xiàn)了SrcObj和DstObj兩個bean之間相同類型相同名字的字段間的拷貝,同時還自動支持基本數(shù)據(jù)類型(int)與它對應(yīng)的裝箱(boxing)類型以及基本數(shù)據(jù)類型與String類型間的自動類型轉(zhuǎn)換。當(dāng)然,非常不建議實際項目在java bean中定義int、double等基本數(shù)據(jù)類型的字段,這里只是做個功能演示。

事實上,mapstruct支持以下情況的自動類型轉(zhuǎn)換

1、基本數(shù)據(jù)類型與它對應(yīng)的裝箱類型之間相互轉(zhuǎn)換。
2、不同基本數(shù)據(jù)類型(及裝箱類型)之間相互轉(zhuǎn)換,如int與long,int與Long。
2、String類型與基本數(shù)據(jù)類型(及裝箱類型)之間相互轉(zhuǎn)換
3、Enum類型與String類型的Enum名字之間相互轉(zhuǎn)換

1.4 字段名字不同

映射方法上添加@Mapping注解,指定源和目的字段名字即可,如下所示:

@Mapper
public interface BaseMapper {

    @Mapping(source = "aliasName", target = "name")
    DstObj toDst(SrcObj src);

}

1.5 ignore指定字段不映射

@Mapper
public interface BaseMapper {

    @Mapping(source = "aliasName", target = "name")
    @Mapping(source = "x", target = "x", ignore = true)
    DstObj toDst(SrcObj src);

}

1.6 多個源對象映射到一個對象

mapstruct支持從多個源對象映射到一個目的對象(組合多個源對象的信息),只需要針對多個源對象中字段名字相同的字段明確指定使用哪個源對象的值即可,其他不沖突的字段跟一對一映射時處理一樣。

@Mapper
public interface BaseMapper {
    
    // 將src1對象中的name字段映射到DstObj,忽略src2中name字段的值
    @Mapping(source = "src1.name", target = "name")
    DstObj toDst(SrcObj src1, Src2Obj src2);
}

1.7 源對象成員字段的字段直接映射到目的字段中

@Mapper
public interface BaseMapper {

    // subObj是SrcObj對象中的一個字段,它有兩個字段名字分別為x和y,分別映射到DstObj的字段x和y
    @Mapping(source = "subObj.x", target = "x")
    @Mapping(source = "subObj.y", target = "y")
    DstObj toDst(SrcObj src);
}

1.8 更新現(xiàn)有目標(biāo)對象的值,而不是創(chuàng)建一個新的實例

@Mapper
public interface BaseMapper {
    
    // 使用 @MappingTarget 注解即可
    void toDst(SrcObj src, @MappingTarget DstObj dst);
}

1.9 集合間的映射

直接支持,什么都不用做

@Mapper
public interface BaseMapper {

    List<DstObj> toDstList(List<SrcObj> src);
    Set<DstObj> toDstSet(Set<SrcObj> src);
    Set<DstObj> toDstSet(List<SrcObj> src);
    Map<String, DstObj> toDstMap(Map<String, SrcObj> src);
}

2 類型轉(zhuǎn)換

2.1 不同Enum類型之間的轉(zhuǎn)換

不同的Enum類型之間轉(zhuǎn)換時,相同的Enum名字值會直接映射,不同名字的值使用@ValueMapping指定映射關(guān)系即可

public enum SrcEnum {

    AA,
    B,
    C,
    D

}
public class SrcObj {

    private SrcEnum type;

}
public enum DstEnum {
    A,
    B,
    C
}
public class DstObj {
    private DstEnum type;
}
@Mapper
public interface BaseMapper {

    @ValueMapping(source = "AA", target = "A")
    // 找不到映射關(guān)系的,全部映射到C
    @ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "C")
    DstObj toDst(SrcObj src);

}

2.2 mapstruct mapper實現(xiàn)字段類型轉(zhuǎn)換

如下所示,SrcObj有一個成員字段SubSrcObj sub,DstObj有一個成員字段SubDstObj sub,需要把SubSrcObj sub內(nèi)容映射到SubDstObj sub

public class SubSrcObj {

    private String name;
}

public class SrcObj {

    private SubSrcObj sub;

}

public class SubDstObj {
    private String name;
}

public class DstObj {
    private SubDstObj sub;
}

這種情況可以通過一下兩種方式之一解決:
1)在同一個mapper接口定義字段類型轉(zhuǎn)換方法

@Mapper
public interface BaseMapper {

    SubDstObj toSubDst(SubSrcObj sub);
    DstObj toDst(SrcObj src);
}

2)在不同mapper接口定義字段轉(zhuǎn)換方法,然后通過uses映入依賴mapper,如下所示

@Mapper
public interface SubMapper {

    SubDstObj toSubDst(SubSrcObj sub);
}

@Mapper(uses = {SubMapper.class})
public interface BaseMapper {

    DstObj toDst(SrcObj src);
}

2.3 expression表達(dá)式轉(zhuǎn)換

使用expression表達(dá)式可以調(diào)用任意java方法來進(jìn)行映射。例如,實現(xiàn)上面一樣的功能,自定義一個映射方法

package com.javatest.mapstruct;

public class SubSrcToDstConverter {

    public static SubDstObj toDst(SubSrcObj src) {
        return new SubDstObj(src.getName());
    }

}

然后通過expression指定映射方法:

@Mapper
public interface BaseMapper {

    @Mapping(target = "sub", expression = "java(com.javatest.mapstruct.SubSrcToDstConverter.toDst(src.getSub()))")
    DstObj toDst(SrcObj src);
}

3 其他特性

3.1 為目標(biāo)對象賦值常量

@Mapper
public interface BaseMapper {
    // 不管src對象中是否有name字段,以及值是什么,目的對象name字段值始終為harry
    @Mapping(target = "name", constant = "harry")
    DstObj toDst(SrcObj src);
}

3.2 默認(rèn)值

@Mapper
public interface BaseMapper {
    // 如果src對象的name字段值為null,則為目的對象name字段值賦值harry
    @Mapping(source = "name", target = "name", defaultValue = "harry")
    DstObj toDst(SrcObj src);
}

3.3 mapstruct mapper為抽象類

前面介紹功能時,舉的例子@Mapper注解都是注解在接口上,其實@Mapper也可以注解抽象類,這樣抽象類可以引入其他對象或?qū)崿F(xiàn)其他功能,例如實現(xiàn)@BeforeMapping@AfterMapping,讓mapper在執(zhí)行映射前后分別執(zhí)行特定的功能。例子代碼如下:

package com.javatest.mapstruct;

import lombok.extern.slf4j.Slf4j;
import org.mapstruct.AfterMapping;
import org.mapstruct.BeforeMapping;
import org.mapstruct.Mapper;

@Slf4j
@Mapper
public abstract class AbstractMapper {

    @BeforeMapping
    public void doBefore() {
        log.info("before mapping");
    }

    @AfterMapping
    public void doAfter() {
        log.info("after mapping");
    }

    abstract DstObj toDst(SrcObj src);

}

3.4 spring環(huán)境下為mapper實例注冊spring bean實例

@Mapper注解中直接提供componentModel名字,即spring bean名字即可

@Mapper(componentModel = "baseMapper")
public interface BaseMapper {

    DstObj toDst(SrcObj src);
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容