前言
这是MapStruct的参考文档,MapStruct是一个用于生成类型安全,高性能和无依赖的bean映射代码的注释处理器。本指南涵盖了MapStruct提供的所有功能。如果本指南未回答您的所有问题,请加入MapStruct Google小组以获取帮助。
您在本指南中发现了拼写错误或其他错误?请通过在MapStruct GitHub存储库中打开一个问题告诉我们,或者更好的是,帮助社区并发送拉取请求来修复它!
1.简介
MapStruct是一个Java 注释处理器,用于生成类型安全的bean映射类。
您所要做的就是定义一个mapper接口,该接口声明任何所需的映射方法。在编译期间,MapStruct将生成此接口的实现。此实现使用普通的Java方法调用来在源对象和目标对象之间进行映射,即没有反射或类似。
与手工编写映射代码相比,MapStruct通过生成繁琐且易于编写的代码来节省时间。遵循约定优于配置方法,MapStruct使用合理的默认值,但在配置或实现特殊行为时会采取措施。
与动态映射框架相比,MapStruct具有以下优势:
-
通过使用普通方法调用而不是反射来快速执行
-
编译时类型安全:只能映射相互映射的对象和属性,不会将订单实体意外映射到客户DTO等。
-
在构建时清除错误报告,如果
-
映射不完整(并非所有目标属性都已映射)
-
映射不正确(找不到合适的映射方法或类型转换)
-
2.设置
MapStruct是一个基于JSR 269的Java注释处理器,因此可以在命令行构建(javac,Ant,Maven等)中以及在IDE中使用。
它包含以下工件:
-
org.mapstruct:mapstruct:包含所需的注释,例如
@Mapping
-
org.mapstruct:mapstruct-processor:包含生成映射器实现的注释处理器
2.1。Apache Maven
对于基于Maven的项目,将以下内容添加到POM文件中以使用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
34
...
<properties>
<org.mapstruct.version>1.3.0.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.5.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>
...
如果您正在使用Eclipse IDE,请确保拥有当前版本的M2E插件。导入如上所示配置的Maven项目时,它将设置MapStruct注释处理器,以便在您保存映射器类型时在IDE中运行。整洁,不是吗? 要仔细检查一切是否按预期工作,请转到项目的属性,然后选择“Java编译器”→“注释处理”→“工厂路径”。应该在那里列出并启用MapStruct处理器JAR。通过编译器插件(见下文)配置的任何处理器选项都应列在“Java编译器”→“注释处理”下。 如果处理器未启动,请检查是否已启用通过M2E的注释处理器配置。为此,请转到“首选项”→“Maven”→“注释处理”,然后选择“自动配置JDT APT”。或者,在 还要确保您的项目使用的是Java 1.8或更高版本(项目属性→“Java编译器”→“编译合规性级别”)。它不适用于旧版本。 |
2.2。摇篮
将以下内容添加到Gradle构建文件中以启用MapStruct:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
plugins {
...
id 'net.ltgt.apt' version '0.20'
}
// You can integrate with your IDEs.
// See more details: https://github.com/tbroyer/gradle-apt-plugin#usage-with-ides
apply plugin: 'net.ltgt.apt-idea'
apply plugin: 'net.ltgt.apt-eclipse'
dependencies {
...
implementation "org.mapstruct:mapstruct:${mapstructVersion}"
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
// If you are using mapstruct in test code
testAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
}
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
plugins {
...
id 'net.ltgt.apt' version '0.20'
}
// You can integrate with your IDEs.
// See more details: https://github.com/tbroyer/gradle-apt-plugin#usage-with-ides
apply plugin: 'net.ltgt.apt-idea'
apply plugin: 'net.ltgt.apt-eclipse'
dependencies {
...
compile "org.mapstruct:mapstruct:${mapstructVersion}"
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
// If you are using mapstruct in test code
testAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
}
...
您可以在GitHub上的mapstruct-examples项目中找到完整的示例。
2.3。Apache Ant
将以下javac
配置的任务添加到build.xml文件中,以便在基于Ant的项目中启用MapStruct。根据项目布局的需要调整路径。
1
2
3
4
5
6
7
8
9
...
<javac
srcdir="src/main/java"
destdir="target/classes"
classpath="path/to/mapstruct-1.3.0.Final.jar">
<compilerarg line="-processorpath path/to/mapstruct-processor-1.3.0.Final.jar"/>
<compilerarg line="-s target/generated-sources"/>
</javac>
...
您可以在GitHub上的mapstruct-examples项目中找到完整的示例。
2.4。配置选项
可以使用注释处理器选项配置MapStruct代码生成器。
直接调用javac时,这些选项将以-Akey = value的形式传递给编译器。通过Maven使用MapStruct时,可以使用options
Maven处理器插件配置中的元素传递任何处理器选项,如下所示:
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
...
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.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>
<compilerArgs>
<compilerArg>
-Amapstruct.suppressGeneratorTimestamp=true
</compilerArg>
<compilerArg>
-Amapstruct.suppressGeneratorVersionInfoComment=true
</compilerArg>
</compilerArgs>
</configuration>
</plugin>
...
1
2
3
4
5
6
7
8
...
compileJava {
options.compilerArgs = [
'-Amapstruct.suppressGeneratorTimestamp=true',
'-Amapstruct.suppressGeneratorVersionInfoComment=true'
]
}
...
存在以下选项:
选项 | 目的 | 默认 |
---|---|---|
|
如果设置为 |
|
|
如果设置为 |
|
|
组件模型的名称(请参阅检索映射器)基于应生成的映射器。 支持的值是:
如果为特定映射器via提供了组件模型 |
|
|
如果未使用源值填充映射方法的目标对象的属性,则应用默认报告策略。 支持的值是:
如果为特定映射器via提供了策略 |
|
2.5。在Java 9上使用MapStruct
MapStruct可以与Java 9(JPMS)一起使用,对它的支持是实验性的。
Java 9的核心主题是JDK的模块化。这样做的一个结果是需要为项目启用特定模块才能使用javax.annotation.Generated
注释。@Generated
由MapStruct添加到生成的映射器类,以将它们标记为生成的代码,说明生成日期,生成器版本等。
要允许使用@Generated
注释,必须启用模块java.xml.ws.annotation。使用Maven时,可以这样做:
export MAVEN_OPTS =“ - add-modules java.xml.ws.annotation”
如果@Generated
注释不可用,MapStruct将检测到这种情况,而不是将其添加到生成的映射器中。
在Java 9 |
3.定义映射器
在本节中,您将学习如何使用MapStruct定义bean映射器以及必须执行哪些选项。
3.1。基本映射
要创建映射器,只需使用所需的映射方法定义Java接口,并使用注释对其进行org.mapstruct.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);
}
该@Mapper
注释将使得MapStruct代码生成器创建的执行CarMapper
过程中生成时的界面。
在生成的方法实现中,源类型(例如Car
)中的所有可读属性将被复制到目标类型中的相应属性中(例如CarDto
):
-
当某个属性与其目标实体对应的名称相同时,它将被隐式映射。
-
当属性在目标实体中具有不同的名称时,可以通过
@Mapping
注释指定其名称。
必须在注释中指定JavaBeans规范中定义的属性名称 |
通过 |
还支持流利的制定者。Fluent setter是与所修改类型返回相同类型的setter。 例如
|
为了更好地理解MapStruct的作用,请查看carToCarDto()
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
34
35
36
37
38
39
40
41
42
43
44
// GENERATED CODE
public class CarMapperImpl implements CarMapper {
@Override
public CarDto carToCarDto(Car car) {
if ( car == null ) {
return null;
}
CarDto carDto = new CarDto();
if ( car.getFeatures() != null ) {
carDto.setFeatures( new ArrayList<String>( car.getFeatures() ) );
}
carDto.setManufacturer( car.getMake() );
carDto.setSeatCount( car.getNumberOfSeats() );
carDto.setDriver( personToPersonDto( car.getDriver() ) );
carDto.setPrice( String.valueOf( car.getPrice() ) );
if ( car.getCategory() != null ) {
carDto.setCategory( car.getCategory().toString() );
}
carDto.setEngine( engineToEngineDto( car.getEngine() ) );
return carDto;
}
@Override
public PersonDto personToPersonDto(Person person) {
//...
}
private EngineDto engineToEngineDto(Engine engine) {
if ( engine == null ) {
return null;
}
EngineDto engineDto = new EngineDto();
engineDto.setHorsePower(engine.getHorsePower());
engineDto.setFuel(engine.getFuel());
return engineDto;
}
}
MapStruct的一般原理是生成尽可能多的代码,就像你自己亲自编写代码一样。特别地,这意味着通过普通的getter / setter调用而不是反射或类似的方法将值从源复制到目标。
如示例所示,生成的代码会考虑通过指定的任何名称映射@Mapping
。如果源和目标实体中映射属性的类型不同,MapStruct将应用自动转换(例如,对于price属性,另请参见隐式类型转换)或者可选地调用/创建另一种映射方法(例如,对于驱动程序) / engine属性,另请参见映射对象引用)。当且仅当source和target属性是Bean的属性并且它们本身是Bean或简单属性时,MapStruct才会创建新的映射方法。即它们不是Collection
或Map
类型属性。
将通过创建包含source属性中的元素的目标集合类型的新实例来复制具有相同元素类型的集合类型属性。对于具有不同元素类型的集合类型属性,每个元素将单独映射并添加到目标集合中(请参阅映射集合)。
MapStruct会考虑源和目标类型的所有公共属性。这包括在超类型上声明的属性。
3.2。向映射器添加自定义方法
在某些情况下,可能需要手动实现从一种类型到另一种类型的特定映射,这是MapStruct无法生成的。处理此问题的一种方法是在另一个类上实现自定义方法,然后由MapStruct生成的映射器使用该方法(请参阅调用其他映射器)。
或者,在使用Java 8或更高版本时,您可以直接在映射器接口中实现自定义方法作为默认方法。如果参数和返回类型匹配,生成的代码将调用默认方法。
作为一个例子,我们假设从映射Person
到PersonDto
需要一些MapStruct无法生成的特殊逻辑。然后,您可以从上一个示例中定义映射器,如下所示:
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
属性时调用手动实现的方法。
映射器也可以以抽象类而不是接口的形式定义,并直接在mapper类中实现自定义方法。在这种情况下,MapStruct将使用所有抽象方法的实现生成抽象类的扩展。这种方法优于声明默认方法的一个优点是可以在mapper类中声明其他字段。
前面的例子,其中从映射Person
到PersonDto
需要一些特殊的逻辑然后可以定义如下:
1
2
3
4
5
6
7
8
9
10
11
@Mapper
public abstract class CarMapper {
@Mapping(...)
...
public abstract CarDto carToCarDto(Car car);
public PersonDto personToPersonDto(Person person) {
//hand-written mapping logic
}
}
MapStruct将生成一个子类,该子类CarMapper
具有该carToCarDto()
方法的实现,因为它被声明为abstract。生成的代码carToCarDto()
将personToPersonDto()
在映射driver
属性时调用手动实现的方法。
3.3。具有多个源参数的映射方法
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
属性所示。如果不解决这种歧义,将会引发错误。对于在给定源对象中仅存在一次的属性,可以选择指定源参数的名称,因为它可以自动确定。
使用 |
|
MapStruct还提供了直接引用源参数的可能性。
1
2
3
4
5
6
7
@Mapper
public interface AddressMapper {
@Mapping(source = "person.description", target = "description")
@Mapping(source = "hn", target = "houseNumber")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Integer hn);
}
在这种情况下,源参数直接映射到目标,如上面的示例所示。参数hn
,非bean类型(在本例中java.lang.Integer
)映射到houseNumber
。
3.4。更新现有的bean实例
在某些情况下,您需要映射,这些映射不会创建目标类型的新实例,而是更新该类型的现有实例。可以通过为目标对象添加参数并使用标记此参数来实现此类映射@MappingTarget
。以下是一个示例:
1
2
3
4
5
@Mapper
public interface CarMapper {
void updateCarFromDto(CarDto carDto, @MappingTarget Car car);
}
生成的updateCarFromDto()
方法代码将Car
使用给定CarDto
对象的属性更新传递的实例。可能只有一个参数被标记为映射目标。相反的void
,你还可以设置方法的返回类型为目标参数的类型,这将导致生成的实现来更新通过映射目标并返回它。这允许对映射方法进行流畅的调用。
将更新要更新的目标bean的集合或映射类型属性,然后使用相应源集合或映射中的值填充这些属性。
3.5。直接现场访问的映射
MapStruct还支持public
没有getter / setter 的字段的映射。如果无法为属性找到合适的getter / setter方法,MapStruct将使用这些字段作为读/写访问器。
如果字段是public
或,则字段被视为读取访问器public final
。如果字段static
不被视为读访问者。
只有字段被认为是写访问器public
。如果字段是final
和/或static
它不被视为写访问器。
小例子:
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
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(source = "customerName", target = "name")
Customer toCustomer(CustomerDto customerDto);
@InheritInverseConfiguration
CustomerDto fromCustomer(Customer customer);
}
对于上面的配置,生成的映射器如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// GENERATED CODE
public class CustomerMapperImpl implements CustomerMapper {
@Override
public Customer toCustomer(CustomerDto customerDto) {
// ...
customer.setId( customerDto.id );
customer.setName( customerDto.customerName );
// ...
}
@Override
public CustomerDto fromCustomer(Customer customer) {
// ...
customerDto.id = customer.getId();
customerDto.customerName = customer.getName();
// ...
}
}
您可以 在GitHub上的mapstruct-examples-field-mapping项目中找到完整的示例 。
3.6。使用构建器
MapStruct还支持通过构建器映射不可变类型。执行映射时,MapStruct会检查是否存在要映射的类型的构建器。这是通过BuilderProvider
SPI 完成的。如果某个类型的构建器存在,则该构建器将用于映射。
默认实现BuilderProvider
假设如下:
-
该类型具有无参数的公共静态构建器创建方法,该方法返回构建器。例如,
Person
有一个返回的公共静态方法PersonBuilder
。 -
构建器类型具有无参数的公共方法(构建方法),它返回正在构建的类型在我们的示例中
PersonBuilder
有一个返回的方法Person
。 -
如果有多个构建方法,MapStruct将查找一个调用的方法
build
,如果存在这样的方法,则会使用此方法,否则将创建编译错误。 -
可以通过
@Builder
在:@BeanMapping
,@Mapper
或中使用来定义特定的构建方法@MapperConfig
-
如果有多个构建器创建方法满足上述条件,那么
MoreThanOneBuilderCreationMethodException
将从DefaultBuilderProvider
SPI 抛出a 。如果MoreThanOneBuilderCreationMethodException
MapStruct将在编译中写入警告而不使用任何构建器。
如果找到这样的类型,则MapStruct将使用该类型执行映射(即,它将查找该类型的setter)。要完成映射,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
public class Person {
private final String name;
protected Person(Person.Builder builder) {
this.name = builder.name;
}
public static Person.Builder builder() {
return new Person.Builder();
}
public static class Builder {
private String name;
public Builder name(String name) {
this.name = name;
return this;
}
public Person create() {
return new Person( this );
}
}
}
1
2
3
4
public interface PersonMapper {
Person map(PersonDto dto);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// GENERATED CODE
public class PersonMapperImpl implements PersonMapper {
public Person map(PersonDto dto) {
if (dto == null) {
return null;
}
Person.Builder builder = Person.builder();
builder.name( dto.getName() );
return builder.create();
}
}
支持的构建器框架:
-
Lombok - 需要在单独的模块中使用Lombok类。有关更多信息,请参阅rzwitserloot / lombok#1538
-
不可变 - 当注释处理器路径上存在不可变项时,默认情况下将使用
ImmutablesAccessorNamingStrategy
和ImmutablesBuilderProvider
-
FreeBuilder - 当注释处理器路径上存在FreeBuilder时
FreeBuilderAccessorNamingStrategy
,默认情况下将使用它。使用FreeBuilder时,应遵循JavaBean约定,否则MapStruct将无法识别流畅的getter。 -
如果实现支持默认的已定义规则,它也适用于自定义构建器(手写的构建器)
BuilderProvider
。否则,您需要编写自定义BuilderProvider
如果您想要使用构建器禁用,则可以使用在目录中 |
4.检索映射器
4.1。Mappers工厂
可以通过org.mapstruct.factory.Mappers
类检索Mapper实例。只需调用该getMapper()
方法,传递mapper的接口类型即可返回:
1
CarMapper mapper = Mappers.getMapper( CarMapper.class );
按照惯例,映射器接口应该定义一个名为的成员INSTANCE
,该成员包含mapper类型的单个实例:
1
2
3
4
5
6
7
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
CarDto carToCarDto(Car car);
}
此模式使客户端可以非常轻松地使用映射器对象,而无需重复实例化新实例:
1
2
Car car = ...;
CarDto dto = CarMapper.INSTANCE.carToCarDto( car );
请注意,MapStruct生成的映射器是线程安全的,因此可以安全地从多个线程同时访问。
4.2。使用依赖注入
如果您正在使用依赖注入框架(如CDI(Java TM EE的上下文和依赖注入)或Spring Framework),建议通过依赖注入获取映射器对象。为此,您可以指定生成映射器类的组件模型应基于通过@Mapper#componentModel
或使用处理器选项,如配置选项中所述。
目前支持CDI和Spring(后者通过其自定义注释或使用JSR 330注释)。有关属性的允许值,请参阅配置选项,这些值componentModel
与mapstruct.defaultComponentModel
处理器选项相同。在这两种情况下,所需的注释都将添加到生成的映射器实现类中,以便使相同的主题依赖注入。以下显示了使用CDI的示例:
1
2
3
4
5
@Mapper(componentModel = "cdi")
public interface CarMapper {
CarDto carToCarDto(Car car);
}
生成的映射器实现将使用@ApplicationScoped
注释进行标记,因此可以使用@Inject
注释将其注入到字段,构造函数参数等中:
1
2
@Inject
private CarMapper mapper;
使用其他映射器类的映射器(请参阅调用其他映射器)将使用配置的组件模型获取这些映射器。因此,如果CarMapper
从前面的示例中使用另一个映射器,那么另一个映射器也必须是可注入的CDI bean。
4.3。注射策略
使用依赖项注入时,可以在字段和构造函数注入之间进行选择。这可以通过提供注射策略@Mapper
或@MapperConfig
注释来完成。
1
2
3
4
@Mapper(componentModel = "cdi", uses = EngineMapper.class, injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface CarMapper {
CarDto carToCarDto(Car car);
}
生成的映射器将注入uses属性中定义的所有类。当InjectionStrategy#CONSTRUCTOR
被使用时,构造函数都会有相应的注释和字段不会。当InjectionStrategy#FIELD
被使用时,注释是在球场上本身。目前,默认的注入策略是现场注入。建议使用构造函数注入来简化测试。
对于抽象类或装饰器,应该使用setter注入。 |
5.数据类型转换
并非总是映射属性在源对象和目标对象中具有相同的类型。例如,属性可以是int
源bean中的类型Long
,但可以是目标bean中的类型。
另一个例子是对其他对象的引用,这些对象应该映射到目标模型中的相应类型。例如,类Car
可能具有在映射对象时需要转换为对象driver
的类型的属性。Person
PersonDto
Car
在本节中,您将了解MapStruct如何处理此类数据类型转换。
5.1。隐式类型转换
在许多情况下,MapStruct会自动处理类型转换。例如,如果属性int
在源bean中是类型但String
在目标bean中是类型,则生成的代码将分别通过调用String#valueOf(int)
和透明地执行转换Integer#parseInt(String)
。
目前,会自动应用以下转化:
-
之间的所有Java基本数据类型及其相应的包装类型,例如之间
int
和Integer
,boolean
和Boolean
等生成的代码是null
转换一个包装型成相应的原始类型时一个感知,即,null
检查将被执行。 -
所有Java基本号码类型和包装类型,例如之间
int
和long
或byte
和Integer
。
从较大的数据类型转换成更小的(例如,从 |
-
所有Java基本类型之间(包括其包装)和
String
之间,例如int
和String
或Boolean
和String
。java.text.DecimalFormat
可以指定理解的格式字符串。
1
2
3
4
5
6
7
8
9
@Mapper
public interface CarMapper {
@Mapping(source = "price", numberFormat = "$#.00")
CarDto carToCarDto(Car car);
@IterableMapping(numberFormat = "$#.00")
List<String> prices(List<Integer> prices);
}
-
在
enum
类型和之间String
。 -
在大数字类型(
java.math.BigInteger
,java.math.BigDecimal
)和Java原始类型(包括它们的包装器)以及String之间。java.text.DecimalFormat
可以指定理解的格式字符串。
1
2
3
4
5
6
7
@Mapper
public interface CarMapper {
@Mapping(source = "power", numberFormat = "#.##E0")
CarDto carToCarDto(Car car);
}
-
之间
JAXBElement<T>
和T
,List<JAXBElement<T>>
和List<T>
-
介于
java.util.Calendar
/java.util.Date
和JAXB 之间XMLGregorianCalendar
-
之间
java.util.Date
/XMLGregorianCalendar
和String
。java.text.SimpleDateFormat
可以通过dateFormat
选项指定理解的格式字符串,如下所示:
1
2
3
4
5
6
7
8
9
@Mapper
public interface CarMapper {
@Mapping(source = "manufacturingDate", dateFormat = "dd.MM.yyyy")
CarDto carToCarDto(Car car);
@IterableMapping(dateFormat = "dd.MM.yyyy")
List<String> stringListToDateList(List<Date> dates);
}
-
Jodas之间
org.joda.time.DateTime
,org.joda.time.LocalDateTime
,org.joda.time.LocalDate
,org.joda.time.LocalTime
和String
。java.text.SimpleDateFormat
可以通过dateFormat
选项指定理解的格式字符串(参见上文)。 -
Jodas之间
org.joda.time.DateTime
和javax.xml.datatype.XMLGregorianCalendar
,java.util.Calendar
。 -
Jodas之间
org.joda.time.LocalDateTime
,org.joda.time.LocalDate
和javax.xml.datatype.XMLGregorianCalendar
,java.util.Date
。 -
之间
java.time.ZonedDateTime
,java.time.LocalDateTime
,java.time.LocalDate
,java.time.LocalTime
从Java 8日期时间包装和String
。java.text.SimpleDateFormat
可以通过dateFormat
选项指定理解的格式字符串(参见上文)。 -
之间
java.time.Instant
,java.time.Duration
,java.time.Period
从Java 8日期时间封装和String
使用parse
中的每个类的方法从映射String
,并使用toString
映射到String
。 -
在
java.time.ZonedDateTime
Java 8 Date-Time包之间java.util.Date
,当ZonedDateTime
从给定映射aDate
时,使用系统默认时区。 -
在
java.time.LocalDateTime
Java 8 Date-Time包和java.util.Date
时区UTC用作时区之间。 -
在
java.time.LocalDate
Java 8 Date-Time包和java.util.Date
/之间java.sql.Date
使用时区UTC作为时区。 -
之间
java.time.Instant
从Java 8日期时间包装和java.util.Date
。 -
之间
java.time.ZonedDateTime
从Java 8日期时间包装和java.util.Calendar
。 -
之间
java.sql.Date
和java.util.Date
-
之间
java.sql.Time
和java.util.Date
-
之间
java.sql.Timestamp
和java.util.Date
-
从a
String
省略转换时Mapping#dateFormat
,会导致使用默认语言环境的默认模式和日期格式符号。此规则的一个例外是根据XML Schema 1.0 Part 2,Section 3.2.7-14.1,Lexical RepresentationXmlGregorianCalendar
进行解析。String
-
之间
java.util.Currency
和String
。-
从a转换时
String
,该值必须是有效的ISO-4217字母代码,否则IllegalArgumentException
抛出
-
5.2。映射对象引用
通常,对象不仅具有原始属性,还具有引用其他对象的功能。例如,Car
类可以包含Person
对象的引用(表示汽车的驱动程序),该PersonDto
对象应该映射到CarDto
类引用的对象。
在这种情况下,只需为引用的对象类型定义一个映射方法:
1
2
3
4
5
6
7
@Mapper
public interface CarMapper {
CarDto carToCarDto(Car car);
PersonDto personToPersonDto(Person person);
}
生成的carToCarDto()
方法代码将调用personToPersonDto()
用于映射driver
属性的方法,而生成的实现用于personToPersonDto()
执行person对象的映射。
这样就可以映射任意深度对象图。从实体映射到数据传输对象时,在某个点切割对其他实体的引用通常很有用。为此,请实现自定义映射方法(请参阅下一节),例如将引用的实体映射到目标对象中的id。
在生成映射方法的实现时,MapStruct将对源和目标对象中的每个属性对应用以下例程:
-
如果source和target属性具有相同的类型,则该值将简单地从源复制到目标。如果属性是集合(例如a
List
),则集合的副本将被设置到目标属性中。 -
如果源属性和目标属性类型不同,请检查是否存在另一种映射方法,该方法将源属性的类型作为参数类型,并将目标属性的类型作为返回类型。如果存在这样的方法,则将在生成的映射实现中调用它。
-
如果不存在此类方法,MapStruct将查看是否存在属性的源类型和目标类型的内置转换。如果是这种情况,生成的映射代码将应用此转换。
-
如果没有找到这样的方法,MapStruct将尝试生成一个自动子映射方法,该方法将执行源和目标属性之间的映射。
-
如果MapStruct无法创建基于名称的映射方法,则会在构建时引发错误,指示不可映射的属性及其路径。
为了阻止MapStruct生成自动子映射方法,可以使用 |
5.3。控制嵌套的bean映射
如上所述,MapStruct将基于源和目标属性的名称生成方法。不幸的是,在许多情况下这些名称不匹配。
'。' @Mapping
源名称或目标类型中的表示法可用于控制名称不匹配时应如何映射属性。我们的示例存储库中有一个精心设计的示例,用于解释如何克服此问题。
在最简单的场景中,嵌套级别上的属性需要更正。例如fish
,在FishTankDto
和中具有相同名称的属性FishTank
。对于此属性,MapStruct会自动生成映射:FishDto fishToFishDto(Fish fish)
。MapStruct不可能意识到偏离的属性kind
和type
。因此,这可以在映射规则中解决:@Mapping(target="fish.kind", source="fish.type")
。这告诉MapStruct不要kind
在此级别查找名称并将其映射到type
。
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 );
}
可以使用相同的构造来忽略嵌套级别的某些属性,如第二个@Mapping
规则中所示。
当源和目标不共享相同的嵌套级别(相同数量的属性)时,MapStruct甚至可以用于“樱桃挑选”属性。这可以在源 - 和目标类型中完成。这将在接下来的两条规则中得到证明:@Mapping(target="ornament", source="interior.ornament")
和@Mapping(target="material.materialType", source="material")
。
当映射首先共享一个共同基础时,甚至可以完成后者。例如:共享相同名称的所有属性Quality
都映射到QualityDto
。同样,所有属性Report
都映射到ReportDto
,但有一个例外:organisation
in OrganisationDto
为空(因为源级别没有组织)。只有the name
填充了organisationName
from Report
。这证明了@Mapping(target="quality.report.organisation.name", source="quality.report.organisationName")
再回到原来的例子:如果kind
和type
将豆子自己?在这种情况下,MapStruct将再次生成继续映射的方法。这在下一个例子中得到证明:
1
2
3
4
5
6
7
8
9
10
11
12
13
@Mapper
public interface FishTankMapperWithDocument {
@Mapping(target = "fish.kind", source = "fish.type")
@Mapping(target = "fish.name", expression = "java(\"Jaws\")")
@Mapping(target = "plant", ignore = true )
@Mapping(target = "ornament", ignore = true )
@Mapping(target = "material", ignore = true)
@Mapping(target = "quality.document", source = "quality.report")
@Mapping(target = "quality.document.organisation.name", constant = "NoIdeaInc" )
FishTankWithNestedDocumentDto map( FishTank source );
}
注意发生了什么@Mapping(target="quality.document", source="quality.report")
。
DocumentDto
在目标方面不存在。它映射自Report
。MapStruct继续在此生成映射代码。该映射本身可以引导到另一个名称。这甚至适用于常量和表达式。最后一个例子中显示了:@Mapping(target="quality.document.organisation.name", constant="NoIdeaInc")
。
MapStruct将对源中的每个嵌套属性执行空检查。
我们鼓励用户显式编写自己的嵌套方法,而不是通过父方法配置所有内容。这将嵌套映射的配置放在一个位置(方法),在这里可以从上层的几个方法重用它,而不是在所有这些上层方法上重新配置相同的东西。 |
在某些情况下 |
5.4。调用其他映射器
除了在同一映射器类型上定义的方法之外,MapStruct还可以调用其他类中定义的映射方法,无论是MapStruct生成的映射器还是手写映射方法。这对于在多个类中构建映射代码(例如,每个应用程序模块使用一个映射器类型)或者如果要提供MapStruct无法生成的自定义映射逻辑非常有用。
例如,Car
类可能包含一个属性,manufacturingDate
而相应的DTO属性是String类型。为了映射这个属性,你可以像这样实现一个mapper类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DateMapper {
public String asString(Date date) {
return date != null ? new SimpleDateFormat( "yyyy-MM-dd" )
.format( date ) : null;
}
public Date asDate(String date) {
try {
return date != null ? new SimpleDateFormat( "yyyy-MM-dd" )
.parse( date ) : null;
}
catch ( ParseException e ) {
throw new RuntimeException( e );
}
}
}
在接口的@Mapper
注释中CarMapper
引用DateMapper
类如下:
1
2
3
4
5
@Mapper(uses=DateMapper.class)
public class CarMapper {
CarDto carToCarDto(Car car);
}
在为carToCarDto()
方法的实现生成代码时,MapStruct将寻找一种方法,该方法将Date
对象映射到String中,在DateMapper
类上找到它并生成asString()
用于映射manufacturingDate
属性的调用。
生成的映射器使用为其配置的组件模型检索引用的映射器。如果例如CDI用作组件模型CarMapper
,则DateMapper
也必须是CDI bean。使用默认组件模型时,MapStruct生成的映射器要引用的任何手写映射器类必须声明一个公共的无参数构造函数才能实例化。
5.5。将映射目标类型传递给自定义映射器
将自定义映射器连接到生成的映射器时@Mapper#uses()
,Class
可以在自定义映射方法中定义其他类型的参数(或其超类型),以便为特定目标对象类型执行常规映射任务。必须使用@TargetType
MapStruct 注释该属性,以生成传递Class
表示目标bean的相应属性类型的实例的调用。
例如,CarDto
可以具有包含实体主键的owner
类型属性。您现在可以创建一个通用的自定义映射器,将任何对象解析为其对应的托管JPA实体实例。Reference
Person
Reference
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ApplicationScoped // CDI component model
public class ReferenceMapper {
@PersistenceContext
private EntityManager entityManager;
public <T extends BaseEntity> T resolve(Reference reference, @TargetType Class<T> entityClass) {
return reference != null ? entityManager.find( entityClass, reference.getPk() ) : null;
}
public Reference toReference(BaseEntity entity) {
return entity != null ? new Reference( entity.getPk() ) : null;
}
}
@Mapper(componentModel = "cdi", uses = ReferenceMapper.class )
public interface CarMapper {
Car carDtoToCar(CarDto carDto);
}
然后MapStruct将生成如下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//GENERATED CODE
@ApplicationScoped
public class CarMapperImpl implements CarMapper {
@Inject
private ReferenceMapper referenceMapper;
@Override
public Car carDtoToCar(CarDto carDto) {
if ( carDto == null ) {
return null;
}
Car car = new Car();
car.setOwner( referenceMapper.resolve( carDto.getOwner(), Owner.class ) );
// ...
return car;
}
}
5.6。将上下文或状态对象传递给自定义方法
可以通过生成的映射方法将附加上下文或状态信息传递给具有@Context
参数的自定义方法。这些参数在适用时传递给其他映射方法,@ObjectFactory
方法(请参阅对象工厂)或@BeforeMapping
/ @AfterMapping
方法(请参阅使用映射前映射和映射后方法映射自定义),因此可以在自定义代码中使用。
@Context
搜索参数的@ObjectFactory
方法,如果适用,则在提供的上下文参数值上调用这些方法。
@Context
还会搜索@BeforeMapping
/ @AfterMapping
methods的参数,如果适用,将在提供的上下文参数值上调用这些参数。
注意:null
在上下文参数的映射方法之前/之后调用之前,不执行任何检查。调用者需要确保null
在这种情况下不通过。
对于生成的代码来调用使用@Context
参数声明的方法,生成的映射方法的声明也需要包含至少那些(或可指定的)@Context
参数。生成的代码不会创建缺少@Context
参数的新实例,也不会传递文字null
。
@Context
参数将数据传递给手写属性映射方法
1
2
3
4
5
public abstract CarDto toCar(Car car, @Context Locale translationLocale);
protected OwnerManualDto translateOwnerManual(OwnerManual ownerManual, @Context Locale locale) {
// manually implemented logic to translate the OwnerManual with the given Locale
}
然后MapStruct将生成如下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
//GENERATED CODE
public CarDto toCar(Car car, Locale translationLocale) {
if ( car == null ) {
return null;
}
CarDto carDto = new CarDto();
carDto.setOwnerManual( translateOwnerManual( car.getOwnerManual(), translationLocale );
// more generated mapping code
return carDto;
}
5.7。映射方法解析
将属性从一种类型映射到另一种类型时,MapStruct会查找将源类型映射到目标类型的最具体方法。该方法可以在相同的映射器接口上声明,也可以在通过其注册的另一个映射器上声明@Mapper#uses()
。这同样适用于工厂方法(请参阅对象工厂)。
用于查找映射或工厂方法的算法尽可能类似于Java的方法解析算法。特别是,具有更具体源类型的方法将优先(例如,如果有两种方法,一种映射搜索到的源类型,另一种映射超类型的方法)。如果找到多个最具体的方法,将引发错误。
使用JAXB时,例如,在将a转换 |
5.8。基于限定符的映射方法选择
在许多情况下,需要具有相同方法签名(除了名称)的具有不同行为的映射方法。MapStruct有一个便利的机制来处理这种情况:@Qualifier
(org.mapstruct.Qualifier
)。“限定符”是用户可以编写的自定义注释,“坚持”作为使用的映射器包含的映射方法,并且可以在bean属性映射,可迭代映射或映射映射中引用。多个限定符可以“粘贴”到方法和映射上。
所以,让我们说有一个手写的方法来映射带有String
返回类型和String
参数的标题,其中许多其他引用的映射器具有相同的String
返回类型 - String
参数签名:
1
2
3
4
5
6
7
8
9
10
public class Titles {
public String translateTitleEG(String title) {
// some mapping logic
}
public String translateTitleGE(String title) {
// some mapping logic
}
}
还有一个使用这个手写映射器的映射器,其中源和目标具有应该映射的属性“title”:
1
2
3
4
5
6
@Mapper( uses = Titles.class )
public interface MovieMapper {
GermanRelease toGerman( OriginalRelease movies );
}
如果不使用限定符,这将导致模糊的映射方法错误,因为找到了2个限定方法(translateTitleEG
,translateTitleGE
),而MapStruct没有提示选择哪一个。
输入限定符方法:
1
2
3
4
5
6
7
import org.mapstruct.Qualifier;
@Qualifier
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface TitleTranslator {
}
并且,一些限定符用于指示使用哪个翻译器从源语言映射到目标语言:
1
2
3
4
5
6
7
import org.mapstruct.Qualifier;
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface EnglishToGerman {
}
1
2
3
4
5
6
7
import org.mapstruct.Qualifier;
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface GermanToEnglish {
}
请把保留的音符TitleTranslator
上一流水平,EnglishToGerman
,GermanToEnglish
在方法的水平!
然后,使用限定符,映射可能如下所示:
1
2
3
4
5
6
7
@Mapper( uses = Titles.class )
public interface MovieMapper {
@Mapping( target = "title", qualifiedBy = { TitleTranslator.class, EnglishToGerman.class } )
GermanRelease toGerman( OriginalRelease movies );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@TitleTranslator
public class Titles {
@EnglishToGerman
public String translateTitleEG(String title) {
// some mapping logic
}
@GermanToEnglish
public String translateTitleGE(String title) {
// some mapping logic
}
}
请确保使用的保留策略等于保留策略 |
使用限定符注释的类/方法将不再符合没有该 |
bean映射上也存在相同的机制 |
在许多情况下,声明一个新的注释以帮助选择过程对于您尝试实现的目标来说可能太多了。对于这些情况,MapStruct具有@Named
注释。此注释是预定义的限定符(使用@Qualifier
自身注释),可用于命名Mapper,或者更直接地通过其值来定义映射方法。上面的例子如下:
@Named
1
2
3
4
5
6
7
8
9
10
11
12
13
@Named("TitleTranslator")
public class Titles {
@Named("EnglishToGerman")
public String translateTitleEG(String title) {
// some mapping logic
}
@Named("GermanToEnglish")
public String translateTitleGE(String title) {
// some mapping logic
}
}
1
2
3
4
5
6
7
@Mapper( uses = Titles.class )
public interface MovieMapper {
@Mapping( target = "title", qualifiedByName = { "TitleTranslator", "EnglishToGerman" } )
GermanRelease toGerman( OriginalRelease movies );
}
虽然使用的机制是相同的,但用户必须更加小心。在IDE中重构已定义限定符的名称也会巧妙地重构所有其他出现的事件。这显然不是更改名称的情况。 |
6.映射集合
集合类型(的映射List
,Set
等等)以相同的方式映射bean类型,通过定义与在映射器接口所需的源和目标类型的映射方法,即完成。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);
}
将所生成的执行的integerSetToStringSet
执行从转换Integer
到String
每个元件,而所产生的carsToCarDtos()
方法调用carToCarDto()
用于如下面所示的每个包含的元素的方法:
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
//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;
}
请注意,当映射bean的集合类型属性(例如从Car#passengers
(类型List<Person>
)到CarDto#passengers
(类型List<PersonDto>
)时,MapStruct将查找具有匹配参数和返回类型的集合映射方法。
1
2
3
//GENERATED CODE
carDto.setPassengers( personsToPersonDtos( car.getPassengers() ) );
...
有些框架和库只公开JavaBeans getter,但没有sets类型属性的setter。默认情况下,使用JAXB从XML模式生成的类型遵循此模式。在这种情况下,用于映射此类属性的生成代码将调用其getter并添加所有映射元素:
1
2
3
//GENERATED CODE
carDto.getPassengers().addAll( personsToPersonDtos( car.getPassengers() ) );
...
不允许使用可迭代源和不可迭代目标声明映射方法,反之亦然。检测到这种情况时会出错。 |
6.1。映射地图
还支持基于映射的映射方法。以下是一个示例:
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
//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;
}
6.2。集合映射策略
MapStruct有CollectionMappingStrategy
,与可能的值:ACCESSOR_ONLY
,SETTER_PREFERRED
,ADDER_PREFERRED
和TARGET_IMMUTABLE
。
在下表中,破折号-
表示属性名称。接下来,尾随s
表示复数形式。该表说明了选择和它们是如何施加到的存在/由于缺少set-s
,add-
和/或get-s
在目标对象上的方法:
选项 | 只有目标set-s可用 | 只有目标添加 - 可用 | set-s / add-都可用 | 没有set-s / add-可用 | 现有目标(@TargetType ) |
---|---|---|---|---|---|
|
设置-S |
得到-S |
设置-S |
得到-S |
得到-S |
|
设置-S |
加- |
设置-S |
得到-S |
得到-S |
|
设置-S |
加- |
加- |
得到-S |
得到-S |
|
设置-S |
例外 |
设置-S |
例外 |
设置-S |
一些背景:一种adder
方法通常用于生成(JPA)实体的情况,以将单个元素(实体)添加到底层集合。调用加法器在parent(调用加法器的bean(实体))和子(ren)(集合中的元素(实体))之间建立父子关系。为了找到合适的adder
,MapStruct将尝试在底层集合的泛型参数类型和候选集的单个参数之间进行匹配adder
。当有更多候选者时,复数setter
/ getter
名称将转换为单数,并且除了进行匹配之外还将使用。
DEFAULT
不应明确使用该选项。它用于区分显式用户希望覆盖a @MapperConfig
中的隐式Mapstruct选项中的默认值@Mapper
。该选项DEFAULT
是同义词ACCESSOR_ONLY
。
使用 |
6.3。用于集合映射的实现类型
当iterable或map映射方法将接口类型声明为返回类型时,其实现类型之一将在生成的代码中实例化。下表显示了在生成的代码中实例化的受支持的接口类型及其相应的实现类型:
接口类型 | 实施类型 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7.映射流
映射java.util.Stream
是以与集合类型的映射类似的方式完成的,即通过在映射器接口中定义具有所需源和目标类型的映射方法。
生成的代码将包含Stream
从提供的Iterable
/数组创建的一个或将收集提供Stream
到Iterable
/数组中。如果存在源和目标元素类型的映射方法或隐式转换,则将完成此转换Stream#map()
。以下是一个示例:
1
2
3
4
5
6
7
8
9
@Mapper
public interface CarMapper {
Set<String> integerStreamToStringSet(Stream<Integer> integers);
List<CarDto> carsToCarDtos(Stream<Car> cars);
CarDto carToCarDto(Car car);
}
将所生成的执行的integerStreamToStringSet()
执行从转换Integer
到String
每个元件,而所产生的carsToCarDtos()
方法调用carToCarDto()
用于如下面所示的每个包含的元素的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//GENERATED CODE
@Override
public Set<String> integerStreamToStringSet(Stream<Integer> integers) {
if ( integers == null ) {
return null;
}
return integers.stream().map( integer -> String.valueOf( integer ) )
.collect( Collectors.toCollection( HashSet<String>::new ) );
}
@Override
public List<CarDto> carsToCarDtos(Stream<Car> cars) {
if ( cars == null ) {
return null;
}
return integers.stream().map( car -> carToCarDto( car ) )
.collect( Collectors.toCollection( ArrayList<CarDto>::new ) );
}
如果执行从a |
同样实现类型中用于集合映射实现类型都在做的时候用于创建的集合Stream
来Iterable
映射。
8.映射值
8.1。映射枚举类型
MapStruct支持生成将一个Java枚举类型映射到另一个Java枚举类型的方法。
默认情况下,源枚举中的每个常量都映射到目标枚举类型中具有相同名称的常量。如果需要,可以借助@ValueMapping
注释将源枚举中的常量映射到具有其他名称的常量。源枚举中的几个常量可以映射到目标类型中的相同常量。
以下是一个示例:
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_;
}
}
默认情况下,如果源枚举类型的常量没有目标类型中具有相同名称的对应常量,并且未映射到另一个常量via,则MapStruct将引发错误@ValueMapping
。这可确保以安全且可预测的方式映射所有常量。如果由于某种原因发生了无法识别的源值,则生成的映射方法将抛出IllegalStateException。
MapStruct还具有将任何剩余(未指定)映射映射到默认值的机制。这只能在一组值映射中使用一次。它有两种口味:<ANY_REMAINING>
和<ANY_UNMAPPED>
。
在源的情况下,<ANY_REMAINING>
MapStruct将继续将源枚举常量映射到具有相同名称的目标枚举常量。源枚举常量的其余部分将映射到@ValueMapping
with <ANY_REMAINING>
source中指定的目标。
MapStruct 不会尝试这种基于名称的映射,<ANY_UNMAPPED>
并直接将@ValueMapping
with <ANY_UNMAPPED>
source中指定的目标应用于余数。
MapStruct能够通过关键字处理null
源和null
目标<NULL>
。
适用于该类的常量 |
最后@InheritInverseConfiguration
,并@InheritConfiguration
可以与组合使用@ValueMappings
。
1
2
3
4
5
6
7
8
9
10
11
12
@Mapper
public interface SpecialOrderMapper {
SpecialOrderMapper INSTANCE = Mappers.getMapper( SpecialOrderMapper.class );
@ValueMappings({
@ValueMapping( source = MappingConstants.NULL, target = "DEFAULT" ),
@ValueMapping( source = "STANDARD", target = MappingConstants.NULL ),
@ValueMapping( source = MappingConstants.ANY_REMAINING, target = "SPECIAL" )
})
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
// GENERATED CODE
public class SpecialOrderMapperImpl implements SpecialOrderMapper {
@Override
public ExternalOrderType orderTypeToExternalOrderType(OrderType orderType) {
if ( orderType == null ) {
return ExternalOrderType.DEFAULT;
}
ExternalOrderType externalOrderType_;
switch ( orderType ) {
case STANDARD: externalOrderType_ = null;
break;
case RETAIL: externalOrderType_ = ExternalOrderType.RETAIL;
break;
case B2B: externalOrderType_ = ExternalOrderType.B2B;
break;
default: externalOrderType_ = ExternalOrderType.SPECIAL;
}
return externalOrderType_;
}
}
注意: MapStruct将避免映射RETAIL
和使用B2B
何时<ANY_UNMAPPED>
代替<ANY_REMAINING>
。
通过 |
9.对象工厂
默认情况下,生成的用于将一种bean类型映射到另一种bean或更新bean的代码将调用默认构造函数来实例化目标类型。
或者,您可以插入自定义对象工厂,这些工厂将被调用以获取目标类型的实例。一个用例是JAXB,它创建ObjectFactory
用于获取模式类型的新实例的类。
要使用自定义工厂@Mapper#uses()
,请按照调用其他映射器中的说明注册它们,或者直接在映射器中实现它们。在创建bean映射的目标对象时,MapStruct将查找无参数方法,使用注释@ObjectFactory
的方法或只有一个@TargetType
返回所需目标类型的参数的方法,并调用此方法而不是调用默认构造函数:
1
2
3
4
5
6
public class DtoFactory {
public CarDto createCarDto() {
return // ... custom factory logic
}
}
1
2
3
4
5
6
public class EntityFactory {
public <T extends BaseEntity> T createEntity(@TargetType Class<T> entityClass) {
return // ... custom factory logic
}
}
1
2
3
4
5
6
7
8
9
@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);
}
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
//GENERATED CODE
public class OwnerMapperImpl implements OwnerMapper {
private final DtoFactory dtoFactory = new DtoFactory();
private final EntityFactory entityFactory = new EntityFactory();
private final OwnerMapper ownerMapper = Mappers.getMapper( OwnerMapper.class );
@Override
public void updateOwnerDto(Owner owner, @MappingTarget OwnerDto ownerDto) {
if ( owner == null ) {
return;
}
if ( owner.getCar() != null ) {
if ( ownerDto.getCar() == null ) {
ownerDto.setCar( dtoFactory.createCarDto() );
}
// update car within ownerDto
}
else {
ownerDto.setCar( null );
}
// updating other properties
}
@Override
public void updateOwner(OwnerDto ownerDto, @MappingTarget Owner owner) {
if ( ownerDto == null ) {
return;
}
if ( ownerDto.getCar() != null ) {
if ( owner.getCar() == null ) {
owner.setCar( entityFactory.createEntity( Car.class ) );
}
// update car within owner
}
else {
owner.setCar( null );
}
// updating other properties
}
}
此外,使用工厂方法注释@ObjectFactory
可以访问映射源。源对象可以作为参数添加,其方式与映射方法相同。@ObjectFactory
为了让MapStruct知道给定的方法只是一个工厂方法,注释是必要的。
@ObjectFactory
1
2
3
4
5
6
7
public class DtoFactory {
@ObjectFactory
public CarDto createCarDto(Car car) {
return // ... custom factory logic
}
}
10.高级映射选项
本章介绍了几个高级选项,可以根据需要微调生成的映射代码的行为。
10.1。默认值和常量
如果相应的源属性为,则可以指定默认值以将预定义值设置为目标属性null
。可以指定常量以在任何情况下设置这样的预定义值。默认值和常量指定为String值。当目标类型是基元类型或盒装类型时,String值是文字的。在这种情况下允许位/八进制/十进制/十六进制模式,只要它们是有效的文字。在所有其他情况下,常量或默认值可通过内置转换或其他映射方法的调用进行类型转换,以匹配目标属性所需的类型。
具有常量的映射不得包含对source属性的引用。以下示例显示了使用默认值和常量的一些映射:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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);
}
如果s.getStringProp() == null
,则将目标属性stringProperty
设置为"undefined"
而不是应用该值s.getStringProp()
。如果s.getLongProperty() == null
,则将target属性longProperty
设置为-1
。String "Constant Value"
按原样设置为target属性stringConstant
。该值"3001"
被类型转换为Long
target属性的(包装器)类longWrapperConstant
。日期属性还需要日期格式。该常量"jack-jill-tom"
演示了如何StringListMapper
调用手写类来将以破折号分隔的列表映射到a List<String>
。
10.2。表达式
通过表达式,可以包含来自多种语言的结构。
目前只支持Java作为语言。此功能对于调用构造函数非常有用。整个源对象可在表达式中使用。应该注意只插入有效的Java代码:MapStruct不会在生成时验证表达式,但在编译期间会在生成的类中显示错误。
下面的示例演示了如何将两个源属性映射到一个目标:
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);
}
The example demonstrates how the source properties time
and format
are composed into one target property TimeAndFormat
. Please note that the fully qualified package name is specified because MapStruct does not take care of the import of the TimeAndFormat
class (unless it’s used otherwise explicitly in the SourceTargetMapper
). This can be resolved by defining imports
on the @Mapper
annotation.
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);
}
10.3. Default Expressions
Default expressions are a combination of default values and expressions. They will only be used when the source attribute is null
.
The same warnings and restrictions apply to default expressions that apply to expressions. Only Java is supported, and MapStruct will not validate the expression at generation-time.
下面的示例演示了如何将两个源属性映射到一个目标:
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);
}
该示例演示了ID
如果源字段为null,如何使用defaultExpression设置字段,如果设置sourceId
了源对象,则可以使用它来获取现有对象,如果不是,则创建新对象Id
。请注意,指定了完全限定的包名称,因为MapStruct不会处理UUID
类的导入(除非在其中明确使用SourceTargetMapper
)。这可以通过在@Mapper注释上定义导入来解决(参见表达式)。
10.4。确定结果类型
当结果类型具有继承关系时,选择mapping method(@Mapping
)或factory method(@BeanMapping
)可能会变得模糊不清。假设一个Apple和一个Banana,它们都是Fruit的特色。
1
2
3
4
5
6
7
@Mapper( uses = FruitFactory.class )
public interface FruitMapper {
@BeanMapping( resultType = Apple.class )
Fruit map( FruitDto source );
}
1
2
3
4
5
6
7
8
9
10
public class FruitFactory {
public Apple createApple() {
return new Apple( "Apple" );
}
public Banana createBanana() {
return new Banana( "Banana" );
}
}
那么,哪个Fruit
必须在映射方法中分解Fruit map(FruitDto source);
?A Banana
还是Apple
?这是@BeanMapping#resultType
派上用场的。它控制工厂方法以选择或缺少工厂方法,以创建返回类型。
映射中存在相同的机制: |
该机制也存在于可迭代映射和映射映射中。 |
10.5。控制'null'参数的映射结果
当映射方法的source参数等于时,MapStruct提供对要创建的对象的控制null
。默认情况下null
将返回。
但是,通过指定nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT
上@BeanMapping
,@IterableMapping
,@MapMapping
,或全局上@Mapper
或@MappingConfig
,映射结果可以被改变以返回空默认值。这意味着:
-
Bean映射:将返回一个“空”目标bean,除常量和表达式外,它们将在存在时填充。
-
基元:将返回基元的默认值,例如
false
forboolean
或0
forint
。 -
Iterables / Arrays:将返回一个空的iterable。
-
地图:将返回空地图。
该策略以分层方式运作。nullValueMappingStrategy
映射方法级别的设置将覆盖@Mapper#nullValueMappingStrategy
,@Mapper#nullValueMappingStrategy
并将覆盖@MappingConfig#nullValueMappingStrategy
。
10.6。控制bean映射中“null”属性的映射结果(仅限更新映射方法)。
@MappingTarget
当source属性等于null
或者presence检查方法导致'absent' 时,MapStruct提供对要在带注释的目标bean中设置的属性的控制。
默认情况下,target属性将设置为null。
然而:
-
通过指定
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT
的@Mapping
,@BeanMapping
,@Mapper
或@MappingConfig
,映射结果可以改变返回默认值。对于List
MapStruct产生ArrayList
,对于Map
一个HashMap
,对数组空数组,对于String
""
和用于原始/盒装类型的表示false
或0
。对于所有其他对象,将创建一个新实例。请注意,需要默认构造函数。如果没有,请使用@Mapping#defaultValue
。 -
通过指定
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE
上@Mapping
,@BeanMapping
,@Mapper
或@MappingConfig
,所述映射的结果将是等于原值@MappingTarget
注解目标。
该策略以分层方式运作。Mapping#nullValuePropertyMappingStrategy
映射级别nullValuePropertyMappingStrategy
上的设置将覆盖映射方法级别将覆盖@Mapper#nullValuePropertyMappingStrategy
,@Mapper#nullValuePropertyMappingStrategy
并将覆盖@MappingConfig#nullValuePropertyMappingStrategy
。
某些类型的映射(集合,映射),其中指示MapStruct使用getter或adder作为目标访问器,请参阅 |
|
10.7。控制bean映射中'null'属性的检查结果
MapStruct可以控制何时生成null
检查。默认情况下(nullValueCheckStrategy = NullValueCheckStrategy.ON_IMPLICIT_CONVERSION
)null
将生成一个检查:
-
当目标是原始的而源不是源时,将源值直接设置为目标值。
-
应用类型转换,然后:
-
在目标上调用setter。
-
调用另一个类型转换,然后调用目标上的setter。
-
调用映射方法,然后调用目标上的setter。
-
首先在source属性上调用映射方法不受null检查的保护。因此,生成的映射方法将在对源属性执行映射之前进行空检查。手写映射方法必须处理空值检查。他们有可能添加“意义” null
。例如:映射null
到默认值。
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS
除非源bean上定义了源存在检查器,否则该选项将始终包含源非非原始时的空检查。
该策略以分层方式运作。@Mapping#nullValueCheckStrategy
将覆盖@BeanMapping#nullValueCheckStrategy
,@BeanMapping#nullValueCheckStrategy
将覆盖@Mapper#nullValueCheckStrategy
和@Mapper#nullValueCheckStrategy
将覆盖@MappingConfig#nullValueCheckStrategy
。
10.8。来源存在检查
一些框架生成具有源存在检查器的bean属性。通常这是一种方法的形式hasXYZ
,XYZ
是bean映射方法中源bean的属性。当MapStruct 找到这样的方法时,它将调用它hasXYZ
而不是执行null
检查hasXYZ
。
可以在MapStruct服务提供程序接口(SPI)中更改源存在检查程序名称。它也可以通过这种方式停用。 |
某些类型的映射(集合,映射),其中指示MapStruct使用getter或adder作为目标访问器,请参阅 |
10.9。例外
调用应用程序可能需要在调用映射方法时处理异常。这些异常可以通过手写逻辑以及生成的内置映射方法或MapStruct的类型转换来引发。当调用应用程序需要处理异常时,可以在映射方法中定义throws子句:
1
2
3
4
5
@Mapper(uses = HandWritten.class)
public interface CarMapper {
CarDto carToCarDto(Car car) throws GearException;
}
手写逻辑可能如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HandWritten {
private static final String[] GEAR = {"ONE", "TWO", "THREE", "OVERDRIVE", "REVERSE"};
public String toGear(Integer gear) throws GearException, FatalException {
if ( gear == null ) {
throw new FatalException("null is not a valid gear");
}
if ( gear < 0 && gear > GEAR.length ) {
throw new GearException("invalid gear");
}
return GEAR[gear];
}
}
现在,MapStruct将FatalException
一个try-catch
块包装起来并重新抛出一个未经检查的块RuntimeException
。MapStruct委托处理GearException
应用程序逻辑,因为它在carToCarDto
方法中定义为throws子句:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// GENERATED CODE
@Override
public CarDto carToCarDto(Car car) throws GearException {
if ( car == null ) {
return null;
}
CarDto carDto = new CarDto();
try {
carDto.setGear( handWritten.toGear( car.getGear() ) );
}
catch ( FatalException e ) {
throw new RuntimeException( e );
}
return carDto;
}
有关空检查的一些注意事项 MapStruct仅在需要时才提供空检查:应用类型转换或通过调用其构造函数构造新类型时。这意味着用户负责手写代码以返回有效的非null对象。此外,null对象可以传递给手写代码,因为MapStruct不希望对用户分配给null对象的含义做出假设。手写代码必须处理这个问题。
11.重用映射配置
本章讨论了为多种映射方法重用映射配置的不同方法:从其他方法“配置”配置并在多个映射器类型之间共享中央配置。
11.1。映射配置继承
方法级配置注解,例如@Mapping
,@BeanMapping
,@IterableMapping
,等等,都可以继承从一个映射方法的类似使用注释方法@InheritConfiguration
:
1
2
3
4
5
6
7
8
9
@Mapper
public interface CarMapper {
@Mapping(target = "numberOfSeats", source = "seatCount")
Car carDtoToCar(CarDto car);
@InheritConfiguration
void carDtoIntoCar(CarDto carDto, @MappingTarget Car car);
}
上面的示例声明了一个映射方法carDtoToCar()
,该方法具有一个配置,用于定义如何映射numberOfSeats
类型中的属性Car
。在现有实例上执行映射的更新方法Car
需要相同的配置才能成功映射所有属性。声明@InheritConfiguration
该方法允许MapStruct搜索继承候选者以应用从中继承的方法的注释。
如果所有类型的A(源类型和结果类型)可分配给相应类型的B,则一种方法A可以从另一方法B继承该配置。
需要在当前映射器,超类/接口或共享配置接口(如共享配置中所述)中定义被认为是继承的方法。
如果多个方法适用于继承的源,则必须在注释中指定方法名称:@InheritConfiguration( name = "carDtoToCar" )
。
一种方法,可以使用@InheritConfiguration
和覆盖或通过另外施加修改的配置@Mapping
,@BeanMapping
等等。
|
11.2。逆映射
在双向映射的情况下,例如从实体到DTO以及从DTO到实体,前向方法和反向方法的映射规则通常是相似的,并且可以通过切换source
和简单地反转target
。
使用注释@InheritInverseConfiguration
指示方法应继承相应反向方法的反向配置。
1
2
3
4
5
6
7
8
9
10
@Mapper
public interface CarMapper {
@Mapping(source = "numberOfSeats", target = "seatCount")
CarDto carToDto(Car car);
@InheritInverseConfiguration
@Mapping(target = "numberOfSeats", ignore = true)
Car carDtoToCar(CarDto carDto);
}
这里的carDtoToCar()
方法是反向映射方法carToDto()
。请注意,任何属性映射carToDto()
也将应用于相应的反向映射方法。它们会自动反转并使用@InheritInverseConfiguration
注释复制到方法中。
从反转方法特定映射可以(可选地)被重写时 ignore
,expression
或constant
在映射,例如是这样的:@Mapping(target = "numberOfSeats", ignore=true)
。
一种方法,阿被认为是反向的方法的方法乙,如果结果类型阿是相同的单一源类型的乙并且如果单一来源型阿是相同的结果类型的乙。
需要在当前映射器(超类/接口)中定义被认为是反向继承的方法。
如果多个方法符合条件,则需要使用如下name
属性指定继承配置的方法:@InheritInverseConfiguration(name = "carToDto")
。
@InheritConfiguration
在冲突优先于的情况下采取@InheritInverseConfiguration
。
配置是可传递的。因此,如果方法C
定义了一个映射@Mapping( target = "x", ignore = true)
,B
定义了一个映射@Mapping( target = "y", ignore = true)
,那么如果A
从B
继承继承C
,A
则将继承属性x
和映射的映射y
。
表达式和常量被排除(默默忽略)@InheritInverseConfiguration
。
嵌套源属性的反向映射在1.1.0.Beta2版本中是实验性的。当源属性名称和目标属性名称相同时,将自动进行反向映射。否则,@Mapping
应指定目标名称和源名称。在所有情况下,需要采用合适的映射方法来进行反向映射。
|
11.3。共享配置
MapStruct提供了通过指向注释的中央接口来定义共享配置的可能性@MapperConfig
。要使映射器使用共享配置,需要在@Mapper#config
属性中定义配置界面。
该@MapperConfig
注释具有相同的属性@Mapper
注释。未通过via提供的任何属性@Mapper
都将从共享配置继承。指定@Mapper
的属性优先于通过引用的配置类指定的属性。列表属性,例如uses
简单组合:
1
2
3
4
5
6
@MapperConfig(
uses = CustomMapperViaMapperConfig.class,
unmappedTargetPolicy = ReportingPolicy.ERROR
)
public interface CentralConfig {
}
1
2
3
4
5
6
7
8
9
@Mapper(config = CentralConfig.class, uses = { CustomMapperViaMapper.class } )
// Effective configuration:
// @Mapper(
// uses = { CustomMapperViaMapper.class, CustomMapperViaMapperConfig.class },
// unmappedTargetPolicy = ReportingPolicy.ERROR
// )
public interface SourceTargetMapper {
...
}
保存@MapperConfig
注释的接口还可以声明可用于从中继承方法级映射注释的映射方法的原型。这些原型方法并不意味着要作为映射器API的一部分来实现或使用。
1
2
3
4
5
6
7
8
9
10
11
@MapperConfig(
uses = CustomMapperViaMapperConfig.class,
unmappedTargetPolicy = ReportingPolicy.ERROR,
mappingInheritanceStrategy = MappingInheritanceStrategy.AUTO_INHERIT_FROM_CONFIG
)
public interface CentralConfig {
// Not intended to be generated, but to carry inheritable mapping annotations:
@Mapping(target = "primaryKey", source = "technicalKey")
BaseEntity anyDtoToEntity(BaseDto dto);
}
1
2
3
4
5
6
7
8
@Mapper(config = CentralConfig.class, uses = { CustomMapperViaMapper.class } )
public interface SourceTargetMapper {
@Mapping(target = "numberOfSeats", source = "seatCount")
// additionally inherited from CentralConfig, because Car extends BaseEntity and CarDto extends BaseDto:
// @Mapping(target = "primaryKey", source = "technicalKey")
Car toCar(CarDto car)
}
当方法级映射配置注释从接口中的原型方法继承到映射器中的方法时,属性@Mapper#mappingInheritanceStrategy()
/ @MapperConfig#mappingInheritanceStrategy()
配置:
-
EXPLICIT
(默认):只有目标映射方法注释时才会继承配置,@InheritConfiguration
并且源和目标类型可分配给原型方法的相应类型,所有这些都在映射配置继承中描述。 -
AUTO_INHERIT_FROM_CONFIG
:如果目标映射方法的源和目标类型可分配给原型方法的相应类型,则配置将自动继承。如果多个原型方法匹配,则必须解决歧义@InheritConfiguration(name = …)
,这将导致AUTO_INHERIT_FROM_CONFIG
被忽略。 -
AUTO_INHERIT_REVERSE_FROM_CONFIG
:如果目标映射方法的源和目标类型可分配给原型方法的相应类型,则将自动继承反向配置。如果多个原型方法匹配,则必须解决歧义@InheritInverseConfiguration(name = …)
,这将导致`AUTO_INHERIT_REVERSE_FROM_CONFIG
被忽略。 -
AUTO_INHERIT_ALL_FROM_CONFIG
:配置和反向配置都将自动继承。同样的规则适用于AUTO_INHERIT_FROM_CONFIG
或AUTO_INHERIT_REVERSE_FROM_CONFIG
。
12.自定义映射
有时需要在某些映射方法之前或之后应用自定义逻辑。MapStruct提供了两种方法:装饰器允许特定映射方法的类型安全定制,以及映射前和映射后生命周期方法,允许对给定源或目标类型的映射方法进行通用定制。
12.1。使用装饰器映射自定义
在某些情况下,可能需要定制生成的映射方法,例如,在目标对象中设置不能由生成的方法实现设置的附加属性。MapStruct使用装饰器支持此要求。
使用组件模型时cdi ,请使用带有MapStruct映射器的CDI装饰器,而不是@DecoratedWith 此处描述的注释。
|
要将装饰器应用于映射器类,请使用@DecoratedWith
注释指定它。
1
2
3
4
5
6
7
8
9
10
@Mapper
@DecoratedWith(PersonMapperDecorator.class)
public interface PersonMapper {
PersonMapper INSTANCE = Mappers.getMapper( PersonMapper.class );
PersonDto personToPersonDto(Person person);
AddressDto addressToAddressDto(Address address);
}
装饰器必须是装饰映射器类型的子类型。您可以将其设置为一个抽象类,它只允许实现您要自定义的映射器接口的那些方法。对于所有未实现的方法,将使用默认生成例程生成对原始映射器的简单委派。
在PersonMapperDecorator
下面示出定制personToPersonDto()
。它设置了一个附加属性,该属性在映射的源类型中不存在。该addressToAddressDto()
方法不是自定义的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class PersonMapperDecorator implements PersonMapper {
private final PersonMapper delegate;
public PersonMapperDecorator(PersonMapper delegate) {
this.delegate = delegate;
}
@Override
public PersonDto personToPersonDto(Person person) {
PersonDto dto = delegate.personToPersonDto( person );
dto.setFullName( person.getFirstName() + " " + person.getLastName() );
return dto;
}
}
该示例显示了如何选择使用生成的默认实现注入委托,并在自定义装饰器方法中使用此委托。
对于映射器componentModel = "default"
,使用单个参数定义构造函数,该参数接受装饰映射器的类型。
当与组件模型的工作spring
或者jsr330
,这需要不同的处理。
12.1.1。具有Spring组件模型的装饰器
在@DecoratedWith
具有组件模型spring
的映射器上使用时,使用Spring注释对生成的原始映射器实现进行注释@Qualifier("delegate")
。要在装饰器中自动装配该bean,还要添加该限定符注释:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class PersonMapperDecorator implements PersonMapper {
@Autowired
@Qualifier("delegate")
private PersonMapper delegate;
@Override
public PersonDto personToPersonDto(Person person) {
PersonDto dto = delegate.personToPersonDto( person );
dto.setName( person.getFirstName() + " " + person.getLastName() );
return dto;
}
}
扩展装饰器的生成类使用Spring的@Primary
注释进行注释。要在应用程序中自动装配已装饰的映射器,不需要做任何特殊操作:
1
2
@Autowired
private PersonMapper personMapper; // injects the decorator, with the injected original mapper
12.1.2。具有JSR 330组件模型的装饰器
JSR 330没有指定限定符,只允许特定地命名bean。因此,生成的原始映射器的实现被注释@Named("fully-qualified-name-of-generated-implementation")
(请注意,当使用装饰器时,映射器实现的类名以下划线结尾)。要在装饰器中注入该bean,请将相同的注释添加到委托字段(例如,通过从生成的类复制/粘贴它):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class PersonMapperDecorator implements PersonMapper {
@Inject
@Named("org.examples.PersonMapperImpl_")
private PersonMapper delegate;
@Override
public PersonDto personToPersonDto(Person person) {
PersonDto dto = delegate.personToPersonDto( person );
dto.setName( person.getFirstName() + " " + person.getLastName() );
return dto;
}
}
与其他组件模型不同,使用站点必须知道是否装饰了映射器,对于装饰映射器,@Named
必须添加无参数注释以选择要注入的装饰器:
1
2
3
@Inject
@Named
private PersonMapper personMapper; // injects the decorator, with the injected original mapper
|
12.2。使用before-mapping和after-mapping方法映射自定义
在定制映射器时,装饰器可能并不总是满足需要。例如,如果您不仅需要为少数选定的方法执行自定义,而且还需要为映射特定超类型的所有方法执行自定义:在这种情况下,您可以使用在映射开始之前或映射完成之后调用的回调方法。
回调方法可以在抽象映射器本身,类型引用中Mapper#uses
或用作@Context
参数的类型中实现。
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
@Mapper
public abstract class VehicleMapper {
@BeforeMapping
protected void flushEntity(AbstractVehicle vehicle) {
// I would call my entity manager's flush() method here to make sure my entity
// is populated with the right @Version before I let it map into the DTO
}
@AfterMapping
protected void fillTank(AbstractVehicle vehicle, @MappingTarget AbstractVehicleDto result) {
result.fuelUp( new Fuel( vehicle.getTankCapacity(), vehicle.getFuelType() ) );
}
public abstract CarDto toCarDto(Car car);
}
// Generates something like this:
public class VehicleMapperImpl extends VehicleMapper {
public CarDto toCarDto(Car car) {
flushEntity( car );
if ( car == null ) {
return null;
}
CarDto carDto = new CarDto();
// attributes mapping ...
fillTank( car, carDto );
return carDto;
}
}
如果@BeforeMapping
/ @AfterMapping
方法具有参数,则仅在方法的返回类型(如果为非void
)可分配给映射方法的返回类型且所有参数可由映射的源或目标参数指定时,才会生成方法调用。方法:
-
注释的参数
@MappingTarget
将填充映射的目标实例。 -
注释的参数
@TargetType
将填充映射的目标类型。 -
注释
@Context
的参数使用映射方法的上下文参数填充。 -
使用映射的source参数填充任何其他参数。
对于非void
方法,如果不是,则返回方法调用的返回值作为映射方法的结果null
。
与映射方法一样,可以为before / after-mapping方法指定类型参数。
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
@Mapper
public abstract class VehicleMapper {
@PersistenceContext
private EntityManager entityManager;
@AfterMapping
protected <T> T attachEntity(@MappingTarget T entity) {
return entityManager.merge(entity);
}
public abstract CarDto toCarDto(Car car);
}
// Generates something like this:
public class VehicleMapperImpl extends VehicleMapper {
public CarDto toCarDto(Car car) {
if ( car == null ) {
return null;
}
CarDto carDto = new CarDto();
// attributes mapping ...
CarDto target = attachEntity( carDto );
if ( target != null ) {
return target;
}
return carDto;
}
}
所有之前/之后映射方法可被应用到的映射方法将被使用。基于限定符的映射方法选择可用于进一步控制可以选择哪些方法而不选择哪些方法。为此,限定符注释需要应用于before / after-method并在BeanMapping#qualifiedBy
or中引用IterableMapping#qualifiedBy
。
方法调用的顺序主要取决于它们的变体:
-
@BeforeMapping
@MappingTarget
在对源参数进行任何空值检查并构造新的目标bean之前,将调用没有参数的方法。 -
@BeforeMapping
@MappingTarget
在构造新的目标bean之后调用带有参数的方法。 -
@AfterMapping
在最后一个return
语句之前的映射方法的末尾调用方法。
在这些组中,方法调用按其定义位置排序:
-
在
@Context
参数上声明的方法,按参数顺序排序。 -
映射器本身实现的方法。
-
从
Mapper#uses()
注释中的类型声明的顺序引用的类型中的方法。 -
在一个类型中声明的方法在超类型声明的方法之后使用。
重要提示:无法保证在一种类型中声明的方法顺序,因为它取决于编译器和处理环境实现。
13.使用MapStruct SPI
13.1。自定义访问者命名策略
MapStruct提供了AccessorNamingStrategy
通过服务提供接口(SPI)覆盖的可能性。一个很好的例子是在源对象GolfPlayer
及其GolfPlayerDto
下方使用流畅的API 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class GolfPlayer {
private double handicap;
private String name;
public double handicap() {
return handicap;
}
public GolfPlayer withHandicap(double handicap) {
this.handicap = handicap;
return this;
}
public String name() {
return name;
}
public GolfPlayer withName(String name) {
this.name = name;
return this;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class GolfPlayerDto {
private double handicap;
private String name;
public double handicap() {
return handicap;
}
public GolfPlayerDto withHandicap(double handicap) {
this.handicap = handicap;
return this;
}
public String name() {
return name;
}
public GolfPlayerDto withName(String name) {
this.name = name;
return this
}
}
我们希望GolfPlayer
映射到GolfPlayerDto
类似于我们总是这样做的目标对象:
1
2
3
4
5
6
7
8
9
10
@Mapper
public interface GolfPlayerMapper {
GolfPlayerMapper INSTANCE = Mappers.getMapper( GolfPlayerMapper.class );
GolfPlayerDto toDto(GolfPlayer player);
GolfPlayer toPlayer(GolfPlayerDto player);
}
这可以通过实现SPI来实现,org.mapstruct.ap.spi.AccessorNamingStrategy
如以下示例所示。这是一个实施的org.mapstruct.ap.spi.AccessorNamingStrategy
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* A custom {@link AccessorNamingStrategy} recognizing getters in the form of {@code property()} and setters in the
* form of {@code withProperty(value)}.
*/
public class CustomAccessorNamingStrategy extends DefaultAccessorNamingStrategy {
@Override
public boolean isGetterMethod(ExecutableElement method) {
String methodName = method.getSimpleName().toString();
return !methodName.startsWith( "with" ) && method.getReturnType().getKind() != TypeKind.VOID;
}
@Override
public boolean isSetterMethod(ExecutableElement method) {
String methodName = method.getSimpleName().toString();
return methodName.startsWith( "with" ) && methodName.length() > 4;
}
@Override
public String getPropertyName(ExecutableElement getterOrSetterMethod) {
String methodName = getterOrSetterMethod.getSimpleName().toString();
return IntrospectorUtils.decapitalize( methodName.startsWith( "with" ) ? methodName.substring( 4 ) : methodName );
}
}
该CustomAccessorNamingStrategy
品牌使用的DefaultAccessorNamingStrategy
(在mapstruct处理器也可),并依赖于一流的离开最不变的默认行为。
要使用自定义SPI实现,它必须与META-INF/services/org.mapstruct.ap.spi.AccessorNamingStrategy
具有自定义实现的完全限定名称的文件一起位于单独的JAR文件中作为内容(例如org.mapstruct.example.CustomAccessorNamingStrategy
)。需要将此JAR文件添加到注释处理器类路径(即将其添加到添加了mapstruct-processor jar的位置旁边)。
更多详细信息:上面的示例存在于我们的示例存储库(https://github.com/mapstruct/mapstruct-examples)中。 |
13.2。映射排除提供程序
MapStruct提供了MappingExclusionProvider
通过服务提供程序接口(SPI)覆盖的可能性。一个很好的例子是不允许MapStruct为某种类型创建自动子映射,即MapStruct不会尝试为排除类型生成自动子映射方法。
该 |
1
2
3
4
5
6
7
8
9
10
public class Source {
static class NestedSource {
private String property;
// getters and setters
}
private NestedSource nested;
// getters and setters
}
1
2
3
4
5
6
7
8
9
10
public class Target {
static class NestedTarget {
private String property;
// getters and setters
}
private NestedTarget nested;
// getters and setters
}
1
2
3
4
5
@Mapper
public interface ErroneousCustomExclusionMapper {
Target map(Source source);
}
我们想要NestedTarget
从自动子映射方法生成中排除。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.regex.Pattern;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import org.mapstruct.ap.spi.MappingExclusionProvider;
public class CustomMappingExclusionProvider implements MappingExclusionProvider {
private static final Pattern JAVA_JAVAX_PACKAGE = Pattern.compile( "^javax?\\..*" );
@Override
public boolean isExcluded(TypeElement typeElement) {
Name name = typeElement.getQualifiedName();
return name.length() != 0 && ( JAVA_JAVAX_PACKAGE.matcher( name ).matches() ||
name.toString().equals( "org.mapstruct.ap.test.nestedbeans.exclusions.custom.Target.NestedTarget" ) );
}
}
要使用自定义SPI实现,它必须与META-INF/services/org.mapstruct.ap.spi.MappingExclusionProvider
具有自定义实现的完全限定名称的文件一起位于单独的JAR文件中作为内容(例如org.mapstruct.example.CustomMappingExclusionProvider
)。需要将此JAR文件添加到注释处理器类路径(即将其添加到添加了mapstruct-processor jar的位置旁边)。
13.3。Custom Builder Provider
MapStruct提供了DefaultProvider
通过服务提供程序接口(SPI)覆盖的可能性。一个很好的例子是为自定义构建器策略提供支持。
1
2
3
4
5
6
7
8
9
10
import javax.lang.model.type.TypeMirror;
public class NoOpBuilderProvider implements BuilderProvider {
@Override
public BuilderInfo findBuilderInfo(TypeMirror type) {
return null;
}
}