MapStruct教程-操作三種集合類型和兩個(gè)關(guān)鍵點(diǎn)
今天我們一起看下,如何使用MapStruct映射對(duì)象集合。
一、映射集合
通常來說,使用MapStruct映射集合的方式與映射簡(jiǎn)單類型相同。
我們需要?jiǎng)?chuàng)建一個(gè)簡(jiǎn)單的接口或抽象類,并聲明映射方法。
根據(jù)我們的聲明,MapStruct將自動(dòng)生成映射代碼。
通常,生成的代碼會(huì)遍歷源集合,將每個(gè)元素轉(zhuǎn)換為目標(biāo)類型,并將它們包含在目標(biāo)集合中。
讓我們看一個(gè)簡(jiǎn)單的例子。
(一)映射List
首先,我們將一個(gè)簡(jiǎn)單的POJO作為映射器的映射源:
public
class
Employee
{
private String firstName;
private String lastName;
}
目標(biāo)將是一個(gè)簡(jiǎn)單的DTO:
public
class
EmployeeDTO
{
private String firstName;
private String lastName;
}
接下來,我們定義映射器:
@Mapper
public
interface
EmployeeMapper
{
List<EmployeeDTO> map(List<Employee> employees);
}
最后,讓我們看看MapStruct從EmployeeMapper接口生成的代碼:
public
class
EmployeeMapperImpl
implements
EmployeeMapper
{
@Override
public List<EmployeeDTO> map(List<Employee> employees)
{
if ( employees == null ) {
return
null;
}
List<EmployeeDTO> list = new ArrayList<EmployeeDTO>( employees.size() );
for ( Employee employee : employees ) {
list.add( employeeToEmployeeDTO( employee ) );
}
return list;
}
protected EmployeeDTO employeeToEmployeeDTO(Employee employee)
{
if ( employee == null ) {
return
null;
}
EmployeeDTO employeeDTO = new EmployeeDTO();
employeeDTO.setFirstName( employee.getFirstName() );
employeeDTO.setLastName( employee.getLastName() );
return employeeDTO;
}
}
需要注意的是,MapStruct會(huì)自動(dòng)為我們生成了從Employee到EmployeeDTO的映射方法。
需要注意,自動(dòng)生成的映射方法只會(huì)匹配字段名一致的情況,如果名字不一致,就不行了。
比如:
public
class
EmployeeFullNameDTO
{
private String fullName;
}
在這種情況下,如果我們只是聲明從Employee列表到EmployeeFullNameDTO列表的映射方法,最終是沒有任何字段賦值的,MapStruct無法自動(dòng)為我們生成映射,如果沒有配置忽略邏輯(參考MapStruct教程(四)-忽略未映射屬性的三種方式),編譯時(shí)會(huì)拋出異常:
[WARNING] /path/to/EmployeeMapper.java:[15,31] Unmapped target property: "fullName". Mapping from Collection element "Employee employee" to "EmployeeFullNameDTO employeeFullNameDTO".
因此,我們需要手動(dòng)定義Employee和EmployeeFullNameDTO之間的映射。
鑒于這些要點(diǎn),讓我們手動(dòng)定義它:
@Mapper
public
interface
EmployeeFullNameMapper
{
List<EmployeeFullNameDTO> map(List<Employee> employees);
@Mapping(target = "fullName", expression = "java(employee.getFirstName() + \" \" + employee.getLastName())")
EmployeeFullNameDTO map(Employee employee);
}
生成的代碼將使用我們定義的方法將源列表的元素映射到目標(biāo)列表:
public
class
EmployeeFullNameMapperImpl
implements
EmployeeFullNameMapper
{
@Override
public List<EmployeeFullNameDTO> map(List<Employee> employees)
{
if ( employees == null ) {
return
null;
}
List<EmployeeFullNameDTO> list = new ArrayList<EmployeeFullNameDTO>( employees.size() );
for ( Employee employee : employees ) {
list.add( map( employee ) );
}
return list;
}
@Override
public EmployeeFullNameDTO map(Employee employee)
{
if ( employee == null ) {
return
null;
}
EmployeeFullNameDTO employeeFullNameDTO = new EmployeeFullNameDTO();
employeeFullNameDTO.setFullName( employee.getFirstName() + " " + employee.getLastName() );
return employeeFullNameDTO;
}
}
(二)映射Set和Map
使用MapStruct映射Set的方式與List相同。
例如,假設(shè)我們要將一組Employee實(shí)例映射到一組EmployeeDTO實(shí)例。
和之前一樣,我們需要一個(gè)映射器:
@Mapper
public
interface
EmployeeMapper
{
Set<EmployeeDTO> map(Set<Employee> employees);
}
然后MapStruct將生成相應(yīng)的代碼:
@Override
public Set<EmployeeDTO> map(Set<Employee> employees)
{
if ( employees == null ) {
return
null;
}
Set<EmployeeDTO> set = new LinkedHashSet<EmployeeDTO>( Math.max( (int) ( employees.size() / .75f ) + 1, 16 ) );
for ( Employee employee : employees ) {
set.add( employeeToEmployeeDTO( employee ) );
}
return set;
}
對(duì)于Map也是如此。
假設(shè)我們要將一個(gè)Map<String, Employee>映射到一個(gè)Map<String, EmployeeDTO>。
我們可以按照之前的相同步驟進(jìn)行操作:
@Mapper
public
interface
EmployeeMapper
{
Map<String, EmployeeDTO> map(Map<String, Employee> idEmployeeMap);
}
MapStruct編譯后的結(jié)果:
@Override
public Map<String, EmployeeDTO> map(Map<String, Employee> idEmployeeMap)
{
if ( idEmployeeMap == null ) {
return
null;
}
Map<String, EmployeeDTO> map = new LinkedHashMap<String, EmployeeDTO>( Math.max( (int) ( idEmployeeMap.size() / .75f ) + 1, 16 ) );
for ( java.util.Map.Entry<String, Employee> entry : idEmployeeMap.entrySet() ) {
String key = entry.getKey();
EmployeeDTO value = employeeToEmployeeDTO( entry.getValue() );
map.put( key, value );
}
return map;
}
可以看到,無論是Set還是Map,MapStruct在初始化容器的時(shí)候,都會(huì)指定容器大小,優(yōu)秀的工具細(xì)節(jié)方面都是很好的。
二、集合映射策略
有時(shí)候,我們還會(huì)用到有父子關(guān)系的數(shù)據(jù)類型:一個(gè)數(shù)據(jù)類型(父類型),它有一個(gè)包含另一個(gè)數(shù)據(jù)類型(子類型)的集合字段。
對(duì)于這種情況,MapStruct提供了一種選擇如何將子類型設(shè)置或添加到父類型的方法。特別是,@Mapper注解有一個(gè)collectionMappingStrategy屬性,可以是ACCESSOR_ONLY、SETTER_PREFERRED、ADDER_PREFERRED或TARGET_IMMUTABLE。所有這些值都指的是將子類型設(shè)置或添加到父類型的方式。
默認(rèn)值是ACCESSOR_ONLY,即只能使用訪問器來設(shè)置子類型的集合。
(一)ACCESSOR_ONLY策略
我們通過例子看看。首先,我們創(chuàng)建一個(gè)Company類作為我們的源類型:
public
class
Company
{
private List<Employee> employees;
}
再定義一個(gè)DTO作為目標(biāo)類型:
public
class
CompanyDTO
{
private List<EmployeeDTO> employees;
public List<EmployeeDTO> getEmployees()
{
return employees;
}
public
void
setEmployees(List<EmployeeDTO> employees)
{
this.employees = employees;
}
public
void
addEmployee(EmployeeDTO employeeDTO)
{
if (employees == null) {
employees = new ArrayList<>();
}
employees.add(employeeDTO);
}
}
注意,在這里我們定義了setter和adder兩種方法,adder中做了屬性初始化。
為什么要定義setter和adder?接下看。
如果我們需要將一個(gè)Company映射到一個(gè)CompanyDTO,定義一個(gè)映射器:
@Mapper(uses = EmployeeMapper.class, collectionMappingStrategy
= ACCESSOR_ONLY)
public
interface
CompanyMapper
{
CompanyDTO map(Company company);
}
我們這里復(fù)用了EmployeeMapper,并定義了collectionMappingStrategy屬性是ACCESSOR_ONLY(默認(rèn)是ACCESSOR_ONLY,所以也可以忽略)。
現(xiàn)在讓我們看看MapStruct生成的代碼:
public
class
CompanyMapperImpl
implements
CompanyMapper
{
private
final EmployeeMapper employeeMapper = Mappers.getMapper( EmployeeMapper.class );
@Override
public CompanyDTO map(Company company)
{
if ( company == null ) {
return
null;
}
CompanyDTO companyDTO = new CompanyDTO();
companyDTO.setEmployees( employeeMapper.map( company.getEmployees() ) );
return companyDTO;
}
}
如上,MapStruct使用setter(setEmployees)來設(shè)置EmployeeDTO實(shí)例的列表,這是因?yàn)槲覀兪褂昧薃CCESSOR_ONLY方式操作集合。
而且,MapStruct使用了EmployeeMapper的將List映射到List的方法。
(二)ADDER_PREFERRED策略
如果我們使用ADDER_PREFERRED策略:
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
uses = EmployeeMapper.class)
public
interface
CompanyMapperAdderPreferred
{
CompanyDTO map(Company company);
}
ADDER_PREFERRED策略是一個(gè)一個(gè)的追加,我們?cè)贓mployeeMapper中創(chuàng)建單個(gè)Employee轉(zhuǎn)換為EmployeeDTO的方法:
EmployeeDTO map(Employee employee);
當(dāng)然,也可以不用增加單個(gè)對(duì)象映射的方法,因?yàn)镸apStruct會(huì)自動(dòng)生成,不過,為了系統(tǒng)邊界清晰,還是單獨(dú)定義比較好。
生成的映射器代碼是:
public
class
CompanyMapperAdderPreferredImpl
implements
CompanyMapperAdderPreferred
{
private
final EmployeeMapper employeeMapper = Mappers.getMapper( EmployeeMapper.class );
@Override
public CompanyDTO map(Company company)
{
if ( company == null ) {
return
null;
}
CompanyDTO companyDTO = new CompanyDTO();
if ( company.getEmployees() != null ) {
for ( Employee employee : company.getEmployees() ) {
companyDTO.addEmployee( employeeMapper.map( employee ) );
}
}
return companyDTO;
}
}
如果CompanyDTO中沒有adder方法,也會(huì)再使用setter方法。
三、目標(biāo)集合的實(shí)現(xiàn)類型
MapStruct支持將集合接口作為映射方法的目標(biāo)類型。
在這種情況下,生成的代碼中使用了一些默認(rèn)實(shí)現(xiàn)。如下是對(duì)應(yīng)關(guān)系:
接口類型 | 實(shí)現(xiàn)類型 |
Iterable | ArrayList |
Collection | ArrayList |
List | ArrayList |
Set | LinkedHashSet |
SortedSet | TreeSet |
NavigableSet | TreeSet |
Map | LinkedHashMap |
SortedMap | TreeMap |
NavigableMap | TreeMap |
ConcurrentMap | ConcurrentHashMap |
ConcurrentNavigableMap | ConcurrentSkipListMap |