MapStruct 1.1.0.Final中文参考指南

版权声明:转载请注明出处 https://blog.csdn.net/YoshinoNanjo/article/details/81363285

序言

      这是MapStruct的参考文档,它是一个用于生成类型安全、高性能和无依赖的bean映射代码的注解处理器。本文档涵盖了MapStruct提供的所有功能。如果这个指南没有回答你所有的问题,只要加入MapStruct谷歌论坛就可以得到帮助。

       你在文档中发现了排版错误或其他错误?请通过MapStruct GitHub Repository反馈问题,或者更好的是,帮助社区修改并合并代码来修复错误!

       这项工作由Creative Commons Attribution-ShareAlike 4.0 International License授权许可。


一、简介

       MapStruct是用于生成类型安全的bean映射类的Java注解处理器

       你所要做的就是定义一个映射器接口,声明任何需要映射的方法。在编译过程中,MapStruct将生成该接口的实现。此实现使用纯Java的方法调用源对象和目标对象之间进行映射,并非Java反射机制。

       与手工编写映射代码相比,MapStruct通过生成冗长且容易出错的代码来节省时间。在配置方法的约定之后,MapStruct使用了合理的默认值,但在配置或实现特殊行为时将不再适用。

       与动态映射框架相比,MapStruct具有以下优点:

  • 使用纯Java方法代替Java反射机制快速执行
  • 编译时类型安全:只能映射彼此的对象和属性,不能映射一个Order实体到一个Customer DTO中等等
  • 如果无法映射实体或属性,则在编译时清除错误报告

二、准备

       MapStruct是基于JSR 269的Java注解处理器,因此可以在命令行构建中使用(javac、Ant、Maven等等),也可以在IDE内使用。

       它包括以下工件:

  • org.mapstruct:mapstruct:包含了必要的注解,例如@Mapping;在Java 8或更高版本中,使用org.mapstruct:mapstruct-jdk8,而不是利用Java 8中引入的语言进行改进。
  • org.mapstruct:mapstruct-processor:包含生成映射器实现的注解处理器

2.1.Apache Maven

       对于基于Maven的项目,将以下内容添加到POM文件中以使用MapStruct:

       示例1:Maven 配置

...
<properties>
    <org.mapstruct.version>1.1.0.Final</org.mapstruct.version>
</properties>
...
<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-jdk8</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 Compiler”→“Annotation Processing”→“Factory Path”。MapStruct处理器JAR包应该在那里列出并启用。通过编译器插件配置的任何处理器选项(见下文)应列在“Java Compiler”→“Annotation Processing”下。

      如果处理器没有生效,请通过M2E检查注解处理器的配置是否已启用。为此,转到"Preferences"→"Maven"→"Annotation Processing",并选择“Automatically configure JDT APT”。或者,在POM文件的属性部分指定以下内容:

<m2e.apt.activation>jdt_apt</m2e.apt.activation>

      还要确保项目使用Java 1.6或更高版本(project properties→"Java Compiler"→"Compile Compliance Level")。MapStruct无法在低版本的JDK中工作。

2.2.Gradle

       将下面的内容添加到你的Gradle构建文件中,以便启用MapStruct:

       示例2:Gradle配置

...
plugins {
    ...
    id 'net.ltgt.apt' version '0.8'
}
dependencies {
    ...
    compile 'org.mapstruct:mapstruct-jdk8:1.1.0.Final'

    apt 'org.mapstruct:mapstruct-processor:1.1.0.Final'
}
...

       你可以在GitHub上的mapstruct-examples中找到完整的示例。

2.3. Apache Ant

       将以下配置的 javac 任务添加到build.xml文件中,以便启用基于Ant的项目中的MapStruct。调整项目布局所需的路径。

       示例3:Ant配置

...
<javac
    srcdir="src/main/java"
    destdir="target/classes"
    classpath="path/to/mapstruct-jdk8-1.1.0.Final.jar">
    <compilerarg line="-processorpath path/to/mapstruct-processor-1.1.0.Final.jar"/>
    <compilerarg line="-s target/generated-sources"/>
</javac>
...

       你可以在GitHub上的mapstruct-examples中找到完整的示例。

2.4.配置选项

       可以使用注解处理器选项配置MapStruct代码生成器。

       当直接调用javac时,这些选项以表单-AKEY=value的形式传递给编译器。当通过Maven使用MapStruct时,可以使用Maven处理器插件的配置中的选项元素来传递任何处理器选项,如下所示:

          示例4:Maven配置

...
<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>
...

          示例5:Gradle配置 

...
compileJava {
    options.compilerArgs = [
        '-Amapstruct.suppressGeneratorTimestamp=true',
        '-Amapstruct.suppressGeneratorVersionInfoComment=true'
    ]
}
...

       存在以下选项:

       表格1:MapStruct处理器选项

选项 作用 默认值
mapstruct. suppressGeneratorTimestamp 如果设置为true,则在生成的映射器类中,@Generated 注解中不会生成时间戳。 false
mapstruct. suppressGeneratorVersionInfoComment 如果设置为true,则在生成的映射器类中,@Generated 注解中不会生成comment属性。comment属性包含了MapStruct的版本信息和用于注解处理的Java编译器信息。 false
mapstruct.defaultComponentModel

组件模型的名称以被生成的映射器为依据(参见:四、检索映射器)。

支持的值:

  • default:映射器不使用组件模型,实例通常通过Mappers.getMapper(Class clazz)检索。
  • cdi:生成的映射器是应用程序范围的CDI bean,可以通过@Inject注解来检索。
  • spring:生成的映射器是一个单例范围的Spring bean,可以通过@Autowired检索。
  • jsr330:生成的映射器使用{@code @Named}进行注释,并且可以通过@Inject检索,例如使用Spring。

如果通过@Mapper#componentModel()为特定映射器提供组件模型,则注解中的值优先,例如@Mapper(componentModel="spring")

default
mapstruct.unmappedTargetPolicy

在映射方法的target对象的属性没有填充source值的情况下应用的默认报告策略。

支持的值:

  • ERROR:任何未映射的target属性都将导致映射代码生成失败
  • WARN:任何未映射的target属性都会在构建时发出警告
  • IGNORE:未映射的target属性将被忽略

如果通过@Mapper#unmappedTargetPolicy()为特定映射器提供策略,注解的值优先。

WARN

