天天看點

【教程】如何利用MapStruct 解決對象之間轉換問題(一)

原創 楓秀 淘系技術  5月12日

【教程】如何利用MapStruct 解決對象之間轉換問題(一)

在日常開發中,我們會定義多種不同的Javabean,比如DTO(Data Transfer Object:資料傳輸對象),DO(Data Object:資料庫映射對象,與資料庫一一映射),VO(View Object:顯示層對象,通常是 Web 向模闆渲染引擎層傳輸的對象)等等這些對象。在這些對象與對象之間轉換通常是調對象的set和get方法進行複制,這種轉換通常也是很無聊的操作,如果有一個專門的工具來解決Javabean之間的轉換問題,讓我們從這種無聊的轉換操作中解放出來。

MapStruct就是這樣一個屬性映射工具,用于解決上述對象之間轉換問題。MapStruct官網給出的定義:MapStruct是一個Java注釋處理器,用于生成類型安全的bean映射類。

本篇文章主要用于記錄自己整理的MapStruct使用教程。

簡單使用

學習一個新的架構較好的方式是先把demo跑起來,然後一步步的熟悉架構的用法。接下來讓我們跑一個簡單的demo,簡單熟悉MapStruct的使用通常在項目中,mapStruct和lombox會同時使用,具體的maven配置如下。

