1. 程式人生 > >優雅的物件轉換解決方案-MapStruct使用進階(二)

優雅的物件轉換解決方案-MapStruct使用進階(二)

在前面, 介紹了 MapStruct 及其入門。 本文則是進一步的進階。

MapStruct 生成對應的實現類的時候, 有如下的幾個情景。

1 屬性名稱相同,則進行轉化

在實現類的時候, 如果屬性名稱相同, 則會進行對應的轉化。這個在之前的文章程式碼中已經有所體現。 通過此種方式, 我們可以快速的編寫出轉換的方法。

源物件類

import lombok.Data;

@Data
public class Source {

    private String id;

    private Integer num;
}

目標物件類

import lombok.Data;

@Data
public class Target {

    private String id;

    private Integer num;
}

轉化類


@Mapper
public interface SourceMapper {

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

    Target source2target(Source source);

}

由於 SourceTarget 需要轉化的屬性是完全相同的。因此, 在 Mapper 中, source2target 方法很快就可以編寫出來了。 只需要確定入參和返回值即可。

2 屬性名不相同, 可通過 @Mapping 註解進行指定轉化。

屬性名不相同, 在需要進行互相轉化的時候, 則我們可以通過 @Mapping

註解來進行轉化。

在上面的 Source 類中, 增加一個屬性 totalCount

@Data
public class Source {

    private String id;

    private Integer num;

    private Integer totalCount;
}

而對應的 Target 中, 定義的屬性是 count

@Data
public class Target {

    private String id;

    private Integer num;

    private Integer count;
}

如果方法沒做任何的改變, 那麼,在轉化的時候, 由於屬性名稱不相同, 會導致 count 屬性沒有值。

這時候, 可以通過 @Mappimg 的方式進行對映。

@Mapper
public interface SourceMapper {

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

    @Mapping(source = "totalCount", target = "count")
    Target source2target(Source source);

}

僅僅是在方法上面加了一行。再次允許測試程式。

3 Mapper 中使用自定義的轉換

有時候, 對於某些型別, 無法通過程式碼生成器的形式來進行處理。 那麼, 就需要自定義的方法來進行轉換。 這時候, 我們可以在介面(同一個介面, 後續還有呼叫別的 Mapper 的方法)中定義預設方法(Java8及之後)。

Source 類中增加

private SubSource subSource;

對應的類


import lombok.Data;

@Data
public class SubSource {

    private Integer deleted;

    private String name;
}

相應的, 在 Target

private SubTarget subTarget;

對應的類

import lombok.Data;

@Data
public class SubTarget {
    private Boolean result;

    private String name;
}

然後在 SourceMapper 中新增方法及對映, 對應的方法更改後

@Mapper
public interface SourceMapper {

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

    @Mapping(source = "totalCount", target = "count")
    @Mapping(source = "subSource", target = "subTarget")
    Target source2target(Source source);

    default SubTarget subSource2subTarget(SubSource subSource) {
        if (subSource == null) {
            return null;
        }
        SubTarget subTarget = new SubTarget();
        subTarget.setResult(!subSource.getDeleted().equals(0));
        subTarget.setName(subSource.getName()==null?"":subSource.getName()+subSource.getName());
        return subTarget;
    }
}

進行測試

4 多轉一

我們在實際的業務中少不了將多個物件轉換成一個的場景。 MapStruct 當然也支援多轉一的操作。

AddressPerson 兩個物件。

import lombok.Data;

@Data
public class Address {

    private String street;
    private int zipCode;
    private int houseNo;
    private String description;

}

@Data
public class Person {

    private String firstName;
    private String lastName;
    private int height;
    private String description;

}

而在實際的使用時, 我們需要的是 DeliveryAddress

import lombok.Data;

@Data
public class DeliveryAddress {

    private String firstName;
    private String lastName;
    private int height;
    private String street;
    private int zipCode;
    private int houseNumber;
    private String description;
}