2.5.在Java 9中使用MapStruct

       MapStruct可以与Java 9一起使用,但是由于Java版本尚未最终确定,因此对它的支持是实验性的。

       Java 9的一个核心主题是JDK的模块化。这样做的一个作用是,为了使用javax.annotation.Generated注解,需要为项目启用特定模块。@Generated由MapStruct添加到生成的映射器类中,以将它们标记为生成代码,说明生成日期、生成器版本等信息。

       为了允许使用@Generated的注解,必须启用模块java.annotations.common。当使用Maven时,可以这样做:

export MAVEN_OPTS="--add-modules java.annotations.common"

       如果@Generated注解不可用,MapStruct将检测到这种情况,而不是将其添加到生成的映射器中。

三、定义一个映射器

       在本节中,你将学习如何用MapStruct定义Bean映射器,以及你必须执行哪些选项。

3.1.基本映射

       要创建映射器,只需使用所需要映射的方法定义一个Java接口,并使用org.mapstruct.Mapper注解对其进行注释:

       示例6:Maven配置

@Mapper
public interface CarMapper {

    @Mappings({
        @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注解指定其名称。

      在JavaBean中定义的属性名必须在@Mapping注解中指定,例如具有存取器方法getSeatCount()和setSeatCount()的属性seatCount。

       当使用Java 8或更高版本时,可以省略@Mappings包装器注解,并在一个方法上直接指定多个@Mapping注解。

       为了更好地理解MapStruct所做的事情,请看下面的实现,MapStruct生成的carToCarDto()方法:

       示例7:MapStruct生成的代码

// 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() );
        }

        return carDto;
    }

    @Override
    public PersonDto personToPersonDto(Person person) {
        //...
    }
}

       MapStruct的一般原理是生成尽可能多的代码,就像你自己亲自编写代码一样。特别是这意味着通过普通的getter / setter方法调用而不是反射或类似的方法将值从源复制到目标。

       如示例所示,生成的代码考虑到了通过@Mapping指定的任何名称映射。如果源和目标实体中映射属性的类型不同,MapStruct将自动转换(例如对于price属性,另请参见:5.1.隐式类型转换)或者可选地调用另一个映射方法(例如,对于driver属性,请参见:5.2.映射对象引用)。

        通过创建一个包含源属性中元素的目标集合类型的新实例来复制具有相同元素类型的集合类型属性。对于具有不同元素类型的集合类型属性,每个元素将单独映射并添加到目标集合中(请参见:六、映射集合)。

        MapStruct会考虑源和目标类型的所有公共属性。这包括在父类型上声明的属性。

3.2.给映射器添加自定义方法

       在某些情况下,可能需要手动实现从一种类型映射到另一种类型的特定映射,而这是MapStruct无法生成的。一种方法是在另一个类上实现这样的方法,然后由MapStruct生成的映射器使用(参见:5.3.调用其他映射器)。

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

       举个例子,我们假设从Person到PersonDto的映射需要一些MapStruct无法生成的特殊逻辑。然后,你可以从上一个示例中定义映射器,如下所示:

       示例8:使用默认方法定义一个自定义映射的映射器

@Mapper
public interface CarMapper {

    @Mappings({...})
    CarDto carToCarDto(Car car);

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

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

       映射器也可以以抽象类的形式而不是接口的形式定义,并直接在此映射器类中实现自定义方法。在这种情况下,MapStruct将生成抽象类的扩展,并实现所有抽象方法。这种方法优于声明默认方法的一个优点是可以在映射器类中声明其他字段。

       上一个示例从Person到PersonDto的映射需要一些特殊逻辑的例子可以像这样定义:

       示例9:通过抽象类定义映射器

@Mapper
public abstract class CarMapper {

    @Mappings(...)
    public abstract CarDto carToCarDto(Car car);

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

       MapStruct将生成CarMapper的子类,其中carToCarDto()方法的实现被声明为abstract。在映射 driver 属性时,carToCarDto()中生成的代码将调用手动实现的personToPersonDto()方法。

3.3.多个源参数的映射方法

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

       示例10:多个源参数的映射方法

@Mapper
public interface AddressMapper {

    @Mappings({
        @Mapping(source = "person.description", target = "description"),
        @Mapping(source = "address.houseNo", target = "houseNumber")
    })
    DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}

       上例所示的映射方法采用两个源参数并返回组合的目标对象。与单参数映射方法一样,属性按名称映射。

       如果多个源对象定义具有相同名称的属性,则必须使用@Mapping注解指定从中检索属性的source参数,如示例中的description属性所示。如果不解决这种歧义,将会引发错误。对于在给定源对象中仅存在一次的属性,可以选择指定源参数的名称,因为它可以自动确定。

       使用@Mapping注解时,必须指定属性所在的参数。

       如果所有源参数都为null,则具有多个源参数的映射方法将返回null。否则,将实例化目标对象,并且将传递所提供参数的所有属性。

3.4.嵌套映射

       MapStruct将通过“.”符号处理嵌套映射(在源代码中):

       示例11:具有多个源参数的映射方法

@Mappings({
    @Mapping(target = "chartName", source = "chart.name"),
    @Mapping(target = "title", source = "song.title"),
    @Mapping(target = "artistName", source = "song.artist.name"),
    @Mapping(target = "recordedAt", source = "song.artist.label.studio.name"),
    @Mapping(target = "city", source = "song.artist.label.studio.city"),
    @Mapping(target = "position", source = "position")
})
ChartEntry map(Chart chart, Song song, Integer position);

       注意:参数名称(chartsongposition)是必需的,因为映射中有多个源参数。 如果只有一个源参数,则可以省略参数名称。

       MapStruct将对源中的每个嵌套属性执行空检查。

       也可以以这种方式映射非java bean源参数(如java.lang.Integer)。

3.5.更新现有的bean实例

       在某些情况下,你需要不创建目标类型的新实例,而是更新该类型的现有实例的映射。可以通过为目标对象添加参数并使用@MappingTarget标记此参数来实现此类映射。以下是一个示例:

       示例12:更新方法

@Mapper
public interface CarMapper {

    void updateCarFromDto(CarDto carDto, @MappingTarget Car car);
}

       生成的updateCarFromDto()方法代码将使用给定CarDto对象的属性更新传递的Car实例。可能只有一个参数标记为映射目标。你也可以将方法的返回类型设置为目标参数的类型,而不是void,这将导致生成的实现更新传递的映射目标并返回它。这允许对映射方法进行流畅的调用。

