实习记录(二)Java常用工具库

一.Lombok

1.背景概述

        Lombok是一个非常高效的专用于Java的自动构建插件库,其简化了 JavaBean 的编写,避免了冗余和样板式代码的出现,让编写的类更加简洁明了,可以帮助大家节省很多重复低效的代码编写。比如重复性的Setter、Getter、ToString、构造函数、日志等等,只需要一行注解就可以帮我们自动完成。

2.原理分析

        Lombok 使用的是编译期增强技术。目前Java语言比较通用的编译期增强技术有两种方案:1.使用特殊的编译器,如 AspectJ 的编译器 AJC;2.使用 JSR 269 提供的相关接口,如 Lombok 的实现方案。Aspectj 的 AJC 我们在下面会进行讨论,所以这里我们先讨论一下 Lombok 的实现方案 JSR 269。

        JSR 269的具体内容是 Pluggable Annotation Processing API,翻译过来就是插件化注解处理应用程序接口,这是Java在编译期提供出来的一个扩展点,用户可以在代码编译成字节码阶段对类的内容做调整,整体工作流程如下图所示:

         上图展示了一个一般 Javac 的编译过程,Java 文件首先通过源码解析构建出一个AST(Abstract Syntax Tree 抽象语法树),然后执行 JSR 269 的插件扩展进行注解处理,最后经过分析优化将最终的 Java 文件生成二进制的 .class 文件。

        Lombok 就是利用了 JSR 269 提供的能力,在我们进行代码编译的阶段完成了我们非常诟病的 getter、setter 等的重复工作,但是由于是在进行代码编译阶段时类的 getter、setter 等方法才会生成,所以当我们使用IDE工具的时候如果没有进行特殊的功能支持(没有安装 Lomobk 插件)的话,我们是无法使用IDE的代码提示功能的,并且还会代码报红。

3.安装配置

(1)引入maven依赖

<dependencies>
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<version>1.18.28</version>
		<scope>provided</scope>
	</dependency>
</dependencies>

(2)安装IDEA插件

  • Go to File > Settings > Plugins

  • Click on Browse repositories...

  • Search for Lombok Plugin

  • Click on Install plugin

  • Restart IntelliJ IDEA

        注意:lombok的引入,在.java文件编译之后的.class文件中会包含get、set方法,但源码找不到方法的定义,IDEA会认为这是错误,所以需要安装一个lombok插件

4.常用注解

(1)@Getter / @Setter 注解

        使用@Getter 和/或@Setter 注释任何字段属性或整个类,让lombok 自动生成默认的getter/setter方法,默认为public。其使用方式如下:

  • 编译前:
@Getter
@Setter
public class LombokAnnotationTest {
    private Integer id;
    private String name;
}
  • 编译后:
public class LombokAnnotationTest {
    private Integer id;
    private String name;

    public LombokAnnotationTest() {
    }

    public Integer getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }
}

(2)@ToString/@EqualsAndHashCode

        这两个注解也比较好理解,就是生成toString,equals和hashcode方法,同时后者还会生成一个canEqual方法,用于判断某个对象是否是当前类的实例。生成方法时默认只会作用于类中的非静态成员变量字段。

@ToString
@EqualsAndHashCode
public class Demo {

    private String name;
    private int age;
}

//@EqualsAndHashCode也有类似的下面的属性,
@ToString(
        includeFieldNames = true, //是否使用字段名
        exclude = {"name"}, //排除某些字段
        of = {"age"}, //只使用某些字段
        callSuper = true //是否让父类字段也参与 默认false
)
//@EqualsAndHashCode也有类似的下面的属性,
@ToString(
        includeFieldNames = true, //是否使用字段名
        exclude = {"name"}, //排除某些字段
        of = {"age"}, //只使用某些字段
        callSuper = true //是否让父类字段也参与 默认false
)

(3)@NoArgsConstructor/@RequiredArgsConstructor/@AllArgsConstructor

        这 3 个注释都用于生成构造函数,该构造函数将接受某些字段的 1 个参数,并简单地将此参数分配给该字段。其中:

  • @NoArgsConstructor:用于生成无参构造函数

  • @RequiredArgsConstructor:生成所有未初始化的 final 字段以及标记为 @NonNull 且未在声明处初始化的字段的特殊字段构造函数。 对于那些标有@NonNull 的字段,还会生成显式空检查。

  • @AllArgsConstructor:用于生成所有属性字段的有参构造函数,标有@NonNull 的字段会导致对这些参数进行空检查。

