Apache MapStruct 常用总结

只是简单使用还是比较简单的,但是有一些细节还是要注意的,不然会不知不觉出错

MapStruct 是一个用于生成类型安全的 bean Mapper类。也就是不同类型对象之间的转换器,一般的assembler以及convertor,不用手动写实现避免错误。

https://mapstruct.org/documentation/stable/reference/html/#Preface

要做的就是定义一个映射器接口,该接口声明任何所需的映射方法。在编译期间,MapStruct 将生成此接口的实现。这个实现使用普通的Java 方法调用来映射源对象和目标对象

@Mapping方法:https://mapstruct.org/documentation/stable/api/org/mapstruct/Mapping.html

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
...
<properties>
<org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
</properties>
...
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
...
<build>
<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>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
...

定义Mapper

1
2
3
4
5
6
7
8
9
10
@Mapper
public interface CarMapper {

@Mapping(source = "make", target = "manufacturer")
@Mapping(source = "numberOfSeats", target = "seatCount")
CarDto carToCarDto(Car car);

@Mapping(source = "name", target = "fullName")
PersonDto personToPersonDto(Person person);
}
  • 当属性与其目标实体副本同名时,它将被隐式映射。
    • 名字一样类型不一样的时候,将会自动转换
  • 当目标实体中的属性具有不同名称时,可以通过@Mapping注释指定其名称。

同名属性类型不一致

隐式Implicit类型转换

  • 基本类型和对应的包装类型之间是自动转换的
  • 基本类型和其他的基本类型的包装类型之间是自动转换的:
    • int 和 String之间的类型转换:生成的代码将分别通过调用String#valueOf(int)和透明地执行转换Integer#parseInt(String)。
    • int 和 long 之间,byte和Integer之间,Boolean 和 String 之间
  • enum 和 String 之间
  • 在大的数字类型之间(java.math.BigInteger、java.math.bigdecimal)和Java基元类型(包括它们的包装类)以及String。可以指定java.text.DecimalFormat所理解的格式字符串。
1
2
3
4
5
6
7
8
9
10
11
@Mapper
public interface CarMapper {

@Mapping(source = "power", numberFormat = "#.##E0")
// @Mapping(source = "price", numberFormat = "$#.00")
CarDto carToCarDto(Car car);

@IterableMapping(numberFormat = "$#.00")
List<String> prices(List<Integer> prices);

}
  • 日期类型之间的转换,比如sql的Date等对象和java.time里面的一些LocalDateTime以及java.util.Date之间的各种转换,具体可不可以可以在编译之后查看生成的实现类,没有的话再自定义。

调用已有的类型转换方法

1
2
3
4
5
6
@Mapper
public interface CarMapper {
CarDto carToCarDto(Car car);

PersonDto personToPersonDto(Person person);
}

如果Car里面有Person类型,可以直接调用下面的方法转换

自定义属性转换方法

在某些情况下,可能需要手动实现 MapStruct 无法生成的从一种类型到另一种类型的特定映射。

在使用 Java 8 或更高版本时,您可以直接在映射器接口中实现自定义方法作为默认方法。如果参数和返回类型匹配,生成的代码将调用默认方法。