       目标bean的集合或Map类型的属性将在更新中被清除,然后使用相应源集合或Map中的值填充这些属性。

四、检索映射器

4.1.映射器工厂

       可以通过org.mapstruct.factory.Mappers类检索映射器实例。 只需调用getMapper()方法,传递映射器的接口类型即可返回:

       示例13:使用映射器工厂

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

       按照惯例,映射器接口应定义一个名为 INSTANCE 的成员变量,该成员变量包含映射器类型的单个实例:

       示例14:声明映射器的实例

@Mapper
public interface CarMapper {

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

    CarDto carToCarDto(Car car);
}

       这种方式使得客户端可以非常轻松地使用映射器对象而无需重复实例化新实例:

       示例15:访问映射器

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

       请注意,MapStruct生成的映射器是线程安全的,因此可以安全地从多个线程同时访问它们。

4.2.使用依赖注入

       如果您正在使用依赖注入框架,如CDI(Java^{TM}的Contexts and Dependency Injection)或Spring Framework,建议通过依赖注入获取映射器对象。 为此,您可以指定生成映射器类的组件模型应基于@Mapper#componentModel或使用配置选项(参见:2.4.配置选项)中所描述的处理器选项。

       目前支持CDI和Spring(后者通过其自定义注解或使用JSR 330注解)。有关componentModel属性的允许值,请参阅配置选项(参见:2.4.配置选项),这些值与mapstruct.defaultComponentModel处理器选项相同。在这两种情况下,所需的注解都将添加到生成的映射器实现类中,以便使相同的主体依赖注入。 以下展示了使用CDI的示例:

       示例16:使用CDI组件模型的映射器

@Mapper(componentModel = "cdi")
public interface CarMapper {

    CarDto carToCarDto(Car car);
}

       生成的映射器实现将使用@ApplicationScoped注解进行标记,因此可以使用@Inject注解将其注入到字段,构造函数等参数中:

       示例17:通过依赖注入获取映射器

@Inject
private CarMapper mapper;

       使用其他映射器类的映射器(参见:5.3.调用其他映射器)将使用配置的组件模型获取这些映射器。 因此,如果上一个示例中的CarMapper使用另一个映射器,则此另一个映射器也必须是可注入的CDI bean。

五、数据类型转换

       映射属性在源对象和目标对象中具有相同的类型,这种情况不全有。 例如,属性在源bean中可以是int类型,但在目标bean中可以是Long类型。

       另一个例子是对其他对象的引用,这些对象应该映射到目标模型中的相应类型。 例如:Car类可能有一个Person类型的属性 driver ,在映射Car对象时需要将其转换为PersonDto对象。

       在本节中,你将了解MapStruct如何处理此类数据类型转换。

5.1.隐式类型转换

       在许多情况下,MapStruct会自动处理类型转换。 例如,如果属性在源bean中的类型为int,但在目标bean中的类型为String,则生成的代码将分别通过调用String#valueOf(int)和Integer#parseInt(String)来透明地执行转换。

       目前,自动应用以下转化:

  • 在所有Java基本数据类型和它们相应的包装器类型之间,例如,int和Integer之间,boolean和Boolean等。生成的代码是null,即,当将包装器类型转换成相应的基本类型时,将执行null检查。
  • 在所有Java基本数字类型和包装器类型之间,例如,int和long之间或者byte和Integer之间。

       从较大的数据类型转换为较小的数据类型(例如从long转换为int)可能导致值或精度损失。在将来的MapStruct版本中,可以选择在这种情况下发出警告。

  • 在所有Java基本类型(包括它们的包装器)和String之间,例如int和String之间或Boolean和String之间。可以指定java.text.DecimalFormat理解的格式字符串。

       示例18:从int转换为String

@Mapper
public interface CarMapper {

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

    @IterableMapping(numberFormat = "$#.00")
    List<String> prices(List<Integer> prices);
}
  • 在枚举类型和字符串之间
  • 大数字类型(java.math.BigInteger,java.math.BigDecimal)和Java基本类型(包括它们的包装器)以及String之间。可以指定java.text.DecimalFormat理解的格式字符串。

       示例19:从BigDecimal到String的转换

@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之间。可以通过dateFormat选项指定java.text.SimpleDateFormat可以理解的格式化字符串,如下所示:

       示例20:从Date到String的转换

@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之间。可以通过dateFormat选项指定java.text.SimpleDateFormat可以理解的格式化字符串(参见上文)
  • 在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 8的Date-Time包中,java.time.ZonedDateTime,java.time.LocalDateTime,java.time.LocalDate,java.time.LocalTime和String之间。可以通过dateFormat选项指定java.text.SimpleDateFormat可以理解的格式化字符串(参见上文)
  • 在Java 8的Date-Time包中,java.time.ZonedDateTime和java.util.Date之间,当从给定Date映射ZonedDateTime时,使用系统默认时区
  • 在Java 8的Date-Time包中,java.time.LocalDateTime和使用UTC时区的java.util.Date之间
  • 在Java 8的Date-Time包中,java.time.LocalDate和使用UTC时区的java.util.Date之间
  • 在Java 8的Date-Time包中,java.time.ZonedDateTime和java.util.Calendar之间
  • java.sql.Date和java.util.Date之间
  • java.sql.Time和java.util.Date之间
  • java.sql.Timestamp和java.util.Date之间
  • 从String转换时,省略Mapping#dateFormat,会使用默认语言环境的默认模式和日期格式符号。此规则的一个例外是XmlGregorianCalendar,它将根据XML Schema 1.0 Part 2,Section 3.2.7-14.1,Lexical Representation解析String。

5.2.映射对象引用

       通常,对象不仅具有基本属性,还具有其他对象的引用。 例如,Car类可以包含对Person对象(表示汽车的司机)的引用,该对象应该映射到CarDto类引用的PersonDto对象。

       在这种情况下,只需为引用的对象类型定义一个映射方法:

       示例21:使用另一种映射方法的映射器

@Mapper
public interface CarMapper {

    CarDto carToCarDto(Car car);

    PersonDto personToPersonDto(Person person);
}

       生成的carToCarDto()方法代码将调用personToPersonDto()方法来映射 driver 属性,而生成的personToPersonDto()实现则执行了person对象的映射。