(4)@Data

        @Data 是一个复合注解,它将@ToString、@EqualsAndHashCode、@Getter / @Setter 和@RequiredArgsConstructor 的特性捆绑在一起。@Data 生成所有字段的 getter,所有非 final 字段的 setter,以及涉及类字段的适当的 toString、equals 和 hashCode 实现,以及初始化所有 final 字段以及所有非 final 字段的构造函数 没有标有@NonNull 的初始化器,以确保该字段永远不会为空。

        注意:@RequiredArgsConstructor注解当类中没有 final 和 @NonNull 注解的成员变量时会生成一个无参构造方法(因为没有符合要求的参数),而很多人认为 @Data 会生成无参构造方法就是此导致的。

(5)@Slf4j

        日志类注解用在类上,可以省去从日志工厂生成日志对象这一步,直接进行日志记录。具体注解根据日志工具的不同而不同,此处以Slf4j为例。日志门面(Simple Logging Facade For Java) , Slf4j主要是为了给Java日志访问提供一套标准、规范的API框架,其主要意义在于提供接口,具体的实现可以交由其他日志框架,例如log4j和logback等。

  • 编译前:

@Slf4j
public class LombokAnnotationTest {
    private Integer id;
    private String name;
}
  • 编译后:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LombokAnnotationTest {
    private static final Logger log = LoggerFactory.getLogger(LombokAnnotationTest.class);
    private Integer id;
    private String name;

    public LombokAnnotationTest() {
    }
}

二.MapStruct

1.MapStruct概述

(1)项目架构分层背景

        常见的项目架构开发过程中,都会对软件项目进行分层设计,层次设计表格与架构设计图大致如下:

层次

模块名

方法名称

缩写

表现层

web

controller

VO(view object):视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。

领域层

service

service

DTO(Data Transfer Object):数据传输对象,展示层与服务层之间的数据传输对象。

应用层

biz

domain/application

DO(Domain Object)领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。

持久层

dal

dao

PO(Persistent Object)持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应PO的一个(或若干个)属性。

 请求流程如下:

  1. 用户发出请求(可能是填写表单),表单的数据在展示层被匹配为VO。

  2. 展示层把VO转换为服务层对应方法所要求的DTO,传送给服务层。

  3. 服务层首先根据DTO的数据构造(或重建)一个DO,调用DO的业务方法完成具体业务。

  4. 服务层把DO转换为持久层对应的PO,调用持久层的持久化方法,把PO传递给它,完成持久化操作。

  5. 对于一个逆向操作,如读取数据,也是用类似的方式转换和传递。

(2)编码问题

        分层过程代带来一个编码问题:应用与应用之间,还有单独的应用细分模块之后,DO 一般不会让外部依赖,这时候需要在提供对外接口的模块里放 DTO 用于对象传输,也即是 DO 对象对内,DTO对象对外,DTO 可以根据业务需要变更,并不需要映射 DO 的全部属性。这种 对象与对象之间的互相转换,就需要有一个专门用来解决转换问题的工具,毕竟每一个字段都 get/set 会很麻烦。因此,对象转换框架的出现,解决了上面的编码痛点。

        没有出现对象装换工具之前,通常我们用get/set等方式逐一进行对象字段之间的映射操作,非常繁杂且硬编码;后来出现了很多开源的对象转换工具包,比如常见的BeanUtils.copyProperties,但是限制较多且不够灵活,因此便有了MapStruct。

        MapStruct 是一个基于 Java 注释的映射处理器,用于生成类型安全的 bean 映射类。您只需定义一个 mapper接口,该接口声明任何必需的映射方法。在编译期间,MapStruct 将自动生成此接口的实现。此实现使用普通的 Java 方法调用在源对象和目标对象之间进行映射,在使用过程中需要只需要配置完成后运行 mvn compile就会发现 target文件夹中生成了一个mapper接口的实现类。

2.MapStruct使用指南