<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
        <org.projectlombok.version>1.18.12</org.projectlombok.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${org.mapstruct.version}</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${org.projectlombok.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
            <version>4.12</version>
        </dependency>
    </dependencies>

<!-- 配置lombok 和mapStruct注解處理器 -->
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.1</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                        <annotationProcessorPaths>
                            <path>
                                <groupId>org.mapstruct</groupId>
                                <artifactId>mapstruct-processor</artifactId>
                                <version>${org.mapstruct.version}</version>
                            </path>
                            <path>
                                <groupId>org.projectlombok</groupId>
                                <artifactId>lombok</artifactId>
                                <version>${org.projectlombok.version}</version>
                            </path>
                        </annotationProcessorPaths>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>      

java代碼如下:

定義Person實體

@Data
public class Person {
    private String name;
    private String lastName;
}      

定義PersonDTO

@Data
public class PersonDTO {
    private String firstName;
    private String lastName;
}      

使用MapStruct定義Person和PersonDTO之間的轉換接口

@Mapper
public interface PersonMapper {

    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

    @Mapping(source = "firstName",target = "name")
     Person personDTOToPerson(PersonDTO personDTO);
}      

使用上面定義的轉換器,例子如下

public class PersonMapperTest {

    @Test
    public void personDTOToPerson() {
        PersonMapper personMapper = PersonMapper.INSTANCE;
        PersonDTO personDTO = new PersonDTO();
        personDTO.setFirstName("feng");
        personDTO.setLastName("xiu");
        Person person = personMapper.personDTOToPerson(personDTO);
        Assert.assertEquals(person.getLastName(),personDTO.getLastName());
        Assert.assertEquals(person.getName(),personDTO.getFirstName());
    }
}      

從上面的例子可以看出,使用MapStruct定義一個對象轉換器,分為以下幾步

  1. 建立一個對象轉換接口,使用@Mapper注解
  2. 定義轉換方法,設定需要轉換的對象作為參數,傳回值是轉換後的對象
  3. 使用@Mapping注解方法,設定轉換對應的屬性,如果屬性名相同,則不需要設定。
  4. 接口中定義一個屬性,使用Mappers.getMapper方擷取對應的實作,友善使用。

通過上面4步,就可以定義出一個對象轉換器,相比于之前來說簡單很多。

定義Mapper(Bean映射器)

上面已經看了一個簡單的demo,下面我們來具體了解下,如何建立或者說定義一個對象轉換器,也就是定義一個Mapper。

▐  基本的映射

建立一個bean的轉換器,隻需要定義一個接口,并将需要的轉換方法定義在接口中,然後使用

org.mapstruct.Mapper

注釋對其進行注釋。

比如上面的PersonMapper

@Mapper
public interface PersonMapper {

    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

    @Mapping(source = "firstName",target = "name")
     Person personDTOToPerson(PersonDTO personDTO);
}      

@Mapper注解作用是:在build-time時,MapStruct會自動生成一個實作PersonMapper接口的類。

接口中定義的方法,在自動生成時,預設會将source對象(比如PersonDTO)中所有可讀的屬性拷貝到target(比如Person)對象中相關的屬性,轉換規則主要有以下倆條:

  1. 當target和source對象中屬性名相同,則直接轉換
  2. 當target和source對象中屬性名不同,名字的映射可以通過@Mapping注解來指定。比如上面firstName映射到name屬性上。

其實上面PersonMapper通過MapStruct生成的類和我們自己寫一個轉換類是沒有什麼差別,上面PersonMapper自動生成的實作類如下:

public class PersonMapperImpl implements PersonMapper {
    public PersonMapperImpl() {
    }

    public Person personDTOToPerson(PersonDTO personDTO) {
        if (personDTO == null) {
            return null;
        } else {
            Person person = new Person();
            person.setName(personDTO.getFirstName());
            person.setLastName(personDTO.getLastName());
            return person;
        }
    }
}      

從上面可以看出,MapStruct的哲學是盡可能的生成看起來和手寫的代碼一樣。是以,這也說明MapStruct映射對象屬性使用的是getter/setter而不是反射。

正如上面例子這種顯示的,在進行映射的時候,也會考慮通過@Mapping中指定的屬性。如果指定的屬性類型不同,MapStruct可能會通過隐式的類型轉換,這個會在後面講,或者通過調用/建立另外一個映射方法個,這個會在映射對象引用這一節說道。當一個bean的source和target屬性是簡單類型或者是Bean,才會建立一個新的映射方法,比如屬性不能是Collection或者Map類型的屬性。至于集合類型的映射将在後面講。

MapStruct映射target和source的所有公共屬性。這包括在父類型上聲明的屬性。

▐  在Mapper中自定義轉換屬性方法

當倆種類型的映射不能通過MapStruct自動生成,我們需要自定義一些方法。自定義方法的方式主要有以下倆種。

如果其他Mapper中已經有此方法,可以在

@Mapper(uses=XXXMapper.class)

來調用自定義的方法,這樣可以方法重用。這個後面會說。

java8或者更新的版本,可以直接在Mapper接口中添加default方法。當參數和傳回值類型比對,則生成的代碼會自動調用這個方法。

例子如下

@Mapper
public interface CarMapper {

    @Mapping(...)
    ...
    CarDto carToCarDto(Car car);

    default PersonDto personToPersonDto(Person person) {
        //hand-written mapping logic
    }
}      

在MapStruct自動生成代碼,需要将Person轉換成PersonDTO對象時,就會直接調用default方法。

也可以使用抽象類來定義,比如上面的例子使用抽象類定義如下

@Mapper
public abstract class CarMapper {

    @Mapping(...)
    ...
    public abstract CarDto carToCarDto(Car car);

    public PersonDto personToPersonDto(Person person) {
        //hand-written mapping logic
    }
}      

▐  多個source參數的映射方法

MapStruct也支援帶有多個source參數的映射方法。這個在将多個bean合并成一個bean的時候非常有用。

例子如下:

@Mapper
public interface AddressMapper {

    @Mapping(source = "person.description", target = "description")
    @Mapping(source = "address.houseNo", target = "houseNumber")
    DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}      

上面顯示的就是将倆個source參數映射成一個target對象。和單個參數一樣,屬性映射也是通過名稱。如果多個source參數中的屬性具有相同的名稱,必須通過@Mapping指定哪個source裡面的屬性映射到target屬性中。如果存在多個相同的屬性,并且沒有指定,則會報錯。MapStruct也支援直接引用一個source參數映射到target對象中。例子如下

@Mapper
public interface AddressMapper {

    @Mapping(source = "person.description", target = "description")
    @Mapping(source = "hn", target = "houseNumber")
    DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Integer hn);
}      

上面的例子将hn直接映射到target的houseNumber屬性上。

▐  處理内嵌bean屬性映射

@Mapper
 public interface CustomerMapper {

     @Mapping( target = "name", source = "record.name" )
     @Mapping( target = ".", source = "record" )
     @Mapping( target = ".", source = "account" )
     Customer customerDtoToCustomer(CustomerDto customerDto);
 }      
  1. 如果隻是某一個内嵌屬性的映射,可以類似

    @Mapping( target = "name", source = "record.name" )

    這樣寫
  2. 如果是映射多個内嵌屬性到target上,可以用

    .

    代替,表示把對應屬性bean比對的内嵌屬性映射到target上

▐  更新Bean執行個體