       这样就可以映射任意深度的对象。从实体映射到数据传输对象时,在某个点切割对其他实体的引用通常很有用。为此,实现一个自定义映射方法(参见下一节),例如将引用的实体映射到目标对象中的id。

       在生成映射方法的实现时,MapStruct将对源和目标对象中的每个“属性对”应用以下规则:

  • 如果源和目标属性具有相同的类型,则该值将简单地从源复制到目标。如果属性是集合(例如List),则集合的副本将被设置到目标属性中。
  • 如果源属性和目标属性类型不同,请检查是否存在另一种映射方法,该方法将源属性的类型作为参数类型,并将目标属性的类型作为返回类型。如果存在这样的方法,则将在生成的映射实现中调用它。
  • 如果不存在此类方法,MapStruct将查看是否存在属性的源类型和目标类型的内置转换。如果是这种情况,生成的映射代码将应用此转换。
  • 否则,将在构建时引发错误,指示不可映射的属性。

5.3.调用其他映射器

       除了在同一映射器类型上定义的方法之外,MapStruct还可以调用其他类中定义的映射方法,无论是MapStruct生成的映射器还是手写映射方法。这对于在几个类中构建映射代码(例如每个应用程序模块使用一个映射器类型)或者如果要提供MapStruct无法生成的自定义映射逻辑时非常有用。

       例如Car类可能包含属性manufacturingDate,而相应的DTO属性是String类型。为了映射这个属性,你可以实现一个映射器类,如下所示:

       示例22:手动实现的映射器类

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 );
        }
    }
}

       在CarMapper接口的@Mapper注解中引用DateMapper类,如下所示:

       示例23:引用另一个映射器类

@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.4.将映射目标类型传递给自定义映射器

       当使用@Mapper#uses()将自定义映射器连接到生成的映射器时,可以在自定义映射方法中定义类型Class(或其父类型)的附加参数,以便执行特定目标对象类型的常规映射任务。该属性必须使用@TargetType注解,以便MapStruct生成传递代表目标bean的相应属性类型的类实例的调用。

       例如,CarDto具有一个 Reference 类型的属性 owner ,该属性包含一个Person实体的主键。你现在可以创建一个通用的自定义映射器,将任何 Reference 对象解析为其对应的托管JPA实体实例。

       示例24:映射目标类型作为参数的映射方法

@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将生成如下内容:

       示例25:生成的代码

//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.5.映射方法解析

       将属性从一种类型映射到另一种类型时,MapStruct会查找将源类型映射到目标类型最具体的方法。 该方法可以在同一个映射器接口上声明,也可以在另一个通过@Mapper#uses()注册的映射器上声明。这同样适用于工厂方法(参见:八、对象工厂)。

       用于查找映射方法或工厂方法的算法尽可能的类似于Java的方法解析算法。特别是,具有更具体的源类型的方法将优先(例如,如果有两种方法,一种映射搜索到的源类型,另一种映射父类型的方法)。如果找到多个具体的方法,将引发错误。

       使用JAXB时,例如将String转换为相应的JAXBElement <String>时,MapStruct会在查找映射方法时考虑@XmlElementDecl注解的范围和名称属性。这可确保创建的JAXBElement实例具有正确的QNAME值。你可以在此处找到映射JAXB对象的测试。

5.6.基于限定符的映射方法选择

       在许多情况下,需要使用具有不同行为的相同方法签名(除了方法名以外)的映射方法。MapStruct有一个方便的机制来处理这种情况:@Qualifier。“限定符(qualifier)”是用户可以编写的自定义注解,修饰包含在使用的映射器中的方法,并且可以在bean属性映射,可迭代映射或map映射中引用。多个限定符可以修饰在方法和映射上。

       所以,假设有一个手写的方法,入参类型和返回值类型均是String,该类中也包括许多其他具有相同的入参类型和返回值类型的映射器:

       示例26:具有相同源和目标类型的几种映射方法

public class Titles {

    public String translateTitleEG(String title) {
        // some mapping logic
    }

    public String translateTitleGE(String title) {
        // some mapping logic
    }
}

       还有一个使用上述这个手写映射器的映射器,其中源和目标具有应该映射的属性“title”:

       示例27:映射器导致模糊映射方法错误

@Mapper( uses = Titles.class )
public interface MovieMapper {

     GermanRelease toGerman( OriginalRelease movies );

}

       如果不使用限定符,这将导致模糊的映射方法错误,因为找到了2个限定方法(translateTitleEG,translateTitleGE),而MapStruct没有提示选择哪一个。

       键入限定符方法:

       示例28:声明限定符类型

@Qualifier
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface TitleTranslator {
}

       并且,一些限定符用于指示使用哪个翻译器从源语言映射到目标语言:

       示例29:为映射方法声明限定符类型

@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface EnglishToGerman {
}
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface GermanToEnglish {
}

       请注意TitleTranslator在类级别上,EnglishToGerman和GermanToEnglish在方法级别上的保留。

       然后,使用限定符,映射可能如下所示:

       示例30:使用限定符的映射器

@Mapper( uses = Titles.class )
public interface MovieMapper {

     @Mapping( target = "title", qualifiedBy = { TitleTranslator.class, EnglishToGerman.class } )
     GermanRelease toGerman( OriginalRelease movies );

}

       示例31:用自定义映射器注解修饰方法

@TitleTranslator
public class Titles {

    @EnglishToGerman
    public String translateTitleEG(String title) {
        // some mapping logic
    }

    @GermanToEnglish
    public String translateTitleGE(String title) {
        // some mapping logic
    }
}

       请确保使用的保留策略为CLASS级别,即@Retention(RetentionPolicy.CLASS)

       使用限定符注解的类/方法将不能修饰任何没有 qualifiedBy 的元素的映射。

       bean映射上也存在相同的机制:@BeanMapping#qualifiedBy:它选择用指明的限定符标记的工厂方法。

       在许多情况下,声明一个新的注解以帮助选择过程,对于您尝试实现的目标来说可能太多了。对于这些情况,MapStruct具有@Named注解。此注解是预定义的限定符(使用@Qualifier本身注解),可用于命名映射类,或者更直接地通过其值来命名映射方法。上面的例子如下:

       示例32:自定义映射器,通过@Named注解修饰方法