2.1 配置引入

...
<!-- 版本定义 -->
<properties>
    <org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
</properties>
...
<!-- mapstruct核心依赖 -->
<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>
...
<!-- mapstruct代码构建和生成插件 -->
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>1.8</source> <!-- depending on your project -->
                <target>1.8</target> <!-- depending on your project -->
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                    <!-- other annotation processors -->
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

2.2 映射器定义

        使用 mapstruct 首先要创建映射转换器,将核心注解 @Mapper 标记在接口或抽象类上就定义了一个映射器,作为访问入口。当我们构建/编译应用程序时,MapStruct注解处理器插件会识别出对应接口并为其生成一个实现类,具体的转换/映射方法会在接口中声明。

Annotation Description Optional Elements
@Mapper Marks an interface or abstract class as a mapper and activates the generation of a implementation of that type via MapStruct.
  • injectionStrategy:转换字段的生成注入策略,包括构造函数注入、字段注入,默认为 InjectionStrategy.FIELD
  • componentModel:生成映射器的组件模型(定义使用方式),包括default(getMapper方式)、spring(spring注入方式)等,默认为 default

(1)工厂单例注入

//接口定义
@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    //convert method
    //...
}
//单例使用
CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);

(2)Spring依赖注入

//接口定义
@Mapper(componentModel = "spring")
public interface CarMapper {
    //convert method
    //...
}
//注入
@Autowired
private CarMapper carMapper;
//使用
CarDto carDto = carMapper.carToCarDto(car);

2.3 映射

2.3.1 基本映射

        最简单使用方式就是在映射器中直接声明转换方法(不用配置任何注解),转换方法需要源对象作为参数并需要目标对象作为返回值,该方法的方法名可以自由选择,映射器只需要关注入参类型和返回类型就可以确定生成转换代码,公共的可读属性都会被复制到目标中的相应属性中(包括超类上声明的属性),所有名称与类型一样的字段都会被隐式复制。基本映射包括以下几种情况,并在代码示例中说明:

  • 一致映射:转换之间属性字段名、字段类型、和字段数量完全一致并一一对应,此时会完全等价转换
  • 字段不一致:转换之间属性字段名称不完全一致,不一致的属性名不会自动映射,采用默认值或null
  • 数量不一致:转换之间属性字段数量不完全一致,包括多转少、少转多,缺失或多余的属性采用默认值或null
  • 类型不一致:转换之间属性字段类型不完全一致,有些同名字段之间会做隐式转换,包括基本类型与包装类型、基本类型与String、String与枚举类型等

        项目构建编译以后就会生成对应的实现类,相关文件位于项目中的 target/generated-sources/annotations/...

@Mapper
public interface ConvertMapper{
    ConvertMapper INSTANCE = Mappers.getMapper(ConvertMapper.class);

    /**
     * 单对象转换: Source to Target
     */
    public Target singleConvert(Source source);

    /**
     * 集合List转换:List<Source> to List<Target>
     */
    public List<Target> listConvert(List<Source> sourceList);

    /**
     * 集合Set转换:Set<Integer> to Set<String>
     */
    public Set<String> setConvert(Set<Integer>  integerSet);
}

        其中集合类型的映射(ListSet等等)以对象映射相同的方式映射元素类型,通过循环调用定义的对应单对象转换方法完成集合映射,若没有在接口事先声明对应的单对象映射方法则会隐式生成代码。注意不允许使用可迭代源和不可迭代目标声明映射方法,反之亦然。其生成的实现方法源码如下:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-06-14T14:05:44+0800",
    comments = "version: 1.5.5.Final, compiler: javac, environment: Java 11.0.19 (Oracle Corporation)"
)
public class ConvertMapperImpl implements ConvertMapper {

    /**
     * 单对象转换: Source to Target
     */
    @Override
    public Target singleConvert(Source source){
        if ( source == null ) {
            return null;
        }

        Target target = new Target();

        target.setId( source.getId() );
        target.setName( source.getName() );
        target.setXXX...

        return target;
    }