其對應的資訊不僅僅來自一個類, 那麼, 我們也可以通過配置來實現多到一的轉換。

@Mapper
public interface AddressMapper {

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

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

測試

在多對一轉換時, 遵循以下幾個原則

  1. 當多個物件中, 有其中一個為 null, 則會直接返回 null
  2. 如一對一轉換一樣, 屬性通過名字來自動匹配。 因此, 名稱和型別相同的不需要進行特殊處理
  3. 當多個原物件中,有相同名字的屬性時,需要通過 @Mapping 註解來具體的指定, 以免出現歧義(不指定會報錯)。 如上面的 description

屬性也可以直接從傳入的引數來賦值。

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

在上面的例子中, hn 直接賦值給 houseNumber

5 更新 Bean 物件

有時候, 我們不是想返回一個新的 Bean 物件, 而是希望更新傳入物件的一些屬性。這個在實際的時候也會經常使用到。

AddressMapper 類中, 新增如下方法

    /**
     * Person->DeliveryAddress, 缺失地址資訊
     * @param person
     * @return
     */
    DeliveryAddress person2deliveryAddress(Person person);

    /**
     * 更新, 使用 Address 來補全 DeliveryAddress 資訊。 注意註解 @MappingTarget
     * @param address
     * @param deliveryAddress
     */
    void updateDeliveryAddressFromAddress(Address address,
                                          @MappingTarget DeliveryAddress deliveryAddress);

註解 @MappingTarget後面跟的物件會被更新。 以上的程式碼可以通過以下的測試。

@Test
public void updateDeliveryAddressFromAddress() {
    Person person = new Person();
    person.setFirstName("first");
    person.setDescription("perSonDescription");
    person.setHeight(183);
    person.setLastName("homejim");

    DeliveryAddress deliveryAddress = AddressMapper.INSTANCE.person2deliveryAddress(person);
    assertEquals(deliveryAddress.getFirstName(), person.getFirstName());
    assertNull(deliveryAddress.getStreet());

    Address address = new Address();
    address.setDescription("addressDescription");
    address.setHouseNo(29);
    address.setStreet("street");
    address.setZipCode(344);

    AddressMapper.INSTANCE.updateDeliveryAddressFromAddress(address, deliveryAddress);
    assertNotNull(deliveryAddress.getStreet());
}

6 獲取 mapper

6.1 通過 Mapper 工廠獲取

在上面的例子中, 我們都是通過 Mappers.getMapper(xxx.class) 的方式來進行對應 Mapper 的獲取。 此種方法為通過 Mapper 工廠獲取。

如果是此種方法, 約定俗成的是在介面內定義一個介面本身的例項 INSTANCE, 以方便獲取對應的例項。

@Mapper
public interface SourceMapper {

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

    // ......
}

這樣在呼叫的時候, 我們就不需要在重複的去例項化物件了。類似下面

Target target = SourceMapper.INSTANCE.source2target(source);

6.2 使用依賴注入

對於 Web 開發, 依賴注入應該很熟悉。 MapSturct 也支援使用依賴注入, 同時也推薦使用依賴注入。

注入方式
default 預設的方式, 使用 Mappers.getMapper(Class) 來進行獲取 Mapper
cdi Contexts and Dependency Injection. 使用此種方式, 需要使用 @Inject 來進行注入
spring Spring 的方式, 可以通過 @Autowired 來進行注入
jsr330 生成的 Mapper 中, 使用 @javax.inject.Named@Singleton 註解, 通過 @Inject 來注入

6.3 依賴注入策略

可以選擇是通過構造方法或者屬性注入, 預設是屬性注入。

public enum InjectionStrategy {

    /** Annotations are written on the field **/
    FIELD,

    /** Annotations are written on the constructor **/
    CONSTRUCTOR
}

類似如此使用

@Mapper(componentModel = "cdi", uses = EngineMapper.class, injectionStrategy = InjectionStrategy.CONSTRUCTOR)