1
2
3
4
5
6
7
8
9
10
11
@Mapper
public interface CarMapper {

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

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

MapStruct 生成的类实现了方法carToCarDto() 中生成的代码carToCarDto()将personToPersonDto()在映射driver属性时调用手动实现的方法。

对象转换方法有多个参数

MapStruct 还支持具有多个源参数的映射方法。这很有用,例如,为了将多个实体组合成一个数据传输对象。

1
2
3
4
5
6
7
@Mapper
public interface AddressMapper {

@Mapping(source = "person.description", target = "description")
@Mapping(source = "address.houseNo", target = "houseNumber")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}
  • 只出现一次的属性直接按名称映射。
  • 如果多个源对象定义了具有相同名称的属性,则必须使用 @Mapping 注释指定从中检索属性的源参数,如示例中的description属性所示。当这种歧义未解决时,将引发错误。

source的嵌套属性转为target的属性

属性名一样的时候

1
2
3
4
5
6
7
8
@Mapper
public interface CustomerMapper {

@Mapping( target = "name", source = "record.name" )
@Mapping( target = ".", source = "record" )
@Mapping( target = ".", source = "account" )
Customer customerDtoToCustomer(CustomerDto customerDto);
}

生成的代码将直接映射从 CustomerDto.record 到 Customer 的每个属性,无需手动命名它们中的任何一个。Customer.account 也是如此。

当存在冲突时,可以通过明确定义映射来解决这些冲突。例如在上面的例子中。发生在CustomerDto.record 和 中CustomerDto.account都有name。映射@Mapping( target = “name”, source = “record.name” )解决了这个冲突。

1
2
3
4
5
6
7
8
9
@Mapper
public interface FishTankMapper {
@Mapping(target = "fish.kind", source = "fish.type")
@Mapping(target = "fish.name", ignore = true)
@Mapping(target = "ornament", source = "interior.ornament")
@Mapping(target = "material.materialType", source = "material")
@Mapping(target = "quality.report.organisation.name", source = "quality.report.organisationName")
FishTankDto map( FishTank source );
}

更新现有的对象的部分属性

1
2
3
4
@Mapper
public interface CarMapper {
void updateCarFromDto(CarDto carDto, @MappingTarget Car car);
}

将会使用 @MappingTarget 标记的参数 car 来更新 carDto 的属性

调用其他映射器的方法

在接口的@Mapper注解中CarMapper引用了DateMapper这样的类

1
2
3
4
@Mapper(uses=DateMapper.class)
public interface CarMapper {
CarDto carToCarDto(Car car);
}

在为该carToCarDto()方法的实现生成代码时,MapStruct 将查找将Date对象映射到 String 的方法,在DateMapper类中找到它并生成asString()用于映射manufacturingDate属性的调用。

转换Collection

MapStruct 支持来自Java Collection Framework 的各种可迭代类型。生成的代码将包含一个循环,循环遍历源集合,转换每个元素并将其放入目标集合。如果在给定的映射器或它使用的映射器中找到集合元素类型的映射方法,则调用此方法来执行元素转换。或者,如果存在源元素和目标元素类型的隐式转换,则将调用此转换方法。

1
2
3
4
5
6
7
8
9
@Mapper
public interface CarMapper {

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

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

CarDto carToCarDto(Car car);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//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;
}

Car 里面有 passengers 的 List,转换的时候回去找对应类型的转换方法。方法参数是 List。返回值是 List,转换之后使用set方法直接注入。

转换Map

1
2
3
4
5
public interface SourceTargetMapper {

@MapMapping(valueDateFormat = "dd.MM.yyyy")
Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//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;
}

枚举值映射到枚举类型

MapStruct 支持生成将一种 Java 枚举类型映射到另一种类型的方法。

默认情况下,源枚举中的每个常量都映射到目标枚举类型中具有相同名称的常量。如果需要,可以在@ValueMapping注释的帮助下将源枚举中的常量映射到具有另一个名称的常量。源枚举中的几个常量可以映射到目标类型中的同一个常量。

如果源枚举类型的常量在目标类型中没有对应的同名常量,并且也没有通过@ValueMapping 映射到另一个常量,则 MapStruct 将引发错误

1
2
3
4
5
6
7
8
9
10
11
12
@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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 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_;
}
}

如果需要给不同对象的枚举添加前缀或者后缀:

  • suffix - 在源枚举上应用后缀
  • stripSuffix - 从源枚举中去除后缀
  • prefix - 在源枚举上应用前缀
  • stripPrefix - 从源枚举中去除前缀
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public enum CheeseType {

BRIE,
ROQUEFORT
}

public enum CheeseTypeSuffixed {

BRIE_TYPE,
ROQUEFORT_TYPE
}