@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
    }
}

       示例33:使用命名的映射器

@Mapper( uses = Titles.class )
public interface MovieMapper {

     @Mapping( target = "title", qualifiedByName = { "TitleTranslator", "EnglishToGerman" } )
     GermanRelease toGerman( OriginalRelease movies );

}

       虽然使用的机制是相同的,但用户必须更加小心。在IDE中重构已定义限定符的名称也会巧妙地重构所有其他事件。这显然不是更改名称的情况。

六、映射集合

       集合类型(List,Set等)的映射方式和bean类型的映射方式相同,即通过在映射器接口中定义具有所需源和目标类型的映射方法。 MapStruct支持Java集合框架中的各种可迭代类型。

       生成的代码将包含一个循环,该循环遍历源集合,转换每个元素并将其放入目标集合中。 如果在给定的映射器或它使用的映射器中找到集合元素类型的映射方法,则调用此方法来执行元素转换。 或者,如果存在源元素类型和目标元素类型的隐式转换,则将调用此转换程序。 以下是一个示例:

       示例34:具有集合映射方法的映射器

@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()方法,如下所示:

       示例35:生成的集合映射方法

//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的集合类型属性时,MapStruct将查找具有匹配参数和返回类型的集合映射方法,例如,从Car#passengers(List <Person>类型)到CarDto#passengers(List <PersonDto>类型)。

       示例36:使用集合映射方法来映射bean属性

//GENERATED CODE
carDto.setPassengers( personsToPersonDtos( car.getPassengers() ) );
...

       一些框架和库只暴露JavaBeans的getters方法,但对于集合类型却没有setters方法。默认情况下,使用JAXB从XML模式生成的类型遵循此模式。在这种情况下,用于映射此类属性的生成代码将调用其getters方法并添加所有映射元素:

       示例37:用于集合映射的补充方法的使用

//GENERATED CODE
carDto.getPassengers().addAll( personsToPersonDtos( car.getPassengers() ) );
...

       不允许使用可迭代源和不可迭代目标来声明映射方法,反之亦然。检测到这种情况时会出错。

6.1.映射Map

       MapStruct还支持基于Map的映射方法。以下是一个示例:

       示例38:Map映射方法

public interface SourceTargetMapper {

    @MapMapping(valueDateFormat = "dd.MM.yyyy")
    Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source);
}

       与可迭代映射类似,生成的代码将遍历源Map,转换每个值和键(通过隐式转换或通过调用另一个映射方法)并将它们放入目标映射中:

       示例39:生成的Map映射方法的实现

//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.集合映射策略(Collection mapping strategies)

       MapStruct有一个CollectionMappingStrategy,其值可能为:ACCESSOR_ONLY,SETTER_PREFERRED和ADDER_PREFERRED。

       在下表中,短划线“-”表示属性名称。然后,后缀“s”表示复数形式。该表解释了每个选项的意思以及它们如何应用于目标对象上的set-s,add-,和/或get-s方法的存在/缺失:

       表格2:集合映射策略选项

选项 目标只有set-s可用 目标只有add-可用 set-s和add-都可用 set-s和add-都不可用 现有目标(@TargetType)
ACCESSOR_ONLY set-s get-s set-s get-s get-s
SETTER_PREFERRED set-s add- set-s get-s get-s
ADDER_PREFERRED set-s add- add- get-s get-s

       一些背景:通常在生成(JPA)实体的情况下使用adder方法,以将单个元素(实体)添加到基础集合。调用adder方法在parent(调用adder方法的bean(实体))和子(ren)(集合中的元素(实体))之间建立父子关系。为了找到合适的adder方法,MapStruct将尝试在底层集合的泛型参数类型和候选adder方法的单个参数之间进行匹配。当有更多候选者时,复数setter / getter名称将转换为单数,并将用于匹配。

       选项DEFAULT不应该被明确使用。它用于区分用户明确希望从@Mapper中的隐式Mapstruct选项覆盖@MapperConfig中的默认值。选项DEFAULT与ACCESSOR_ONLY同义。

       使用adder方法和JPA实体时,Mapstruct会假定使用集合实现(例如ArrayList)初始化目标集合。你可以使用工厂创建具有初始化集合的新目标实体,而不是Mapstruct通过其构造函数创建目标实体。

6.3.用于集合映射的实现类型

       当iterable或map映射方法将接口类型声明为返回类型时,其实现类型之一将在生成的代码中实例化。 下表展示了在生成的代码中实例化的受支持的接口类型及其相应的实现类型:

       表格3:集合映射实现类型

接口类型 实现类型
Iterable ArrayList
Collection ArrayList
List ArrayList
Set HashSet
SortedSet TreeSet
NavigableSet TreeSet
Map HashMap
SortedMap TreeMap
NavigableMap TreeMap
ConcurrentMap ConcurrentHashMap
ConcurrentNavigableMap ConcurrentSkipListMap

七、映射值

7.1.映射枚举类型

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

       默认情况下,源枚举中的每个常量都映射到目标枚举类型中具有相同名称的常量。如果需要,可以使用@ValueMapping注解将源枚举中的常量映射到具有其他名称的常量。源枚举中的几个常量可以映射到目标类型中的相同常量。

       以下是一个示例:

       示例40:枚举映射方法

@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);
}

       示例41:枚举映射方法结果

// 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_;
    }
}

       默认情况下,如果源枚举类型的常量在目标类型中没有具有相同名称的相应常量,并且未通过@ValueMapping映射到另一个常量,则MapStruct将会报错。这可确保以安全且可预测的方式映射所有常量。如果由于某种原因发生了无法识别的源值,则生成的映射方法将抛出IllegalStateException。

       MapStruct还有一种机制,用于将任何剩余(未指定)的映射映射到默认值上。这只能在一组值映射中使用一次。它有两种形式:<ANY_REMAINING>和<ANY_UNMAPPED>。

       在源<ANY_REMAINING>的情况下,MapStruct会继续将源枚举常量映射到具有相同名称的目标枚举常量。 源枚举常量的其余部分将映射到带有<ANY_REMAINING>源的@ValueMapping中指定的目标。

       MapStruct不会为<ANY_UNMAPPED>尝试这种基于名称的映射,并直接将带有<ANY_UNMAPPED>源的@ValueMapping中指定的目标应用于剩余部分。

       MapStruct能够通过<NULL>关键字处理空源和空目标。

       MappingConstants类中提供了<ANY_REMAINING>,<ANY_UNMAPPED>和<NULL>的常量。

       最后,@InheritInverseConfiguration注解和@InheritConfiguration注解可以与@ValueMappings结合使用。

       示例42:枚举映射方法,<NULL>和<ANY_REMAINING>

