今天我们一起看下,如何使用MapStruct映射对象集合。
一、映射集合
通常来说,使用MapStruct映射集合的方式与映射简单类型相同。
我们需要创建一个简单的接口或抽象类,并声明映射方法。
根据我们的声明,MapStruct将自动生成映射代码。
通常,生成的代码会遍历源集合,将每个元素转换为目标类型,并将它们包含在目标集合中。
让我们看一个简单的例子。
(一)映射List
首先,我们将一个简单的POJO作为映射器的映射源:
public
class
Employee
{
private String firstName;
private String lastName;
}
目标将是一个简单的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会自动为我们生成了从Employee到EmployeeDTO的映射方法。
需要注意,自动生成的映射方法只会匹配字段名一致的情况,如果名字不一致,就不行了。
比如:
public
class
EmployeeFullNameDTO
{
private String fullName;
}
在这种情况下,如果我们只是声明从Employee列表到EmployeeFullNameDTO列表的映射方法,最终是没有任何字段赋值的,MapStruct无法自动为我们生成映射,如果没有配置忽略逻辑(参考MapStruct教程(四)-忽略未映射属性的三种方式),编译时会抛出异常:
[WARNING] /path/to/EmployeeMapper.java:[15,31] Unmapped target property: "fullName". Mapping from Collection element "Employee employee" to "EmployeeFullNameDTO employeeFullNameDTO".
因此,我们需要手动定义Employee和EmployeeFullNameDTO之间的映射。
鉴于这些要点,让我们手动定义它:
@Mapper
public
interface
EmployeeFullNameMapper
{
List<EmployeeFullNameDTO> map(List<Employee> employees);
@Mapping(target = "fullName", expression = "java(employee.getFirstName() + \" \" + employee.getLastName())")
EmployeeFullNameDTO map(Employee employee);
}
生成的代码将使用我们定义的方法将源列表的元素映射到目标列表:
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相同。
例如,假设我们要将一组Employee实例映射到一组EmployeeDTO实例。
和之前一样,我们需要一个映射器:
@Mapper
public
interface
EmployeeMapper
{
Set<EmployeeDTO> map(Set<Employee> employees);
}
然后MapStruct将生成相应的代码:
@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;
}
对于Map也是如此。
假设我们要将一个Map<String, Employee>映射到一个Map<String, EmployeeDTO>。
我们可以按照之前的相同步骤进行操作:
@Mapper
public
interface
EmployeeMapper
{
Map<String, EmployeeDTO> map(Map<String, Employee> idEmployeeMap);
}
MapStruct编译后的结果:
@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在初始化容器的时候,都会指定容器大小,优秀的工具细节方面都是很好的。
二、集合映射策略
有时候,我们还会用到有父子关系的数据类型:一个数据类型(父类型),它有一个包含另一个数据类型(子类型)的集合字段。
对于这种情况,MapStruct提供了一种选择如何将子类型设置或添加到父类型的方法。特别是,@Mapper注解有一个collectionMappingStrategy属性,可以是ACCESSOR_ONLY、SETTER_PREFERRED、ADDER_PREFERRED或TARGET_IMMUTABLE。所有这些值都指的是将子类型设置或添加到父类型的方式。
默认值是ACCESSOR_ONLY,即只能使用访问器来设置子类型的集合。
(一)ACCESSOR_ONLY策略
我们通过例子看看。首先,我们创建一个Company类作为我们的源类型:
public
class
Company
{
private List<Employee> employees;
}
再定义一个DTO作为目标类型:
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?接下看。
如果我们需要将一个Company映射到一个CompanyDTO,定义一个映射器:
@Mapper(uses = EmployeeMapper.class, collectionMappingStrategy
= ACCESSOR_ONLY)
public
interface
CompanyMapper
{
CompanyDTO map(Company company);
}
我们这里复用了EmployeeMapper,并定义了collectionMappingStrategy属性是ACCESSOR_ONLY(默认是ACCESSOR_ONLY,所以也可以忽略)。
现在让我们看看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)来设置EmployeeDTO实例的列表,这是因为我们使用了ACCESSOR_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策略是一个一个的追加,我们在EmployeeMapper中创建单个Employee转换为EmployeeDTO的方法:
EmployeeDTO map(Employee employee);
当然,也可以不用增加单个对象映射的方法,因为MapStruct会自动生成,不过,为了系统边界清晰,还是单独定义比较好。
生成的映射器代码是:
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方法,也会再使用setter方法。
三、目标集合的实现类型
MapStruct支持将集合接口作为映射方法的目标类型。
在这种情况下,生成的代码中使用了一些默认实现。如下是对应关系:
接口类型 | 实现类型 |
Iterable | ArrayList |
Collection | ArrayList |
List | ArrayList |
Set | LinkedHashSet |
SortedSet | TreeSet |
NavigableSet | TreeSet |
Map | LinkedHashMap |
SortedMap | TreeMap |
NavigableMap | TreeMap |
ConcurrentMap | ConcurrentHashMap |
ConcurrentNavigableMap | ConcurrentSkipListMap |