@Mapper
public interface CheeseMapper {

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

@EnumMapping(nameTransformationStrategy = "suffix", configuration = "_TYPE")
CheeseTypeSuffixed map(CheeseType cheese);

@InheritInverseConfiguration
CheeseType map(CheeseTypeSuffix cheese);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// GENERATED CODE
public class CheeseSuffixMapperImpl implements CheeseSuffixMapper {

@Override
public CheeseTypeSuffixed map(CheeseType cheese) {
if ( cheese == null ) {
return null;
}

CheeseTypeSuffixed cheeseTypeSuffixed;

switch ( cheese ) {
case BRIE: cheeseTypeSuffixed = CheeseTypeSuffixed.BRIE_TYPE;
break;
case ROQUEFORT: cheeseTypeSuffixed = CheeseTypeSuffixed.ROQUEFORT_TYPE;
break;
default: throw new IllegalArgumentException( "Unexpected enum constant: " + cheese );
}

return cheeseTypeSuffixed;
}

@Override
public CheeseType map(CheeseTypeSuffixed cheese) {
if ( cheese == null ) {
return null;
}

CheeseType cheeseType;

switch ( cheese ) {
case BRIE_TYPE: cheeseType = CheeseType.BRIE;
break;
case ROQUEFORT_TYPE: cheeseType = CheeseType.ROQUEFORT;
break;
default: throw new IllegalArgumentException( "Unexpected enum constant: " + cheese );
}

return cheeseType;
}
}

借助工厂创建实例

一般直接使用默认构造函数 new获取实例,或者可以直接使用自定义工厂,@Mapper#uses()注册它们,或直接在您的Mapper 中实现它们。

创建 bean 映射的目标对象时,MapStruct 将查找无参数方法、用@ObjectFactory注释的方法或只有一个@TargetType参数的方法返回所需的目标类型,而不是调用默认构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class DtoFactory {
public CarDto createCarDto() {
return // ... custom factory logic
}

// 可以访问映射源
//@ObjectFactory
//public CarDto createCarDto(Car car) {
//return // ... custom factory logic
//}
}

public class EntityFactory {

public <T extends BaseEntity> T createEntity(@TargetType Class<T> entityClass) {
return // ... custom factory logic
}
}

@Mapper(uses= { DtoFactory.class, EntityFactory.class } )
public interface CarMapper {

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

CarDto carToCarDto(Car car);

Car carDtoToCar(CarDto carDto);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//GENERATED CODE
public class CarMapperImpl implements CarMapper {

private final DtoFactory dtoFactory = new DtoFactory();

private final EntityFactory entityFactory = new EntityFactory();

@Override
public CarDto carToCarDto(Car car) {
if ( car == null ) {
return null;
}

CarDto carDto = dtoFactory.createCarDto();

//map properties...

return carDto;
}

@Override
public Car carDtoToCar(CarDto carDto) {
if ( carDto == null ) {
return null;
}

Car car = entityFactory.createEntity( Car.class );

//map properties...

return car;
}
}
1
2
3
4
5
6
7
8
9
@Mapper(uses = { DtoFactory.class, EntityFactory.class, CarMapper.class } )
public interface OwnerMapper {

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

void updateOwnerDto(Owner owner, @MappingTarget OwnerDto ownerDto);

void updateOwner(OwnerDto ownerDto, @MappingTarget Owner owner);
}

@Mapping中的属性

默认值以及常量

1
2
3
4
5
6
7
8
9
10
11
12
13
@Mapper(uses = StringListMapper.class)
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );

@Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
@Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")
@Mapping(target = "stringConstant", constant = "Constant Value")
@Mapping(target = "integerConstant", constant = "14")
@Mapping(target = "longWrapperConstant", constant = "3001")
@Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")
@Mapping(target = "stringListConstants", constant = "jack-jill-tom")
Target sourceToTarget(Source s);
}

表达式

1
2
3
4
5
6
7
8
9
@Mapper
public interface SourceTargetMapper {

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

@Mapping(target = "timeAndFormat",
expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")
Target sourceToTarget(Source s);
}

表达式中不想写全限定类名,可在@Mapper里面使用imports=导入使用的类:

1
2
3
4
5
6
7
8
9
10
11
imports org.sample.TimeAndFormat;

@Mapper( imports = TimeAndFormat.class )
public interface SourceTargetMapper {

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

@Mapping(target = "timeAndFormat",
expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
Target sourceToTarget(Source s);
}

默认表达式

source 是 null的时候使用

1
2
3
4
5
6
7
8
9
10
imports java.util.UUID;

@Mapper( imports = UUID.class )
public interface SourceTargetMapper {

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

@Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")
Target sourceToTarget(Source s);
}

使用Mapper

直接通过工厂实例

1
CarMapper mapper = Mappers.getMapper( CarMapper.class );
1
2
3
4
5
6
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );

CarDto carToCarDto(Car car);
}
1
2
Car car = ...;
CarDto dto = CarMapper.INSTANCE.carToCarDto( car );

借助spring等依赖注入的框架

1
2
3
4
5
@Mapper(componentModel = "spring")
public interface CarMapper {

CarDto carToCarDto(Car car);
}
1
2
@Resource
private CarMapper mapper;