    /**
     * 集合List转换:List<Source> to List<Target>
     */
    @Override
    public List<Target> listConvert(List<Source> sourceList){
        if ( sourceList == null ) {
            return null;
        }
        //- 若元素映射方法事先声明:mapstruct则会自动查找对应的源和目标方法,并进行隐式调用
        //- 若元素映射方法未声明:mapstruct则自动会隐式生成singleConvert转换方法
        List<Target> list = new ArrayList<Target>( sourceList.size() );
        for ( Source source : sourceList ) {
            list.add( singleConvert( source ) );
        }

        return list;
    }

    /**
     * 集合Set转换:Set<Integer> to Set<String>
     */
    @Override
    public Set<String> integerSetToStringSet(Set<Integer> integerSet) {
        if ( integerSet == null ) {
            return null;
        }

        Set<String> set = new LinkedHashSet<String>();
        //自动调用元素类型的隐式转换
        for ( Integer integer : integerSet ) {
            set.add( String.valueOf( integer ) );
        }

        return set;
    }

}

2.3.2 不一致映射

        在实际开发中,通常不同模型之间字段名不会完全相同,名称可能会有轻微的变化。对于不一致的映射我们这里主要考虑名称不一致、多源映射的情况,对于数据不一致我们单独开一节去讲。不一致映射主要通过 @Mapping 和 @Mappings 注解转换:

Annotation Description
@Mapping  Configures the mapping of one bean attribute or enum constant.
  • target:要映射的目标对象的字段名,同一目标属性不得多次映射。

  • source:数据源对象的字段名。注意此属性不能与constant()或expression()一起使用。

  • expression:计算表达式必须以 Java 表达式的形式给出,格式如下: java( <expression> )。表达式中引用的任何类型都必须通过它们的完全限定名称给出。或者,可以通过Mapper.imports()导入类型。注意此属性不能与source() 、 defaultValue() 、 defaultExpression() 、 qualifiedBy() 、 qualifiedByName()或constant()一起使用。

  • ignore:如果设置为true,则指定的字段不会做转换,默认false。

  • dateFormat:如果属性从String映射到Date或反之亦然,则可以由SimpleDateFormat处理的格式字符串。对于所有其他属性类型和映射枚举常量时,将被忽略。例:dateFormat = "yyyy-MM-dd HH:mm:ss"

  • numberFormat:如果带注释的方法从Number映射到String ,则可以由DecimalFormat处理的格式字符串,反之亦然。对于所有其他元素类型将被忽略。例:numberFormat = "$#.00"

  • defaultValue:默认值。如果 source 属性为null ,则会使用此处的默认值。如果target字段不是String类型,会尝试找到可以匹配的转换方法,否则会报错。注意此属性不能与constant() 、 expression()或defaultExpression()一起使用。

  • constant:不管原属性值,直接将目标属性设置为指定的常量。如果target字段不是String类型,会尝试找到可以匹配的转换方法,否则会报错。注意此属性不能与source() 、 defaultValue() 、 defaultExpression()或expression()一起使用。

  • qualifiedBy:选择映射器对target字段赋值。这在多个映射方法(手写或生成)符合条件并因此导致“发现模糊映射方法”错误的情况下很有用。

Annotation Description Optional Elements
@Mappings 可以包装配置一组多个 @Mapping 转换,当使用 Java 8 或更高版本时,可以省略 @Mappings 包装器注释并直接在一个方法上指定多个 @Mapping 注释。
  • Mapping [ ]:配置 @Mapping 集合

(1)字段名称不一致

        此处字段名称不一致主要指的是类型一致、但字段名称不一致的情况,类型不一致我们放到数据转换章节分析。

// We need map Source.sourceName to Target.targetName
@Mapper
public interface ConvertMapper{
    ConvertMapper INSTANCE = Mappers.getMapper(ConvertMapper.class);

    @Mapping(source="sourceName", target="targetName")
    @Mapping(source="sourceId", target="targetId")
    public Target singleConvert(Source source);
}

// generates:
@Override
public Target singleConvert(Source source) {
    target.setTargetName( source.getSourceName() );
    target.setTargetId( source.getSourceId() );
    // ...
}

