在前面, 介紹了 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);
}
由於 Source 和 Target 需要轉化的屬性是完全相同的。因此, 在 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 當然也支持多轉一的操作。
有 Address 和 Person 兩個對象。
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);
}
測試

在多對一轉換時, 遵循以下幾個原則
- 當多個對象中, 有其中一個為 null, 則會直接返回 null
- 如一對一轉換一樣, 屬性通過名字來自動匹配。 因此, 名稱和類型相同的不需要進行特殊處理
- 當多個原對象中,有相同名字的屬性時,需要通過
@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)