有時我們并不一定建立一個新的Bean,可能需要更新某一個執行個體。這種類型的映射我們可以通過在參數上增加一個@MappingTarget注解。例子如下:

@Mapper
public interface CarMapper {

    void updateCarFromDto(CarDto carDto, @MappingTarget Car car);
}      

這個例子會把CarDto中的屬性值更新的Car對象執行個體上。上面的例子我們也可以将void改成Car類型傳回值。

對于Collection或者Map類型,預設會将集合中所有的值清空,然後使用相關source集合中的值來填充,即CollectionMappingStrategy.ACCESSOR_ONLY政策。另外也提供了CollectionMappingStrategy.ADDER_PREFERRED 或者 CollectionMappingStrategy.TARGET_IMMUTABLE。這些政策可以在@Mapper(collectionMappingStrategy=CollectionMappingStrategy.TARGET_IMMUTABLE)來指定。

▐  集合映射

基本的定義方式和普通的bean沒什麼差別,簡單例子如下

@Mapper
public interface CarMapper {

    Set<String> integerSetToStringSet(Set<Integer> integers);

    List<CarDto> carsToCarDtos(List<Car> cars);

    CarDto carToCarDto(Car car);
}      

對應的生成方法如下

//GENERATED CODE
@Override
public Set<String> integerSetToStringSet(Set<Integer> integers) {
    if ( integers == null ) {
        return null;
    }

    Set<String> set = new HashSet<String>();

    for ( Integer integer : integers ) {
        set.add( String.valueOf( integer ) );
    }

    return set;
}

@Override
public List<CarDto> carsToCarDtos(List<Car> cars) {
    if ( cars == null ) {
        return null;
    }

    List<CarDto> list = new ArrayList<CarDto>();

    for ( Car car : cars ) {
        list.add( carToCarDto( car ) );
    }

    return list;
}      

對于Map的映射,還提供了

@MapMapping

注解,用于處理value的轉換

具體的例子如下

public interface SourceTargetMapper {

    @MapMapping(valueDateFormat = "dd.MM.yyyy")
    Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source);
}      

生成的代碼如下

//GENERATED CODE
@Override
public Map<Long, Date> stringStringMapToLongDateMap(Map<String, String> source) {
    if ( source == null ) {
        return null;
    }
    Map<Long, Date> map = new HashMap<Long, Date>();
    for ( Map.Entry<String, String> entry : source.entrySet() ) {
        Long key = Long.parseLong( entry.getKey() );
        Date value;
        try {
            value = new SimpleDateFormat( "dd.MM.yyyy" ).parse( entry.getValue() );
        }
        catch( ParseException e ) {
            throw new RuntimeException( e );
        }
        map.put( key, value );
    }
    return map;
}      
  • 集合映射政策

通過@Mapping#collectionMappingStrategy設定集合的映射政策:CollectionMappingStrategy.ACCESSOR_ONLY:預設、CollectionMappingStrategy.SETTER_PREFERRED、CollectionMappingStrategy.ADDER_PREFERRED、CollectionMappingStrategy.TARGET_IMMUTABLE。

【教程】如何利用MapStruct 解決對象之間轉換問題(一)

政策具體的意義如果沒有看懂,可以參考下這篇文章MapStruct文檔(五)——集合映射

▐  枚舉映射處理

  • 枚舉映射枚舉

直接上例子,友善了解

@Mapper
public interface OrderMapper {

    OrderMapper INSTANCE = Mappers.getMapper( OrderMapper.class );

    @ValueMappings({
        @ValueMapping(source = "EXTRA", target = "SPECIAL"),
        @ValueMapping(source = "STANDARD", target = "DEFAULT"),
        @ValueMapping(source = "NORMAL", target = "DEFAULT")
    })
    ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
}      
// GENERATED CODE
public class OrderMapperImpl implements OrderMapper {

    @Override
    public ExternalOrderType orderTypeToExternalOrderType(OrderType orderType) {
        if ( orderType == null ) {
            return null;
        }
        ExternalOrderType externalOrderType_;
        switch ( orderType ) {
            case EXTRA: externalOrderType_ = ExternalOrderType.SPECIAL;
            break;
            case STANDARD: externalOrderType_ = ExternalOrderType.DEFAULT;
            break;
            case NORMAL: externalOrderType_ = ExternalOrderType.DEFAULT;
            break;
            case RETAIL: externalOrderType_ = ExternalOrderType.RETAIL;
            break;
            case B2B: externalOrderType_ = ExternalOrderType.B2B;
            break;
            default: throw new IllegalArgumentException( "Unexpected enum constant: " + orderType );
        }
        return externalOrderType_;
    }
}      