@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);
}

       示例43:枚举映射方法结果,<NULL>和<ANY_REMAINING>

// 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_;
    }
}

       注意:当使用<ANY_UNMAPPED>而不是<ANY_REMAINING>时,MapStruct将避免映射RETAIL和B2B。

       通过@Mapping注解将枚举映射到枚举是被弃用的。它将从MapStruct的未来版本中删除。请调整现有的枚举映射方法,以便使用@ValueMapping。

八、对象工厂

       默认情况下,用于将一种bean类型映射到另一种bean类型的生成代码将调用默认构造函数来实例化目标类型。

       或者,您可以插入自定义对象工厂,这些工厂将被调用以获取目标类型的实例。一个用例就是JAXB,它创建了ObjectFactory类来获取模式类型的新实例。

       要使用自定义工厂,请通过@Mapper#uses()注册它们,如5.3.调用其他映射器中所述。在创建bean映射的目标对象时,MapStruct将查找无参数方法,或者仅使用一个返回所需目标类型的@TargetType参数的方法,并调用此方法而不是调用默认构造函数:

       示例44:自定义对象工厂

public class DtoFactory {

     public CarDto createCarDto() {
         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 {

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

    CarDto carToCarDto(Car car);

    Car carDtoToCar(CarDto carDto);
}
//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;
    }
}

九、高级映射选项

       本节介绍了几个高级选项,可以根据需要微调生成的映射代码。

9.1.默认值和常量

       如果相应的source属性为null,则可以指定默认值以将预定义值设置为目标属性。在任何情况下都可以指定常量来设置这样的预定义值。默认值和常量被指定为String值,并且可以通过内置转换或其他映射方法的调用进行类型转换,以匹配target属性所需的类型。

      具有常量的映射不得包含对source属性的引用。以下示例显示了使用默认值和常量的一些映射:

      示例45:使用默认值和常量的映射方法

@Mapper(uses = StringListMapper.class)
public interface SourceTargetMapper {

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

    @Mappings( {
        @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,则目标属性longProperty将设置为-1。字符串“常量值”按原样设置为目标属性stringConstant。值“3001”被类型转换为目标属性longWrapperConstant的Long(包装器)类。日期属性还需要日期格式。常量“jack-jill-tom”演示了如何调用手写类StringListMapper以将以短划线分隔的列表映射到List <String>。

9.2.表达式

       通过表达式,可以包含来自多种语言的结构。

       目前只支持Java语言。此功能,举个例子,用于调用构造函数。整个源对象可在表达式中使用。应该注意只插入有效的Java代码:MapStruct不会在生成时验证表达式,但在编译期间会在生成的类中显示错误。

       下面的示例演示了如何将两个源属性映射到一个目标:

       示例46:使用表达式的映射方法

@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);
}

       该示例演示了如何将源属性时间和格式组合到一个目标属性TimeAndFormat中。请注意,指定了完全限定的包名称,因为MapStruct不会处理TimeAndFormat类的导入(除非在SourceTargetMapper中以其他方式显式使用)。 这可以通过在@Mapper注解上定义导入来解决。

       示例47:声明导入

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);
}

9.3.确定结果类型

       当结果类型具有继承关系时,选择映射方法(@Mapping)或工厂方法(@BeanMapping)可能变得不明确。假设一个Apple和一个Banana,它们都是Fruit的一种。

       示例48:指定bean映射方法的结果类型

@Mapper( uses = FruitFactory.class )
public interface FruitMapper {

    @BeanMapping( resultType = Apple.class )
    Fruit map( FruitDto source );

}
public class FruitFactory {

    public Apple createApple() {
        return new Apple( "Apple" );
    }

    public Banana createBanana() {
        return new Banana( "Banana" );
    }
}

       那么,哪个Fruit必须在映射方法 Fruit map(FruitDto source);中使用?香蕉还是苹果?以下是@BeanMapping#resultType派上用场。 它控制工厂方法进行选择,或缺少工厂方法时以创建返回类型。

       映射中存在相同的机制:@Mapping#resultType并且工作方式与预期的一样:它选择具有所需结果类型的映射方法(如果存在)。

       该机制也存在于可迭代映射和Map映射中。@IterableMapping#elementTargetType用于在生成的Iterable中选择具有所需元素的映射方法。对于@MapMapping,通过#MapMapping#keyTargetType和MapMapping#valueTargetType达到类似的目的。

9.4.控制“null”参数的映射结果

       当映射方法的source参数等于null时,MapStruct提供对要创建的对象的控制。默认情况下,将返回null。

       但是,通过@BeanMapping,@IterableMapping,@MapMapping或@Mapper或@MappingConfig上的全局指定nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT,可以更改映射结果以返回空的默认值。这意味着:

  • Bean映射:将返回一个“empty”目标bean,除常量和表达式外,它们将在存在时填充。
  • 基本数据类型:将返回基本数据类型的默认值,例如boolean类型的默认值时false,int类型的默认值时0。
  • 集合/数组(Iterables/Arrays):将返回一个空(empty)的iterable。
  • Maps:将返回一个空(empty)的Map。

       该策略以等级方式运作。在映射方法级别上设置nullValueMappingStrategy将覆盖@Mapper#nullValueMappingStrategy,而@Mapper#nullValueMappingStrategy将覆盖@MappingConfig#nullValueMappingStrategy。

9.5.控制bean映射中“null”属性的检查结果

       MapStruct控制何时生成空检查。默认情况下(nullValueCheckStrategy = NullValueMappingStrategy.ON_IMPLICIT_CONVERSION)将为以下内容生成空检查:

  • 当目标是基本数据类型且源不是的时候,将源值直接设置为目标值。
  • 应用类型转换,然后:
    1. 在目标上调用setter方法。
    2. 调用另一个类型转换,然后调用目标上的setter方法。
    3. 调用映射方法,然后调用目标上的setter方法。

       首先在源属性上调用映射方法不受null检查的保护。因此,生成的映射方法将在对源属性执行映射之前进行空检查。手写的映射方法必须处理空值检查。他们有可能将“meaning”添加到null。例如:将null映射到默认值。

       除非在源bean上定义了源存在检查器,否则当源不是基本数据类型时,nullValueCheckStrategy = NullValueMappingStrategy.ALWAYS选项将始终包含空检查。

       该策略以等级方式运作。@Mapper#nullValueMappingStrategy将覆盖@MappingConfig#nullValueMappingStrategy。

