MapStruct 是一个开源的基于 Java 注释处理器 (Annotation Processing Tool (apt)) 的代码生成器,用于创建实现 Java Bean 之间转换的扩展映射器。使用 MapStruct,只需要创建接口,而该库会通过注解在编译过程中自动创建具体的映射实现,大大减少了通常需要手工编写的样板代码的数量。
数据传输对象(Data Transfer Objects, DTO) 隐藏不需要的属性。微服务间需要大量的实体映射。
- 安全性高。因为是编译期就实现源对象到目标对象的映射, 如果编译器能够通过,运行期就不会报错。
- 速度快。速度快指的是运行期间直接调用实现类的方法,不会在运行期间使用反射进行转化。
原理解析
mapstruct 是基于 JSR 269 实现的,JSR 269 是 JDK 引进的一种规范。有了它,能够实现在编译期处理注解,并且读取、修改和添加抽象语法树中的内容。JSR 269 使用 Annotation Processor 在编译期间处理注解,Annotation Processor 相当于编译器的一种插件,因此又称为插入式注解处理。想要实现 JSR 269,主要有以下几个步骤:
- 继承
AbstractProcessor
类,并且重写 process
方法,在 process
方法中实现自己的注解处理逻辑。
- 在META-INF/services目录下创建
javax.annotation.processing.Processor
文件注册自己实现的 Annotation Processor
Java程序编译流程:
- 生成抽象语法树。Java编译器对Java源码进行编译,生成抽象语法树(Abstract Syntax Tree,AST)。
- 调用实现了JSR 269 API的程序。只要程序实现了JSR 269 API,就会在编译期间调用实现的注解处理器。
- 修改抽象语法树。在实现JSR 269 API的程序中,可以修改抽象语法树,插入自己的实现逻辑。
- 生成字节码。修改完抽象语法树后,Java编译器会生成修改后的抽象语法树对应的字节码文件件。
调试分析
根据 JSR 269 可以知道 MappingProcessor 就是 mapstruct 的入口。com.sun.tools.javac.main.JavaCompiler#compile
方法的有段代码在编译的时候会调用到 MappingProcessor。
- cd 到项目根目录 执行
mvnDebug compile
命令后,终端会提示已经监听了8000端口
- 在 IDEA 中创建添加
Remote JVM Debug
,localhost 端口号是8000
- 在
JavaCompiler#compile
方法打断点,及 AbstractProcessor#init
和 MappingProcessor#init
打断点。然后点击 debug 按钮。
MapStruct
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
|
<properties>
<org.mapstruct.version>1.5.3.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> <!-- depending on your project -->
<target>1.8</target> <!-- depending on your project -->
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<!-- other annotation processors -->
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
|
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
|
public class Car {
private String make;
private int numberOfSeats;
private CarType type;
//constructor, getters, setters etc.
}
public class CarDto {
private String make;
private int seatCount;
private String type;
//constructor, getters, setters etc.
}
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
@Mapping(source = "numberOfSeats", target = "seatCount")
CarDto carToCarDto(Car car);
}
@Test
public void shouldMapCarToDto() {
//given
Car car = new Car( "Morris", 5, CarType.SEDAN );
//when
CarDto carDto = CarMapper.INSTANCE.carToCarDto( car );
//then
assertThat( carDto ).isNotNull();
assertThat( carDto.getMake() ).isEqualTo( "Morris" );
assertThat( carDto.getSeatCount() ).isEqualTo( 5 );
assertThat( carDto.getType() ).isEqualTo( "SEDAN" );
}
|
定义 mapper
1
2
3
4
5
6
7
8
9
10
|
@Mapper
public interface CarMapper {
@Mapping(target = "manufacturer", source = "make")
@Mapping(target = "seatCount", source = "numberOfSeats")
CarDto carToCarDto(Car car);
@Mapping(target = "fullName", source = "name")
PersonDto personToPersonDto(Person person);
}
|
Adding custom methods to mappers
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
// Basic mappings
@Mapper
public interface CarMapper {
@Mapping(...)
...
CarDto carToCarDto(Car car);
default PersonDto personToPersonDto(Person person) {
//hand-written mapping logic
}
}
// Mapping methods with several source parameters
@Mapper
public interface AddressMapper {
@Mapping(target = "description", source = "person.description")
@Mapping(target = "houseNumber", source = "address.houseNo")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}
// Mappings with direct field access
public class Customer {
private Long id;
private String name;
//getters and setter omitted for brevity
}
public class CustomerDto {
public Long id;
public String customerName;
}
@Mapper
public interface CustomerMapper {
CustomerMapper INSTANCE = Mappers.getMapper( CustomerMapper.class );
@Mapping(target = "name", source = "customerName")
Customer toCustomer(CustomerDto customerDto);
@InheritInverseConfiguration
CustomerDto fromCustomer(Customer customer);
}
// Mapping Map to Bean
public class Customer {
private Long id;
private String name;
//getters and setter omitted for brevity
}
@Mapper
public interface CustomerMapper {
@Mapping(target = "name", source = "customerName")
Customer toCustomer(Map<String, String> map);
}
|
取回 mapper
1
2
3
4
5
6
7
8
9
10
|
CarMapper mapper = Mappers.getMapper( CarMapper.class );
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
CarDto carToCarDto(Car car);
}
|
Using dependency injection
1
2
3
4
5
6
7
8
|
@Mapper(componentModel = MappingConstants.ComponentModel.CDI)
public interface CarMapper {
CarDto carToCarDto(Car car);
}
@Inject
private CarMapper mapper;
|
1
2
3
4
5
6
7
8
9
10
11
12
|
@Mapper(componentModel = "spring")
public interface SimpleSourceDestinationMapper
@Mapper(componentModel = "spring")
public abstract class SimpleDestinationMapperUsingInjectedService {
@Autowired
protected SimpleService simpleService;
@Mapping(target = "name", expression = "java(simpleService.enrichName(source.getName()))")
public abstract SimpleDestination sourceToDestination(SimpleSource source);
}
|
Data type conversions
Controlling nested bean mappings
1
2
3
4
5
6
7
8
9
10
|
@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 );
}
|
Combining qualifiers with defaults
1
2
3
4
5
6
7
8
9
10
11
|
@Mapper
public interface MovieMapper {
@Mapping( target = "category", qualifiedByName = "CategoryToString", defaultValue = "DEFAULT" )
GermanRelease toGerman( OriginalRelease movies );
@Named("CategoryToString")
default String defaultValueForQualifier(Category cat) {
// some mapping logic
}
}
|
Mapping collections
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Mapper
public interface CarMapper {
Set<String> integerSetToStringSet(Set<Integer> integers);
List<CarDto> carsToCarDtos(List<Car> cars);
CarDto carToCarDto(Car car);
}
public interface SourceTargetMapper {
@MapMapping(valueDateFormat = "dd.MM.yyyy")
Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source);
}
|
Mapping Values
Mapping enum to enum types
1
2
3
4
5
6
7
8
9
10
11
12
|
@Mapper
public interface OrderMapper {
OrderMapper INSTANCE = Mappers.getMapper( OrderMapper.class );
@ValueMappings({
@ValueMapping(target = "SPECIAL", source = "EXTRA"),
@ValueMapping(target = "DEFAULT", source = "STANDARD"),
@ValueMapping(target = "DEFAULT", source = "NORMAL")
})
ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
}
|
Mapping between Enum & Integer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Mapper(uses = MapStructUtils.class)
public interface StudentDTOMapper {
StudentDTOMapper INSTANCE = Mappers.getMapper(StudentDTOMapper.class);
StudentDTO do2dto(Student student);
@Mapping(source = "userType.value", target = "userType")
Student dtoTdo(StudentDTO dto);
}
public class MapStructUtils {
public EnumUserType toEnumUserType(int value) {
return Arrays.stream(EnumUserType.values())
.filter(e -> e.getValue() == value)
.findFirst()
.orElse(null);
}
|
BeforeMapping & AfterMapping
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Mapper
public abstract class CarsMapper {
@BeforeMapping
protected void enrichDTOWithFuelType(Car car, @MappingTarget CarDTO carDto) {
if (car instanceof ElectricCar) {
carDto.setFuelType(FuelType.ELECTRIC);
}
if (car instanceof BioDieselCar) {
carDto.setFuelType(FuelType.BIO_DIESEL);
}
}
@AfterMapping
protected void convertNameToUpperCase(@MappingTarget CarDTO carDto) {
carDto.setName(carDto.getName().toUpperCase());
}
public abstract CarDTO toCarDto(Car car);
}
|
Lombok
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
<properties>
<org.mapstruct.version>1.5.3.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.16</org.projectlombok.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<!-- lombok dependency should not end up on classpath -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
<scope>provided</scope>
</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>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
<!-- additional annotation processor required as of Lombok 1.18.16 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
|
MapStruct Spring Extensions
这是一个注释处理器(Annotation Processing Tool (apt)),它扩展了众所周知的MapStruct 项目,具有特定于 Spring Framework 的特性。
您所要做的就是定义您的 MapStruct 映射器以扩展 Spring 的Converter 接口。在编译期间,扩展将生成一个适配器,允许标准 MapStruct 映射器使用 Spring 的ConversionService。
这使开发人员能够仅 ConversionService 在其 uses 属性中定义 MapStruct 映射器,而不必单独导入每个 Mapper,从而允许 Mapper 之间的松散耦合。
包含 annotations 与 extensions 将生成 class 桥接 MapStruct 的协议与 Spring 的 ConversionService API
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
|
<properties>
<org.mapstruct.extensions.spring.version>0.1.2</org.mapstruct.extensions.spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct.extensions.spring</groupId>
<artifactId>mapstruct-spring-annotations</artifactId>
<version>${org.mapstruct.extensions.spring.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.extensions.spring</groupId>
<artifactId>mapstruct-spring-extensions</artifactId>
<version>${org.mapstruct.extensions.spring.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
|
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
42
|
@Mapper(componentModel = "spring")
public interface CarMapper extends Converter<Car, CarDto> {
@Mapping(target = "seats", source = "seatConfiguration")
CarDto convert(Car car);
}
@Autowired
private ConversionService conversionService;
Car car = ...;
CarDto carDto = conversionService.convert(car, CarDto.class);
@Mapper
public interface SeatConfigurationMapper extends Converter<SeatConfiguration, SeatConfigurationDto> {
@Mapping(target = "seatCount", source = "numberOfSeats")
@Mapping(target = "material", source = "seatMaterial")
SeatConfigurationDto convert(SeatConfiguration seatConfiguration);
}
@Component
public class ConversionServiceAdapter {
private final ConversionService conversionService;
public ConversionServiceAdapter(@Lazy final ConversionService conversionService) {
this.conversionService = conversionService;
}
public CarDto mapCarToCarDto(final Car source) {
return conversionService.convert(source, CarDto.class);
}
public SeatConfigurationDto mapSeatConfigurationToSeatConfigurationDto(
final SeatConfiguration source) {
return conversionService.convert(source, SeatConfigurationDto.class);
}
}
@Mapper(uses = ConversionServiceAdapter.class)
public interface CarMapper extends Converter<Car, CarDto> {
@Mapping(target = "seats", source = "seatConfiguration")
CarDto convert(Car car);
}
|
附录