預設情況下,如果存在不比對的情形,則直接抛出異常。這種預設行為是可以被修改的,主要有以下三種政策

  1. MappingConstants.NULL : 處理null值,
  2. MappingConstants.ANY_REMAINING : 處理所有未被定義或者名字比對不上的
  3. MappingConstants.ANY_UNMAPPED :處理任何違背比對的情形
  • 枚舉與String之間的映射

枚舉到字元串的映射,不支援MappingConstants.ANY_REMAINING

@Mapper
public interface TestMapper {
    @ValueMappings({
            @ValueMapping(source = "able_status", target = "PERFECT"),
            @ValueMapping(source = MappingConstants.NULL, target = "PASS"),
            @ValueMapping(source = "failed_status", target = MappingConstants.NULL),
            @ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "normal"),
    })
    String toEnum(DisableStatus disableStatus);

}

@Component
public class TestMapperImpl implements TestMapper {
    @Override
    public String toEnum(DisableStatus disableStatus) {
        if ( disableStatus == null ) {
            return "PASS";
        }
        String string;
        switch ( disableStatus ) {
            case able_status: string = "PERFECT";
            break;
            case failed_status: string = null;
            break;
            default: string = "normal";
        }
        return string;
    }
}      

字元串到枚舉的映射

@Mapper
public interface TestMapper {

    @ValueMappings({
            @ValueMapping(source = "PERFECT", target = "able_status"),
            @ValueMapping(source = "PASS", target = MappingConstants.NULL),
            @ValueMapping(source = MappingConstants.NULL, target = "failed_status"),
            @ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "normal_status"),
    })
    DisableStatus toEnum(String disableStatus);

}

@Component
public class TestMapperImpl implements TestMapper {

    @Override
    public DisableStatus toEnum(String disableStatus) {
        if ( disableStatus == null ) {
            return DisableStatus.failed_status;
        }

        DisableStatus disableStatus1;

        switch ( disableStatus ) {
            case "PERFECT": disableStatus1 = DisableStatus.able_status;
            break;
            case "PASS": disableStatus1 = null;
            break;
            default: disableStatus1 = DisableStatus.normal_status;
        }

        return disableStatus1;
    }
}

@Mapper
public interface TestMapper {

    @ValueMappings({
            @ValueMapping(source = "PERFECT", target = "able_status"),
            @ValueMapping(source = "PASS", target = MappingConstants.NULL),
            @ValueMapping(source = MappingConstants.NULL, target = "failed_status"),
            @ValueMapping(source = MappingConstants.ANY_REMAINING, target = "normal_status"),
    })
    DisableStatus toEnum(String disableStatus);

}

@Component
public class TestMapperImpl implements TestMapper {

    @Override
    public DisableStatus toEnum(String disableStatus) {
        if ( disableStatus == null ) {
            return DisableStatus.failed_status;
        }

        DisableStatus disableStatus1;

        switch ( disableStatus ) {
            case "PERFECT": disableStatus1 = DisableStatus.able_status;
            break;
            case "PASS": disableStatus1 = null;
            break;
            case "able_status": disableStatus1 = DisableStatus.able_status;
            break;
            case "disable_status": disableStatus1 = DisableStatus.disable_status;
            break;
            case "normal_status": disableStatus1 = DisableStatus.normal_status;
            break;
            case "failed_status": disableStatus1 = DisableStatus.failed_status;
            break;
            case "ok_status": disableStatus1 = DisableStatus.ok_status;
            break;
            case "fine_status": disableStatus1 = DisableStatus.fine_status;
            break;
            default: disableStatus1 = DisableStatus.normal_status;
        }

        return disableStatus1;
    }
}      
  • 自定義名稱轉換

可以通過删除或添加源枚舉字元串的前字尾來映射目标枚舉對象。

public enum LevelEnum {
    able(1, "完美"),
    disable(2, "合格"),
    normal(3, "普通"),
    failed(4, "不及格"),
    ok(5, "還行"),
    fine(6, "可以");

    private Integer code;

    private String desc;

    LevelEnum(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}

public enum DisableStatus {

    able_status(1, "完美"),
    disable_status(2, "合格"),
    normal_status(3, "普通"),
    failed_status(4, "不及格"),
    ok_status(5, "還行"),
    fine_status(6, "可以");