9.6.源存在检查

       某些框架生成具有源存在检查器的bean属性。通常有这样的一种形式的方法hasXYZ,XYZ是bean映射方法中源bean的属性。当找到这样的hasXYZ方法时,MapStruct将调用此hasXYZ而不是执行空检查。

       可以在MapStruct服务提供程序接口(SPI)中更改源存在检查程序名称。它也可以通过这种方式停用。

9.7.例外

       调用应用程序可能需要在调用映射方法时处理异常。这些异常可以通过手写逻辑以及生成的内置映射方法或MapStruct的类型转换来抛出。当调用应用程序需要处理异常时,可以在映射方法中定义throws子句:

       示例49:使用自定义方法声明已检查异常的映射器

@Mapper(uses = HandWritten.class)
public interface CarMapper {

    CarDto carToCarDto(Car car) throws GearException;
}

       手写逻辑可能如下所示:

       示例50:声明已检查异常的自定义映射方法

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子句:

       示例51:生成的实现代码中的try-catch块

// 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对象的含义做出假设。手写代码必须处理这个问题。

十、重用映射配置

       本节讨论了为多种映射方法重用映射配置的不同方法:来自其他方法的配置的“继承”和在多个映射器类型之间共享中央配置。

10.1.映射配置继承

       可以使用注解@InheritConfiguration将方法级配置注解(如@Mapping,@BeanMapping,@IterableMapping等)从一个映射方法继承到类似方法:

       示例52:继承其配置的更新方法

@Mapper
public interface CarMapper {

    @Mapping(target = "numberOfSeats", source = "seatCount")
    Car carDtoToCar(CarDto car);

    @InheritConfiguration
    void carDtoIntoCar(CarDto carDto, @MappingTarget Car car);
}

       上面的示例声明了一个映射方法carToDto(),其配置用于定义如何映射Car类型中的属性numberOfSeats。在现有Car实例上执行映射的更新方法需要相同的配置才能成功映射所有属性。在方法上声明@InheritConfiguration允许MapStruct搜索继承候选以应用从中继承的方法的注解。

       如果A的所有类型(源类型和结果类型)可分配给B的相应类型,则一方法A可以从另一方法B继承配置。需要在当前映射器,父类/接口或共享配置界面(如10.3.共享配置中所述)中定义被认为是继承的方法。

       如果多个方法用于继承,则必须在注解中指定方法名称:@InheritConfiguration(name =“carDtoToCar”)。

       方法可以使用@InheritConfiguration并通过另外应用@Mapping,@BorderMapping等来覆盖或修改配置。

10.2.反向映射

       在双向映射的情况下,例如,从实体到DTO以及从DTO到实体,正向方法和反向方法的映射规则通常是相似的,并且可以通过切换源和目标来简单地反转。

       使用注解@InheritInverseConfiguration指示方法应继承相应反向方法的反向配置。

       示例53:反向映射方法继承其配置

@Mapper
public interface CarMapper {

    @Mapping(source = "numberOfSeats", target = "seatCount")
    CarDto carToDto(Car car);

    @InheritInverseConfiguration
    Car carDtoToCar(CarDto carDto);
}

       这里carDtoToCar()方法是carToDto()的反向映射方法。请注意,来自carToDto()的任何属性映射也将应用于相应的反向映射方法。它们会自动反转并使用@InheritInverseConfiguration注解复制到方法中。

       来自反转方法的特定映射可以(可选地)被映射中的ignore,expression或constant覆盖,例如,像这样:@Mapping(target = "numberOfSeats", ignore=true)。

       如果A的结果类型与B的单一源类型相同并且A的单一源类型与B的结果类型相同,则方法A被认为是方法B的反向方法。

       需要在当前映射器(父类/接口)中定义考虑用于反向继承的方法。

       如果多个方法符合条件,则需要使用name属性指定从中继承配置的方法:@InheritInverseConfiguration(name = "carToDto")。

       表达式和常量被排除(默默忽略)。从1.1.0.Beta2开始,嵌套源属性的反向映射是实验性的。当源属性名称和目标属性名称相同时,将自动进行反向映射。否则,@Mapping应指定目标名称和源名称。在所有情况下,需要采用合适的映射方法进行反向映射。

10.3.共享配置

       MapStruct提供了通过指向使用@MapperConfig注解的中央接口来定义共享配置的可能性。要使映射器使用共享配置,需要在@Mapper#config属性中定义配置接口。

       @MapperConfig注解与@Mapper注解具有相同的属性。任何未通过@Mapper提供的属性都将从共享配置继承。@Mapper中指定的属性优先于通过引用的配置类指定的属性。List属性(如uses)可以简单地合并起来:

       示例54:映射配置类和使用它的映射类

@MapperConfig(
    uses = CustomMapperViaMapperConfig.class,
    unmappedTargetPolicy = ReportingPolicy.ERROR
)
public interface CentralConfig {
}
@Mapper(config = CentralConfig.class, uses = { CustomMapperViaMapper.class } )
// Effective configuration:
// @Mapper(
//     uses = { CustomMapperViaMapper.class, CustomMapperViaMapperConfig.class },
//     unmappedTargetPolicy = ReportingPolicy.ERROR
// )
public interface SourceTargetMapper {
  ...
}

       保存@MapperConfig注解的接口也可以声明可用于从中继承方法级映射注解的映射方法的原型。此类原型方法并不意味着要作为映射器API的一部分来实现或使用。

       示例55:使用原型方法的映射配置类

@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);
}
@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注解,并且源和目标类型可分配给原型方法的相应类型,配置只会被继承,这在10.1.映射配置继承中有描述。
  • AUTO_INHERIT_FROM_CONFIG:如果目标映射方法的源和目标类型可分配给原型方法的相应类型,则配置将自动继承。如果多个原型方法匹配,则必须使用@InheritConfiguration(name = ...)解决歧义。