(2)多源映射

        多源映射是指映射方法具有多个源参数,将多个源参数实体组合成一个返回目标对象。因为有时单个类不足以构建目标,我们可能希望将多个类中的值聚合为一个返回目标。多源映射与单参数映射方法一样,公共属性按名称对应自动映射。但如果多个源对象定义具有相同名称的公共属性,则必须使用@Mapping注解指定从中检索属性的源参数是哪个。

@Mapper
public interface ConvertMapper{
    ConvertMapper INSTANCE = Mappers.getMapper(ConvertMapper.class);

    @Mapping(source="source_a.Id", target="Id")
    @Mapping(source="source_b.sourceName", target="Name")
    public Target singleConvert(SourceA source_a,SourceB source_b);
}

2.3.3 更新现有实例

        在某些情况下,我们并不需要映射一个新的对象出来(以上方式的实现都是new object and return),而是需要对已有对象实例的某些映射属性值进行更新。可以通过为目标对象参数添加 @MappingTarget 注解来实现:

Annotation Description Optional Elements
@MappingTarget Declares a parameter of a mapping method to be the target of the mapping.

最多只能将一个方法参数声明为 MappingTarget。注意:作为映射目标传递的参数不能为空(null)。

//1. Update exist bean without return value - 无返回值
@Mapper
public interface ConvertMapper {
    void updateHuman(HumanDto humanDto, @MappingTarget Human human);
}
// generates
@Override
public void updateHuman(HumanDto humanDto, Human human) {
    human.setName( humanDto.getName() );
    // ...
}
 
//2. Update exist bean and return it - 更新并返回
@Mapper
public interface ConvertMapper {
    Human updateHuman(HumanDto humanDto, @MappingTarget Human human);
}
// generates:
@Override
public Human updateHuman(HumanDto humanDto, Human human) {
    // ...
    human.setName( humanDto.getName() );
    return human;
}

注意:更新现有实例也可以同步添加@Mapping注解来映射不一致字段,二者并不冲突 ,非映射项则不会更新。

2.3.4 自定义映射

(1)使用表达式

        有时我们的目标属性并不只是进行简单的映射,MapStruct允许在@Mapping注解中定义Java表达式来进行简单的逻辑映射。该注解参数包括 defaultExpression source 取值为 null时生效),或者 expression(固定执行,不能与 source 、defaultExpression 一起使用),整个源对象都可以在表达式中使用,但表达式中引用的任何类型都必须通过其完全限定名称给出, 或者通过 Mapper.imports() 导入类型。

//1. 全限定类名引用
@Mapper
public interface SourceTargetMapper {

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

    @Mapping(target="id", source="sourceId", defaultExpression = "java( java.util.UUID.randomUUID().toString() )")
    @Mapping(target = "timeAndFormat",expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")
    @Mapping(target = "targetTime", expression = "java(date2Long(s.getSourceTime()))")
    Target sourceToTarget(Source s);

    default Long date2Long(Date date) {
        return date != null ? date.getTime() : null;
    }
}

//2. imports 导入引用
imports org.sample.TimeAndFormat;
imports java.util.UUID;

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

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

    @Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")
    @Mapping(target = "timeAndFormat",
         expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
    Target sourceToTarget(Source s);

    //...
}

注意:MapStruct在任何方法中数据类型不匹配需要转换时(比如date2Long),会根据入参和返回值类型,自动匹配定义的方法(即使不指明),即隐式调用。但是如果你的Mapper接口比较复杂了,里面定义了出参和返回值相同的两个方法,则必须使用@Mapping指定使用哪个方法(或者使用@Named标记方法防止隐式调用),否则在编译时MapStruct会因为不知道用哪个方法而报错。当然你可以不用想这么多,先编译再说,如果报错了再去处理即可,这也是MapStruct的一个好处:在编译期就可以发现对象转换的错误,而不是到运行时。

(2)自定义方法

        大部分情况下我们通过mapstruct 注解自动生成的映射代码就可以进行各种属性转换,但有时我们也需要实现一些复杂、自定义的映射逻辑,这种情况下mapstruct允许我们在映射器中添加自定义的映射方法,并且像其他自动映射方法一样访问:

  • 接口 interface:通过接口方法的默认实现 default
  • 抽象类 abstract:通过类方法的实现,可以在类中声明其他字段
//1. 接口方式
@Mapper
public interface ConvertMapper {

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

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

//2. 抽象类方式
@Mapper
public abstract class CarMapper {

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

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

注意:如果参数和返回类型匹配,mapstruct自动生成的代码将会在嵌套映射或集合映射需要时调用自定义方法实现做元素转换。 

2.4 数据转换

2.4.1 隐式类型转换

        对于映射数据类型不一致的情况,MapStruct支持sourcetarget属性之间的大部分常见数据类型的自动转换,或称为隐式转换。自动类型转换适用于:

  • 基本类型及其对应的包装类之间。比如, intIntegerfloatFloatlongLongbooleanBoolean 等。
  • 任意基本类型与任意包装类之间。如 intlongbyteInteger 等。
  • 所有基本数据类型及包装类与String之间。如 booleanStringIntegerStringfloatString 等。
  • 枚举类和String之间。
  • Java大数类型(java.math.BigIntegerjava.math.BigDecimal) 和Java基本类型(包括其包装类)与String之间。
  • Java日期类型 DateTimeLocalDateTimeLocalDateLocalTime和 String 之间等,可以通过SimpleDateFormat的dateFormat选项指定格式字符串。
  • Java String 和 StringBuilder、UUID String 之间等。
  • 其它情况详见MapStruct官方文档

        因此,在生成映射器代码的过程中,如果源字段和目标字段之间属于上述任何一种情况,则MapStrcut会自行处理类型转换即隐式转换。

@Mapper
public interface CarMapper {
    //int to String
    @Mapping(source = "price", numberFormat = "$#.00")
    CarDto carToCarDto(Car car);
    //Date to String
    @Mapping(source = "manufacturingDate", dateFormat = "dd.MM.yyyy")
    CarDto carToCarDto(Car car);
}

2.4.2 嵌套对象转换

(1)引用映射与隐式调用

        通常情况下,对象的属性并不止包括基本数据类型,还有对其它对象的引用或更深的嵌套。比如 Car 类可以包含一个名为 driver 的 Person 对象(代表汽车驾驶员)的引用属性,该对象应映射到 CarDto 类引用的 PersonDto 对象。

@Mapper
public interface CarMapper {

    CarDto carToCarDto(Car car);

    PersonDto personToPersonDto(Person person);
}

        生成的carToCarDto()方法代码将自动调用personToPersonDto()用于映射driver属性的方法,而生成的personToPersonDto()实现用于执行person对象的映射。 理论上这样就可以映射任意深度对象图,这种自动映射也被称为隐式调用,常出现在以下场景:

  • 如果source和target属性具有相同的类型,则该值将简单地从源复制到目标(浅拷贝)。如果属性是集合(例如 List),则集合的副本将被设置到目标属性中。

  • 如果源属性和目标属性类型不同,则会自动检查是否存在一种已经声明的映射方法(该方法将源属性的类型作为参数类型,并将目标属性的类型作为返回类型),如果存在这样的方法,则会在生成的映射实现中自动调用它来转换。

  • 如果不存在此类方法,MapStruct将查判断否存在属性的源类型和目标类型的内置转换(隐式类型转换)。如果是这种情况,生成的映射代码将应用此转换。

  • 如果不存在这样的方法,MapStruct 将尝试应用复杂的转换

    • target = method1( method2( source ) )

    • target = method( conversion( source ) )

    • target = conversion( method( source ) )

  • 如果没有找到这样的方法,MapStruct将尝试生成一个自动子映射方法,该方法将执行源和目标属性之间的简单映射。

  • 如果MapStruct无法创建基于名称的映射方法,则会在构建编译时引发错误,指示不可映射的属性及其路径。

        注意:可以使用 @Mapper( disableSubMappingMethodsGeneration = true ) 阻止MapStruct 生成自动子映射方法,也可以通过元注释完全控制映射比如 @DeepClone

(2)嵌套映射

        在一些复杂情况下,嵌套对象属性的引用可能包含多个层级并且很多情况下名称也存在不匹配的差异,这时就需要人为的在自动映射的基础上进行映射控制。现有source和target如下:

//1. spurce FishTank
public class Fish {

    private String type;
}

public class Interior {

    private String designer;
    private Ornament ornament;
}

public class FishTank {