    private Integer code;

    private String desc;

    DisableStatus(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }
}

@Mapper
public interface TestMapper {

    @EnumMapping(nameTransformationStrategy = "stripSuffix", configuration = "_status")
    LevelEnum toEnum(DisableStatus disableStatus);

}

@Component
public class TestMapperImpl implements TestMapper {

    @Override
    public LevelEnum toEnum(DisableStatus disableStatus) {
        if ( disableStatus == null ) {
            return null;
        }

        LevelEnum levelEnum;

        switch ( disableStatus ) {
            case able_status: levelEnum = LevelEnum.able;
            break;
            case disable_status: levelEnum = LevelEnum.disable;
            break;
            case normal_status: levelEnum = LevelEnum.normal;
            break;
            case failed_status: levelEnum = LevelEnum.failed;
            break;
            case ok_status: levelEnum = LevelEnum.ok;
            break;
            case fine_status: levelEnum = LevelEnum.fine;
            break;
            default: throw new IllegalArgumentException( "Unexpected enum constant: " + disableStatus );
        }

        return levelEnum;
    }
}      

@EnumMapping#nameTransformationStrategy支援的參數有:suffix(添加源字尾)、stripSuffix(删除源字尾)、prefix(添加源字首)、stripPrefix(删除源字首)。

檢索映射器

前面已經了解如何自定義對象轉換器,接下來看看如何使用已經定義好的對象轉換器。

▐  非依賴注入的方式

當我們不使用DI架構,Mapper執行個體可以通過

org.mapstruct.factory.Mappers

。隻需要調用getMapper方法,傳遞接口類型的mapper就可以獲得MapStruct自動生成的Mapper像前面的例子,我們可以定義INSTANCE屬性用于調用方法。例如

@Mapper(componentModel = "default")
public interface CarMapper {

    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );

    CarDto carToCarDto(Car car);
}      

通過MapStruct自動生成的mapper是無狀态的和線程安全的,可以同時被若幹個線程通路。

▐  使用依賴注入

如果項目使用了依賴注入架構,比如spring。可以使用依賴注入的方式擷取映射器。定義的方式如下:

@Mapper(componentModel = "spring")
public interface CarMapper {

    CarDto carToCarDto(Car car);
}      

使用的方式和普通的spring bean一樣,

@AutoWired
private CarMapper mapper;      

▐  注入政策

當使用DI注入政策模式時,可以選擇field和constructor倆種注入方式。這個可以被@Mapper或者@MapperConfig注解來指定。

使用constructor注入的例子如下:

@Mapper(componentModel = "spring", uses = EngineMapper.class, injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface CarMapper {
    CarDto carToCarDto(Car car);
}      

生成的映射器将注入uses屬性中定義的所有類。當使用

InjectionStrategy#CONSTRUCTOR

,構造函數将具有适當的注解,而字段則沒有。當使用

InjectionStrategy#FIELD

,注解字段位于field本身。目前,預設的注入政策是field注入。建議使用構造函數注入來簡化測試。

▐  檢索總結

檢索映射器主要有以下幾種,支援的值包括:

  1. default:通過Mapper#getMapper(class)來擷取執行個體
  2. cdi:生成的映射器是一個應用程式範圍的CDI bean,可以通過@Inject進行檢索
  3. spring:生成的映射器是一個單例範圍的spring bean,可以通過@Autowired進行檢索
  4. jsr330:生成的映射器用

    {@code@Named}

    注釋,可以通過@Inject檢索,

這些檢索政策可以通過@Mapper(componentModel="")來指定,也可以在maven的配置參數裡面指定。

總結

通過上面的一些介紹,可以看出我們要做的就是定義一個映射器接口,聲明任何必需的映射方法。在編譯的過程中,MapStruct會生成此接口的實作。該實作使用純java方法調用的源對象和目标對象之間的映射。對比手寫這些映射方法,MapStruct通過自動生成代碼完成繁瑣和手寫容易出錯的代碼邏輯進而節省編碼時間。遵循配置方法上的約定,MapStruct使用合理的預設值,但在配置或實作特殊行為時不加理會。

同時與動态映射架構相比,MapStruct具有以下優點:

  1. 速度快:使用普通的方法代替反射
  2. 編譯時類型安全性 : 隻能映射彼此的對象和屬性,不會将商品實體意外映射到使用者DTO等