十一、自定义映射

       有时候需要在某些映射方法之前或之后执行自定义逻辑。MapStruct提供了两种方法:允许对特定映射方法进行类型安全定制的装饰器以及允许对给定源或目标类型的映射方法进行通用定制的映射前和映射后的生命周期方法。

11.1.使用装饰器进行映射定制

       在某些情况下,可能需要定制生成的映射方法,例如,在目标对象中设置一个无法由生成的方法实现设置的附加属性。 MapStruct使用装饰器支持此要求。

       使用组件模型cdi时,请使用带有MapStruct映射器的CDI装饰器,而不是此处描述的@DecoratedWith注解。

       要将装饰器应用于映射器类,请使用@DecoratedWith注解指定它。

       示例56:应用装饰器

@Mapper
@DecoratedWith(PersonMapperDecorator.class)
public interface PersonMapper {

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

    PersonDto personToPersonDto(Person person);

    AddressDto addressToAddressDto(Address address);
}

       装饰器必须是装饰映射器类型的子类型。你可以将其设置为一个抽象类,它只允许实现你要自定义的映射器接口的那些方法。对于所有未实现的方法,将使用默认生成规则生成对原始映射器的简单委派。

       下面显示的PersonMapperDecorator自定义personToPersonDto()。它设置了一个附加属性,该属性在映射的源类型中不存在。addressToAddressDto()方法未自定义。

       示例57:实现装饰器

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时,需要以不同方式处理。

11.1.1.使用Spring组件模型的装饰器

       在使用组件模型spring的映射器上使用@DecoratedWith时,生成的原始映射器实现将使用Spring注解@Qualifier(“delegate”)。

       示例58:基于Spring的装饰器

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注解。要在应用程序中自动装配已装饰的映射器,不需要做任何特殊操作:

       示例59:使用已装饰过的映射器

@Autowired
private PersonMapper personMapper; // injects the decorator, with the injected original mapper

11.1.2.使用JSR 330组件模型的装饰器

       JSR 330没有指定限定符,只允许特定地命名bean。因此,生成的原始映射器的实现使用@Named注解(“完全限定名生成的实现”)(请注意,使用装饰器时,映射器实现的类名以下划线结尾)。要在装饰器中注入该bean,请将相同的注解添加到委托字段(例如,通过从生成的类复制/粘贴它):

       示例60:基于JSR 330的装饰器

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注解以选择要注入的装饰器:

       示例61:使用带有JSR 330的装饰映射器

@Inject
@Named
private PersonMapper personMapper; // injects the decorator, with the injected original mapper

       @DecoratedWith结合组件模型JSR 330是1.0.0.CR2版本的实验性功能。在装饰器中引用原始映射器的方式或装饰映射器在应用程序代码中的注入方式可能仍会发生变化。

11.2.使用before-mapping和after-mapping方法进行映射定制

       在定制映射器时,装饰器可能并不总是满足需求。例如,如果你需要不仅针对少数选定方法执行自定义逻辑,而且针对映射特定父类型的所有方法执行自定义逻辑:在这种情况下,你可以使用在映射开始之前或映射完成之后调用的回调方法。

       回调方法可以在抽象映射器本身中实现,也可以在Mapper#uses中的类型引用中实现。

       示例62:使用@BeforeMapping和@AfterMapping注解的映射器

@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;
        }
        // ...

        fillTank( car, carDto );

        return carDto;
    }
}

       只有返回类型为void的方法可以使用@BeforeMapping或@AfterMapping进行注解。这些方法可能有也可能没有参数。

       如果@BeforeMapping / @AfterMapping方法具有参数,则只有在所有参数都可以由映射方法的源或目标参数指定时,才会生成方法调用:

  • 使用@MappingTarget注解的参数将填充映射的目标实例。
  • 使用@TargetType注解的参数将填充映射的目标类型。
  • 使用映射的source参数填充任何其他参数,而每个源参数最多使用一次。

       将使用所有那些可应用于映射方法的before/after-mapping方法。5.6.基于限定符的映射方法选择可用于进一步控制可以选择哪些方法以及不可以选择哪些方法。为此,限定符注解需要应用于before / after-method并在BeanMapping#qualifiedBy或IterableMapping#qualifiedBy中引用。

       应用所选方法的顺序大致取决于它们的定义位置(如果您需要依赖它们的顺序,您也应该将考虑代码风格):

  • 无法保证一种类型内的方法顺序,因为它取决于编译器和处理环境实现。
  • 在一个类型中声明的方法在父类型声明的方法之后使用。
  • 在Mapper#uses中引用的类型的方法之前,使用映射器本身实现的方法。
  • Mapper#uses中引用的类型按照注解中指定的顺序搜索before / after-mapping方法。

       @BeforeMapping和@AfterMapping在1.0.0.CR1版本中是实验性功能。选择适用于映射方法的before/after mapping方法的细节或它们被调用的顺序可能仍然会改变。

十二、使用MapStruct SPI

12.1.自定义访问者命名策略

       MapStruct提供了通过服务提供接口(SPI)覆盖AccessorNamingStrategy的可能性。一个很好的例子是在下面的源对象GolfPlayer和GolfPlayerDto上使用流畅的API。

       示例63:使用流畅API的源对象GolfPlayer

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;
    }
}

       示例64:使用流畅API的源对象GolfPlayerDto

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,就像我们“总是”这样做:

       示例65:使用流畅API的源对象

@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 :

       示例66:CustomAccessorNamingStrategy

/**
 * 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 Introspector.decapitalize( methodName.startsWith( "with" ) ? methodName.substring(  4 ) : methodName );
    }
}

       CustomAccessorNamingStrategy使用DefaultAccessorNamingStrategy(也可在mapstruct-processor中使用)并依赖该类来保持大多数默认行为不变。

       要使用自定义SPI实现,它必须与文件META-INF / services / org.mapstruct.ap.spi.AccessorNamingStrategy一起位于单独的.jar文件中,并将自定义实现的完全限定名称作为内容(例如org.mapstruct.example.CustomAccessorNamingStrategy)。 需要将此.jar文件添加到注解处理器类路径(即将其添加到添加了mapstruct-processor jar的位置旁边)。

       更多细节:上述的示例都在我们的示例仓库中(https://github.com/mapstruct/mapstruct-examples)。

猜你喜欢

转载自blog.csdn.net/YoshinoNanjo/article/details/81363285