    private Fish fish;
    private String name;
    private MaterialType material;
    private Interior interior;
    private WaterQuality quality;
}

//2. target FishTankDto 
public class FishDto {

    private String kind;

    // make sure that mapping on name does not happen based on name mapping
    private String name;
}

public class MaterialDto {

    private String manufacturer;
    private MaterialTypeDto materialType;// same to MaterialType
}

public class FishTankDto {

    private FishDto fish;
    private String name;
    private MaterialDto material;
    private OrnamentDto ornament; //same to Interior.ornament
    private WaterQualityDto quality;
}

        在简单的场景中,我们只需要对嵌套级别上的属性需要更正(如fish);可以使用相同的构造来忽略嵌套级别的某些属性(如ignore);当源和目标不共享相同的嵌套级别(相同深度的属性)时,可以进行层级选择(如material);当映射首先共享一个共同映射基础时,可以在基础上进行映射补全(如quality和report),其映射器如下:

@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(source = "userInfo.idCard", target = "idCard")
@Mapping(source = "userInfo.avatar", target = "avatar")
UserVO convert(UserDO person);

//第二种写法:利用隐式转换对所有同名字段做转换
//优点:书写简单
@Mapping(source = "userInfo", target = ".")
UserVO convert(UserDO person);

2.5 高级映射

2.5.1 自定义切面

        为了进一步控制和定制化,MapStruct 还提供了两个自定义切面注解 @BeforeMapping, @AfterMapping 用来实现在对应类型映射方法前后进行方法增强和统一的逻辑控制。注解注释的方法必须有对应的实现体,在接口interface中以default方法实现,在抽象类abstract中以非抽象方法实现。

If the @BeforeMapping / @AfterMapping method has parameters, the method invocation is only generated if the return type of the method (if non-void) is assignable to the return type of the mapping method and all parameters can be assigned by the source or target parameters of the mapping method

  • 两个注解没有任何参数
  • 两个注解可以标记多个方法为切面,同一类型调用顺序按照定义顺序
@Mapper
public abstract class HumanConvertor {
    //前置通知类型1:无参类型
    @BeforeMapping
    public void calledWithoutArgsBefore() {
         // ...
    }
    //前置通知类型2:参数含有对应Source类型
    @BeforeMapping
    protected void calledWithHuman(Human human) {
        // ...
    }
    //前置通知类型3:参数含有对应Source类型和对应Target类型@MappingTarget(性别判断需求)
    @BeforeMapping
    protected void calledWithSourceAndTargetBefore(Human human, @MappingTarget HumanDto humanDto) {
        if (human instanceof Man) {
            humanDto.setGenderType(GenderType.MAN);
        } else if (human instanceof Woman) {
            humanDto.setGenderType(GenderType.WOMAN);
        }
    }


    //后置通知类型1:无参类型
    @AfterMapping
    public void calledWithoutArgsAfter() {
         // ...
    }
    //后置通知类型2:参数含有对应Target类型(@MappingTarget)
    @AfterMapping
    protected void calledWithDto(@MappingTarget HumanDto humanDto) {
        humanDto.setName(String.format("【%s】", humanDto.getName()));
    }
    //后置通知类型3:参数含有对应Source类型和对应Target类型@MappingTarget
    @AfterMapping
    public void calledWithSourceAndTargetAfter(Human human, @MappingTarget HumanDto humanDto) {
         // ...
    }

    public abstract HumanDto toHumanDto(Human human);
}

        生成的代码调用顺序如下:

 // generates:
public class HumanConvertorImpl extends HumanConvertor {

    @Override
    public HumanDto toHumanDto(Human human) {
        //前置通知1
        calledWithoutArgsBefore();
        //前置通知2
        calledWithHuman(human);

        if (human == null) {
            return null;
        }

        HumanDto humanDto = new HumanDto();

        //前置通知3
        calledWithSourceAndTargetBefore( human, humanDto );

        ...

        //后置通知1
        calledWithoutArgsAfter();
        //后置通知2
        calledWithDto( humanDto );
        //后置通知3
        calledWithSourceAndTargetAfter( human, humanDto );

        return humanDto;
    }
}

猜你喜欢

转载自blog.csdn.net/qq_40772692/article/details/131179152